C / LOOPS AND JUMPS
Loop bounds, off-by-one errors and fencepost bugs
Pick loop bounds in C from a stated trip count, and avoid fencepost, off-by-one and unsigned-wrap errors at array edges.
What you will learn
- Write ranges half-open: i < n visits n elements, i <= n runs one time too many.
- Count boundaries as n - 1 and inclusive spans as hi - lo + 1.
- Prefer i + 1 < n over i < n - 1 so an empty array cannot wrap the bound.
- Never test i >= 0 on a size_t or unsigned counter; it is always true.
Understanding Loop bounds, off-by-one errors and fencepost bugs
A loop bound is a claim about the trip count, and the half-open form for (i = 0; i < n; i++) makes that claim easy to read: the number of iterations is the distance between the bounds, n - 0, so the body runs exactly n times and the largest index it produces is n - 1. That lines up with C arrays exactly, because an array of n elements has valid indices 0 through n - 1. Changing the test to i <= n adds one iteration and one index, and that extra index is precisely the one the array does not own.
The fencepost question is whether you are counting things or the spaces between things. A run of n items has n - 1 boundaries, so a loop that compares a[i] with a[i + 1], prints a separator between items, or measures a gap has to stop one iteration earlier than a loop that merely visits each item. The mirror image is the inclusive range: from lo to hi inclusive there are hi - lo + 1 values, which is why for (i = 1; i <= 10; i++) runs ten times and dropping the = makes it nine.
Bounds arithmetic is also where the counter's type matters. If n is a size_t or any unsigned type, i < n - 1 is a trap: when n is 0, n - 1 does not become -1, it wraps to the largest representable value, and a loop that should not have run at all walks over memory it does not own. Writing the condition as i + 1 < n removes the subtraction entirely. For the same reason for (size_t i = n - 1; i >= 0; i--) never terminates, since an unsigned value is never negative; count with i = n while i > 0 and index with i - 1.
<stdio.h>
int main(void)
{
int fence[] = {2, 4, 6, 8, 10};
int n = (int)(sizeof fence / sizeof fence[0]);
int i;
printf("%d posts\n", n);
for (i = 0; i < n; i++) /* half-open [0,n): exactly n visits */
printf("post %d = %d\n", i, fence[i]);
printf("%d gaps\n", n - 1);
for (i = 0; i + 1 < n; i++) /* body touches i+1, so stop one early */
printf("gap %d spans %d..%d\n", i, fence[i], fence[i + 1]);
return 0;
}
A loop bound encodes a trip count, so decide whether you are counting items or the boundaries between them before choosing < or <=.
Worked examples
Counting down without going negative
Walking an array backwards with an unsigned counter, and why i >= 0 cannot be the stopping test.
<stdio.h>
<stddef.h>
int main(void)
{
char word[] = "loop";
size_t n = sizeof word - 1; /* 4 characters, terminator excluded */
size_t i;
for (i = n; i > 0; i--) /* counter n..1, index i-1 is n-1..0 */
putchar(word[i - 1]);
putchar('\n');
i = 0;
if (i - 1 > n)
printf("size_t 0 - 1 wraps upward, so i >= 0 never becomes false\n");
return 0;
}
Example explained
Line 1sizeof word is 5 because the initialiser stores 'l','o','o','p','\0'; subtracting 1 gives the 4 characters.
Line 2The counter takes the values 4,3,2,1 and the body indexes word[i - 1], so it reads indices 3 down to 0 and stops before i can wrap.
Line 3The test i - 1 > n succeeds because 0 - 1 in size_t is the largest unsigned value rather than -1, which is exactly why a countdown guarded by i >= 0 spins forever.
The bound for adjacent pairs
A loop that reads a[i + 1] must stop at i + 1 < n, and that form also survives a length of zero.
<stdio.h>
<stddef.h>
static int max_gap(const int *a, size_t n)
{
int best = 0;
size_t i;
for (i = 0; i + 1 < n; i++) /* never evaluates n - 1 */
if (a[i + 1] - a[i] > best)
best = a[i + 1] - a[i];
return best;
}
int main(void)
{
int a[] = {1, 3, 9, 12};
printf("max gap over 4 values = %d\n", max_gap(a, 4));
printf("max gap over 0 values = %d\n", max_gap(a, 0));
return 0;
}
Example explained
Line 1The bound is i + 1 < n because the body reads a[i + 1]; the last usable start index is n - 2.
Line 2With n = 4 the loop compares 3-1, 9-3 and 12-9, so best ends at 6.
Line 3With n = 0 the condition 0 + 1 < 0 is false immediately; the equivalent-looking i < n - 1 would compute a huge unsigned bound and read a[0] and a[1] on an array declared empty.
Room for the terminator
Filling a fixed char buffer while reserving the final slot for '\0'.
<stdio.h>
<stddef.h>
int main(void)
{
char buf[6]; /* 5 characters plus a terminator */
size_t cap = sizeof buf;
size_t i;
for (i = 0; i + 1 < cap; i++)
buf[i] = (char)('a' + i);
buf[i] = '\0';
printf("%s (%zu chars in a %zu-byte buffer)\n", buf, i, cap);
return 0;
}
Example explained
Line 1cap is 6, the size of the whole buffer, but only 5 slots may hold characters because index 5 has to hold the terminator.
Line 2i + 1 < cap leaves that last slot untouched, and after the loop i is exactly 5, the index where '\0' belongs.
Line 3%s stops at the terminator, so the reported length 5 is one less than sizeof buf, which is the usual strlen-versus-buffer-size off-by-one.
Separators between, not after
The fencepost pattern in output formatting: n values need n - 1 separators.
<stdio.h>
static void join(const int *a, int n)
{
int i;
for (i = 0; i < n; i++) {
if (i > 0) /* separator before every item except the first */
printf(", ");
printf("%d", a[i]);
}
printf("\n");
}
int main(void)
{
int a[] = {5, 7, 11};
join(a, 3);
join(a, 1);
join(a, 0);
return 0;
}
Example explained
Line 1The visiting loop keeps the plain i < n bound, and the fencepost correction lives in the if instead of the bound.
Line 2Guarding on i > 0 emits 2 separators for 3 items, which is the n - 1 count.
Line 3join(a, 1) prints no separator and join(a, 0) prints only the newline, so both edge lengths are handled without a special case.
Important notes
Reading arr[n] rarely crashes; it usually returns a plausible-looking number, so a run that produced correct-looking output is not evidence the bound was right.
sizeof arr / sizeof arr[0] only yields the element count where the array itself is in scope; inside a function that received it as a pointer, sizeof measures the pointer, so the count must be passed as a separate argument.
Common mistakes
Writing for (i = 0; i <= n; i++) over an n-element array: the extra iteration reads or writes arr[n], one past the end, which is undefined behaviour and often quietly corrupts a neighbouring variable or the loop counter itself.
Counting down with for (size_t i = n - 1; i >= 0; i--): the condition can never be false for an unsigned type, so after index 0 the counter wraps to a huge value and the program keeps indexing until it faults.
Sizing a string buffer from strlen(s) rather than strlen(s) + 1: the copy fits but the closing '\0' lands one byte past the allocation, and later reads run off the end looking for it.
Try it yourself
Change, predict, then run
Print int a[] = {5, 7, 11, 13, 17} as 5 -> 7 -> 11 -> 13 -> 17 using one loop, with an arrow between elements and none after the last. Then rerun it with the count set to 1 and to 0 and confirm no stray arrow appears.
Open the C workspaceCheck your understanding
An array declared int a[8] is cleared with for (i = 0; i <= 8; i++) a[i] = 0; The program compiles, runs, and prints nothing unusual. What is the most accurate description?
- The loop runs 9 times and the final store is past the end of the array, so the behaviour is undefined even though this run looked fine.
- The loop runs 8 times, because a[8] does not exist and is skipped when the index reaches the array's size.
- The loop is correct: C reserves one extra slot at a[8] as a terminator and zeroing it is expected.
- The compiler clamps the out-of-range subscript, so a[8] writes to a[7] a second time.
Show answer
The trip count of an inclusive range is hi - lo + 1, so i takes 9 values, 0 through 8, and a[8] is one past the last element a[7]. C performs no bounds checking, so that store may land in padding, in another local, or in the counter, and a clean-looking run proves nothing. Option 1 is tempting because the 8 in int a[8] and the 8 in a[8] look like the same number, but a declaration's 8 is a count while a subscript's 8 is an offset from the first element; nothing skips it.