Off-by-one error adalah bug klasik di mana loop melakukan iterasi satu kali terlalu banyak atau satu kali terlalu sedikit. Bug ini sulit dideteksi karena program sering masih berjalan tanpa crash—hasil yang dihasilkan terlihat masuk akal tetapi sebetulnya salah. Penyebab paling umum adalah kekeliruan memilih antara operator < dan <= di kondisi loop, atau kekeliruan memilih indeks awal 0 versus 1 pada array berbasis nol. Pada chapter ini kita akan menelaah dua pola bug off-by-one yang paling sering dijumpai, masalah fence post, serta strategi mencegahnya.
Bug Klasik: i <= n pada Array Berbasis Nol #
Bug paling sering dijumpai pada iterasi array adalah memakai i <= n padahal seharusnya i < n. Karena indeks array di C berbasis nol, array berukuran n memiliki indeks 0 sampai n-1. Memakai i <= n menyebabkan loop mengakses data[n] yang berada di luar batas array—perilaku undefined behavior yang dapat menyebabkan nilai sampah atau crash:
#include <stdio.h>
int main(void) {
int data[] = {10, 20, 30, 40, 50};
int n = sizeof(data) / sizeof(data[0]);
int total = 0;
/* BUG: kondisi i <= n menyebabkan akses data[n] yang di luar batas */
for (int i = 0; i <= n; i = i + 1) {
printf("Akses data[%d] = %d\n", i, data[i]);
total = total + data[i];
}
printf("Total (salah): %d\n", total);
return 0;
}Output yang Dihasilkan (output sebenarnya bergantung pada memory layout, dapat berbeda di mesin lain):
Akses data[0] = 10
Akses data[1] = 20
Akses data[2] = 30
Akses data[3] = 40
Akses data[4] = 50
Akses data[5] = 5
Total (salah): 155Pada output di atas, data[5] bernilai 5—nilai ini sebenarnya bukan bagian dari array, melainkan byte di memori setelah array. Bahwa nilainya 5 (sama dengan n) hanyalah kebetulan layout memori di mesin ini. Di mesin lain, kompilator lain, atau bahkan eksekusi berikutnya, nilai tersebut bisa berbeda. Inilah esensi undefined behavior: perilaku tidak terdefinisi, tidak dapat diprediksi, dan tidak boleh diandalkan.
Perbaikannya sederhana—ganti <= dengan <:
#include <stdio.h>
int main(void) {
int data[] = {10, 20, 30, 40, 50};
int n = sizeof(data) / sizeof(data[0]);
int total = 0;
/* BENAR: kondisi i < n hanya mengakses indeks 0..n-1 */
for (int i = 0; i < n; i = i + 1) {
printf("Akses data[%d] = %d\n", i, data[i]);
total = total + data[i];
}
printf("Total (benar): %d\n", total);
return 0;
}Output yang Dihasilkan:
Akses data[0] = 10
Akses data[1] = 20
Akses data[2] = 30
Akses data[3] = 40
Akses data[4] = 50
Total (benar): 150Loop sekarang hanya mengakses indeks 0 sampai 4, dan total menjadi 150 yang benar (10+20+30+40+50).
Bug Klasik: i < n pada Loop Berbasis 1 #
Bug kebalikannya terjadi saat loop dimulai dari 1 (bukan 0) dan seharusnya berhenti di n (bukan n-1). Banyak pemula menulis for (int i = 1; i < n; i = i + 1) dengan asumsi loop mencakup nilai 1 sampai n, padahal sebetulnya hanya mencakup 1 sampai n-1:
#include <stdio.h>
int main(void) {
int n = 5;
/* BUG: i < n seharusnya i <= n; cetak angka 1 sampai n */
printf("Versi salah (i < n):\n");
for (int i = 1; i < n; i = i + 1) {
printf("%d ", i);
}
printf("\n");
/* BENAR: i <= n mencakup nilai n */
printf("Versi benar (i <= n):\n");
for (int i = 1; i <= n; i = i + 1) {
printf("%d ", i);
}
printf("\n");
return 0;
}Output yang Dihasilkan:
Versi salah (i < n):
1 2 3 4
Versi benar (i <= n):
1 2 3 4 5 Pada versi salah, loop mencetak 1, 2, 3, 4—nilai 5 terlewatkan karena kondisi i < 5 menjadi false ketika i = 5. Pada versi benar, kondisi i <= 5 tetap true untuk i = 5, sehingga 5 ikut dicetak. Aturan praktis: jika loop dimulai dari 1 dan ingin mencakup n, gunakan <=; jika loop dimulai dari 0 dan ingin mencakup n elemen, gunakan <.
Mari kita telaah perbedaan kedua pola:
- Versi salah (
i = 1; i < n): loop mengiterasi sebanyakn - 1kali (1, 2, …, n-1). - Versi benar (
i = 1; i <= n): loop mengiterasi sebanyaknkali (1, 2, …, n). - Alternatif lain (
i = 0; i < n): loop mengiterasi sebanyaknkali (0, 1, …, n-1).
Masalah Fence Post #
Masalah fence post adalah kelas off-by-one yang muncul saat kita menghitung jumlah “titik” (post) dibandingkan jumlah “interval” antar titik. Misalnya pagar sepanjang 10 meter dengan tiang setiap 2 meter—berapa jumlah tiang? Jawaban intuitif 10/2 = 5 adalah salah, karena ada tiang tambahan di ujung akhir. Jawaban benar adalah 10/2 + 1 = 6:
#include <stdio.h>
int main(void) {
/* Masalah fence post: 10 meter pagar, tiang setiap 2 meter */
int panjang = 10;
int jarak_antar_tiang = 2;
printf("Panjang pagar: %d meter\n", panjang);
printf("Jarak antar tiang: %d meter\n", jarak_antar_tiang);
/* BUG: panjang/jarak hanya menghitung interval, bukan tiang */
int salah = panjang / jarak_antar_tiang;
/* BENAR: jumlah tiang = (panjang/jarak) + 1 karena tiang di kedua ujung */
int benar = panjang / jarak_antar_tiang + 1;
printf("Jumlah tiang (salah): %d\n", salah);
printf("Jumlah tiang (benar): %d\n", benar);
return 0;
}Output yang Dihasilkan:
Panjang pagar: 10 meter
Jarak antar tiang: 2 meter
Jumlah tiang (salah): 5
Jumlah tiang (benar): 6Masalah fence post muncul di banyak konteks pemrograman: jumlah iterasi untuk mencetak n elemen terpisah oleh pemisah adalah n - 1 pemisah (bukan n), jumlah angka dari a sampai b inklusif adalah b - a + 1 (bukan b - a), dan jumlah segmen dari n titik yang dihubungkan adalah n - 1. Selalu periksa apakah perhitungan menghasilkan jumlah “titik” atau jumlah “interval”.
Strategi Mencegah Off-by-One #
Beberapa praktik yang dapat membantu mencegah bug off-by-one:
- Pakai idiom standar: untuk iterasi array berbasis nol, selalu pakai
for (size_t i = 0; i < n; i = i + 1). Pola ini sudah teruji dan konsisten dengan idiom industri. - Uji dengan kasus batas: jalankan program dengan
n = 0,n = 1, dann = 2untuk memastikan loop berperilaku benar di kasus batas. - Hindari konversi tipe implisit: pastikan tipe counter dan tipe batas sama (misalnya keduanya
size_tatau keduanyaint) untuk mencegah bug komparasi signed/unsigned. - Pakai AddressSanitizer saat pengujian: kompilasi dengan
-fsanitize=address -gsaat development untuk mendeteksi out-of-bounds access secara otomatis. - Code review dengan fokus pada batas loop: saat mereview kode, periksa setiap kondisi loop dan tanyakan “apakah iterasi terakhir benar-benar valid?”.