Pointers hold addresses, and addresses are numbers - so it is reasonable to expect arithmetic to work on them. It does, but not quite the way plain integer arithmetic does. C scales every pointer operation by the size of the thing being pointed at, which turns "add one" into the far more useful "move to the next element".
That single design decision is why walking an array with a pointer is as natural in C as walking it with an index.
Adding 1 Moves by One Object, Not One Byte
Take a pointer, add 1, and print both addresses:
The int * jumps 4 bytes, the double * jumps 8, the char * jumps 1 - each one lands exactly where the next object of its type would start. The rule is simple:
ptr + n == (address in ptr) + n * sizeof(*ptr)
This is exactly why a pointer has a type at all. A bare address with no type attached would have no idea how far "next" is.
Walking an Array by Pointer
Now the payoff. An array's elements sit in consecutive memory, so stepping a pointer visits them in order:
Both loops print the same five numbers. In the second, scores decays to a pointer to its first element, p++ advances by one int, and *p reads the element sitting there. Laid out in memory:
scores[0] scores[1] scores[2] scores[3] scores[4]
+---------+---------+---------+---------+---------+
| 88 | 92 | 75 | 60 | 100 |
+---------+---------+---------+---------+---------+
^ ^ ^
p p+1 scores+5
(start) (4 bytes later) (one past the end)
The relationship between the two forms is exact and worth memorizing: scores[i] is defined as *(scores + i). More on that in pointers and arrays.
Increment, Decrement, and the *p++ Trap
++ and -- work on pointers with the same scaling.
The expression that confuses everyone is *p++. Postfix ++ binds tighter than the *, so it means "take the current p, advance p, then dereference the old value":
Three expressions, three different meanings:
| Expression | Pointer after | Value produced |
|---|---|---|
*p++ | advanced | the element before the move |
*++p | advanced | the element after the move |
(*p)++ | unchanged | the old value, and the element is incremented |
*p++ is idiomatic C - it is how string and buffer copies are written - but write the parentheses when you mean anything else.
Subtracting Two Pointers
Subtracting one pointer from another gives the number of elements between them, not the number of bytes:
The result type is ptrdiff_t, a signed integer type from <stddef.h>, printed with %td. Subtracting pointers into two different arrays produces an undefined result - the standard only defines the operation within one object.
You can also subtract an integer from a pointer (p - 3 steps back three elements), but you cannot add two pointers together. "The sum of two addresses" names nothing.
Comparing Pointers
Relational operators work on pointers into the same array, and they mean exactly what the memory layout suggests: p < q is true when p addresses an earlier element.
front < back is the loop's whole termination story, and it is correct because both pointers stay inside one array. == and != are also the natural way to test a loop against its end marker, which brings us to the rule that makes all of this legal.
The One-Past-the-End Rule
C explicitly permits you to form a pointer to the position one past the last element of an array. You may compute it, store it, and compare against it. You may not dereference it.
int arr[4];
arr[0] arr[1] arr[2] arr[3] (no element)
+------+ +------+ +------+ +------+ +- - - -+
| | | | | | | | | |
+------+ +------+ +------+ +------+ +- - - -+
^ ^
arr arr + 4
legal to use legal to FORM and COMPARE
never legal to dereference
That rule is what makes the standard loop shape valid:
for (int *p = arr; p != arr + n; p++) {
/* ... */
}
On the last iteration p becomes arr + n, the comparison fails, and the loop exits - without ever reading that position.
Two things remain undefined even so. Forming a pointer two past the end, or one before the start, is undefined behavior even if you never dereference it:
int *bad1 = arr + n + 1; // undefined - too far past
int *bad2 = arr - 1; // undefined - before the beginning
That second one matters in practice: a backward loop written as for (int *p = arr + n - 1; p >= arr; p--) computes arr - 1 on its final decrement. It works on every common compiler and is still formally undefined. The clean backward loop avoids it:
void Pointers and the Arithmetic You Cannot Do
A void * holds an address with no type attached, which is how malloc and qsort stay generic. Because there is no element size, pointer arithmetic on a void * is not allowed by the standard - cast to a concrete type (or to char * for byte-level work) first.
void *v = buffer;
// v + 1; // not standard C - no size to scale by
char *b = v;
b + 1; // fine - one byte forward
GCC and Clang accept void * arithmetic as an extension that treats it like char *, so code relying on it compiles until the day it is built somewhere stricter. Compile with -std=c17 -pedantic if you want to be told.
What This Buys You
Pointer arithmetic is not an optimization trick you reach for occasionally - it is the mechanism underneath array indexing, string handling, and every buffer walk in the standard library. strlen is a pointer advanced to the terminating '\0' and subtracted from the start. memcpy is two pointers stepping in lockstep.
Next, see how the array/pointer equivalence actually works, including the sizeof trap that catches everyone the first time they pass an array to a function: pointers and arrays.
Frequently Asked Questions
What does ptr + 1 do in C?
It moves the pointer forward by one object, not one byte. For an int * on a typical machine that is 4 bytes; for a double * it is 8. The compiler multiplies by sizeof(*ptr) for you, which is why the pointer's type matters.
Can you subtract two pointers in C?
Yes, when both point into the same array. end - start gives the number of elements between them, with type ptrdiff_t (print it with %td). Subtracting pointers into unrelated objects is undefined behavior.
Is it legal to point one past the end of an array?
Yes - forming a pointer to one element past the last is explicitly allowed so that loops like for (int *p = arr; p != arr + n; p++) work. You may compute and compare that pointer, but dereferencing it is undefined behavior.
Why is *p++ not the same as (*p)++?
*p++ increments the pointer and dereferences the old value - it reads the current element, then advances. (*p)++ leaves the pointer alone and increments the value it points at. Postfix ++ binds tighter than *, so the parentheses are what change the meaning.