C Part I · Module 6 · Arrays
Array Bounds and What Happens When You Cross Them
The previous chapter ended on a deliberate cliffhanger. Every traversal it recommended ends by computing an index one element past the end of the array and then declining to use it, and the obvious question is what would happen if it did not decline. The answer is more uncomfortable than most beginners expect and more interesting than the usual summary of it. C performs no check, there is no error, and the program will usually carry on as though nothing happened — which is precisely what makes this the most consequential bug class in the language. This chapter explains why there is no check, what the standard actually says, which of the plausible stories about the consequences are true, and what tooling exists to make a bug that is invisible by default into one you can see in a report with exact byte offsets.
Foundation17 min readCPart IFundamentalsArrays
Part I · Module 6 · Chapter 6.4 · Array Bounds and What Happens When You Cross Them
1. There Is Nothing to Check Against
Start with why, because the rule makes far more sense derived than announced.
Chapter 6.1 established, with sizeof as evidence, that int samples[4] occupies exactly sixteen bytes and that all sixteen are elements. There is no stored count, no end marker, no header. The array is a run of storage and nothing else.
Now ask what a run-time bounds check would consult. Given samples[i] with i computed moments earlier, the machine would need the array's length — and the length is not there. The compiler knew it, from the declaration, while compiling. At run time it is gone.
C does not check array bounds, and the reason is visible in the layout: an array carries nothing a check could use.
This is not an oversight to be worked around resentfully. It is the direct consequence of arrays being exactly their elements and nothing more, which is also what makes them predictable, cheap and suitable for the hardware-adjacent work Part II is heading toward. The cost is that the bounds are your responsibility, completely.
2. What the Standard Says
int samples[4] = {10, 20, 30, 40};
size_t count = 4;
int total = 0;
for (size_t i = 0; i <= count; i++) { /* <= instead of < */
total = total + samples[i];
}This is Chapter 6.3's traversal with one character changed, and it is the single most common array bug in C. On the final pass i is 4, and samples[4] reads storage that is not part of the array.
The classification is short and absolute:
Accessing an array outside the range of its elements is undefined behaviour. The C standard imposes no requirements on what the program does.
Not "returns an unspecified value". Not "is implementation-defined". No requirements at all — on that access, and on the program as a whole.
That is why this chapter does not run the program and show you what it printed. Whatever number appeared would be one accident on one machine on one day, and presenting it as what C does would teach you something false. Chapter 1.3 set up this distinction and Chapter 2.3 used it; this is its sharpest application.
3. What the Compiler Catches
The compiler is not entirely helpless, and knowing exactly where its help stops is worth more than a general reassurance.
A constant index, on an array it can see:
int a[4] = {0};
a[5] = 99;warning: array index 5 is past the end of the array (that has type 'int[4]')
[-Warray-bounds]It knows the type is int[4], it can see the index is 5, and it says so. Note that it is a warning — the program still builds.
A computed index:
int a[4] = {0};
int where = 5;
a[where] = 99;(no diagnostics)Nothing. And a computed index is the interesting case — it is what loops produce, what arithmetic produces, and what input produces. The one the compiler catches is the one you were least likely to write.
| Situation | Caught at compile time? |
|---|---|
| Constant index, array declared in view | yes — -Warray-bounds warning |
| Index computed from a variable | no |
| Index derived from a loop bound | no |
| Index derived from input | no |
4. What Actually Happens When You Run It
Since C requires nothing, what a particular run does depends on what happens to be next to the array and what the compiler chose to generate. Four outcomes are common, and none of them is guaranteed:
It silently corrupts another object. The storage past the array may belong to another variable. Writing there changes that variable, which then misbehaves somewhere else entirely — in a function that looks correct, for reasons that appear to have nothing to do with the array. This is the worst case, because the symptom and the cause are far apart.
Nothing visible happens. The storage may be padding or unused. The program produces the right answers and the bug stays hidden until the layout changes — a new variable, a different compiler, a different optimisation level.
The program is terminated. If the access reaches memory the operating system protects, the program is stopped. On a hosted system this is the lucky case: it is loud and immediate.
The program behaves in a way no storage story explains. A compiler is entitled to assume your program has no undefined behaviour, and to optimise on that basis. If a correct program could not have reached a certain point, code for that case may be transformed or removed. The result can be a program whose behaviour cannot be accounted for by any story about which bytes were overwritten.
5. Making It Visible
The situation sounds bleak and is not, because the checking C does not do at run time can be added by tooling during development.
Rebuild the §3 program with AddressSanitizer:
cc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=address -g bounds.c -o boundsRunning it now produces a report instead of a mystery:
ERROR: AddressSanitizer: stack-buffer-overflow on address 0x00016b876650
WRITE of size 4 at 0x00016b876650 thread T0
#0 in main bounds.c:6
This frame has 1 object(s):
[32, 48) 'a' (line 4) <== Memory access at offset 52 overflows this variableRead the last line against Figure 1. The object a occupies bytes 32 to 48 of its frame — sixteen bytes, four int elements, exactly as Chapter 6.1 established. The access was at offset 52, which is past 48. The tool is describing the same picture you already have, with real numbers.
The classic off-by-one from §2 produces the same kind of report:
ERROR: AddressSanitizer: stack-buffer-overflow
READ of size 4 at ...
#0 in main loop.c:8
[32, 48) 'samples' (line 4) <== Memory access at offset 48 overflows this variableOffset 48 is one element past a sixteen-byte array — the exact index Chapter 6.3 §4 said the loop computes and never uses.
UndefinedBehaviorSanitizer reports it more briefly:
cc -std=c17 -Wall -Wextra -Wpedantic -fsanitize=undefined -g bounds.c -o boundsruntime error: index 5 out of bounds for type 'int[4]'Two points about these tools, and both matter.
What you are reading is the sanitizer's report, not C's behaviour. The tool adds checks that are not otherwise there, detects the access, and tells you. Without it the behaviour remains undefined — the same program built normally makes no promise at all, and the fact that it ran quietly proves nothing.
They are development tools. They make the program slower and larger, so the normal arrangement is to build your tests and development builds with them and ship without. A test suite run under a sanitizer finds this class of bug on the day it is written rather than in the field.
6. The Habits
Nothing in this chapter changes what you should write. It explains why Chapter 6.3's recommendations were not fussiness.
Write i < count. It cannot produce an index of count. Chapter 6.3 §5 showed that i <= count - 1 can be far worse than it looks.
Take the count from the declaration. The sizeof idiom cannot disagree with the array; a repeated literal can.
Keep the count beside the array. Every time you handle an array you need its length from somewhere, and Chapter 6.1 established there is nowhere to get it except from the type or from a value you carry. Chapter 7.7 is where this stops being a convenience and starts being mandatory.
Validate an index that came from outside. An index derived from input, a packet field or a calculation is exactly the case the compiler cannot check. Check it against the count before using it — once, at the point it enters your code.
Build your tests with a sanitizer. It is one flag, and it converts the most dangerous bug class in C into a report with a line number.
7. What You Should Be Able to Say Now
- C does not check array bounds, because the array stores nothing a check could consult — Chapter 6.1's figure shows all the bytes are elements.
- An out-of-range access is undefined behaviour: the standard imposes no requirements.
- That is why no out-of-range result is printed in this chapter — any value shown would be one accident, not a rule.
- The compiler warns about a constant out-of-range index on a visible array (
-Warray-bounds) — and it is only a warning. - It catches nothing for a computed index, which is the case loops and input produce.
- Possible consequences include silent corruption of another object, no visible effect, termination, and transformations based on the assumption that UB does not occur.
- "It crashes" and "it gives garbage" are both wrong as descriptions — nothing is promised, including those.
- A program that appears to work with an out-of-range access has not got away with it.
- AddressSanitizer reports the overflow with byte offsets that match the layout; UBSan reports the index and the type.
- Those reports are the tool's, not C's. Without the tool, the behaviour is still undefined.
8. Exercises
Exercise 1 — name the valid range. For each declaration, give the valid indices and the first index that is not valid.
int a[1];
int b[10];
char c[256];
double d[3];Exercise 2 — find the out-of-range access. Each of these reads or writes outside its array. Say which index does it and on which pass.
int v[5];
for (size_t i = 0; i <= 5; i++) { v[i] = 0; }int v[5];
for (size_t i = 1; i <= 5; i++) { v[i] = 0; }int v[5];
for (size_t i = 5; i > 0u; i--) { v[i] = 0; }Exercise 3 — predict the diagnostics. For each, say whether -Wall -Wextra -Wpedantic reports anything, and why.
int a[4]; a[4] = 1;
int a[4]; int k = 4; a[k] = 1;
int a[4]; for (int i = 0; i <= 4; i++) { a[i] = 0; }Exercise 4 — classify four claims. For each, say whether it is accurate, and correct it if not.
- an out-of-range read returns an unspecified value
- an out-of-range write always crashes the program
- the behaviour of an out-of-range access is undefined
- if the program produces the right answer, the access was harmless
Exercise 5 — read the report. An AddressSanitizer report says [64, 104) 'buffer' (line 12) <== Memory access at offset 104 overflows this variable. Say how many bytes the array occupies, how many int elements that is on a 4-byte-int implementation, which index was accessed, and what the correct last index would have been.
Exercise 6 — explain the silence. A colleague says their program "can't have an array bug, because it compiles with no warnings and runs fine." Give two reasons that is not evidence, using §3 and §4, and name the one change to their build command that would actually test the claim.
Exercise 7 — repair and justify. This is meant to copy the last four readings into a smaller array. Identify every out-of-range access, repair it, and state the valid index range for both arrays.
int source[8] = {1,2,3,4,5,6,7,8};
int recent[4];
for (size_t i = 0; i <= 4; i++) {
recent[i] = source[4 + i];
}Exercise 8 — validate an index. A value selected arrives from outside your code and will be used as settings[selected], where settings has 6 elements and selected is an int. Write the check you would perform before the access, say which two conditions it must test and why both are needed, and explain what you would do if it fails.
9. Summary
- C performs no bounds check, because Chapter 6.1's layout leaves nothing to check against.
- An out-of-range access is undefined behaviour — no requirements at all.
- The compiler catches a constant out-of-range index on a visible array, as a warning, and catches nothing computed.
- Consequences range from silent corruption of a neighbour, through no visible effect, to termination — and to transformations that assume the access never happened.
- It does not reliably crash and it does not "return garbage" — both are outcomes, not rules.
- Appearing to work is not evidence of correctness.
- AddressSanitizer and UBSan turn it into a report with byte offsets and line numbers; their output is the tool's, not C's.
- The habits:
i < count, the count from the declaration, carry the count with the array, validate indices from outside, and build tests with a sanitizer.
Every array so far has been a single run of elements, which is the right picture for a list of samples and the wrong one for anything with two dimensions — a grid of pixels, a table of calibration values indexed by gain and by channel, a matrix. C can express those, and the way it does is not a new kind of object but the existing one applied to itself. The next chapter builds that, and the layout it produces is one you can already predict from Chapter 6.1's figure, which is exactly why it is the last chapter of this module rather than the first.
Standards & specifications
- Governing standard
- ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)
Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C Programming curriculum.
