Pointers
Lesson 10 covered references: another name for a variable. This lesson covers C++'s other way of "pointing at" something: the pointer. A pointer is a variable too, but what it holds isn't a score or a price; it's the address of another variable in memory.
1#include <iostream>2 3int main() {4 int score = 80;5 int* p = &score;6 std::cout << *p << '\n';7 *p = 95;8 std::cout << score << '\n';9 return 0;10}
80 95
int* p reads "p is a pointer to an int". &score takes the address of score and stores it in p. *p means "the variable found by following the address in p", which is score, so *p = 95 changes score. Press Step: after line 5, an arrow appears from p to score.
Addresses
Memory is like a very, very long row of boxes, each with a number. That number is an address. Every variable lives in a few of those boxes, and &variable asks "what number do you live at?"
You can show an address: std::cout << &score shows a hex number like 0x7ffd3a2c. But it differs on every run (the system puts the program somewhere different each time), and it isn't much use for writing programs, so this course doesn't show it and looks at the arrows instead. What does matter: if two pointers hold the same address, they point to the same variable.
1#include <iostream>2 3int main() {4 int apples = 5;5 int pears = 5;6 int* a = &apples;7 int* b = &apples;8 int* c = &pears;9 std::cout << (a == b) << '\n';10 std::cout << (a == c) << '\n';11 std::cout << (*a == *c) << '\n';12 return 0;13}
1 0 1
a == b compares addresses: both are the address of apples, so it's 1. a == c is 0: apples and pears live in different places, even though both hold 5. To compare the numbers inside, write *a == *c.
Note: The same symbol means different things in different places. In a declaration, the * in int* p says "p is a pointer"; in use, the * in *p says "follow the address". Same with &: int& r is a reference, &score takes an address.
Changing through a pointer
As with references, pass a pointer to a function and the function can change the variable outside. The difference: the call needs a &, and the function needs a *:
1#include <iostream>2 3void add_stock(int* count, int amount) {4 *count += amount;5}6 7int main() {8 int stock = 12;9 add_stock(&stock, 8);10 add_stock(&stock, 5);11 std::cout << "Stock: " << stock << '\n';12 return 0;13}
Stock: 25
Press Step into add_stock: an arrow goes from count in this layer to stock in main. *count += amount follows the arrow to add.
The call says &stock, so you can see at a glance that "this function might change stock". That's one upside of pointer parameters; still, in modern C++ you usually change a caller's variable with a reference, which reads more cleanly.
nullptr: pointing nowhere
Sometimes a pointer shouldn't point to any variable yet. Give it nullptr (the null pointer), and ask p != nullptr before using it:
1#include <iostream>2#include <vector>3 4int main() {5 int best = 0;6 int* found = nullptr;7 std::vector<int> scores{72, 58, 91, 66};8 for (std::size_t i = 0; i < scores.size(); i++) {9 if (scores[i] > 90) {10 found = &scores[i];11 }12 }13 if (found != nullptr) {14 best = *found;15 }16 std::cout << "Over 90: " << best << '\n';17 return 0;18}
Over 90: 91
found starts as nullptr, meaning "not found yet". Once found, it points to that element. Following a null pointer doesn't work; there's nothing there:
1#include <iostream>2 3int main() {4 int* found = nullptr;5 std::cout << *found << '\n';6 return 0;7}
load of null pointerWhat happened: Stopped at line 5. This pointer is nullptr: nothing can be read or written through it.
A real program will most likely crash here (on Linux it's a Segmentation fault), or something stranger may happen: it's undefined behavior. Our engine stops at line 5; press "see how it went wrong".
Tip: Older code often uses NULL or 0 as the null pointer. Since C++11, use nullptr: it can only be a pointer, so it never gets mixed up with the number 0.
Pointers with no value
Declare a pointer without giving it a value, and it holds some address from nowhere, pointing who knows where. Writing through it is worse than a null pointer:
1#include <iostream>2 3int main() {4 int count = 3;5 int* p;6 *p = count;7 std::cout << *p << '\n';8 return 0;9}
use of uninitialized valueWhat happened: Stopped at line 6. A variable was used before it was given a value. In real C++ it would hold garbage, different on every run.
A real program may crash, or quietly overwrite some other variable and go wrong somewhere else much later. The engine stops at line 6. So give a pointer a value when you declare it: a variable to point to, or = nullptr for now.
Pointing somewhere else
Once bound, a reference can't change; a pointer can. Just give it a new address:
What does this program show?
1#include <iostream>2 3int main() {4 int a = 3;5 int b = 8;6 int* p = &a;7 p = &b;8 *p = 100;9 std::cout << a << " " << b << '\n';10 return 0;11}
p = &b changes the address p itself holds: from then on it points to b. *p = 100 follows the arrow and changes b; a is still 3. (Had line 7 been *p = b, that would store 8 in a.)
Pointing to objects: ->
A pointer can point to an object too. To use a member, you can write (*p).name, parentheses required; more common is the arrow p->name, which means the same:
1#include <iostream>2#include <string>3 4struct Product {5 std::string name;6 double price;7};8 9void discount(Product* item, double rate) {10 item->price = item->price * rate;11}12 13int main() {14 Product milk{"Milk", 6.5};15 Product* p = &milk;16 std::cout << (*p).name << '\n';17 discount(p, 0.8);18 std::cout << p->name << ": $" << milk.price << '\n';19 return 0;20}
Milk Milk: $5.2
Press Step: p and the function's item both point to the same milk, so item->price changes milk.price. new and smart pointers, in the next two lessons, both give you pointers, so you'll use -> all the time.
There's one more arrow you've been using without seeing it: inside a member function there's an invisible pointer, this, pointing to the object the function was called on. *this is that object itself; the balance_ you write in a member function is really this->balance_:
1#include <iostream>2 3class Account {4public:5 Account(int balance) : balance_(balance) {}6 7 void deposit(int amount) {8 this->balance_ += amount;9 balance_ += amount;10 }11 12 int balance() const {13 return balance_;14 }15 16private:17 int balance_;18};19 20int main() {21 Account acc{100};22 acc.deposit(30);23 std::cout << acc.balance() << '\n';24 return 0;25}
160
In acc.deposit(30), this points to acc. Lines 9 and 10 do the same thing, written twice, so 30 goes in twice and it shows 160. Normally you just write balance_; this is the this and *this from lessons 15 and 17.
Pointers vs references
Reference int& r | Pointer int* p | |
|---|---|---|
| When declared | must be bound | can start as nullptr |
| Can it change? | no | yes: p = &b |
| Can it be empty? | no | yes: nullptr |
| How to use | just r | *p, p->name |
In short: use a reference when you can; use a pointer when "there might be none" or "it'll point elsewhere later".
1#include <iostream>2#include <string>3 4void show_owner(const std::string* name) {5 if (name == nullptr) {6 std::cout << "No owner\n";7 } else {8 std::cout << "Owner: " << *name << '\n';9 }10}11 12int main() {13 std::string owner = "Lin";14 show_owner(&owner);15 show_owner(nullptr);16 return 0;17}
Owner: Lin No owner
const std::string* is "a pointer to a std::string that can't be changed"; like a const reference, it only reads. A pointer is used here because "no owner" is a real possibility, and a reference can't say "none".
Practice
Write the code, then press Check. A hint costs nothing; the answer is there when you need it.
Add one through the arrow
✓Don't write visits directly; add 1 to visits only through the pointer p. The program should show 11.
11
1#include <iostream>2 3int main() {4 int visits = 10;5 int* p = &visits;6 // add 1 through p here7 std::cout << visits << '\n';8 return 0;9}
Point at the cheapest
✓Make cheapest point to the smallest element of prices, then set it to 0 through cheapest. The program should show 12 0 9.
12 0 9
1#include <iostream>2#include <vector>3 4int main() {5 std::vector<int> prices{12, 5, 9};6 int* cheapest = &prices[0];7 for (std::size_t i = 1; i < prices.size(); i++) {8 // if it's smaller than the one pointed to, point to it9 }10 *cheapest = 0;11 std::cout << prices[0] << " " << prices[1] << " " << prices[2] << '\n';12 return 0;13}
Fix it: no coupon
✓With no coupon, coupon is nullptr, yet the program follows it. Change pay: subtract the coupon's value if there is one, otherwise don't. The program should show 80 and 100 on two lines.
80 100
1#include <iostream>2 3int pay(int price, const int* coupon) {4 return price - *coupon;5}6 7int main() {8 int twenty = 20;9 std::cout << pay(100, &twenty) << '\n';10 std::cout << pay(100, nullptr) << '\n';11 return 0;12}
What you learned
- A pointer is a variable holding an address:
int* p = &score;.&takes the address;*pfollows it to the variable. - Addresses differ on every run, so we don't usually show them;
p == qcompares addresses,*p == *qthe values inside. - A pointer parameter lets a function change the caller's variable: call with
&stock, use*countinside. nullptrpoints nowhere; check before using it. Following a null or uninitialized pointer is undefined behavior (the engine stops).- A pointer can be pointed elsewhere (
p = &b); a reference can't. - For objects,
p->nameis the same as(*p).name. Inside a member function,thispoints to the object itself;balance_isthis->balance_. - Use a reference when you can; use a pointer for "there might be none" or "it will change".