C / ARRAYS
Array bounds the compiler will not check for you
Explain why C never range-checks a[i], work out an array's valid index range yourself, and add the checks and build flags that catch overruns.
What you will learn
- Read a[i] as *(a + i): an address computed from i, never compared against the length
- Derive a length with sizeof a / sizeof a[0] where a still has array type
- Guard indices that come from input, subtraction or a search before indexing
- Compile with -Wall -Wextra and test with -fsanitize=address,undefined
Understanding Array bounds the compiler will not check for you
In C, a[i] is defined as *(a + i). The compiler takes the address of the first element, multiplies i by the size of one element, adds the two, and emits a load or a store at that address. The element count is part of the array's type, so sizeof can report it at compile time, but that count is not stored anywhere next to the data at run time. There is therefore nothing in the generated instructions for i to be compared against, and the language deliberately requires no comparison: indexing costs one address calculation and nothing else.
The result is that an index outside 0..n-1 is not an error the program detects, it is undefined behaviour. The access lands on whatever bytes happen to occupy that offset: another local variable, alignment padding, a saved register, the return address, or memory the process does not own. Symptoms range from none at all, to a variable changing value with no code touching it, to a crash in a function far from the faulty line, and the same source can behave differently after you change optimisation level or add an unrelated local. Because it is undefined behaviour rather than merely reading junk, an optimiser is also entitled to assume the access is in range and to simplify nearby code on that assumption.
The mental model to carry is that an array is a fixed run of n * sizeof(element) bytes and the only bound that exists is the one you write into your own code. So compute n with sizeof a / sizeof a[0] in the scope where a is still an array, keep the length travelling next to the data, and check any index that came from input, a subtraction, or a search before you use it. The compiler will help where it can prove the index, for example a literal a[7] on a four-element array under -Warray-bounds, but that proof disappears the moment the index is a runtime value; -fsanitize=address and -fsanitize=undefined are what catch the remaining cases during testing.
<stdio.h>
int main(void)
{
int a[4] = {10, 20, 30, 40};
size_t n = sizeof a / sizeof a[0]; /* the only place the bound exists */
printf("a holds %zu ints in %zu bytes\n", n, sizeof a);
printf("valid indices: 0 .. %zu\n\n", n - 1);
/* byte offsets below assume a 4-byte int */
for (size_t i = 0; i < 7; i++) {
size_t off = i * sizeof a[0];
if (i < n)
printf("i=%zu offset=%2zu inside a[%zu]=%d\n", i, off, i, a[i]);
else
printf("i=%zu offset=%2zu OUTSIDE the object\n", i, off);
}
return 0;
}
Indexing is unchecked address arithmetic: an array's length lives in its type at compile time, so every run-time bounds check is one you have to write.
Worked examples
Writing the check yourself
A tiny accessor that refuses an out-of-range index, since nothing in the language will refuse it for you.
<stdio.h>
<stdlib.h>
static int get(const int *a, size_t n, size_t i)
{
if (i >= n) {
fprintf(stderr, "index %zu out of range (n=%zu)\n", i, n);
exit(1);
}
return a[i];
}
int main(void)
{
int temps[3] = {18, 21, 19};
size_t n = sizeof temps / sizeof temps[0];
printf("%d\n", get(temps, n, 0));
printf("%d\n", get(temps, n, 2));
printf("%d\n", get(temps, n, 3));
printf("never reached\n");
return 0;
}
Example explained
Line 1The if in get is the entire bounds check; remove it and a[i] compiles to the same load whether i is 2 or 2000.
Line 2n is computed in main, where temps still has type int[3]; get only ever knows the number it was handed.
Line 3exit(1) terminates at the bad call, which is why the final printf produces no line.
Line 4The diagnostic goes to stderr, which is unbuffered, so redirecting stdout to a file can change the order the three lines appear in.
Where the boundary actually is
The pointer one past the last element is legal to form and compare, but not to read through.
<stdio.h>
int main(void)
{
int a[4] = {1, 2, 3, 4};
int *end = a + 4; /* one past the last element: allowed */
int sum = 0;
for (int *p = a; p != end; p++)
sum += *p;
printf("sum=%d\n", sum);
printf("end - a = %td elements\n", end - a);
printf("may form: a..a+4 may read: a..a+3\n");
return 0;
}
Example explained
Line 1a + 4 is well defined even though no fourth element exists; the standard carves out exactly one position past the end so loops have something to stop at.
Line 2The loop dereferences p only while p != end, so the boundary pointer is compared but never read.
Line 3end - a has type ptrdiff_t and prints with %td; it equals the element count, so the last readable position is one lower.
Line 4a + 5 is already outside the permitted range, even if you never write *(a + 5).
Important notes
Out-of-range access is undefined behaviour, not merely reading garbage; the compiler may assume it cannot happen and reshape surrounding code, so one clean test run proves nothing about correctness.
Forming the one-past-the-end pointer a + n is legal and is what loop conditions compare against; dereferencing it, or forming a + n + 1, is not.
Common mistakes
Writing for (i = 0; i <= n; i++) and touching a[n] on the last pass: that is one element past the object, so it usually overwrites whatever the compiler placed next in the stack frame instead of reporting an error.
Counting down with an unsigned index, as in for (size_t i = n - 1; i >= 0; i--): the condition is always true, i wraps from 0 to SIZE_MAX, and a[i] then addresses memory gigabytes away, which crashes on most runs and silently corrupts on some.
Concluding an array access is in range because the program printed the right answer: a small overrun often changes nothing visible until an unrelated edit or a new optimisation level moves the stack layout.
Try it yourself
Change, predict, then run
Declare int a[5] = {2, 4, 6, 8, 10}; and write int at(const int *a, size_t n, size_t i) that prints "index i out of range" and returns -1 when i >= n, otherwise returns a[i]. Call it with i = 4 and i = 5 and confirm only the second call reports.
Open the C workspaceCheck your understanding
You write to a[5] on an int a[5] inside a loop, and the program still prints the expected results on your machine. What can you conclude?
- Nothing useful: the write is undefined behaviour, and the bytes it hits may only start to matter after an unrelated code change or a different optimisation level
- The array really has six usable slots, because C rounds array sizes up to a convenient boundary
- The write was discarded, because indexing stops at the last element of the array
- It is safe as long as you never read a[5] back afterwards
Show answer
C specifies no behaviour at all for the access, so a correct-looking run tells you only about today's stack layout and today's compiler decisions, not about the code. Option 3 is the tempting one, but the write itself is already the bug: those four bytes belong to some other object, such as a neighbouring local, saved register, or return address, so not reading a[5] back does nothing to make it safe.