Pointer arithmetic is what makes a pointer different from a plain "address number". Adding 1 to an int* doesn't move the address forward by 1 byte; it moves forward by sizeof(int) bytes, which is enough to land on the next int. That single rule, combined with a few comparison and difference operations, lets you walk through a block of memory one element at a time. This chapter covers the full set of pointer arithmetic operations, the rules about where they're valid, and the kinds of bugs that come from breaking those rules.
From the previous chapter, a pointer holds an address. int stock = 50; int* p = &stock; means p stores the memory address where stock lives. Pointer arithmetic is the set of operations that produce new pointer values from an existing one: p + 1, p - 1, p++, comparing two pointers, subtracting two pointers, and so on.
These operations have one important quirk. When you write p + 1, the compiler doesn't add the literal number 1 to the address. It adds 1 * sizeof(T), where T is the type the pointer points at. For an int* on a typical 64-bit machine where sizeof(int) is 4, p + 1 moves the address forward by 4 bytes. For a double* where sizeof(double) is 8, p + 1 moves forward by 8 bytes.
The exact addresses will differ every time you run the program, but the gap between p and p + 1 is 4, not 1. The compiler scaled the addition by sizeof(int). The last line shows the other half of the rule: subtracting two pointers gives the number of elements between them, not the number of bytes. (p + 1) - p is 1, not 4.
This design makes pointer arithmetic useful. If p points at the start of an array of int, then p + 1 points at the next int, p + 2 points at the one after that, and so on. You can step through the array without computing byte offsets by hand.
Pointer arithmetic scales by sizeof(T). Adding 1 to an int* moves 4 bytes on most machines, adding 1 to a double* moves 8 bytes, and adding 1 to a char* moves exactly 1 byte. Forgetting this leads to addresses that look "wrong" but are correct in element terms.
The most common operations on a pointer are ++ and --. They're shorthand for "move forward by one element" and "move back by one element". Like integer increment, they come in prefix and postfix flavors.
p = prices; works because the array name decays to a pointer to its first element. After that, every ++p or p++ slides p along the array. The values dereferenced change accordingly.
Prefix (++p) and postfix (p++) differ the same way they do for integers. ++p increments first and then yields the new pointer value. p++ yields the old pointer value and then increments. As a standalone statement, the difference doesn't matter. Inside a larger expression, it does.
*p++ is a C-and-C++ idiom that takes a minute to parse. It means: take the value *p first (the current pointed-at value), then move p forward. After the first statement, p points at stock[1] (which is 10). The second statement, *++p, moves p forward first to stock[2], then reads the value there, which is 15. Same operators, different ordering, different results.
Most readable code doesn't lean on postfix dereference tricks. For both the value and the next pointer, splitting into two lines is clearer. But *p++ appears in older C-style code and in standard library implementations, so it's worth recognizing.
For built-in types, ++p and p++ compile to the same code when used as a statement. For class types like iterators, prefix ++p can be slightly cheaper because postfix p++ typically constructs a temporary copy of the old value to return. The habit of writing ++p in loops carries over cleanly when moving from raw pointers to STL iterators.
Any integer value can be added or subtracted, not just 1. p + n produces a pointer that points n elements past p. p - n produces a pointer n elements before. Both follow the same scaling rule: the actual byte change is n * sizeof(T).
*(p + i) is the same thing as p[i]. The subscript operator on a pointer is literally defined as "add i then dereference". Both *(p + 2) and p[2] produce 300 here. Pointer arithmetic is the engine behind [].
Addition is commutative for pointer plus integer, a small but occasionally useful detail. p + 2 and 2 + p both mean the same thing. Subtraction is not commutative. p - 2 is a pointer two elements before p, but 2 - p doesn't make sense and the compiler rejects it.
The "if p is at least 2 elements into the array" caveat in the comment matters. Going before the start of an array with pointer subtraction is undefined behavior, even without dereferencing the result. This rule comes up again shortly.
Two pointers can be subtracted when they point into the same array. The result is the number of elements between them, with the type std::ptrdiff_t (a signed integer type wide enough to express any in-array distance on the current platform).
The differences are element counts, not byte counts. end - start = 7 means there are seven int slots between them, which is correct for the start and last index of an 8-element array. The <cstddef> header brings in std::ptrdiff_t; auto also works to let the compiler pick the right type.
Pointer difference is the basis for "how many elements have I covered?" calculations when walking a block of memory. Scanning through an array of stock counts looking for the first zero, the distance between the start pointer and the current position gives the number of slots checked.
The loop walks p forward until it either reaches the end sentinel or finds a zero. After the loop, p - start gives the element-index of the match. No byte math, no sizeof calls, no manual offset arithmetic.
The sign of the result follows from the subtraction order. mid - start is positive when mid is later in the array than start, negative when it's earlier, and zero when they're equal. That property makes pointer difference useful for binary-search style loops where two pointers narrow toward each other.
Pointer arithmetic is essentially free at runtime: an add and possibly a shift, computed by the CPU's address generation unit. The expensive part is the memory access when dereferencing. Walking through 10 million int values is bottlenecked by memory bandwidth, not by the arithmetic on the pointer itself.
Two pointers can be compared with <, <=, >, >=, ==, and !=. Equality and inequality work for any two pointers of compatible types: they ask "do these point to the same address?". The ordering operators (<, <=, >, >=) are only meaningful when both pointers point into the same array (or one past the end). Within those bounds, the ordering matches the order of elements in memory: lower-index elements compare less than higher-index ones.
std::boolalpha is a stream manipulator that prints bool values as true and false instead of 1 and 0. Comparing pointers that point to the same array works as expected: the pointer to the earlier element compares less than the one to the later element.
The most common use of comparison is as a loop condition. Walking from a start pointer to a sentinel pointer that marks "one past the end" is the canonical pointer-arithmetic loop pattern.
cart + 4 is the address one past the end of the array. The loop continues while p != end, dereferencing each element along the way. When p reaches end, the loop stops without dereferencing the sentinel. This is how STL iterators work too: begin() and end() provide a half-open range, walked from one to the other.
Comparing pointers from different arrays with < or > is technically undefined behavior in standard C++. Most implementations compare the raw addresses, but the standard doesn't promise this, and modern compilers can optimize on the assumption that this case doesn't happen. Stick to comparing pointers that live in the same array (or nullptr for equality checks).
The classic way to walk an array uses an integer index: for (int i = 0; i < n; i++) { sum += arr[i]; }. The pointer-arithmetic version uses two pointers and compares them.
Both loops produce the same total. The pointer version stores two values (the moving pointer and the end sentinel), and the index version stores two values too (the moving index and the bound n). Functionally they're equivalent, and modern compilers usually generate near-identical machine code for both.
Why is the pointer version worth knowing? Three reasons.
First, it generalizes. The pattern for (T* p = begin; p != end; ++p) { ... } works for any contiguous range, including parts of arrays, pointers passed into functions that don't know the size, and iterators returned by std::vector::begin() and std::vector::end(). The index version assumes the start is always at index 0 and that n is known directly.
Second, it composes well with two-pointer algorithms. Sliding windows, two-pointer compares, in-place partitions, and many classic algorithm patterns use a pair of pointers (or iterators) moving along the same range, comparing addresses with < or !=.
Third, it sets up the standard library. STL containers don't expose integer indices; they expose iterator pairs. The model "walk a pointer from begin to end, comparing as you go" is the iterator model. Learning it on raw pointers makes the transition to std::vector, std::list, and friends feel natural rather than confusing.
For a one-off loop over a fixed-size local array, the index version is often cleaner to read. For functions that receive a pointer plus length, or that walk a sub-range, the pointer version is the natural fit.
sumRange doesn't care which array the pointers came from or how large the original array is. It only cares about the half-open range [begin, end). Callers pick whichever slice they want with simple pointer arithmetic. This is how std::accumulate and most other STL algorithms are designed.
A diagram showing what happens when you increment a pointer through an array. Each cell is a slot of sizeof(int) bytes (typically 4) in memory. The pointer slides over them, one element at a time.
The four green cells are the actual array elements. The red cell is the "one past the end" position. A pointer can legally hold that address; the standard guarantees it as a valid sentinel value. Dereferencing that pointer is not allowed; the cell has no element to read.
In the loop on the right side of the diagram, the pointer starts at prices[0], advances through each element, and lands on the sentinel. The loop condition p != end stops the iteration at the right moment. A loop that used p <= end and then dereferenced *p would read garbage from a cell that isn't part of the array. That's a classic off-by-one bug.
The pattern of "start pointer, end sentinel, advance until you hit the sentinel" generalizes far beyond arrays. STL iterators use it. Many tree and graph traversals use a sentinel value. C-string handling uses a null byte as a sentinel. The half-open range pattern appears in many places.
C++ is generous with pointer arithmetic but strict about what counts as valid. The standard allows arithmetic with a pointer as long as the result stays within the array (or points exactly one past the end). Anything else is undefined behavior, even without dereferencing the result.
The valid range for a pointer into an array of N elements covers N + 1 positions: index 0 through N - 1 for the actual elements, plus index N as the "one past the end" sentinel.
The first three lines are all valid pointer values. The last two are not, even without *pneg or *p6 being written. Just forming the pointer is enough to invoke undefined behavior. Simple cases like this often "work" because the compiler emits an integer add, the resulting address is a real location somewhere in memory, and nothing crashes. But the program is broken regardless. Optimizers are allowed to assume pointers are in range, and modern compilers sometimes delete code based on that assumption.
The sentinel rule is the most useful part of these constraints. arr + N (where N is the array size) is always legal to form and use for comparisons and pointer differences. Dereferencing it is not allowed.
stock + 3 is the address right after stock[2]. It can be subtracted from stock, compared to another pointer, or used as a loop sentinel. It can't be read through; the standard singles out dereferencing the one-past-the-end pointer as undefined.
Out-of-bounds pointer arithmetic is undefined behavior even without dereferencing. The expression arr - 1 (before the start) or arr + (N + 1) (more than one past the end) is broken on its own merits. Don't write a "harmless" off-by-one and hope it works.
Comparing pointers from two unrelated arrays is also undefined for the ordering operators. If a and b are pointers into different arrays, a < b has no defined answer. Most implementations return something based on the raw address values, but the standard doesn't promise this, and the compiler can assume the operation never happens. a == b and a != b are always defined for any two pointers of compatible types, even from different arrays.
A common bug is the "fence post" loop, where the programmer uses <= instead of < and ends up reading the sentinel:
What's wrong with this code?
The loop continues until p > end_ptr, which means it runs once with p == end_ptr. That dereference is undefined behavior. The fix is < (strictly less than) or !=:
Now the loop stops at the sentinel without reading through it. This is why the canonical pattern uses != (or <), not <=: it lines up with the half-open range.
A larger example that exercises everything in this chapter. A list of product prices and a search for the first one over a threshold, with both its value and its position reported. Walking the list with two pointers makes the loop and the position calculation fall out naturally.
The loop combines two of the operations from this chapter. p != end_ptr is a pointer comparison that stops the loop from running off the end. ++p is the move-forward step. After the loop, p - begin is a pointer difference that gives the element index without byte math.
The "found nothing" case is handled by checking p == end_ptr after the loop. The half-open range pattern produces this answer naturally: if the loop walked all the way to the sentinel without finding a match, p ends up at end_ptr. No special return value needed.
Compare with the index-based version:
Same structure, same logic. The pointer version sometimes feels heavier for a small fixed-size array. It pays off when the range is passed in as a pair of pointers, or when composing two-pointer algorithms (left-and-right pointers that meet in the middle, for example).
10 quizzes