Skip to main content
    Courses/C++/Pointers & References

    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

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

    Try it Yourself »
    C++
    #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.

    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"

    Worked example: address-of, dereference, nullptr

    See &, *, and nullptr in action — change values and re-run.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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".

    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.

    Worked example: reaching struct members with ->

    Compare (*p).name with the p->name shortcut.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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;
    
        
    ...

    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.

    Try it Yourself »
    C++
    #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.

    Try it Yourself »
    C++
    #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. Test if (p) { ... } or if (p != nullptr) before writing *p or p->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; set p = nullptr;, and never return a pointer to a local variable.
    • Memory leak (missing delete): every new without a matching delete leaks. The real fix isn't "remember harder" — it's std::unique_ptr, which deletes for you.
    • Double free: calling delete twice on the same pointer corrupts the heap and crashes. Each new gets exactly one delete; smart pointers guarantee this automatically.
    • Wrong delete form: memory from new[] must be freed with delete[] (not delete), 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 to shared_ptr only when ownership is truly shared.
    • 💡 Prefer make_unique / make_shared over raw new — they're exception-safe and read better.
    • 💡 Use const for "look but don't touch": const int* p lets you read through p but not modify the value it points at.

    📋 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

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

    Try it Yourself »
    C++
    #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, nullptr means "nothing"
    • ✅ Prefer references; use pointers when you need null or re-pointing
    • ✅ Reach struct members through a pointer with p->member
    • ✅ Raw new/delete (and new[]/delete[]) is leak- and crash-prone — avoid owning memory raw
    • ✅ Default to std::unique_ptr + make_unique; use std::shared_ptr only 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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service

    Install LearnCodingFast

    Learn faster with the app on your home screen.