Lesson 02 / 15
Pointer Arithmetic
Why incrementing a pointer, decrementing it, and subtracting two pointers operates in elements rather than bytes; why an arithmetic that runs past the bounds is never run.
Published Updated
In this lesson
- Measures how incrementing and decrementing a pointer advances according to its type's size
- Computes the difference between two pointers in element units and measures their order with comparison
- Explains why an arithmetic that runs past an array's bounds is never run
Contents
The previous lesson’s local array row gave a last - first difference of 3: in a
four-element array, there are three steps between the first and last elements. But that
difference is not in bytes — the actual memory distance between last and first is a
multiple of however many bytes the array’s element type takes up. This lesson measures
where that multiplier comes from, and what it means to increment and decrement a pointer.
The class is still language: moving between an array’s elements requires no allocation
or free call at all.
Increment and Decrement: Advancing by Type Size
// increment.c — incrementing a pointer advances by its type's size
#include <stdio.h>
int main(void) {
int array[5] = {10, 20, 30, 40, 50};
int *p = array;
printf("%s %d\n", "first element", *p);
p++;
printf("%s %d\n", "element after one increment", *p);
p += 2;
printf("%s %d\n", "element after three increments", *p);
p--;
printf("%s %d\n", "element after one decrement", *p);
double pair[3] = {1.5, 2.5, 3.5};
double *d = pair;
d++;
printf("%s %.1f\n", "element after one increment on a double array", *d);
return 0;
}
first element 10 element after one increment 20 element after three increments 40 element after one decrement 30 element after one increment on a double array 2.5
PO5. p++ moves the pointer to the next int — the step written is always one
element, never one byte. p += 2 skips two elements at once: from index 1 to index 3.
p-- steps back by one element. The same p++ spelling was used on the pair array of
double values, and it again advanced by exactly one element — even though int and
double have different sizes, the increment operator means “the next element” for both.
The size itself (sizeof) was never printed anywhere; the measurement was made only
through the pointed-to value.
The Difference and Order of Two Pointers
// diff_and_order.c — the difference between two pointers is in elements, their order in comparison
#include <stdio.h>
int main(void) {
int array[6] = {1, 2, 3, 4, 5, 6};
int *first = &array[0];
int *middle = &array[3];
int *last = &array[5];
printf("%s %d\n", "middle minus first", (int)(middle - first));
printf("%s %d\n", "last minus middle", (int)(last - middle));
printf("%s %d\n", "does first come before middle", (int)(first < middle));
printf("%s %d\n", "does last come before middle", (int)(last < middle));
int *walker = first;
int steps = 0;
while (walker < last) {
walker++;
steps++;
}
printf("%s %d\n", "steps taken from first to last", steps);
return 0;
}
middle minus first 3 last minus middle 2 does first come before middle 1 does last come before middle 0 steps taken from first to last 5
PO6. middle - first gives 3: there is a gap of three elements between array[3]
and array[0]. The result of this subtraction is always the element count, never the
byte difference between two addresses — the expression middle - first prints 3, and the
addresses themselves are never seen. The comparison first < middle gives 1 because
first sits before middle in the array; this ordering comes from the < operator being
defined for two pointers within the same array. The loop built with the condition
walker < last advances exactly 5 steps from first to last — which matches the
last - first difference exactly.
Arithmetic on a char Array: When the Element Step Equals the Byte Step
The previous two examples used int and double arrays; in both, a single increment
advanced by more than one byte, but that advance was never measured directly — only the
pointed-to value was read. The char type creates a special case here: sizeof(char) is
always 1, so incrementing a char * covers a step of one element and a step of one
byte at the same time. This case comes up often when moving through a string’s characters.
// character_walk.c — one increment on a char pointer moves to the next character
#include <stdio.h>
int main(void) {
char string[] = "abcd";
char *letter = string;
printf("%s %c\n", "first character", *letter);
letter++;
printf("%s %c\n", "character after one increment", *letter);
char *last_letter = &string[3];
printf("%s %d\n", "last letter minus first letter", (int)(last_letter - string));
printf("%s %c\n", "value of the last character", *last_letter);
return 0;
}
first character a character after one increment b last letter minus first letter 3 value of the last character d
PO7. The call letter++ moves from string[0] to string[1]; the value read changes
from 'a' to 'b'. The difference last_letter - string gives 3 — string[3] is
three elements from the start of the array. This difference follows the same rule as the
middle - first example in the int array: the result is in elements. That the difference
also happens to equal a byte distance for char is a direct consequence of sizeof(char)
being 1 — not an exception to the rule, but the shape the rule itself takes for char.
Row Stepping in a Two-Dimensional Array
The three examples so far used one-dimensional arrays; the increment always advanced by
one element. In a two-dimensional array this rule does not change, but the definition
of “element” does: when a row pointer (int (*)[4]) is defined over int matrix[3][4],
an increment skips not a single int but an entire row (4 ints).
// row_step.c — a row pointer in a two-dimensional array counts a whole row as one step
#include <stdio.h>
int main(void) {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int (*row)[4] = matrix;
printf("%s %d\n", "first element of the first row", row[0][0]);
row++;
printf("%s %d\n", "first element of the row after one increment", row[0][0]);
printf("%s %d\n", "third element of the same row", row[0][2]);
int *flat = &matrix[0][0];
int *second_row_start = &matrix[1][0];
printf("%s %d\n", "second row start minus first element", (int)(second_row_start - flat));
return 0;
}
first element of the first row 1 first element of the row after one increment 5 third element of the same row 7 second row start minus first element 4
PO8. The row variable has type int (*)[4] — it carries the type “address of an
array of four ints,” not a plain int *. When row++ is called, the pointer moves to
the next row: the value of row[0][0] jumps from 1 to 5, that is, to the start of
matrix’s second row. This is the same rule as p++ advancing by one int in
increment.c, seen on a different type: the step is always the size of the pointed-to
type, and here that type is not a single int but a row of four ints. The difference
between a plain int * (flat) and the start of the second row comes out to 4 — which
confirms why int matrix[3][4] decays to int (*)[4] rather than int ** when passed to
a function: the row size (4) travels as part of the type, otherwise row++ could not
advance to the correct element.
The Same Rule on a Dynamically Allocated Array
The three examples so far ran on local (language class) arrays. The same arithmetic
rules hold exactly on a dynamically allocated block as well — advancing by type size
does not require the pointer to know where the array came from.
// dynamic_increment.c — pointer arithmetic on a dynamically allocated block follows the same rule
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int length = 4;
int *array = malloc((size_t)length * sizeof *array);
if (array == NULL) {
printf("%s\n", "allocation failed");
return 1;
}
for (int i = 0; i < length; i++) {
array[i] = (i + 1) * 10;
}
int *p = array;
printf("%s %d\n", "first element", *p);
p += 2;
printf("%s %d\n", "element after two increments", *p);
int *last = array + length - 1;
printf("%s %d\n", "last minus first", (int)(last - array));
free(array);
array = NULL;
return 0;
}
first element 10 element after two increments 30 last minus first 3
PO9. array is a block allocated with malloc, but the call p += 2 produces
exactly the same result as the local array in increment.c: it skips two elements,
from 10 to 30. The difference last - array is 3 — the same rule as the
middle - first example in diff_and_order.c. The difference is not in the arithmetic but
in ownership: this block was requested with malloc, so its class here is caller —
main allocates array, main uses it, and the obligation to free it stays with main
as well. The arithmetic itself is entirely independent of that ownership fact: whether the
block a pointer points to is a local array in the language class or a dynamic block in
the caller class, how far p++ advances depends only on the type, never on where the
memory came from.
The Equivalence Between Indexing and Arithmetic
So far, p[i] notation and p + i notation have each been used separately, but the
relationship between them was never measured. In C, square-bracket indexing is a
shorthand defined in terms of arithmetic: the expression p[i] means exactly
*(p + i), neither a byte off nor a separate rule.
// index_equivalence.c — p[i] notation and *(p + i) notation point at the same address
#include <stdio.h>
static int index_it(int *p, int i) {
return p[i];
}
static int add_by_dereference(int *p, int i) {
return *(p + i);
}
int main(void) {
int array[5] = {100, 200, 300, 400, 500};
int *p = array;
int index_result = index_it(p, 3);
int arithmetic_result = add_by_dereference(p, 3);
printf("%s %d\n", "p indexed at three", index_result);
printf("%s %d\n", "p advanced three times and dereferenced", arithmetic_result);
printf("%s %d\n", "do the two notations give the same value", (int)(index_result == arithmetic_result));
int zeroth_index = index_it(p, 0);
int zeroth_arithmetic = add_by_dereference(p, 0);
printf("%s %d\n", "zeroth index", zeroth_index);
printf("%s %d\n", "same as dereferenced p", (int)(zeroth_index == zeroth_arithmetic));
return 0;
}
p indexed at three 400 p advanced three times and dereferenced 400 do the two notations give the same value 1 zeroth index 100 same as dereferenced p 1
PO10. The expressions p[3] and *(p + 3) give the same value (400), and the
equality comparison comes out 1 — confirming that the two notations express exactly
the same operation to the compiler. The equality between p[0] and *p is an edge case of
the same rule: the zeroth index is identical to dereferencing directly, with no advance at
all. This equivalence explains why all of this lesson’s p++, p += n, and last - first
measurements gave results consistent with array[i] notation: indexing is a notational
convenience built on top of arithmetic, not a separate mechanism. The class is still
language: both p[3] and *(p + 3) point at the same local array, and the only
difference between them is notation. This equivalence also explains something else: even
array[i] notation is itself a form of pointer arithmetic, because array decays to the
address of its first element whenever it enters a function or an operator context, and
array[i] becomes exactly equivalent to *(array + i).
The Bounding Measurement
Pointer arithmetic is defined only within the same array, or one element past its end
— pointing one past the array’s last element (for loop termination) is valid, but
dereferencing that address, or advancing the array by two elements past it, is
undefined behavior. In the diff_and_order.c example above, walker advances during the
loop only as far as the address last and stops there — an increment that runs past the
array’s bound is never run. This bound is not something the compiler checks: writing
p += 100 compiles too, but its result is undefined, and this lesson never includes such
a call. The validity of the arithmetic depends on the person writing the code knowing the
array’s bounds — the type system does not carry that bound.
The same bound applies to pointer values that fall outside a statically allocated
block such as array: pointing to one before the start of the array with p - 1, or going
further than last + 1, falls into the same class of undefined behavior. This is exactly
why the bound is defined as “one element past the end”: the standard guarantees just
enough that a loop termination condition (like walker < last) can safely reach that one
address, and no more.
This bound also holds between pointers belonging to two unrelated arrays: if int a[4]
and int b[4] are two separately defined local arrays, &b[0] - &a[0] compiles but its
result is undefined — the subtraction operator only guarantees a meaningful element
count for two pointers within the same array (or one element past it); where two different
arrays sit relative to each other in memory is a detail left to the compiler’s decision,
and a program cannot rely on it. Both the diff_and_order.c and dynamic_increment.c
examples above observe this rule indirectly: both take a difference only between two
pointers within a single array or a single block, never comparing two unrelated blocks
like a and b.
Summary
- Pointer increment and decrement (
p++,p--,p += n) always advance by the type’s size; the step count is in elements, never in bytes. - The difference between two pointers (
last - first) gives the element count between them; the address itself is never printed. - Two pointers can be ordered with comparison operators like
<and>— within the same array, that ordering matches their position order in the array. - Pointing one past an array’s bound is valid, but dereferencing that address is undefined behavior; taking the difference between pointers of two unrelated arrays falls into the same class — neither was run in this lesson.
p[i]notation is a shorthand for*(p + i); the two notations always point at the same address and return the same value.- The arithmetic rules do not change whether the block a pointer points to is a local
array (
language) or a dynamically allocated block (caller); the step depends only on the type, never on the memory’s source. This is why the memory this lesson measures is in the language class: the rule is set by the language itself, not by the memory’s owner.
Next Step
An array’s name decays on its own into the address of its first element in most contexts —
the int *first = array notation already used this without dwelling on it. The next
lesson, Arrays and Pointers, measures when this decay happens, how an array is passed to a
function (and why without length information), and the first step into the caller
class: the ownership of a dynamically allocated array.
To keep your progress and take notes, Log in
My notes
Log in to take notes.