Pointers Part 2: Arithmetic, Comparison, and One-Past-the-End
This chapter covers pointer arithmetic, pointer comparison, and the special one-past-the-end pointer. These rules make C arrays and pointer iteration work, and their boundary conditions are a major source of undefined behavior.
Why This Matters
Pointer arithmetic is how you traverse arrays, implement data structures, and read memory in systems code. Its rules are stricter than "pointers are numbers" suggests, and violating them is undefined behavior even when the arithmetic "looks fine."
Prerequisites
c.core.19— pointer basics.c.core.21(recommended) — arrays (but this chapter can stand alone).
Core Concept
Pointer arithmetic
For a pointer p of type T * and an integer n, p + n points to the object n elements away, where "one element" is sizeof(T) bytes. So p + 1 adds sizeof(T) bytes to the address, not 1 byte.
int *p = &arr[0];
int *q = p + 2; /* q points to arr[2], sizeof(int) bytes further */
Subtraction of a pointer and an integer (p - n) is the reverse. Subtraction of two pointers (q - p) yields the number of elements between them, as a ptrdiff_t, and is defined only when both pointers point into the same array object (or one past the end).
Pointer comparison
Two pointers can be compared with ==, !=, <, >, <=, >=. Equality and inequality are well-defined for valid pointers (including comparing against NULL). Relational comparison (<, etc.) is defined when both pointers point into the same array object (or one past the end); otherwise it is unspecified in some cases (see below).
One-past-the-end
A pointer may legally point one element past the end of an array object. Such a pointer may be compared and used in arithmetic, but it must not be dereferenced.
int arr[5];
int *end = arr + 5; /* valid one-past-the-end pointer */
/* *end is UNDEFINED BEHAVIOR */
Syntax
p + n;
p - n;
p - q; /* ptrdiff_t */
p == q; p != q;
p < q; p > q; p <= q; p >= q;
Examples
Iterating with pointer arithmetic
#include <stdio.h>
int main(void)
{
int arr[5] = {10, 20, 30, 40, 50};
for (int *p = arr; p < arr + 5; p++) {
printf("%d\n", *p);
}
return 0;
}
Expected output: 10 through 50, one per line.
Pointer subtraction
int arr[5];
int *a = &arr[1];
int *b = &arr[4];
ptrdiff_t n = b - a; /* 3 */
One-past-the-end in a loop
int arr[5];
int *end = arr + 5;
for (int *p = arr; p != end; p++) {
/* process *p */
}
This is the canonical "iterate to one-past-the-end" pattern.
How It Works
Pointer arithmetic is *typed*: the compiler multiplies n by sizeof(T) to compute the byte offset. This is why int *p and char *p step by different amounts. Subtraction of two pointers divides the byte difference by sizeof(T) to yield an element count.
Variations
void* arithmetic (not allowed)
You cannot do arithmetic on void * in ISO C, because void has no size. (GCC allows it as an extension, treating void as size 1, but that is not portable.) Cast to a concrete type first.
Arithmetic on char pointers
Because sizeof(char) == 1, char * arithmetic operates on individual bytes. This makes char * (or unsigned char *) the right type for raw byte access.
Common Mistakes
- Assuming
p + 1adds 1 byte (it addssizeof(T)bytes). - Dereferencing a one-past-the-end pointer.
- Subtracting pointers from different arrays.
- Doing arithmetic on
void *. - Forgetting that
arr + 5is valid butarr + 6(beyond one-past-the-end)
is not.
Undefined Behavior
- Arithmetic that produces a pointer outside the array bounds, except exactly
one-past-the-end. VERIFIED
- Dereferencing a one-past-the-end pointer.
VERIFIED - Subtracting pointers that do not point into (or one past the end of) the same
array object. VERIFIED
- Arithmetic on a null pointer or an invalid pointer.
VERIFIED - Dereferencing a misaligned pointer.
Portability
- The behavior of relational comparison (
<,>) between pointers not in the
same array is unspecified (in C17; C23 refines this). Use ==/!= for arbitrary valid pointers, and relational comparison only within arrays.
ptrdiff_tis defined in<stddef.h>and is a signed integer type.
Under the Hood
Pointer arithmetic compiles to address computation: for T *p, p + n computes base + n * sizeof(T). On x86-64, this is a scaled addressing mode (lea rax, [rdi + rsi*4] for int). Pointer subtraction compiles to a subtract followed by an arithmetic shift/divide by the element size.
Practical Usage
- Use pointer iteration for tight loops where index arithmetic is not needed.
- Use one-past-the-end pointers as range ends (the basis of the standard
library's iterator-style functions and many container designs).
- Use
char */unsigned char *for byte-level access.
Exercises
1. Write a loop that iterates over an int array with a pointer and prints each element. 2. Demonstrate that p + 1 for int* moves by sizeof(int) bytes, and compare with char*. 3. Compute the number of elements between two pointers using subtraction. 4. Explain why arr + 5 is valid but * (arr + 5) is UB.
Deep Challenge
Write a function size_t arrlen(int *begin, int *end) that returns the number of elements in the range [begin, end), and explain why it is well-defined only when begin and end point into the same array (with end possibly one past the end). Then explain what goes wrong if they point into different arrays.
Related Concepts
c.core.21— arrays and decay.c.obj.provenance— pointer provenance.c.ptr.void— void* and arithmetic.
References
- ISO/IEC 9899:2018 §6.5.6 (additive operators), §6.5.8 (relational
operators), §6.5.9 (equality operators).
Verification
- Pointer arithmetic scales by
sizeof(T).VERIFIED - One-past-the-end is valid to form but not to dereference.
VERIFIED - Cross-array subtraction is UB.
VERIFIED - Relational comparison across arrays is unspecified (C17).
VERIFIED - No example was executed during generation unless noted. `Execution not
verified.`
Progress
- [ ] Read
- [ ] Understand
- [ ] Complete examples
- [ ] Complete exercises
- [ ] Complete deep challenge
Concept checkboxes
- [ ] Pointer arithmetic (typed scaling)
- [ ] Pointer subtraction
- [ ] Pointer comparison
- [ ] One-past-the-end pointers
Mastery levels
| Concept | Current level (0–8) | Target level |
|---|---|---|
| c.ptr.arithmetic | 0 | 6 |
| c.ptr.compare | 0 | 5 |
| c.ptr.onepast | 0 | 5 |