Lesson 4 of 6
Add 1 to a pointer and it does not move forward one byte. It moves forward one element.
#include <iostream>
int main() {
int values[5] = {10, 20, 30, 40, 50};
int* p = values; // an array decays to a pointer to its first element
std::cout << *p << "\n";
std::cout << *(p + 1) << "\n";
std::cout << *(p + 4) << "\n";
return 0;
}
10
20
50
p + 1 skips 4 bytes, because p is an int* and an int is 4 bytes. The compiler scales the
arithmetic by the size of the pointed-at type, so you count in elements and never in bytes.
Note int* p = values; with no &. An array decays to a pointer to its first element
(lesson 6.1), so values and &values[0] are the same address.
Indexing is pointer arithmetic
This is not an analogy. a[i] is defined as *(a + i):
#include <iostream>
int main() {
int values[5] = {10, 20, 30, 40, 50};
int* p = values;
std::cout << values[2] << " " << *(values + 2) << " " << p[2] << " " << *(p + 2) << "\n";
return 0;
}
30 30 30 30
Four spellings of the same thing. This also explains a famous curiosity: since *(a + i) and
*(i + a) are the same, 2[values] is legal C++ and means values[2]. Never write it — but if
you see it, now you know why it compiles.
Walking with a pointer
#include <iostream>
int main() {
int values[5] = {10, 20, 30, 40, 50};
for (int* p = values; p != values + 5; ++p) {
std::cout << *p << " ";
}
std::cout << "\n";
return 0;
}
10 20 30 40 50
values + 5 is one past the last element. The standard specifically permits forming that
address and comparing against it, precisely so loops like this work. What you may not do is
dereference it — there is no element there.
This half-open pattern (start, one-past-the-end) is exactly how the standard library's iterators
work, which Module 13 builds on. begin() and end() are this same idea generalised.
Subtracting pointers
#include <iostream>
int main() {
int values[5] = {10, 20, 30, 40, 50};
int* first = values;
int* fourth = &values[3];
std::cout << (fourth - first) << "\n";
std::cout << (values + 5) - values << "\n";
return 0;
}
3
5
Subtraction gives the number of elements between two addresses, which is why the second line
gives the array's length. The result type is std::ptrdiff_t, a signed integer, so a backwards
subtraction is legal and gives a negative number.
The rules you must not break
Pointer arithmetic is only defined within a single array, plus the one-past-the-end address:
int values[5] = {10, 20, 30, 40, 50};
int* p = values;
int* ok = p + 5; // one past the end: legal to form, illegal to dereference
int* bad = p + 6; // undefined behaviour, even without dereferencing
int* worse = p - 1; // undefined behaviour: before the start
Read that carefully: p + 6 is undefined the moment you compute it. Not when you use it. In
practice it will appear to work, which is what makes this dangerous — the program that "works"
today is relying on behaviour the standard does not promise, and an optimiser is free to assume
you never do it.
And with no bounds checking (lesson 6.1), nothing stops you:
#include <iostream>
int main() {
int values[5] = {10, 20, 30, 40, 50};
int* p = values;
// Reading p[7] is undefined behaviour. It usually "works" and prints garbage,
// which is exactly why this class of bug survives to production.
std::cout << "in range: " << p[4] << "\n";
std::cout << "past the end is not checked, and not reported\n";
return 0;
}
in range: 50
past the end is not checked, and not reported
Module 11's AddressSanitizer catches these at run time with an exact line number. Use it.
Arithmetic scales by type
#include <iostream>
int main() {
char cs[4] = {};
int is[4] = {};
double ds[4] = {};
// Cast to a byte-sized pointer to see the real distance moved.
std::cout << (reinterpret_cast<char*>(cs + 1) - reinterpret_cast<char*>(cs)) << "\n";
std::cout << (reinterpret_cast<char*>(is + 1) - reinterpret_cast<char*>(is)) << "\n";
std::cout << (reinterpret_cast<char*>(ds + 1) - reinterpret_cast<char*>(ds)) << "\n";
return 0;
}
1
4
8
Same + 1, three different byte distances. The pointer's type is what makes it work — which is
why void*, a pointer with no element type, cannot be used in arithmetic at all.
Try it
Write int* find_value(int* begin, int* end, int target) that returns a pointer to the first
matching element, or end if there is none. Use only pointer arithmetic, no indexing.
Checkpoint
p + 1advances by one element, scaled bysizeof(*p)— never by one byte.a[i]is literally*(a + i); indexing and pointer arithmetic are the same operation.- Subtracting two pointers gives the number of elements between them.
- One-past-the-end is legal to form and compare, illegal to dereference.
- Anything further out is undefined behaviour at the moment you compute it.
- The half-open
[begin, end)range is the foundation of the whole standard library.
Questions about this lesson
Log in to ask a question.