Pointers & References

Reviewed & published by Brayan K

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.

Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 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.

#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 = " << score << endl;   // score = 99  (changed via alias!)

    // A reference MUST be initialised when declared, and can NEVER be
    // re-pointed at a different variable afterwards. It is locked to 'score'.
    int other = 7;
    alias = other;          // this does NOT re-bind — it copies other's VALUE
    cout << "score = " << score << endl;   // score = 7   (copied 7 into score)

    return 0;
}

// ✅ Expected output:
//    score = 50
//    alias = 50
//    score = 99
//    score = 7

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.

SymbolNameReads as
&xAddress-of"the address where x lives"
int* pPointer type"p is a pointer to an int"
*pDereference"the value p points at"
nullptrNull pointer"points at nothing"
#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.
    cout << "ptr:  " << ptr  << endl;      // the address (same as &age)
    cout << "*ptr: " << *ptr << endl;      // 25  (the value AT that address)

    // Writing through the pointer changes the original variable.
    *ptr = 30;
    cout << "age:  " << age  << endl;      // age:  30

    // Unlike a reference, a pointer CAN be re-pointed at something else.
    int height = 180;
    ptr = &height;          // ptr now points at 'height'
    cout << "*ptr: " << *ptr << endl;      // 180

    // nullptr = "points at nothing". Always safe to test before using.
    int* empty = nullptr;
    if (empty == nullptr)
        cout << "empty points at nothing yet" << endl;

    return 0;
}

// ⚠️ No expected-output panel for this one, on purpose.
// The addresses are different every single run — modern systems randomise
// where your program's memory lands. What stays true is that ptr prints
// the SAME address as &value, and that *ptr and value are the same number.
//
// One real run on the machine that builds this site printed:
//    value:   25
//    address: 0x7fffcfadc900
//    ptr:  0x7fffcfadc900
//    *ptr: 25
//    age:  30
//    *ptr: 180
//    empty points at nothing yet

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.

#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
    ___ = 4;                 // 👉 *p on the left-hand side changes lives

    cout << "after bonus, lives = " << lives << endl;

    // ✅ Expected output:
    //    lives = 3
    //    after bonus, lives = 4
    return 0;
}

🔎 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".

FeaturePointer 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*pr (automatic)
Reach a memberp->memberr.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.

#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's health through the pointer
    cout << hero.name << " HP: " << hero.health << endl;  // Aria HP: 75

    return 0;
}

// ✅ Expected output:
//    Aria
//    Aria
//    Aria HP: 75

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.

#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;

    // The catch: every 'new' needs a matching 'delete', and every
    // 'new[]' needs a matching 'delete[]'. Forget it and you LEAK memory.
    delete single;        // frees the single int
    delete[] scores;      // frees the array  (note the [] !)

    // After delete, the pointers are DANGLING. Don't use them.
    single = nullptr;     // good habit: null them out
    scores = nullptr;

    cout << "memory cleaned up" << endl;
    return 0;
}

// ✅ Expected output:
//    single = 42
//    scores[0] = 10
//    scores[1] = 20
//    scores[2] = 30
//    memory cleaned up

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.

#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 as " << hero->name << endl;   // -> works just like raw
    }   // <-- hero goes out of scope here: "Hero destroyed" prints automatically

    cout << "---" << endl;

    // shared_ptr — MANY owners share one object; freed when the LAST one leaves.
    shared_ptr<Player> a = make_shared<Player>("Boss");
    cout << "owners: " << a.use_count() << endl;       // owners: 1
    {
        shared_ptr<Player> b = a;                      // b shares ownership
        cout << "owners: " << a.use_count() << endl;   // owners: 2
    }   // <-- b leaves; Boss is NOT destroyed yet (a still owns it)
    cout << "owners: " << a.use_count() << endl;       // owners: 1

    cout << "end of main" << endl;
    return 0;   // "Boss destroyed" prints here as 'a' is cleaned up
}

// ✅ Expected output:
//    Hero created
//    playing as Hero
//    Hero destroyed
//    ---
//    Boss created
//    owners: 1
//    owners: 2
//    owners: 1
//    end of main
//    Boss destroyed

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.

#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 ->)
    acc___balance += 50;     // 👉 replace ___ with the  ->  arrow operator

    cout << acc->owner << " has " << acc->balance << endl;

    // ✅ Expected output:  Sam has 150
    // (no delete needed — acc frees Account automatically at end of main)
    return 0;
}

Common Errors (and the fix)

Pro Tips

📋 Quick Reference

TaskCodeNotes
Make a referenceint& r = x;alias; must init, can't re-bind
Make a pointerint* p = &x;stores x's address
Read the value*pdereference
Empty pointerint* p = nullptr;points at nothing; test first
Member via pointerp->namesame as (*p).name
Heap, single (raw)new int(5) / delete pmust pair them
Heap, array (raw)new int[3] / delete[] pnote 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

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.

#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 output:
    //    Rex is hungry (8)
    //    After feeding: 3
    //
    // Remember: NO new / delete — the unique_ptr cleans up for you.

    // your code here

    return 0;
}

🎉 Lesson Complete

Practice quiz

What is a reference (int& alias = score;)?

  • A copy of score stored elsewhere
  • A pointer to score that can be re-pointed
  • A second name (alias) for the same variable in memory
  • A null placeholder until assigned

Answer: A second name (alias) for the same variable in memory. A reference is an alias: alias and score are the same box in memory, so changing one changes both.

Which two rules make references safe?

  • They must be initialised when declared and can never be re-bound
  • They can be null and must be const
  • They live on the heap and are freed automatically
  • They can only refer to integers

Answer: They must be initialised when declared and can never be re-bound. A reference must be initialised at declaration and can never be reseated to a different variable afterwards.

What does the & operator do in the expression &age?

  • Dereferences age
  • Declares a reference
  • Bitwise-ANDs age with itself
  • Gives the address in memory where age lives

Answer: Gives the address in memory where age lives. In an expression, &age is 'address-of' — it yields the memory address of age.

Given int age = 25; int* ptr = &age; *ptr = 30;, what is age afterwards?

  • 25
  • 30
  • 0
  • Undefined

Answer: 30. Writing through the pointer (*ptr = 30) changes the original variable, so age becomes 30.

What does p->name mean for a pointer p to a struct?

  • Shorthand for (*p).name — dereference then access the member
  • The address of name
  • A new copy of name
  • It only works on smart pointers

Answer: Shorthand for (*p).name — dereference then access the member. The arrow -> is sugar for (*p).name: dereference the pointer, then access the member.

When should you prefer a pointer over a reference?

  • Always — pointers are faster
  • When the target always exists and never changes
  • When you need 'might point at nothing' (nullptr) or 'can point at different things over time'
  • Only inside classes

Answer: When you need 'might point at nothing' (nullptr) or 'can point at different things over time'. Use a reference by default; reach for a pointer when you genuinely need null or re-pointing.

What must every new int(...) be paired with, and every new int[...]?

  • delete for both
  • delete for new, delete[] for new[]
  • free() for both
  • Nothing — they free automatically

Answer: delete for new, delete[] for new[]. A single new pairs with delete; an array new[] pairs with delete[]. Mixing them is undefined behaviour.

Which smart pointer has exactly one owner and frees the object when it goes out of scope?

  • std::shared_ptr
  • std::weak_ptr
  • a raw pointer
  • std::unique_ptr

Answer: std::unique_ptr. unique_ptr is single-ownership and the lightweight default; it deletes automatically with no manual delete.

In the smart-pointer example, after shared_ptr<Player> b = a; inside a block, what does a.use_count() report, and after the block?

  • 1 inside, 0 after
  • 2 inside, 1 after
  • 2 inside, 2 after
  • It always stays 1

Answer: 2 inside, 1 after. Copying the shared_ptr raises the count to 2; when b leaves scope it drops back to 1, and the object lives on with a.

Why prefer nullptr over NULL or 0 for a null pointer?

  • nullptr is faster at run time
  • NULL and 0 are compile errors in C++
  • nullptr is a proper null-pointer type, so the compiler distinguishes 'no pointer' from the integer 0
  • nullptr automatically frees memory

Answer: nullptr is a proper null-pointer type, so the compiler distinguishes 'no pointer' from the integer 0. nullptr (C++11) has a real null-pointer type, avoiding the overload-resolution confusion of the old integer 0/NULL.

Continue this course

Frequently asked questions

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'.

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.

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.

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.

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.

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.