Lesson 2 of 6
A pointer is a variable whose value is an address.
#include <iostream>
int main() {
int x = 42;
int* p = &x; // p holds the address of x
std::cout << x << "\n"; // the value
std::cout << *p << "\n"; // the value at the address p holds
return 0;
}
42
42
Two operators, and they are opposites:
&x— "the address ofx". Takes you out of a variable to where it lives.*p— "the object at addressp". Takes you in, to the value. This is dereferencing.
int* p reads as "p is a pointer to int." The type matters: a pointer knows the type of what
it points at, which is how *p knows to read 4 bytes as an int rather than 8 bytes as a
double.
Writing through a pointer
Dereferencing is not read-only. *p names the object, so it can go on the left of =:
#include <iostream>
int main() {
int x = 42;
int* p = &x;
*p = 99; // writes to x, through p
std::cout << x << "\n";
x = 7; // and the other direction
std::cout << *p << "\n";
return 0;
}
99
7
There is only one int here. x and *p are two names for it.
Where to put the asterisk
These three declare the same thing:
int* p;
int *p;
int * p;
The compiler ignores the spacing. This codebase uses int* p, because the * is part of the
type: p is a pointer-to-int.
But that style has one trap, and it is worth knowing before it bites you:
#include <iostream>
int main() {
int a = 1, b = 2;
int* p, q; // p is int*, q is a plain int — NOT two pointers
p = &a;
q = b;
std::cout << *p << " " << q << "\n";
return 0;
}
1 2
The * binds to the declarator, not the type, so it applies only to p. Declare one pointer
per line and the question never arises.
Pointers are variables too
A pointer can be reassigned to point somewhere else:
#include <iostream>
int main() {
int a = 1;
int b = 2;
int* p = &a;
std::cout << *p << "\n";
p = &b; // p now points at b; a is untouched
std::cout << *p << "\n";
std::cout << a << "\n";
return 0;
}
1
2
1
Note the difference between p = &b and *p = b. The first re-points the pointer. The second
writes b's value into whatever p currently points at. Mixing these up is one of the most
common beginner bugs in the language, and the compiler will not catch it — both are legal.
nullptr: pointing at nothing
A pointer has to hold something. When there is no object to point at, point at nothing deliberately:
#include <iostream>
int main() {
int* p = nullptr;
if (p == nullptr) {
std::cout << "p points at nothing\n";
}
// A pointer converts to false when null, true otherwise — the idiomatic check.
if (!p) {
std::cout << "still nothing\n";
}
int x = 5;
p = &x;
if (p) {
std::cout << "now it points somewhere: " << *p << "\n";
}
return 0;
}
p points at nothing
still nothing
now it points somewhere: 5
Dereferencing a null pointer is undefined behaviour (lesson 8.7). In practice it usually crashes immediately — which is the good case, because the crash is at the mistake. Check before you dereference anything that might be null.
Use nullptr, not NULL and not 0. nullptr has a distinct type that cannot be mistaken for
an integer, which matters for overload resolution (lesson 5.6). NULL is a leftover from C.
Uninitialised is worse than null
int* p; // holds whatever bytes were already there
*p = 42; // writes to an arbitrary address
An uninitialised pointer holds garbage. Dereferencing it writes to some arbitrary address: it might crash, or it might silently corrupt something you only find out about an hour later.
This is undefined behaviour, and note what the compiler does about it — a warning, not an error:
warning: 'p' is used uninitialized [-Wuninitialized]
It still produces a runnable program. And -Wuninitialized only catches the cases the compiler
can see through; hand the pointer to another function first and the warning disappears while the
bug does not. Do not rely on it. Make it a habit instead: every pointer gets a value at
declaration, even if that value is nullptr.
Compiling with -Werror (lesson 11.1) turns warnings like this into hard errors, which is the
right setting for real work.
What a pointer costs
#include <iostream>
int main() {
char c = 'x';
double d = 3.5;
int big[1000] = {};
std::cout << sizeof(&c) << " " << sizeof(&d) << " " << sizeof(&big) << "\n";
return 0;
}
8 8 8
Eight bytes each. Passing a pointer to a 4000-byte array copies 8 bytes, which is the point of
lesson 5.3 and why const T& is the default for large parameters.
Try it
Write void swap_ints(int* a, int* b) that exchanges the two values its arguments point at.
Call it and show that the caller's variables really changed. Then make it safe against a null
argument.
Checkpoint
- A pointer holds an address;
int* p = &x;makesppoint atx. &gets an address,*gets the object at one. They are inverses.*p = vwrites through the pointer;p = &vre-points it. Very different.- Declare one pointer per line: in
int* p, q;onlypis a pointer. - Use
nullptrfor "points at nothing", and check before dereferencing. - Dereferencing a null or uninitialised pointer is undefined behaviour.
- A pointer is 8 bytes whatever it points at.
Questions about this lesson
Log in to ask a question.