C Part I · Module 7 · Pointers
Pointer Arithmetic
Two chapters have now ended by pointing here. Chapter 7.6 said that indexing is defined in terms of pointers and supplied only the first half of the definition. Chapter 7.7's final loop indexed a pointer as though that were unremarkable, which it is, but not for the reason it appears. And Chapter 7.2 deferred one small expression — the difference between (*p)++ and *p++ — because explaining it needed a rule that did not exist yet. This chapter supplies that rule. It has two halves, and the second is the one that separates someone who can use pointers from someone who can use them safely: knowing what p + 1 means is straightforward arithmetic, and knowing which values of that arithmetic C actually permits you to compute is what keeps the result meaningful.
Intermediate18 min readCPart IFundamentalsPointers
Part I · Module 7 · Chapter 7.8 · Pointer Arithmetic
1. Adding One Does Not Add One
#include <stdio.h>
#include <stddef.h>
int main(void)
{
char c[4];
int i[4];
double d[4];
char *pc = c;
int *pi = i;
double *pd = d;
printf("char : difference %td bytes\n", (char *)(pc + 1) - (char *)pc);
printf("int : difference %td bytes\n", (char *)(pi + 1) - (char *)pi);
printf("double : difference %td bytes\n", (char *)(pd + 1) - (char *)pd);
return 0;
}Output:
char : difference 1 bytes
int : difference 4 bytes
double : difference 8 bytesThe same expression, p + 1, on three pointer types, moving three different distances.
Pointer arithmetic is measured in elements, not bytes.
p + nis the addressnelements further on, which isn * sizeof *pbytes.
This is the second job Chapter 7.3 said a pointer type does and declined to explain. The type is what supplies sizeof *p, and without it advance to the next one would have no meaning at all. An address alone could not do this.
2. Where Indexing Comes From
Chapter 7.6 said indexing is defined in terms of pointers. Here is the definition, complete:
a[i]means exactly*(a + i).
Not behaves like. Means. The subscript notation is shorthand for the arithmetic, and the compiler rewrites it as such.
#include <stdio.h>
int main(void)
{
int a[5] = {10, 20, 30, 40, 50};
int *p = a;
printf("a[2] %d\n", a[2]);
printf("*(a + 2) %d\n", *(a + 2));
printf("p[2] %d\n", p[2]);
printf("*(p + 2) %d\n", *(p + 2));
printf("2[a] %d\n", 2[a]);
return 0;
}Output:
a[2] 30
*(a + 2) 30
p[2] 30
*(p + 2) 30
2[a] 30The first four are unsurprising once you have Chapter 7.6's conversion: a becomes a pointer, + 2 moves two elements, * reads.
The fifth is the proof. 2[a] means *(2 + a), and addition is commutative, so it is the same element. If a[i] were a special operation that arrays support, 2[a] could not possibly compile. It does, and it works, because there is no array operation involved — only pointer arithmetic with a nicer notation.
Do not write 2[a]. Know why it works, because knowing why is the difference between having memorised indexing and having understood it. It also explains something Chapter 7.7 passed over without comment: indexing a pointer parameter works because indexing was never about arrays in the first place.
3. Which Arithmetic Is Defined
This is the half that matters and the half most summaries omit. C does not permit arbitrary arithmetic on pointers — it permits arithmetic that stays inside one array, plus one carefully chosen extra position.
Pointer arithmetic is defined only when the pointer and the result both lie within the same array object, or one element past its end. Anything else is undefined behaviour.
Three consequences, in increasing order of how often they are got wrong.
Inside the array is fine. For int a[5], the pointers a + 0 through a + 4 all point at elements, and all may be dereferenced.
One past the end may be formed, and not dereferenced. a + 5 is a legal value. You may compute it, compare it, and subtract from it. Dereferencing it is Chapter 6.4's undefined behaviour — the figure marks it, and §4 explains why C bothers to permit it at all.
Before the beginning is not permitted at all. a - 1 is undefined as an expression, not merely undefined to dereference. There is no symmetrical "one before the start" position.
int a[5];
int *p = a;
int *ok = a + 5; /* defined: one past the end */
int *bad = a - 1; /* undefined, even though nothing is dereferenced */
int *worse = a + 10; /* undefined */Neither of the last two is executed anywhere in this chapter. They are classified, as Chapter 6.4 classified out-of-range accesses, and for the same reason.
4. Why One Past the End Is Allowed
The asymmetry looks arbitrary until you look at a loop.
for (int *q = a; q != a + 5; q++) {
printf("%d ", *q);
}That loop needs a + 5 to exist as a value, because it is the condition. Chapter 6.3 §4 observed exactly this in index form: the final test produces an index one past the end and then declines to use it. C makes the pointer version legal for precisely that reason — so that half-open ranges, which Chapter 4.7 argued for and Chapter 6.3 built on, can be expressed with pointers.
There is no corresponding idiom that needs a position before the first element, so C does not provide one. The practical consequence is a real trap:
for (int *q = a + 4; q >= a; q--) { ... } /* the last decrement is undefined */Walking backwards by decrementing until the pointer goes below the start computes a - 1 on the final iteration. This is the pointer version of Chapter 4.7's unsigned countdown, and it has the same fix — stay one above and subtract inside the body:
for (int *q = a + 5; q != a; q--) {
printf("%d ", *(q - 1));
}5. Subtraction and Comparison
Two pointers into the same array can be subtracted, and the result is a count of elements:
int a[5] = {10, 20, 30, 40, 50};
int *p = a;
int *q = &a[4];
printf("q - p %td elements\n", q - p);
printf("p < q %d\n", p < q);
printf("a + 5 - p %td\n", (a + 5) - p);q - p 4 elements
p < q 1
a + 5 - p 5Three details are worth having.
The result is in elements, not bytes — the same scaling as addition, in reverse.
Its type is ptrdiff_t, a signed type from <stddef.h>, printed with %td. It is signed because the difference can be negative, which is the counterpart of size_t being unsigned because a size cannot be.
Comparison with <, >, <=, >= is defined only within the same array (plus one past the end), exactly like subtraction. Comparing pointers into two unrelated objects is not something C defines an answer for. Equality comparison with == and != is broader and works on any two pointers of compatible type, which is why q != a + 5 is the safer loop condition.
6. *p++, Finally
Chapter 7.2 §7 showed (*p)++ and deferred its parenthesis-free cousin. Now it can be explained.
Postfix ++ binds tighter than unary * — Chapter 3.10's table. So:
*p++ means *(p++) dereference, and move the pointer on
(*p)++ means increment the OBJECT p points at#include <stdio.h>
int main(void)
{
int a[4] = {1, 2, 3, 4};
int *p = a;
printf("*p++ sequence: ");
for (int k = 0; k < 4; k++) {
printf("%d ", *p++);
}
printf("\n");
p = a;
(*p)++;
printf("after (*p)++: ");
for (int k = 0; k < 4; k++) {
printf("%d ", a[k]);
}
printf("\n");
return 0;
}Output:
*p++ sequence: 1 2 3 4
after (*p)++: 2 2 3 4*p++ walked the array and left its contents alone. (*p)++ left the pointer alone and changed element 0 from 1 to 2.
Chapter 3.7's rule explains the first: postfix ++ yields the old value and then moves the pointer, so *p++ dereferences the position the pointer had before the increment. That is what makes it usable as a walking idiom — each evaluation reads one element and advances.
7. What You Should Be Able to Say Now
p + nmovesnelements, which isn * sizeof *pbytes — measured at 1, 4 and 8 for three types.- This is the second job of a pointer type, promised in Chapter 7.3.
a[i]is defined as*(a + i)— and2[a]compiling is the proof.- That is why indexing works on a pointer parameter, as Chapter 7.7's loop did.
- Arithmetic is defined within one array, or one element past its end — everything else is undefined.
- One past the end may be formed but not dereferenced; it exists so half-open loops can be written.
- There is no "one before the beginning" —
a - 1is undefined as an expression. - Backward walking must stay one above:
for (q = a + n; q != a; q--)using*(q - 1). - Subtracting two pointers gives a count of elements, of type
ptrdiff_t, printed with%td. *p++is*(p++)— read the current element, then advance.(*p)++changes the element.
8. Exercises
Exercise 1 — compute the distances. For each, give how many bytes p + 1 is beyond p, assuming char 1, short 2, int 4, double 8.
char *p short *p int *p double *pExercise 2 — rewrite without brackets. Rewrite each using only * and +, given int a[6]; int *p = a;.
a[0] a[3] p[1] p[5]Then say what 3[a] is and why it compiles.
Exercise 3 — classify the arithmetic. For int a[5]; and int *p = a;, say whether each expression is defined, and whether the result may be dereferenced.
p + 2 p + 5 p + 6 p - 1 (p + 5) - p *(p + 5)Exercise 4 — predict the output. Do not compile yet.
int a[4] = {5, 6, 7, 8};
int *p = a;
printf("%d ", *p++);
printf("%d ", *p);
printf("%d ", (*p)++);
printf("%d ", *p);
printf("%d\n", a[1]);Exercise 5 — find the undefined step. This is meant to print the array backwards. Say which iteration computes an undefined value, why the loop usually appears to work anyway, and give the repair from §4.
for (int *q = a + 3; q >= a; q--) {
printf("%d ", *q);
}Exercise 6 — use the difference. Given int *first and int *last pointing into the same array, with last at the final element, write the expression that gives the number of elements from first through last inclusive, and say what type it has.
Exercise 7 — convert the loop. Rewrite Chapter 6.3's accumulate traversal in the pointer-walking form, then say which of the two you would ship and why, using §6's guidance.
Exercise 8 — explain the asymmetry. C allows one position past the end and none before the beginning. Explain the loop idiom that justifies the first, and say why no idiom justifies the second. Then relate it to Chapter 6.3 §4's observation about the final loop test.
9. Summary
- Pointer arithmetic counts elements, not bytes —
p + nmovesn * sizeof *pbytes. - That is the second job of the pointer type, promised in Chapter 7.3.
a[i]is defined as*(a + i), which is why2[a]compiles and why pointers can be indexed.- Arithmetic is defined only within one array, or one past the end.
- One past the end may be formed, not dereferenced — it exists for half-open loops.
a - 1is undefined as an expression; walk backwards froma + nusing*(q - 1).- Pointer subtraction gives a count, of type
ptrdiff_t, printed with%td. *p++reads then advances;(*p)++changes the element.- Prefer indexing unless the end is marked rather than counted.
Every pointer so far has pointed at an ordinary value — an int, a char, a double. Nothing in the definition requires that. A pointer identifies an object, and a pointer is itself an object, so there is no reason a pointer cannot identify another pointer, and no reason an array cannot have pointers as its elements. Both of those turn out to be ordinary rather than exotic, and the first of them is the structure behind a table of entries that a program selects among — which the next chapter builds, using nothing but the rules already established.
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.
