Lesson 8 • Intermediate
Pointers & References
By the end of this lesson you'll be able to alias a variable with a reference, store and follow a memory address with a pointer, reach into a struct with ->, and manage heap memory the modern way — with smart pointers that clean up after themselves so your programs never leak.
What You'll Learn
- Create a reference (int&) as an alias that must be initialised
- Use a pointer (int*) with & (address-of) and * (dereference)
- Choose between a pointer and a reference for the job
- Reach struct members through a pointer with the -> operator
- Allocate heap memory with new/delete — and why to avoid owning it raw
- Default to smart pointers: unique_ptr, make_unique, and shared_ptr
struct. Everything about addresses and memory, we'll build here.💡 Real-World Analogy
Think of your computer's memory as a street of houses, each with a numbered address. A variable is a house with stuff inside it.
A reference is a nickname for one specific house — "Mum's place" always means the same house, you can't later point it at a different one, and there's no such thing as a nickname for a house that doesn't exist. That's why a reference must be initialised and can never be null.
A pointer is a sticky note with a house address written on it. You can read the address, follow it to visit the house (dereference), scribble a new address to point somewhere else, or leave it blank (nullptr). More flexible than a nickname — and easier to get wrong, because a blank or stale note leads nowhere good.
1. References — a Second Name for a Variable
A reference is an alias: a second name for a variable that already exists. You write it with & in the type — int& alias = score; — and from then on alias and score are the exact same box in memory. Two rules make references safe: a reference must be initialised the moment you declare it, and it can never be re-bound to a different variable afterwards. Read this, run it, and watch how changing the alias changes the original.
Worked example: references as aliases
Read every comment, run it, and check the output matches.
#include <iostream>
using namespace std;
int main() {
int score = 50;
// A reference is a second NAME for an existing variable (an "alias").
// Pattern: type& aliasName = existingVariable;
int& alias = score; // 'alias' and 'score' are now the SAME box
cout << "score = " << score << endl; // score = 50
cout << "alias = " << alias << endl; // alias = 50
// Change one, you change BOTH — they are the same memory.
alias = 99;
cout << "score = " << sco
...2. Pointers — Storing an Address
A pointer is a variable that stores a memory address instead of a plain value. Three pieces of syntax do all the work: &x reads "the address of x", int* p declares "p is a pointer to an int", and *p reads "the value p points at" (called dereferencing). A pointer that points at nothing holds nullptr — always safe to test before you follow it.
| Symbol | Name | Reads as |
|---|---|---|
| &x | Address-of | "the address where x lives" |
| int* p | Pointer type | "p is a pointer to an int" |
| *p | Dereference | "the value p points at" |
| nullptr | Null pointer | "points at nothing" |
Worked example: address-of, dereference, nullptr
See &, *, and nullptr in action — change values and re-run.
#include <iostream>
using namespace std;
int main() {
int age = 25;
// & = "address-of": where in memory does 'age' live?
cout << "value: " << age << endl; // value: 25
cout << "address: " << &age << endl; // address: 0x7ff... (varies)
// A pointer is a variable that STORES an address.
// Pattern: type* name = &variable;
int* ptr = &age; // ptr holds the address of 'age'
// * = "dereference": follow the pointer to the value it points at.
c
...Your turn. The program below is almost complete — fill in the three blanks marked ___ using the hints in the comments, then run it and check your output against the expected lines.
🎯 Your turn: make and follow a pointer
Fill in the ___ blanks for address-of and dereference, then run.
#include <iostream>
using namespace std;
int main() {
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
int lives = 3;
// 1) Make a pointer called "p" that holds the ADDRESS of lives
int* p = ___; // 👉 use the & (address-of) operator on lives
// 2) Print the VALUE that p points at (dereference it)
cout << "lives = " << ___ << endl; // 👉 put a * in front of p
// 3) Give the player an extra life by writing THROUGH the pointer
___
...🔎 Deep Dive: Pointer vs Reference
They both let you work with another variable indirectly, so when do you use which? A reference is the safer default — use it when the target always exists and never needs to change. A pointer earns its keep when you need "maybe nothing" (nullptr) or "different things over time".
| Feature | Pointer int* | Reference int& |
|---|---|---|
| Can be null | ✅ Yes (nullptr) | ❌ No |
| Must be initialised | ❌ No | ✅ Yes |
| Can be re-pointed | ✅ Yes | ❌ No (locked at birth) |
| Access the value | *p | r (automatic) |
| Reach a member | p->member | r.member |
3. Pointers to Structs — the -> Operator
When you have a pointer to a struct, reaching a member is so common that C++ gives it a shortcut. Instead of writing (*p).name — dereference first, then use the dot — you write p->name. The arrow does both steps at once and reads much more cleanly. You'll use -> constantly, including with the smart pointers coming up next.
Worked example: reaching struct members with ->
Compare (*p).name with the p->name shortcut.
#include <iostream>
#include <string>
using namespace std;
struct Player {
string name;
int health;
};
int main() {
Player hero{"Aria", 100};
// pointer to a struct
Player* p = &hero;
// Two ways to reach a member through a pointer:
cout << (*p).name << endl; // Aria — dereference, THEN .member
cout << p->name << endl; // Aria — the -> shortcut (preferred)
// p->health is just sugar for (*p).health
p->health -= 25; // change hero
...4. Dynamic Memory — new, delete, and Why It's Risky
So far every variable has lived on the stack and vanished automatically at the end of its scope. Sometimes you need memory that outlives the current function — that's the heap, and you ask for it with new. The price: every new needs a matching delete, and every new[] needs a matching delete[]. Miss one and you leak memory; get the pairing wrong and you crash. This is the manual way — study it, then meet the tool that makes it obsolete.
Worked example: new / delete and new[] / delete[]
See heap allocation and the matching cleanup you must remember.
#include <iostream>
using namespace std;
int main() {
// 'new' asks the OS for memory that OUTLIVES the current scope and
// hands back a pointer to it. YOU now own that memory.
int* single = new int(42); // one int on the heap
cout << "single = " << *single << endl; // single = 42
// 'new[]' allocates an ARRAY on the heap.
int* scores = new int[3]{10, 20, 30};
for (int i = 0; i < 3; i++)
cout << "scores[" << i << "] = " << scores[i] << endl;
...new/delete demands: the right delete form, exactly once, on every path out of the function — even when an exception is thrown. That's a lot to get right by hand, which is exactly why modern C++ hands the job to smart pointers.5. Smart Pointers — the Modern Default
A smart pointer owns heap memory and frees it automatically when it goes out of scope — no delete to remember, ever. They live in <memory>. Use std::unique_ptr for single ownership and create it with std::make_unique; it's the lightweight default you should reach for first. Use std::shared_ptr (made with std::make_shared) only when several parts of your program must share one object — it counts owners and frees the object when the last one leaves. You still use -> to reach members, exactly like a raw pointer.
Worked example: unique_ptr and shared_ptr
Watch the destructors fire automatically — no delete anywhere.
#include <iostream>
#include <memory> // unique_ptr, shared_ptr, make_unique, make_shared
#include <string>
using namespace std;
struct Player {
string name;
Player(string n) : name(n) { cout << name << " created" << endl; }
~Player() { cout << name << " destroyed" << endl; }
};
int main() {
// unique_ptr — ONE owner, frees itself automatically. No delete needed.
{
unique_ptr<Player> hero = make_unique<Player>("Hero");
cout << "playing
...Now you try. Fill in the two blanks below to create a std::unique_ptr and reach a member through it. There is no delete to write — that's the whole point.
🎯 Your turn: own an object with unique_ptr
Declare the unique_ptr type and use -> to update a member, then run.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Account {
string owner;
int balance;
};
int main() {
// 🎯 YOUR TURN — fill in the blanks marked with ___
// 1) Create a unique_ptr<Account> called "acc" using make_unique.
// Initialise it with owner "Sam" and balance 100.
___ acc = make_unique<Account>(Account{"Sam", 100});
// 👉 the type is unique_ptr<Account>
// 2) Add 50 to the balance THROUGH the smart pointer (use ->
...Common Errors (and the fix)
- Dereferencing
nullptr(Segmentation fault): following a pointer that points at nothing crashes instantly. Testif (p) { ... }orif (p != nullptr)before writing*porp->member. - Dangling pointer / use-after-free: using a pointer after the thing it pointed at was deleted or went out of scope reads garbage. After
delete p;setp = nullptr;, and never return a pointer to a local variable. - Memory leak (missing
delete): everynewwithout a matchingdeleteleaks. The real fix isn't "remember harder" — it'sstd::unique_ptr, which deletes for you. - Double free: calling
deletetwice on the same pointer corrupts the heap and crashes. Eachnewgets exactly onedelete; smart pointers guarantee this automatically. - Wrong delete form: memory from
new[]must be freed withdelete[](notdelete), and vice versa. Mixing them is undefined behaviour.
Pro Tips
- 💡 Reference first, pointer when you must: reach for
int&unless you genuinely need null or re-pointing. - 💡 Default to
unique_ptr: it's free of overhead and covers most ownership. Upgrade toshared_ptronly when ownership is truly shared. - 💡 Prefer
make_unique/make_sharedover rawnew— they're exception-safe and read better. - 💡 Use
constfor "look but don't touch":const int* plets you read throughpbut not modify the value it points at.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Make a reference | int& r = x; | alias; must init, can't re-bind |
| Make a pointer | int* p = &x; | stores x's address |
| Read the value | *p | dereference |
| Empty pointer | int* p = nullptr; | points at nothing; test first |
| Member via pointer | p->name | same as (*p).name |
| Heap, single (raw) | new int(5) / delete p | must pair them |
| Heap, array (raw) | new int[3] / delete[] p | note the [] |
| Single owner (modern) | std::unique_ptr<T> = make_unique<T>(...) | auto-frees, no delete |
| Shared owners (modern) | std::shared_ptr<T> = make_shared<T>(...) | freed at last owner |
Frequently Asked Questions
Q: When should I use a reference instead of a pointer?
Reach for a reference whenever the thing you are referring to always exists and never changes — for example a function parameter you want to modify in place, like void scale(int& n). References cannot be null and cannot be reseated, so they remove a whole class of bugs. Use a pointer only when you genuinely need 'might point at nothing' (nullptr) or 'can point at different things over time'.
Q: What is the difference between * in a declaration and * when dereferencing?
They look the same but do opposite jobs. In a declaration, int* p means 'p is a pointer to int' — the * is part of the type. In an expression, *p means 'follow p and give me the value it points at' — the * is the dereference operator. Same symbol, two roles, decided by where it appears.
Q: Why should I avoid raw new and delete in modern C++?
Because it is too easy to get wrong: forget the delete and you leak memory, delete twice and you crash (double free), or use the pointer after deleting and you read garbage (use-after-free). std::unique_ptr and std::shared_ptr delete automatically and exactly once, so the whole category of leaks and double frees disappears. Raw new/delete is mostly for learning what the smart pointers do for you.
Q: What is the difference between unique_ptr and shared_ptr?
A unique_ptr has exactly one owner — when it goes out of scope the object is freed. It is the lightweight default; use it unless you have a reason not to. A shared_ptr lets several owners share one object and keeps a reference count; the object is freed only when the last shared_ptr is gone. Sharing has a small runtime cost, so prefer unique_ptr and upgrade to shared_ptr only when ownership really is shared.
Q: Why do I write -> sometimes and . other times?
Use the dot . when you have the object itself (hero.name) and the arrow -> when you have a pointer or smart pointer to it (p->name). p->name is simply a readable shorthand for (*p).name: dereference the pointer, then access the member.
Q: Is nullptr the same as NULL or 0?
Prefer nullptr. NULL and 0 are old C-style spellings that are really just the integer zero, which can confuse overload resolution and type checking. nullptr (added in C++11) is a proper null-pointer type, so the compiler can tell a 'no pointer' from the number 0. Always initialise unused pointers to nullptr.
Mini-Challenge: Feed the Pet
No blanks this time — just a brief and an outline to keep you on track. Build it, run it, and check your output against the example in the comments. Owning the Pet with a unique_ptr means you never write a single delete.
🎯 Mini-Challenge: own and update a Pet with unique_ptr
Create the smart pointer yourself and update a member with ->.
#include <iostream>
#include <memory>
#include <string>
using namespace std;
struct Pet {
string name;
int hunger; // higher = hungrier
};
int main() {
// 🎯 MINI-CHALLENGE: Feed the pet
// 1. Make a unique_ptr<Pet> called "pet" with name "Rex" and hunger 8
// (use make_unique).
// 2. Print "Rex is hungry (8)" using the -> operator.
// 3. "Feed" Rex by subtracting 5 from hunger through the pointer.
// 4. Print "After feeding: 3".
//
// ✅ Expected ou
...🎉 Lesson Complete
- ✅ A reference (
int&) is an alias — must be initialised, can't be null, can't be re-bound - ✅ A pointer (
int*) stores an address;&gets it,*follows it,nullptrmeans "nothing" - ✅ Prefer references; use pointers when you need null or re-pointing
- ✅ Reach struct members through a pointer with
p->member - ✅ Raw
new/delete(andnew[]/delete[]) is leak- and crash-prone — avoid owning memory raw - ✅ Default to
std::unique_ptr+make_unique; usestd::shared_ptronly for shared ownership - ✅ Next lesson: Object-Oriented Programming — organise code with classes and objects
Sign up for free to track which lessons you've completed and get learning reminders.