Lesson 9 of 13

Pointers, References & Memory

Learn the three ways to refer to a value in C++ (by copy, by and by pointer) and what happens on the and the heap.

C++
#include <iostream>

int main() {
    int x = 42;

    int& ref = x;        // reference: another name for x. Cannot be null or rebound.
    int* ptr = &x;       // pointer: holds the ADDRESS of x. Can be null, can move.

    ref = 10;            // writes through the reference
    *ptr = 20;           // dereference then write
    std::cout << x << "\n";              // 20

    std::cout << ptr << "\n";            // the address itself
    std::cout << *ptr << "\n";           // the value at that address

    int y = 99;
    ptr = &y;            // pointers can be reseated
    // ref = y;          // does NOT rebind: this assigns y's VALUE into x

    int* nothing = nullptr;              // use nullptr, never NULL or 0
    if (nothing) std::cout << *nothing;  // always check before dereferencing
}
C++
#include <iostream>
#include <string>
#include <vector>

// Parameter passing decides who copies what.
void byValue(std::vector<int> v)        { v.push_back(1); }   // copies the whole vector
void byRef(std::vector<int>& v)         { v.push_back(1); }   // modifies the caller's
void byConstRef(const std::vector<int>& v) { std::cout << v.size(); }  // no copy, no writes
void byPointer(std::vector<int>* v)     { if (v) v->push_back(1); }    // may be null

// Rule: pass small things (int, double, pointers) by value.
// Pass anything bigger by const& unless you intend to modify it.

struct Point { double x, y; };

// Returning references to locals is a classic disaster:
// int& broken() { int local = 5; return local; }   // dangling reference, UB

int main() {
    std::vector<int> nums{1, 2, 3};

    byValue(nums);      std::cout << nums.size() << "\n";   // 3 — unchanged
    byRef(nums);        std::cout << nums.size() << "\n";   // 4
    byPointer(&nums);   std::cout << nums.size() << "\n";   // 5

    // Range-for: & to modify, const& to read cheaply
    for (int& n : nums) n *= 2;
    for (const int& n : nums) std::cout << n << ' ';
}
C++
#include <iostream>

// The stack: automatic, fast, freed when the scope ends, limited in size.
// The heap:  manual (or smart-pointer managed), large, survives the scope.

void stackDemo() {
    int arr[1000];          // on the stack, gone at the closing brace
    arr[0] = 1;
}   // freed automatically

void rawHeapDemo() {
    int* single = new int(42);          // one int on the heap
    int* many   = new int[1000];        // an array on the heap

    std::cout << *single << " " << many[0] << "\n";

    delete single;      // every new needs exactly one delete
    delete[] many;      // array form MUST use delete[]

    // The three ways this goes wrong:
    //   forget delete        -> memory leak
    //   delete twice         -> double free, undefined behaviour
    //   use after delete     -> dangling pointer, undefined behaviour
}

int main() {
    stackDemo();
    rawHeapDemo();
    // In modern C++ you almost never write new/delete by hand — the next
    // lesson replaces them with std::unique_ptr and std::vector.
}
Finished reading?Mark it done to track your progress.

Tip: use the ← and → keys to move between lessons.