C++ / REFERENCES, POINTERS, AND NULL
Pointers, dereferencing, and pointer arithmetic basics
Declare pointers, read and write objects through the dereference operator, and reason about pointer arithmetic where +1 moves one element, not one byte.
What you will learn
- Declare a pointer and read or write the pointed-to object through *p
- Predict what *(p + n) reads: +1 moves sizeof(*p) bytes, never one byte
- Use q - p to get the element distance between two pointers in one array
- Explain why *p++, ++*p and *++p all behave differently
Understanding Pointers, dereferencing, and pointer arithmetic basics
A pointer variable stores an address, but the address alone is not enough to use it: the declaration int* p also tells the compiler that the bytes living there are to be treated as an int. The * in that declaration is part of the type, while the * in the expression *p is the dereference operator, which yields the object at that address rather than a copy of it. Because *p names the object itself, it works on either side of an assignment: std::cout << *p reads those bytes as an int, and *p = 11 overwrites them.
Pointer arithmetic counts elements, not bytes. The expression p + 1 produces an address sizeof(*p) bytes further along, so the very same expression advances 4 bytes for an int* and 8 for a double*, because the compiler inserts the scaling from the pointee type. Subscripting is the same operation with friendlier syntax, since p[i] is defined as *(p + i), and the inverse operation q - p gives the number of elements between two pointers as a std::ptrdiff_t. None of this arithmetic touches memory: computing p + 100 only produces an address, and only the dereference actually reads or writes.
The rules that keep pointers usable are about ranges, not about the arithmetic itself. Adding to or subtracting from a pointer is defined only while the result stays inside the same array, or lands exactly one position past its last element; that one-past-the-end address may be stored and compared but never dereferenced. That is why loops are written with the test p != end, so the body never runs while p equals end. Stepping beyond that position, or reading through the end pointer, is undefined behaviour: the program will usually print something, and that something proves nothing.
<iostream>
int main() {
int values[4] = {10, 20, 30, 40};
int* p = &values[0]; // p holds the address of the first element
std::cout << "*p = " << *p << '\n';
*p = 11; // dereference on the left: write through p
std::cout << "values[0] = " << values[0] << '\n';
p = p + 2; // forward two ints, not two bytes
std::cout << "*p = " << *p << '\n';
int* last = &values[3];
std::cout << "last - p = " << last - p << '\n';
std::cout << "byte gap = " << (last - p) * static_cast<long>(sizeof(int)) << '\n';
++p; // p and last now point at the same object
std::cout << "p == last = " << (p == last) << '\n';
std::cout << "*p = " << *p << '\n';
}A pointer carries an address plus a pointee type, and it is that type which decides what a dereference reads and how far a single +1 moves.
Worked examples
Walking a range with two pointers
Uses arithmetic to build a one-past-the-end pointer and iterates until the moving pointer equals it.
<iostream>
int main() {
double temps[3] = {18.5, 21.0, 19.25};
double* first = &temps[0];
double* end = &temps[0] + 3; // one past the last element
double sum = 0.0;
for (double* p = first; p != end; ++p) {
sum += *p;
}
std::cout << "elements = " << end - first << '\n';
std::cout << "sum = " << sum << '\n';
std::cout << "mean = " << sum / (end - first) << '\n';
}Example explained
Line 1&temps[0] + 3 forms the one-past-the-end address, which is legal to hold and compare but illegal to dereference.
Line 2The test p != end stops the loop before the body ever evaluates *end.
Line 3++p advances by sizeof(double) bytes because p's type sets the stride, not any number written in the loop.
Line 4end - first evaluates to the element count 3 as a std::ptrdiff_t, so dividing sum by it gives the mean.
Subscripting is arithmetic plus dereference
Shows that p[i] and *(p + i) are the same operation, including for writes and negative offsets.
<iostream>
int main() {
int data[5] = {2, 4, 6, 8, 10};
int* p = &data[0];
std::cout << *(p + 3) << ' ' << p[3] << '\n';
*(p + 1) = 40; // write through a computed address
p[4] = 100; // identical operation, different spelling
for (int i = 0; i < 5; ++i) {
if (i > 0) std::cout << ' ';
std::cout << data[i];
}
std::cout << '\n';
int* mid = p + 2;
std::cout << *(mid - 1) << ' ' << mid[-1] << '\n';
}Example explained
Line 1*(p + 3) and p[3] print the same value because p[3] is defined as *(p + 3).
Line 2*(p + 1) = 40 changes data[1], proving a dereference produces the object itself and not a copy.
Line 3mid - 1 steps back one int, so mid[-1] reads data[1]; a negative offset is fine while the result stays inside the array.
Line 4Only elements 1 and 4 changed, since arithmetic on p alone never modifies memory.
Precedence of * against ++
Distinguishes moving the pointer from modifying the pointed-to value.
<iostream>
int main() {
int a[3] = {5, 6, 7};
int* p = &a[0];
int x = *p++; // read *p, then move p forward
std::cout << "x = " << x << ", *p = " << *p << '\n';
int y = ++*p; // increment the pointed-to int, then read it
std::cout << "y = " << y << ", a[1] = " << a[1] << '\n';
int z = *++p; // move p forward, then read
std::cout << "z = " << z << ", *p = " << *p << '\n';
std::cout << a[0] << ' ' << a[1] << ' ' << a[2] << '\n';
}Example explained
Line 1*p++ parses as *(p++) because postfix ++ binds tighter than *, so x gets a[0] and p ends up at a[1].
Line 2++*p parses as ++(*p): the dereference happens first, so a[1] goes from 6 to 7 while p stays put.
Line 3*++p moves p to a[2] before reading, which is why z is 7 and no element is touched.
Line 4The final line shows a[1] is the only element that changed, since pointer increments do not write to the array.
Important notes
Pointer arithmetic and subtraction are defined only within one array object plus its one-past-the-end position; subtracting pointers to two unrelated variables compiles but has no defined meaning.
The byte figures above assume sizeof(int) == 4 and sizeof(double) == 8; write sizeof(*p) rather than a literal when you need the stride.
Common mistakes
Scaling by hand, as in p + n * sizeof(int) instead of p + n: the compiler scales again, so the pointer lands four times too far away and the read is outside the array.
Writing int* p, q; and expecting two pointers: q is a plain int, so q = &x is a compile error and *q does not compile at all.
Dereferencing the one-past-the-end pointer, usually by looping with <= instead of !=: the address is legal to hold but reading it is undefined behaviour and often prints a stale value that looks plausible.
Try it yourself
Change, predict, then run
In a browser editor, declare int a[6] = {3, 1, 4, 1, 5, 9}; and one int* pointer, then print the six values in reverse order using only dereferencing and pointer arithmetic, with no [] subscripting after the declaration. Finish by printing &a[5] - &a[0] and explain in a comment why that number is 5 and not 20.
Open the C++ workspaceCheck your understanding
Given int arr[8]; int* p = &arr[0];, a teammate wants a pointer to arr[4] and writes int* q = p + 4 * sizeof(int);. Assuming sizeof(int) == 4, what actually happens?
- Nothing is wrong: multiplying by sizeof(int) is how an element count is turned into a pointer offset.
- It fails to compile, because a pointer may only be added to a value of type std::ptrdiff_t.
- q ends up 16 elements past arr[0], outside the array, because the compiler already scales the offset by sizeof(int).
- q does point at arr[4], but the arithmetic is undefined behaviour because sizeof yields an unsigned type.
Show answer
Scaling happens exactly once, inside the compiler, driven by the pointer's type: p + n already moves n * sizeof(int) bytes. Multiplying by sizeof(int) by hand makes the offset 16 elements, or 64 bytes, past the start of an 8-element array, so even forming q is undefined behaviour. The first option is tempting because 16 looks like the correct byte distance to arr[4], but pointer arithmetic never accepts byte offsets.