Pointer arithmetic and arrays interact in C++ because an array name is really a pointer to its first element, and the arithmetic is defined in terms of that. Adding one to a pointer moves it by the size of the element it points at, which is why [[&array[2] - &array[0]]] is [[2]] and not [[8]]. Understanding that single rule explains indexing, iteration and the [[.end()]] idiom in one go.
Adding one moves by the element size
Pointer arithmetic is scaled by the size of the pointed-to type. This is the whole rule.
#include
int main() {
int ratings[4] = {4, 5, 3, 5};
int* current = ratings;
std::cout << "sizeof(int): " << sizeof(int) << '\n';
std::cout << "*current: " << *current << '\n';
++current;
std::cout << "after ++: " << *current << '\n';
current = current + 2;
std::cout << "after +2: " << *current << '\n';
std::cout << "distance: " << (&ratings[3] - &ratings[0]) << '\n';
return 0;
}The output shows the pointer stepping from element to element and the difference of two pointers counting elements rather than bytes:
sizeof(int): 4
*current: 4
after ++: 5
after +2: 5
distance: 3Indexing is pointer arithmetic
The expression array[i] is defined as *(array + i). The compiler generates exactly the same code, which is why the two forms always agree.
#include
int main() {
double durations[3] = {180.0, 240.0, 200.0};
std::cout << "indexing: " << durations[1] << '\n';
std::cout << "pointer: " << *(durations + 1) << '\n';
double* pointer = durations;
std::cout << "offset: " << pointer[2] << '\n';
std::cout << "same: " << (durations[2] == *(pointer + 2)) << '\n';
return 0;
}The output shows all four expressions agree:
indexing: 240
pointer: 240
offset: 200
same: 1Note that the pointer form allows negative offsets too, as long as the result still points into the array. pointer - 1 is a valid address, even though you could never write durations - 1 as an index.
The four pointer operators
A pointer supports four arithmetic operations.
p + nandp - n— move forwards or backwards by n elements.p - q— the number of elements between two pointers into the same array.++p,--p,p++,p--— move by one element.
Adding two pointers together is not allowed, and neither is multiplying one by an integer. Pointer arithmetic is deliberately narrow so that mistakes are caught at compile time.
#include
int main() {
int scores[5] = {10, 20, 30, 40, 50};
int* begin = scores;
int* end = scores + 5;
int total = 0;
for (int* cursor = begin; cursor != end; ++cursor) {
total += *cursor;
}
std::cout << "total: " << total << '\n';
std::cout << "elements: " << (end - begin) << '\n';
std::cout << "middle: " << *(begin + 2) << '\n';
return 0;
}The begin-and-end pointer pair is the standard library's range idiom, and it is exactly what a std::array or std::vector exposes through .begin() and .end().
Walking off the end is undefined behaviour
undefined behaviourPointer arithmetic has one hard rule: the result must stay inside the same array, or be exactly one past the end. Anything else is , and the compiler is allowed to assume it never happens.undefined behaviour
#include
int main() {
int ratings[3] = {4, 5, 3};
int* begin = ratings;
int* end = ratings + 3;
int sum = 0;
for (int* cursor = begin; cursor != end; ++cursor) {
sum += *cursor;
}
std::cout << "sum: " << sum << '\n';
// One past the end is allowed as a value, but not to be dereferenced.
std::cout << "distance to end: " << (end - begin) << '\n';
return 0;
}The output is the safe, well-defined part:
sum: 12
distance to end: 3Dereferencing end would read memory that belongs to something else. The compiler will not warn you, because from its point of view the pointer arithmetic was fine.
Pointers into the middle of an array
Pointer arithmetic makes it easy to work with a slice of an array, which is how you pass part of a buffer to a function.
#include
double average(const double* begin, const double* end) {
if (begin == end) {
return 0.0;
}
double sum = 0.0;
const double* cursor = begin;
while (cursor != end) {
sum += *cursor;
++cursor;
}
return sum / static_cast(end - begin);
}
int main() {
double durations[5] = {180.0, 240.0, 200.0, 210.0, 195.0};
std::cout << "all five: " << average(durations, durations + 5) << '\n';
std::cout << "middle two: " << average(durations + 1, durations + 3) << '\n';
return 0;
}The output shows the two ranges giving different answers:
all five: 205
middle two: 220Pointer arithmetic in a linked structure
Pointer arithmetic is not only about arrays. In a linked list, adding one to a node pointer is not allowed, because the nodes are not next to each other in memory. This is worth noticing, because it shows exactly what pointer arithmetic means: it is a promise that the memory is contiguous.
#include
#include
struct Node {
std::string title;
Node* next;
};
int main() {
Node third = {"Third", nullptr};
Node second = {"Second", &third};
Node first = {"First", &second};
int count = 0;
for (Node* current = &first; current != nullptr; current = current->next) {
std::cout << current->title << '\n';
++count;
}
std::cout << "nodes: " << count << '\n';
return 0;
}The output walks the chain:
First
Second
Third
nodes: 3You cannot write current + 1 here, because there is no guarantee that the next node lives immediately after the current one in memory. The next pointer exists precisely because the nodes are scattered.
Quick reference
- Adding one to a pointer moves it by
sizeofof the pointed-to type. array[i] is exactly*(array + i).- The difference of two pointers into the same array counts elements.
- Pointer arithmetic is only valid inside the array, or one past the end.
beginandendpointer pairs are the range idiom the standard library uses.- A pointer to a
voidhas no size, so no arithmetic is allowed on it.
Try it yourself: write a function that takes [[int*]] and [[int*]] and returns the largest value in the range, using pointer arithmetic to walk it. Test it on a whole array and then on a slice by passing [[data + 1]] and [[data + 4]]. Finally, print the distance between the start and the end pointers for both calls and confirm the slice is shorter.