Before you begin
Why this matters
Every C loop over an array or a string moves through memory one element at a time, whether you write an index or a pointer. When a loop goes one element too far, C prints no error and the program may still print a number. You can find these bugs only by reading the loop and knowing where each pointer is allowed to point.
What this helps you reason about
- Predict the address and value of p + i for an int, a double, or a char array.
- Write a loop that stops at an end pointer, and say why that pointer may be compared but never read.
- Spot a loop that reads one element past the end, and a sizeof that measures a pointer instead of an array.
In Java, Python, and Go, every index is checked: scores[3] on an array of three elements stops the program with an error. A C array does not store its length, and C checks no index. Variables showed that &x gives an address and *p reaches the value stored there. Function calls showed that an array parameter is a pointer to the first element. This lesson follows that pointer as it moves through an array: in steps of one element, up to the address just past the end, and one step too far.
Already know this? The short version
- An array used in an expression is converted to a pointer to its first element, except under sizeof or &. p + i points i elements further on (i * sizeof *p bytes), and a[i] means *(a + i).
- q - p, for two pointers into the same array, is the number of elements between them, a signed value printed with %td.
- A pointer may point at any element of an array or one past the last element. Reading through the one past the last, or computing any address further out, is undefined behavior. Walk with for (p = a; p < a + n; p++).
- Nothing checks bounds: a loop that reads one element too many prints no error. gcc and clang can catch a read past an array at run time with -fsanitize=address.
- A C string ends at its first '\0', and strlen walks to it. A parameter declared as an array is a pointer, so sizeof on it gives the size of a pointer.
- In real programs, watch for strncpy leaving a full buffer without '\0', malloc(strlen(s)) with no byte for '\0', loops that count down with size_t or below the first element, and pointers kept after realloc: see “What goes wrong with pointers and buffers in real programs?”.
Words in this lesson (8)
- pointer
- A value that holds the address of a variable or of a block of memory, declared as in int *alias. *alias means what is stored at that address. Copying a pointer copies the address, not the memory it points to.
- decay
- The automatic conversion of an array, used in an expression, into a pointer to its first element. It does not happen when the array is the operand of sizeof or &.
- pointer arithmetic
- Adding an integer to a pointer, or subtracting one pointer from another. p + 1 points to the next element, sizeof *p bytes further on, not one byte further on.
- ptrdiff_t
- The signed integer type of the result of subtracting two pointers, declared in <stddef.h> and printed with %td. The result counts elements, not bytes.
- one-past-the-end pointer
- The address just after the last element of an array, such as a + n for an array of n elements. A program may compute it and compare with it, but must never read or write through it.
- undefined behavior
- Code for which the C standard sets no requirements. The program may crash, print wrong results, or seem to work, and the result can change between builds.
- off-by-one error
- A mistake where a loop or a count goes one step too far or stops one step too early, such as a loop that never reaches the last element of an array.
- string
- In C, a sequence of characters that ends with the null character '\0', a byte with value 0. It is usually stored in a char array, and its length is not stored anywhere.
Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.
01 / One step
What does adding one to a pointer do?
Function calls showed an array argument arriving in a function as a pointer to its first element. That conversion happens in almost every expression, not only in calls. Writing pages where a value is needed gives the address of pages[0]. This automatic conversion is called decay: the array decays to a pointer to its first element. So int *p = pages; makes p point at pages[0].
Adding 1 to a pointer gives the address of the next element. The address does not grow by one byte. It grows by the size of one element: sizeof *p, the size in bytes of the thing p points to. The stepper builds C with gcc 12 on 64-bit Linux, and in that build one element is 4 bytes for an int, 8 for a double, and 1 for a char. Adding an integer to a pointer, or subtracting pointers, is called pointer arithmetic.
int pages[3] = {12, 40, 7};
/* pages decays to &pages[0] */
int *p = pages;
/* one int further on: 4 bytes here */
int *q = p + 1;
printf("%d %d\n", *p, *q); /* 12 40 */ p + 1 points to the next element, sizeof *p bytes further on.
Coming from Java: Java has no arithmetic on references: you cannot move an array reference to the next element. In C, p + 1 is a new address, computed from p and the element size.
Precisely: C11 6.3.2.1 and 6.5.6
C11 6.3.2.1p3: an expression of array type is converted to a pointer to the array’s first element, except when it is the operand of sizeof or unary &, or a string literal used to initialize an array. (N1570 also lists _Alignof there by mistake; C17 removed it, because _Alignof takes a type, not an expression.) 6.5.6p8: when an integer is added to a pointer to an array element, the result points that many elements further on. 6.5.6p7: a pointer to a single object counts as a pointer to the first element of an array of length 1, so &x + 1 may be computed. Only sizeof(char) is fixed, at 1. The sizes 4 and 8 are those of the stepper’s build; its addresses beginning 0xffff come from a 64-bit ARM (aarch64) machine, and x86-64 Linux gives the same sizes. Arithmetic on void * is a GNU extension that steps by one byte; standard C rejects it, and gcc reports it with -Wpointer-arith.
Experiment
Take one step in three arrays
In int *ip = pages;, pages is converted to the address of pages[0]. ip + 1 means one int further on, so the address grows by sizeof(int), which is 4 bytes in this build. For a double pointer each step is 8 bytes, and for a char pointer it is 1 byte.
Why it matters When you read p + i, count elements, not bytes. The type that p points to decides how far one step goes.
ip, dp, and cp point at the first element of an int, a double, and a char array. Predict how many bytes each address grows by in ip1, dp1, and cp1, and what prints. Then change int *ip1 = ip + 1; to int *ip1 = ip + 2; and predict the address and value of ip1.
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int pages[3] = {12, 40, 7};
double prices[2] = {1.5, 2.25};
char tag[4] = "elm";
int *ip = pages;
int *ip1 = ip + 1;
double *dp = prices;
double *dp1 = dp + 1;
char *cp = tag;
char *cp1 = cp + 1;
printf("%d %g %c\n", *ip1, *dp1, *cp1);
return 0;
}
What to look for
Each variable appears in main’s frame after its declaration line runs. Addresses are written in hexadecimal (after 9 come a to f) and change from run to run, so compare the last two digits of each pair. ip1 is 4 more than ip and shows → 40. dp1 is 8 more than dp and shows → 2.25. cp1 is 1 more than cp. For a char pointer the debugger also shows the text from that address up to the zero byte: cp1 shows "lm". The run prints 40 2.25 l. After the edit, ip1 is 8 more than ip, shows → 7, and the run prints 7 2.25 l.
Expected output
40 2.25 l02 / Indexing
What does pages[i] mean?
C defines indexing with pointer arithmetic. pages[i] means *(pages + i): decay pages to a pointer, move i elements, then read. A pointer can be indexed the same way, so p[2] means *(p + 2).
Subtraction works in the other direction. If p and q point into the same array, q - p is the number of elements from p to q, not the number of bytes. The result has the type ptrdiff_t, a signed integer type from <stddef.h>. printf prints it with %td.
int pages[3] = {12, 40, 7};
int *p = pages;
int *q = &pages[2];
/* the same read, written two ways */
printf("%d %d\n", pages[1], *(p + 1)); /* 40 40 */
/* 2 elements apart, 8 bytes apart */
printf("%td\n", q - p); /* 2 */ a[i] means *(a + i), and q - p counts elements, not bytes.
Precisely: C11 6.5.2.1 and 6.5.6p9
6.5.2.1p2 defines E1[E2] as (*((E1)+(E2))). Because addition does not depend on order, 2[pages] is also valid C and means pages[2]; nobody should write it. 6.5.6p9: two pointers may be subtracted only if both point into the same array object, or one past its last element, and the result is the difference of the two indexes. Subtracting pointers into different objects is undefined behavior, even when the result looks reasonable. For a distance in bytes, convert both to char * first: (char *)q - (char *)p is 8 here.
03 / The end pointer
Where may a pointer stop?
A loop that walks an array with a pointer needs a place to stop. C lets you compute the address just after the last element: for an array a of n elements, a + n. This is the one-past-the-end pointer. A program may compute it and compare other pointers with it. It must never read or write through it, because no element is stored there.
The usual loop moves p from the first element up to that address, and stops when p reaches it, before any read:
for (p = a; p < a + n; p++)
const int *p declares a pointer that may be used to read ints but not to change them. A loop that only reads should use it.
No other address outside the array may even be computed: a + n + 1 and a - 1 are undefined behavior, whether or not anything reads through them.
int queues[4] = {6, 3, 8, 2};
/* one past the end: compare, never read */
const int *end = queues + 4;
int total = 0;
for (const int *p = queues; p < end; p++) {
total += *p;
}
printf("%d\n", total); /* 19 */ Stop at the one-past-the-end pointer; compare with it, never read through it.
Coming from Rust: A Rust slice keeps a pointer and a length together, and indexing checks one against the other. In C, the pointer and the count or end pointer are separate values, and the compiler does not check that they match.
Precisely: C11 6.5.6p8 and 6.5.8p5
6.5.6p8 allows a result that points one past the last element, provided it is not the operand of a unary * that is evaluated. &queues[4] is also allowed: 6.5.3.2p3 says & and [] cancel, so nothing is read. 6.5.8p5 defines < and > for pointers into the same array object (or one past it) and for pointers to members of the same struct, where a later member compares greater; a + n compares greater than every element’s address. Comparing pointers to unrelated objects with < is undefined. An empty range needs no special case, because a + 0 is a and the loop does not run. A null pointer is not an array, so in C11 even NULL + 0 is undefined; the next standard, C2y, is changing this so that NULL + 0 gives NULL. A function that takes a pointer and a count and allows NULL with a count of 0 must test for NULL first.
Experiment
Walk to the end pointer
end is queues + 4, the one-past-the-end pointer. The loop compares p with end before every read, so *p is never read when p equals end. shortest - queues counts the elements between two pointers into the same array: 3.
Why it matters Stop a walk with p < end, and move one element at a time unless every step is sure to land at or before end. p += 2 works here only because 4 is even: with five elements it would compute queues + 6, and computing that address is already undefined behavior.
Predict what prints, and which address p holds when the loop stops. Then change p++; to p += 2; and predict which queue lengths the loop reads.
#include <stdio.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int queues[4] = {6, 3, 8, 2};
const int *end = queues + 4;
const int *shortest = queues;
const int *p = queues;
while (p < end) {
if (*p < *shortest) {
shortest = p;
}
p++;
}
printf("window %td has %d\n", shortest - queues, *shortest);
return 0;
}
What to look for
end appears with an address 16 bytes (0x10) after the first element, followed by → and a large number that changes from run to run. That number is the debugger reading the memory after the array. The program never reads it, and must not. On each pass p grows by 4 and shows the next queue length. shortest changes after line 11 runs. When the loop ends, p shows the same address as end. The run prints window 3 has 2. After the edit, p shows → 6, then → 8, then end’s address, and the run prints window 0 has 6.
Expected output
window 3 has 204 / Past the end
What happens when a loop reads past the end?
C does not check bounds. Reading or writing through a pointer outside its array is undefined behavior: the C standard sets no requirements. There is no error message. The program may crash, print a wrong number, or seem to work, and the result can change between builds.
The most common cause is an off-by-one error in a loop condition: <= where < was meant. With p <= end, the last pass of the loop reads through the one-past-the-end pointer, and the p++ after it computes an address two past the end. Both are undefined behavior: an address two past the end may not even be computed.
The experiment shows this bug in a way that is safe to run. The function is asked for 3 numbers, but the array in main holds 5, so the extra read still lands on a real element. In a real bug the extra element is outside every array, and nothing about the result can be predicted.
int crates[3] = {4, 6, 5};
int total = 0;
/* bug: the last pass reads *(crates + 3),
then p++ computes crates + 4: both undefined */
for (const int *p = crates; p <= crates + 3; p++) {
total += *p;
} A read past the end gives no error; check every loop’s end condition yourself.
Coming from Java: There is no ArrayIndexOutOfBoundsException in C. The same mistake reads whatever is stored after the array, or crashes later somewhere else.
Coming from Go: Go checks every index and panics with “index out of range”. C has no such check, so the loop condition is the only protection.
Precisely: what the standard and the tools say
C11 Annex J.2 lists as undefined “an array subscript is out of range, even if an object is apparently accessible with the given subscript (as in the lvalue expression a[1][7] given the declaration int a[4][5])”. The same holds inside a struct: in struct { int a[3]; int b; } s, s.a + 3 may be computed, but reading *(s.a + 3) is undefined even though b is stored there. The stepper’s gcc -O0 build simply loads whatever is stored after the array. An optimizing build may remove or reorder code on the assumption that the read never happens, so a correct-looking -O0 run proves nothing. -fsanitize=address (gcc and clang) places unused bytes around each stack, heap, and global object and reports a read or write that lands in them at run time. It does not see an overflow that stays inside one object, such as *(s.a + 3) reading s.b. -Warray-bounds is part of -Wall, but in gcc it finds most cases only at -O2. On a microcontroller without a memory protection unit, a read just past an array usually returns whatever is stored next; the stack usually grows down, so past a local array lie saved registers and the frame of the function that called it. A read of a reserved or unmapped address can still fault (a BusFault or HardFault on Cortex-M), and a read of a peripheral register can change the device, for example by clearing a status flag.
Experiment
Read one element too many
p <= first + n lets p reach first + 3, one past the 3 numbers the caller passed, and the loop reads through it. Then p++ computes first + 4 before the test fails. Both addresses are still inside main’s 5-number array (first + 5 would be its one-past-the-end pointer), so this run is defined and gives 56 every time. When the array really has 3 elements, the read through first + 3 and the computation of first + 4 are both undefined behavior.
Why it matters C does not stop a loop at the end of an array. Use p < end. When a total or a count is too large, or the last value looks strange, check the loop’s end condition first.
main asks sum_rows for the total of the first 3 of its 5 numbers. Predict the result. Then change <= to < in the for line and predict again.
#include <stdio.h>
#include <stddef.h>
int sum_rows(const int *first, size_t n) {
int total = 0;
for (const int *p = first; p <= first + n; p++) {
total += *p;
}
return total;
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
int picked[5] = {12, 9, 15, 20, 7};
printf("%d\n", sum_rows(picked, 3));
return 0;
}
What to look for
Step into sum_rows. first shows the address of main’s array → 12, and n shows 3. p is declared in the for line, so it appears in the frame only inside the loop, at line 7. On each pass p grows by 4. On the fourth pass p is first + 3 and shows → 20, a number the caller did not ask for. total ends at 56, and the run prints 56. Nothing warns. After the edit, the loop stops after three passes, total ends at 36, and the run prints 36.
Expected output
5605 / Strings
How does a string know where it ends?
A string in C is an array of char whose characters end with the null character, '\0', a byte with value 0. The string literal "lamp" stores 5 bytes: four letters and '\0'. No length is stored anywhere.
strlen, from <string.h>, finds the length by walking a pointer from the first character to the '\0' and returning the distance: 4 for "lamp". Its result has the type size_t, the unsigned type that sizeof also gives, printed with %zu. A char array without a '\0' is not a string, and strlen on it reads past the end.
sizeof measures an array only where the array itself is declared. In main, sizeof word is 5, because word is the array and the '\0' is part of it. A parameter written as const char text[] is a pointer, so sizeof text is the size of a pointer, 8 in the stepper’s build. gcc warns: ‘sizeof’ on array function parameter ‘text’ will return size of ‘const char *’. Pass the length as a separate argument, or, for a string, let the '\0' mark the end.
size_t bytes_in(const char text[]) {
/* text is a pointer: 8 here */
return sizeof text;
}
/* in main: */
char word[] = "lamp";
printf("%zu %zu\n", sizeof word, strlen(word)); /* 5 4 */
printf("%zu\n", bytes_in(word)); /* 8 */ A string ends at its first '\0'; sizeof measures an array only where the array is declared.
Coming from Python: A Python string knows its length, and len() returns it at once. strlen walks every character until it finds '\0', so calling it in a loop condition repeats that walk on every pass.
Precisely: C11 7.1.1, 7.24.6.3, and 6.7.6.3p7
7.1.1 defines a string as “a contiguous sequence of characters terminated by and including the first null character”, and 7.24.6.3 says strlen returns the number of characters that precede it. 6.7.6.3p7 adjusts a parameter of type “array of T” to “pointer to T”, so a size written in the brackets is not checked and sizeof sees a pointer. The warning above is gcc’s -Wsizeof-array-argument, which is on by default. One more trap: char tag[4] = "lamp"; is valid C. 6.7.9p14 stores the '\0' only if there is room, so tag holds no '\0' and strlen(tag) is undefined. C++ rejects the same line.
Experiment
Walk a string to its zero byte
The string literal "lamp" fills word with 5 bytes: four letters and '\0'. length moves p until *p is '\0' and returns p - text, the number of characters before it. sizeof word measures the whole array, because main declares word as an array. The edit puts a '\0' at index 2, so the string ends there, and the array still has 6 bytes.
Why it matters A C string’s length is the position of its first '\0', and only a walk through the characters finds it. sizeof gives an array’s size only in the code where the array itself is declared.
Predict both numbers. Then change "lamp" to "la\0mp" (\0 writes the null character inside a string literal) and predict both again.
#include <stdio.h>
#include <stddef.h>
size_t length(const char *text) {
const char *p = text;
while (*p != '\0') {
p++;
}
return p - text;
}
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
char word[] = "lamp";
printf("%zu %zu\n", sizeof word, length(word));
return 0;
}
What to look for
Step into length. text and p each show an address and the text from there up to the zero byte: "lamp". Each pass moves p one byte, and its text gets shorter: "amp", "mp", "p". At line 9, p shows "": no text comes before the zero byte. The run prints 5 4. After the edit, main’s word shows "la\000mp": the debugger writes the null character as \000. text shows only "la", the loop stops after two passes, and the run prints 6 2.
Expected output
5 406 / In production
What goes wrong with pointers and buffers in real programs?
The rules in this lesson explain four bugs that C programmers meet in real programs.
strncpy(dst, src, n) does not always write a '\0'. When src has n characters or more, strncpy copies n of them and nothing else. dst is then not a string, and the next strlen or printf with %s reads past the end of dst. snprintf(dst, n, "%s", src) writes at most n - 1 characters and then '\0'. It returns the full length of src, so a result of n or more means that the text was shortened. The experiment below shows both.
strlen does not count the '\0'. So malloc(strlen(s)) is one byte too small, and strcpy into that memory writes the '\0' one byte past the end. Allocate strlen(s) + 1 bytes.
size_t is unsigned, so i >= 0 is always true, and for (size_t i = n - 1; i >= 0; i--) never stops. When n is 0, n - 1 is the largest size_t value, 18446744073709551615 in the stepper’s build. gcc with -Wextra warns about this loop. The pointer version, for (p = a + n - 1; p >= a; p--), ends by computing a - 1, which is undefined behavior. The snippet shows two loops that test before each step down.
realloc may move a buffer to a new address and free the old memory. Every pointer into the old buffer, such as an end pointer, then points at freed memory. Keep positions as numbers (p - buf), and build the pointers again from the address realloc returns.
/* i takes n - 1 down to 0: i-- > 0
compares i with 0, then subtracts 1 */
for (size_t i = n; i-- > 0;) {
total += a[i];
}
/* p takes a + n - 1 down to a, never a - 1 */
for (const int *p = a + n; p > a;) {
p--;
total += *p;
} Count the '\0' when you size or fill a buffer, test before you step a pointer down, and compute pointers again after realloc.
Precisely: C11 7.24.2.4, 7.21.6.5, 6.2.5p9, and 7.22.3.5
7.24.2.4p3: strncpy appends null characters only when the source is a string shorter than n, and then it fills every remaining byte of the n with them, which costs time when n is large. 7.21.6.5p2–3: snprintf discards output beyond the n − 1st character, writes a null character at the end when n is not 0, and returns the number of characters that would have been written for a large enough n, or a negative value on an encoding error; the text is complete only if the result is nonnegative and less than n. 6.2.5p9: unsigned arithmetic is reduced modulo the largest value plus one, so 0 - 1 on a size_t is defined and gives SIZE_MAX. 7.22.3.5p2: realloc deallocates the old object. 6.2.4p2 then makes every pointer to it indeterminate, and Annex J.2 lists using such a value as undefined, so even comparing an old pointer with the new one is undefined, not only reading through it. The warning for the size_t loop is -Wtype-limits, part of -Wextra: “comparison of unsigned expression in ‘>= 0’ is always true”; the stepper builds with -std=c11 -Wall -Wextra -O0. gcc’s -Wstringop-truncation and -Wstringop-overflow look for the first two bugs, but the -O0 build reported neither for them. Annex K’s strncpy_s is optional, and glibc does not provide it.
Experiment
Copy a word that fills the buffer
strncpy copies at most sizeof label characters, 4. "lamp" has 4, so strncpy writes l, a, m, and p, and no '\0'. The zeros from the initializer are all replaced. memchr searches exactly the 4 bytes it is given, so this check never reads past the array; strlen(label) or printf with %s would. snprintf writes at most 3 characters and then '\0'.
Why it matters A buffer that starts as zeros does not prevent this. When a copy is limited by the buffer size, it must also leave room for '\0': use snprintf, or copy one character fewer and set the last byte to '\0' yourself.
Predict what label holds after the copy, and what prints. Then change strncpy(label, "lamp", sizeof label); to snprintf(label, sizeof label, "%s", "lamp"); and predict again.
#include <stdio.h>
#include <string.h>
int main(void) {
setvbuf(stdout, NULL, _IOLBF, 0); // line by line, as in a terminal
char label[4] = "";
strncpy(label, "lamp", sizeof label);
const char *zero = memchr(label, '\0', sizeof label);
if (zero == NULL) {
printf("no '\\0' in %zu bytes\n", sizeof label);
} else {
printf("\"%s\", '\\0' at %td\n", label, zero - label);
}
return 0;
}
What to look for
Before line 7 runs, label shows "\000\000\000": every byte is 0. After line 7, label shows "lamp". The debugger shows all 4 bytes of the array, and all 4 are letters. After line 8, zero shows 0x0, the null pointer: memchr found no '\0' in the 4 bytes. The run prints no '\0' in 4 bytes. After the edit, label shows "lam", zero shows an address, then "", and the run prints "lam", '\0' at 3.
Expected output
no '\0' in 4 bytes07 / Try it
Can you say where each pointer points?
Work out where each pointer points before you read the printf line. In the stepper’s build, a short is 2 bytes.
#include <stdio.h>
int main(void) {
short stamps[4] = {5, 8, 13, 21};
short *a = stamps + 1;
short *b = stamps + 4;
printf("%d %td %d\n", a[1], b - a, *(b - 1));
return 0;
}
Precisely: indexes or pointers
for (size_t i = 0; i < n; i++) with a[i] and for (p = a; p < a + n; p++) with *p express the same walk, and an optimizing compiler usually produces the same machine code for both. Choose the one that reads better. Pointers fit functions that return a position, such as strchr and memchr, which return a pointer to what they found or NULL; the convention used by C++ iterators returns the end pointer instead.
What does it print? How many bytes apart are a and b? Which of *b, b - 4, and b + 1 is undefined behavior?
Reveal the reasoning
13 3 21. a is stamps + 1, so a[1] is *(a + 1), the element at stamps + 2: 13. b is stamps + 4, the one-past-the-end pointer. b - a is 3 elements, which is 6 bytes with a 2-byte short. *(b - 1) is the last element, 21. b - 4 is stamps itself, which is fine. *b reads through the one-past-the-end pointer, and b + 1 computes an address two past the end. Both are undefined behavior, even though nothing ever reads b + 1.
Practice tickets
- C-101 · Count every car that entered → Free
- C-102 · Count the short rehearsals → Sign in · Pro
- C-103 · Long caller names overwrite the next field → Sign in · Pro