Pointers
A pointer holds the memory address of an object. Modern C++ needs raw pointers far less than C does — references, containers and smart pointers cover most jobs — but you still need to understand them: to read existing code, to express "optional reference", and to work with C libraries and low-level code.
Address and dereference
#include <iostream> int main() { int score = 42; int* p = &score; // p holds the address of score std::cout << *p << '\n'; // * dereferences: the value at that address *p = 100; // change score through the pointer std::cout << score << '\n'; std::cout << std::boolalpha << (p == &score) << '\n'; }
42 100 true
| Syntax | Meaning |
|---|---|
int* p | p is a pointer to int |
&x | The address of x |
*p | The object p points to |
p->m | Member m of the object p points to — same as (*p).m |
Printing an address with std::cout << p shows a hexadecimal number that differs from run to run, so this course compares addresses rather than printing them.
nullptr
A pointer that points to nothing should be nullptr. Dereferencing a null pointer is undefined behaviour — usually a crash — so check before using a pointer that might be null:
#include <iostream> #include <string> #include <vector> struct User { std::string name; int age; }; const User* find_user(const std::vector<User>& users, const std::string& name) { for (const auto& u : users) { if (u.name == name) return &u; } return nullptr; // not found } int main() { std::vector<User> users{{"Ana", 31}, {"Ben", 27}}; for (std::string name : {"Ben", "Zoe"}) { if (const User* u = find_user(users, name)) { std::cout << u->name << " is " << u->age << '\n'; } else { std::cout << name << " not found\n"; } } }
Ben is 27 Zoe not found
That's the main remaining use for raw pointers in modern C++: a non-owning, optional reference to an object that lives elsewhere. Never use NULL or 0 for pointers in C++ — nullptr has its own type and avoids overload confusion.
Pointers and const
int main() { int a = 1, b = 2; const int* p1 = &a; // pointer to const int: can't change *p1 int* const p2 = &a; // const pointer: can't change p2 itself p1 = &b; // fine *p2 = 5; // fine *p1 = 5; // error p2 = &b; // error return a + b; }
main.cpp:7:9: error: read-only variable is not assignable main.cpp:8:8: error: cannot assign to variable 'p2' with const-qualified type 'int *const'
Read right to left: const int* p is "a pointer to an int that is const"; int* const p is "a const pointer to an int".
Pointers and arrays
A C-style array converts to a pointer to its first element, and pointer arithmetic moves in whole elements. Modern code rarely does this by hand — std::span (lesson 19) wraps a pointer and a length safely:
#include <iostream> int main() { int primes[] = {2, 3, 5, 7, 11}; int* p = primes; std::cout << *p << ' ' << *(p + 2) << ' ' << p[4] << '\n'; for (int* it = primes; it != primes + 5; ++it) std::cout << *it << ' '; std::cout << '\n'; }
2 5 11 2 3 5 7 11
Pointers vs. references
Use a reference when there is always an object and it won't change; use a pointer when "no object" is a valid state, or when you need to re-point. For optional values (not references), std::optional (lesson 43) is often clearer than a pointer.
Owning vs. non-owning
The most important question about any pointer is: who deletes the object? In modern C++ the answer is never "whoever holds a raw pointer". Objects are owned by containers, by local variables, or by smart pointers (lesson 29). A raw pointer only observes an object that someone else owns. Keeping to that rule eliminates most memory bugs.
Try it yourself
Write a function that returns a pointer to the cheapest product in a vector, or nullptr if the vector is empty.
cheapest: Eraser empty shop