This page is the "hands dirty" companion to Pointer arithmetic — adding integers to pointers (index 5.1.9) . We take the single rule from the parent —
— and drive it through every situation it can produce: positive steps, negative steps, the zero step, one-past-the-end, tiny char strides, fat double strides, casting to change the stride, a word problem, and an exam trap. Nothing here is new rule ; everything is that one rule reused, exactly as the parent's 80/20 sentence promised.
Before we start, three plain-word reminders so no symbol is unearned:
Definition The three words we lean on
Address = a whole number naming one byte of memory. We write addresses in hex like 0x200 (the 0x just means "read the following digits in base 16"). 0x200 in normal counting is 2 × 256 = 512 .
sizeof(T) = how many bytes one value of type T occupies. See sizeof operator . We use sizeof(char)=1, sizeof(int)=4, sizeof(double)=8 throughout (the common case).
Stride = the jump distance of one +1 step = sizeof ( ∗ p ) bytes. This is the only number that changes between examples.
Every pointer-arithmetic question is one (or a combo) of these cells. The worked examples below are each tagged with the cell they hit, and together they touch every row.
#
Case class
What is special
Example
A
Positive step, int*
ordinary forward move
Ex 1
B
Negative step
subtract lands you behind
Ex 2
C
Zero step
p + 0 degenerate case
Ex 2
D
char* stride = 1
smallest possible stride
Ex 3
E
double* stride = 8
largest common stride
Ex 3
F
Cast changes stride
same address, new lens
Ex 4
G
One-past-the-end limit
legal to form, illegal to deref
Ex 5
H
Walking with p++
loop / accumulate
Ex 6
I
i[arr] commutativity twist
exam gotcha
Ex 7
J
Precedence *p+1 vs *(p+1)
operator-binding trap
Ex 8
K
Word problem (real memory layout)
translate English → arithmetic
Ex 9
Every example below is a walk along the same idea: a row of equal-width boxes in memory. Keep this figure in your head.
Look at the picture: each box is sizeof(T) bytes wide, boxes sit back-to-back (arrays are contiguous — see Arrays decay to pointers ), and the pointer's +1 always lands you cleanly on the next box's left edge , never halfway inside a box.
Worked example Forward move
int a [ 4 ] = { 10 , 20 , 30 , 40 };
int * p = a; // p holds &a[0], say 0x200
// What address is (p + 3), and what value does *(p + 3) read?
Forecast: Guess the address and the value before reading on. (Hint: how many bytes is 3 steps?)
Step 1 — Identify the stride. Why this step? Every calculation begins with "how wide is one box?" Here *p is an int, so stride = sizeof(int) = 4 bytes.
Step 2 — Scale the step. Why this step? The rule multiplies the step count by the stride. 3 × 4 = 12 bytes.
Step 3 — Add to the base address. Why this step? p + n is base address plus the scaled offset. 0 x 200 + 12 = 0 x 20 C (because 0 x 200 = 512 , 512 + 12 = 524 = 0 x 20 C ).
Step 4 — Dereference. Why this step? *(p+3) reads the box at that address, which is element index 3 = a[3] = 40 .
Verify: Cross-check with the identity *(p+3) == a[3] from the parent. a[3] is literally 40. ✓ And 0 x 20 C − 0 x 200 = 12 = 3 × 4 . ✓
Worked example Going backward, and going nowhere
int a [ 5 ] = { 2 , 4 , 6 , 8 , 10 };
int * p = & a [ 3 ]; // p points at the value 8, at address 0x200
// (1) *(p - 2) = ? (2) *(p + 0) = ? (3) address of (p - 3) if 0x200 is &a[3]?
Forecast: Which of the three moves you, and which leaves you put?
Step 1 — Negative works exactly like positive, just signed. Why this step? The scaling rule n × sizeof(T) does not care about the sign of n. With n = -2: offset = − 2 × 4 = − 8 bytes. From a[3] back two boxes lands on a[1]. *(p-2) = a[1] = 4 .
Step 2 — The zero step is the degenerate case. Why this step? We must cover n = 0: offset = 0 × 4 = 0 . So p + 0 == p, pointing at the same box. *(p+0) = a[3] = 8 . (Nothing moves — the identity element.)
Step 3 — Push past the front edge. Why this step? Cover the limiting move toward index 0. p - 3 from a[3] gives a[0] at address 0 x 200 − 3 × 4 = 0 x 200 − 12 = 0 x 1 F 4 . That is exactly &a[0] — still inside the array, so still fine.
Verify: Distances back-check via Pointer subtraction and ptrdiff_t : (p) - (p-2) = 2 and (p) - (p+0) = 0. Element values: a[1]=4, a[3]=8. ✓ Address: 0 x 1 F 4 = 500 = 512 − 12 . ✓
[!mistake] p - 4 from a[3] would land before a[0].
That address is UB even to compute (see Undefined behavior and bounds ). Only offsets that keep you within a[0] .. a[5] (one-past-end) are defined.
char* (stride 1) beside double* (stride 8)
char * c = ( char* ) 0x 1000 ; // c + 5 = ?
double * d = ( double* ) 0x 1000 ; // d + 5 = ?
Same base address, same +5, but different lenses.
Forecast: How far apart are the two results?
Step 1 — Read each stride off the type. Why this step? The type is the only thing that sets the box width. sizeof(char)=1, sizeof(double)=8.
Step 2 — Scale each. Why this step? Apply n × sizeof(T) to each. c: 5 × 1 = 5 bytes → 0 x 1000 + 5 = 0 x 1005 . d: 5 × 8 = 40 bytes → 0 x 1000 + 40 = 0 x 1028 .
Step 3 — Compare. Why this step? To feel how much the type matters, subtract: the two +5 results differ by 0 x 1028 − 0 x 1005 = 0 x 23 = 35 bytes, all from choosing a different type.
Verify: 0 x 1005 = 4096 + 5 = 4101 . 0 x 1028 = 4096 + 40 = 4136 . 4136 − 4101 = 35 . ✓ And 35 = 5 × 8 − 5 × 1 = 5 × 7 . ✓
Look at the figure: the thin char boxes make +5 a short hop; the fat double boxes make the identical +5 a long stride. The pointer type is the box width.
Worked example Same bytes, new ruler
int a [ 2 ] = { 0 , 0 };
int * p = a; // &a[0] = 0x200
// Compare (p + 1) with ((char*)p + 1)
Forecast: One of these lands on a[1]; the other lands inside a[0]. Which is which?
Step 1 — p + 1 uses the int stride. Why this step? p is still an int*, so stride 4. 0 x 200 + 1 × 4 = 0 x 204 = &a[1]. Clean element boundary.
Step 2 — Cast first, then add. Why this step? (char*)p reinterprets the same address 0x200 but with stride 1. Now +1 moves just 1 byte: 0 x 200 + 1 = 0 x 201 .
Step 3 — Interpret 0x201. Why this step? 0x201 is byte 1 of the four bytes of a[0] — you are standing inside an integer. Reading an int there is almost always a bug (see Dereference operator and precedence and Undefined behavior and bounds ).
Verify: 0 x 204 − 0 x 200 = 4 = sizeof(int) ✓ (element hop). 0 x 201 − 0 x 200 = 1 = sizeof(char) ✓ (byte hop). Difference between the two results = 4 − 1 = 3 bytes. ✓
Worked example The furthest legal address
int a [ 3 ] = { 7 , 8 , 9 }; // &a[0] = 0x100
int * end = a + 3 ; // one-past-the-end
// What address is `end`? Is *end allowed? Is a+4 allowed to even form?
Forecast: Guess the three answers: address of end, legality of *end, legality of a+4.
Step 1 — Compute a + 3. Why this step? Standard scaling: 0 x 100 + 3 × 4 = 0 x 10 C . This names the byte just after a[2].
Step 2 — Is it legal to hold ? Why this step? The C standard makes exactly ONE special promise: forming the pointer one element past the last is defined, so loops can test p != end. So end = 0x10C is a valid address to store and compare.
Step 3 — Is it legal to dereference ? Why this step? *end reads a box that is not part of the array → undefined behavior . Never dereference the end pointer.
Step 4 — What about a + 4? Why this step? Cover the limit beyond the limit: a + 4 is two past the end. Forming it is already UB — not just dereferencing. The one-step-past promise does not extend to two.
Verify: end - a = 3 (a valid ptrdiff_t, see Pointer subtraction and ptrdiff_t ). 0 x 10 C = 256 + 12 = 268 . ✓ Legal range to form : indices 0..3; legal range to deref : 0..2.
Worked example Accumulate with a moving pointer
int a [ 5 ] = { 2 , 4 , 6 , 8 , 10 };
int * p = a;
int sum = 0 ;
for ( int k = 0 ; k < 5 ; k ++ ) { sum += * p; p ++ ; }
// sum = ? and where does p point after the loop?
Forecast: Guess the sum, and guess where p ends up.
Step 1 — p++ is p = p + 1. Why this step? Increment on a pointer is the same scaling rule with n = 1: advance one int = 4 bytes = one element forward.
Step 2 — Trace the five reads. Why this step? We must show the whole trajectory, not assume it. Reads: a[0]=2, a[1]=4, a[2]=6, a[3]=8, a[4]=10.
Step 3 — Add them. Why this step? 2 + 4 + 6 + 8 + 10 = 30 . So sum == 30.
Step 4 — Where is p? Why this step? After 5 increments from &a[0], p == a + 5 = one-past-the-end (Cell G) — legal to hold, never dereference.
Verify: Sum of the arithmetic list = 30 (checked in VERIFY). Final p - a = 5. ✓
Worked example Legal-but-weird indexing
int a [ 4 ] = { 100 , 200 , 300 , 400 };
int r = 2 [a]; // yes, this compiles. What is r?
Forecast: Is this even legal C? If so, what value?
Step 1 — Unfold the bracket. Why this step? By the parent's identity, X[Y] is defined as *(X + Y). So 2[a] means *(2 + a).
Step 2 — Use commutativity of +. Why this step? Pointer + integer addition is commutative: 2 + a == a + 2. So *(2 + a) == *(a + 2).
Step 3 — Evaluate normally. Why this step? *(a + 2) reads element 2 = a[2] = 300 . So r == 300.
Verify: a[2] == 300 and 2[a] == a[2] by definition. ✓ (This is the exam trick: 2[a] is not a typo, it's the identity in disguise.)
Worked example One character changes everything
int a [ 3 ] = { 50 , 60 , 70 };
int * p = a; // p -> a[0]
int x = * p + 1 ;
int y = * (p + 1 );
// x = ? y = ?
Forecast: Are x and y equal? Guess both.
Step 1 — Apply the binding rule. Why this step? * (dereference) binds tighter than +. So *p + 1 parses as (*p) + 1 — read first, then add.
Step 2 — Evaluate x. Why this step? *p is a[0] = 50; add 1 → x = 51. We never moved the pointer.
Step 3 — Evaluate y. Why this step? The parentheses force the addition first : p + 1 points at a[1], then * reads it → y = 60. Here we moved the pointer one element.
Verify: x = 50 + 1 = 51, y = a[1] = 60. They differ. ✓ Rule: parentheses beat precedence — see Dereference operator and precedence .
Worked example Real memory layout
A device streams samples into a buffer as doubles. The buffer starts at address 0x4000. You are told sample #0 is at the start. At what byte address does sample #6 begin, and how many bytes into the buffer is that?
Forecast: Guess the address and the byte-offset before computing.
Step 1 — Model it as a double*. Why this step? Translating English to arithmetic: "buffer of doubles from 0x4000" ⇒ double *buf = (double*)0x4000;. Stride = sizeof(double) = 8 .
Step 2 — buf + 6 gives sample #6. Why this step? Sample number = element index. Offset = 6 × 8 = 48 bytes.
Step 3 — Add to base. Why this step? 0 x 4000 + 48 . 0 x 4000 = 16384 , 16384 + 48 = 16432 = 0 x 4030 .
Step 4 — Byte offset into buffer. Why this step? "How many bytes in" is just the scaled offset itself = 48 bytes .
Verify: 0 x 4030 − 0 x 4000 = 48 = 6 × 8 ✓. Sanity units: 6 samples × 8 bytes/sample = 48 bytes. ✓
Recall Which cell is each expression?
p + 0 (int*) ::: Cell C — zero step, p unchanged.
((char*)p) + 1 ::: Cell F — cast changes stride to 1 byte.
a + N for int a[N] ::: Cell G — one-past-the-end, legal to form, not to deref.
3[a] ::: Cell I — commutativity; equals a[3].
*p + 1 ::: Cell J — precedence; (*p)+1, not *(p+1).
Every cell above is the SAME sentence: "Step by TYPE, not by BYTE." Change only the stride and the sign — the machinery never changes.