C / POINTERS
Pointer arithmetic and walking past array ends
Step a pointer through an array in element-sized units, use the one-past-the-end pointer as a loop bound, and spot off-by-one walks that are undefined.
What you will learn
- Read p + n as 'n elements along': the compiler scales the step by sizeof(*p).
- Use a + n as a loop sentinel: legal to form and compare, never legal to read.
- Turn two pointers into an element count with subtraction, which yields ptrdiff_t.
- Walk backwards from the end pointer: decrement before each read so a - 1 never forms.
Understanding Pointer arithmetic and walking past array ends
When you add 1 to an int *, the address does not go up by 1; it goes up by sizeof(int), because pointer arithmetic is counted in whole objects of the pointed-to type. That is why a[i] is defined as exactly *(a + i): the subscript is an element index and the compiler multiplies it by the element size for you. The model to keep in mind is a typed cursor resting on a row of equal-sized boxes, moving in boxes rather than in bytes, unless the type it points to happens to be one byte wide.
Two pointers into the same array can be subtracted, and the result is the number of elements between them, with type ptrdiff_t rather than a byte count. C also guarantees that you may compute the address one past the last element, a + 5 for int a[5], and compare it against any pointer into that array. That guarantee exists so a loop can express the half-open range from a up to a + n and stop on p != end; the address is required to be computable and comparable, but nothing is promised to live there.
Everything outside that window is undefined behaviour, and undefined does not mean 'reads a junk value'. Writing through a + n usually lands on whichever variable the compiler placed next, so the damage surfaces somewhere unrelated; and forming a - 1 for a countdown loop lets an optimiser assume p >= a can never fail, which can turn the loop into an infinite one. The working rule is that every pointer value you create, dereferenced or not, must be inside the array or exactly one element past its end.
Nothing traps at the boundary because the bytes after a small array are normally still mapped memory, which is why an off-by-one walk passes a quick test run and fails months later after unrelated code moves a variable.
<stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *first = a; /* points at a[0] */
int *end = a + 5; /* one past a[4]: legal to form, not to read */
for (int *p = first; p != end; ++p)
printf("element %ld: byte offset %ld, value %d\n",
(long)(p - first),
(long)((char *)p - (char *)first),
*p);
printf("end - first = %ld elements\n", (long)(end - first));
printf("byte span = %ld\n", (long)((char *)end - (char *)first));
printf("sizeof(int) = %zu\n", sizeof(int));
return 0;
}
Pointer arithmetic counts elements of the pointed-to type, and the only addresses you may legally compute run from the first element through one past the last.
Worked examples
The step size comes from the type
Adding 1 to an int * and to a char * that hold the same address lands in different places.
<stdio.h>
int main(void)
{
int a[4] = {1, 2, 3, 4};
int *pi = a;
char *pc = (char *)a;
printf("pi + 1 lands %ld bytes along\n", (long)((char *)(pi + 1) - pc));
printf("pc + 1 lands %ld bytes along\n", (long)((pc + 1) - pc));
printf("*(pi + 2) = %d and a[2] = %d and 2[a] = %d\n", *(pi + 2), a[2], 2[a]);
return 0;
}
Example explained
Line 1pi + 1 moves 4 bytes because the 1 is scaled by sizeof(int), even though the expression is written identically on both lines.
Line 2pc + 1 moves a single byte, so it lands in the middle of a[0]; sizeof(char) is 1 by definition.
Line 3The cast to char * before subtracting is what converts an element distance into a byte distance; subtracting the two int * values would print 1.
Line 4*(pi + 2), a[2] and 2[a] are the same operation because a[i] is defined as *(a + i) and addition commutes.
Walking forwards and backwards without leaving the array
Uses a one-past-the-end pointer as both a loop sentinel and a length, and reverses safely by decrementing before reading.
<stdio.h>
int main(void)
{
int a[4] = {2, 4, 6, 8};
int *end = a + 4;
int sum = 0;
for (int *p = a; p != end; ++p)
sum += *p;
printf("sum = %d over %ld elements\n", sum, (long)(end - a));
printf("reverse:");
for (int *p = end; p != a; ) {
--p;
printf(" %d", *p);
}
putchar('\n');
return 0;
}
Example explained
Line 1int *end = a + 4; builds the one-past-the-end pointer; it is only compared and decremented, never dereferenced.
Line 2p != end stops the loop right after a[3], because p can only ever hold a, a+1, a+2, a+3 or end.
Line 3end - a evaluates to 4, so the same expression that bounds the loop also gives the element count.
Line 4The reverse loop decrements first and reads second, so the lowest pointer value it ever holds is a itself and a - 1 is never formed.
Important notes
Subtraction and relative comparison are only defined for pointers into the same array or its one-past-the-end position; comparing pointers into two unrelated arrays compiles but the answer means nothing.
A single non-array object counts as an array of one for this rule, so &x + 1 is a valid one-past-the-end pointer while *(&x + 1) is not.
Common mistakes
Writing the loop condition as p <= a + n, which reads or writes the one-past-the-end slot and quietly corrupts whichever variable the compiler placed next.
Counting down with for (p = a + n - 1; p >= a; --p), which forms a - 1 on the last decrement; that is undefined, and an optimiser may treat p >= a as always true and never exit.
Advancing with p += sizeof(int) to move one int, which actually skips four ints and jumps past the end of a small array.
Try it yourself
Change, predict, then run
Fill int a[6] with the squares 1, 4, 9, 16, 25, 36 using only a pointer and the condition p != a + 6, then print the values in reverse by starting at a + 6 and decrementing before each read. Also print the element count obtained from (a + 6) - a.
Open the C workspaceCheck your understanding
Given int a[5];, which line is guaranteed by C to be well defined?
- int *p = a + 5; if (p > a) puts("ok");
- int *p = a + 5; printf("%d", *p);
- int *p = a - 1; if (p < a) puts("ok");
- int *p = a + 6; if (p > a) puts("ok");
Show answer
The one-past-the-end address a + 5 may be computed and compared with pointers into the array, which is exactly what makes p != end loops legal. Option 3 looks like the mirror image and is the tempting answer, but there is no 'one before the start' guarantee: forming a - 1 is undefined even if you never dereference it. Option 2 dereferences a slot that holds no object, and option 4 computes an address two elements past the last.