Pointers & Dynamic Memory

Your gateway to low-level power in C++

Every variable has an address. Pointers let you use it.

🏠 What Is Memory? The Big Picture

RAM is like a giant apartment building — every byte has a unique address.

📦 Key Concepts

  • Address — the unique number identifying each byte
  • Value — data stored at that address
  • Stack — local variables, auto-managed
  • Heap — manual memory, you control it
💡 Analogy: Stack = your desk (auto-cleared when you leave). Heap = a storage locker (you must remember to clean it out).

🔬 How Variables Are Stored

Each variable type takes a different amount of space in memory.

int x = 5;       // 4 bytes
char c = 'A';     // 1 byte
double d = 3.14;  // 8 bytes
bool b = true;    // 1 byte
Step 0 / 4 — Click Next to see each variable stored
💡 sizeof: Use sizeof(int) to check how many bytes a type uses on your system. It varies by platform!

🎯 Your First Pointer

A pointer stores the address of another variable. That's it!

int x = 42;          // a normal integer
int* p = &x;        // p stores the ADDRESS of x

cout << x;           // 42  (the value)
cout << &x;          // 0x1004  (the address)
cout << p;           // 0x1004  (same address!)
cout << *p;          // 42  (follow the pointer)
🗺️ Treasure map analogy: x is the treasure. p is a map showing where to find it. *p means "follow the map and grab the treasure."

🤔 Why Do We Need Pointers?

Three key reasons pointers are essential in C++:

📦

1. Avoid Expensive Copies

// BAD: copies entire vector!
void process(vector<int> v);

// GOOD: just passes an address
void process(vector<int>* v);

A pointer is always 8 bytes. The data could be gigabytes.

🔗

2. Dynamic Data Structures

struct Node {
    int data;
    Node* next;  // → next node
};

Linked lists, trees, graphs — all built with pointers connecting nodes.

🎭

3. Polymorphism

Shape* s = new Circle();
s->draw();  // calls Circle::draw

s = new Square();
s->draw();  // calls Square::draw

Base pointer → derived object. Enables runtime flexibility.

📝 Declaring & Using Pointers

Step through the code and watch the memory diagram update.

// Step 1: Declare an int
int val = 10;

// Step 2: Declare a pointer
int* ptr;

// Step 3: Point to val
ptr = &val;

// Step 4: Read through pointer
cout << *ptr;  // prints 10
Step 0 / 4 — Click Next to start

⚠️ Pointer Syntax Gotchas

These traps catch almost every beginner. Learn them now!

Trap 1: Multiple Declarations

int* p, q;  // SURPRISE!
// p is int*
// q is just int (NOT a pointer!)

// Correct way:
int *p, *q;  // both pointers

The * binds to the variable name, not the type. Best practice: one declaration per line.

Trap 2: Style Wars

int* p;  // "pointer to int" style
int *p;  // "p is a pointer" style
int * p; // also valid

// All three are identical to the
// compiler! Pick one and be consistent.

We'll use int* p in this course (groups the * with the type).

Trap 3: const Pointer Combos

const int* p;      // can't change *p
int* const p = &x; // can't change p
const int* const p = &x;
                    // can't change either!

Read right-to-left: "p is a const pointer to const int"

📏 Tip: Read pointer declarations right to left: const int* const p → "p is a const pointer to a const int."

🎮 The Dereference Operator (*)

Use *p to read or write the value at the address p points to.

int x = 42;
int* p = &x;

*p = 99;     // changes x through p!
cout << x;   // prints 99
📺 Remote control analogy: The pointer is a remote control. Pressing buttons (*p = 99) changes the TV (the original variable), not the remote itself.

🔄 Pointers & Function Parameters

Pass-by-value copies. Pass-by-pointer lets the function modify the original.

❌ Broken Swap (by value)

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
} // only local copies changed!

✅ Working Swap (by pointer)

void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
} // modifies the originals!
swap(&x, &y); // pass addresses
Step 0 / 4 — Watch the swap happen on the stack

🔢 Pointer Arithmetic & Arrays

When a pointer points to an array, p++ moves to the next element (not the next byte!).

int arr[5] = {10, 20, 30, 40, 50};
int* p = arr;     // points to arr[0]

cout << *p;       // 10
p++;               // now points to arr[1]
cout << *p;       // 20
cout << *(p+2);   // 40  (arr[3])
Pointer at index 0 — Click p++ to advance

🧮 Pointer Arithmetic Deep Dive

p + 1 doesn't add 1 byte — it adds sizeof(*p) bytes!

int* ip = (int*)0x1000;
ip + 1;  // → 0x1004 (moved 4 bytes)
ip + 2;  // → 0x1008 (moved 8 bytes)

char* cp = (char*)0x1000;
cp + 1;  // → 0x1001 (moved 1 byte)
cp + 2;  // → 0x1002 (moved 2 bytes)

double* dp = (double*)0x1000;
dp + 1;  // → 0x1008 (moved 8 bytes)
🧠 Why? The compiler automatically scales pointer arithmetic by the element size. This lets you treat memory as an array of typed elements, not raw bytes.

🚫 nullptr — The Safe Empty Pointer

A pointer that doesn't point anywhere should be set to nullptr.

int* p = nullptr;  // points to nothing

if (p != nullptr) {
    cout << *p;     // safe — only dereference if valid
}

*p = 5;             // 💥 CRASH! Undefined behavior!
🏠 Analogy: nullptr is like having a GPS with "no destination." If you try to drive there, you crash.

🔀 References vs Pointers

Both let you access another variable, but they have key differences.

Pointer

int x = 10;
int* p = &x;
*p = 20;
p = &y; // can reassign
// can be nullptr

Reference

int x = 10;
int& r = x;
r = 20; // no * needed
// CANNOT reassign
// CANNOT be null

⚡ When to Use Which?

  • Reference — default choice, simpler syntax
  • Pointer — when you need nullptr or reassignment
  • Pointer — for dynamic memory (new/delete)
  • Pointer — for data structures (linked lists, trees)

🔄 Pass by Reference vs Pointer

Both avoid copying. When should you use each?

By Reference

void print(const string& s) {
    cout << s;
}
void modify(int& x) {
    x = 42;
}
modify(myVar); // clean syntax
  • ✅ Cleaner syntax (no * or &)
  • ✅ Can never be null
  • ✅ Use const& for read-only

By Pointer

void maybeModify(int* p) {
    if (p) *p = 42;
}
maybeModify(&x); // explicit
maybeModify(nullptr); // optional
  • ✅ Can be nullptr (optional param)
  • ✅ Can reassign to different object
  • ✅ Required for dynamic memory

🗺️ Decision Guide

  • 📖 Read-only, no null: const T&
  • ✏️ Modify, no null: T&
  • Optional/nullable: T*
  • 🆕 Heap/ownership: T* or smart ptr
💡 Modern C++ tip: Prefer references by default. Use pointers only when you need nullptr or dynamic memory.

🧩 Challenge A: Pointer Tracing

Predict what each snippet prints. Watch out for aliasing!

Snippet 1

int a = 5, b = 10;
int* p = &a;
*p = 20;
p = &b;
*p = 30;
cout << a << " " << b;

Snippet 2

int x = 7;
int* p1 = &x;
int* p2 = p1;
*p2 = 42;
cout << x << " " << *p1;

Snippet 3

int a = 1, b = 2;
int* p = &a;
int* q = &b;
*p = *q;
cout << a << " " << b;
cout << " " << (p==q);

✏️ Exercise 1: Pointer Basics

Time for hands-on practice! (~25 min)

What you'll do:

Open Exercise 1 →

🏗️ Stack vs Heap — Where Things Live

Two regions of memory with very different lifetimes and rules.

Step 0 / 5 — Click Next to simulate function calls

📚 Stack

  • ✅ Automatic (compiler manages)
  • ✅ Fast allocation
  • ⚠️ Fixed size, small (~1MB)
  • ⚠️ Dies when scope ends

🗄️ Heap

  • ✅ Huge (GBs available)
  • ✅ Lives until you free it
  • ⚠️ Manual — you must delete
  • ⚠️ Slower allocation
💡 Rule of thumb: Use the stack by default. Use the heap only when you need data to outlive the current scope or when you need large/variable-size allocations.

📚 Stack Frames In Detail

Each function call creates a stack frame with its parameters, local variables, and return address.

int add(int a, int b) {
    int result = a + b;
    return result;
}
int main() {
    int x = 3;
    int y = add(x, 4);
}
Step 0 / 4 — Watch stack frames push and pop

🚧 When Stack Isn't Enough

Three scenarios where you must use the heap:

Unknown Size at Compile Time

int n;
cin >> n;
// int arr[n]; ← NOT standard C++!
int* arr = new int[n]; // ✅

Stack arrays need compile-time constant sizes. Heap arrays can be any size.

🕰️

Data Outlives the Function

int* create() {
    int* p = new int(42);
    return p; // ✅ survives!
}
// (can't return &localVar)

Heap data persists until you explicitly free it.

🐘

Very Large Allocations

// Stack: ~1-8 MB (crash!)
// int huge[10000000];

// Heap: GBs available
int* huge = new int[10000000]; // ✅

Stack overflow is a real thing! Large data belongs on the heap.

🆕 new and delete — Manual Memory

new allocates on the heap and returns a pointer. delete frees it.

// Allocate on the heap
int* p = new int(42);

cout << *p;  // 42

// Free the memory
delete p;
p = nullptr;  // good practice!
Step 0 / 3 — Watch the heap grow and shrink
🔑 Golden rule: Every new must have exactly one matching delete. No more, no less.

💥 new Can Fail!

When the heap is exhausted, new throws an exception.

Default: throws std::bad_alloc

try {
    int* p = new int[1000000000];
} catch (std::bad_alloc& e) {
    cerr << "Out of memory: "
         << e.what() << endl;
}

Alternative: nothrow version

int* p = new(nothrow) int[1000000000];
if (!p) {
    cerr << "Allocation failed!";
}
// Returns nullptr instead of throwing

In Practice

  • 💻 Modern systems have virtual memory — allocation rarely fails outright
  • 📱 Embedded/mobile devices have limited RAM — failure is real
  • 🛡️ Smart pointers + containers handle this for you
  • 🎮 Game engines often pre-allocate pools to avoid runtime alloc
💡 Takeaway: In most C++ code, just let bad_alloc propagate. If you're writing a library or embedded code, consider the nothrow form.

📦 Dynamic Arrays: new[] and delete[]

Allocate arrays on the heap when you don't know the size at compile time.

int n;
cin >> n;
int* arr = new int[n];

for(int i=0; i<n; i++)
    arr[i] = i * 10;

delete[] arr;  // NOTE: delete[] not delete!
⚠️ Mismatch bug: Using delete (without []) on an array is undefined behavior. Always match new[] with delete[].

🏗️ 2D Dynamic Arrays

A 2D array on the heap: an array of pointers, each pointing to a row.

// Allocate 3 rows × 4 cols
int** grid = new int*[3];
for (int i = 0; i < 3; i++)
    grid[i] = new int[4];

grid[1][2] = 42;  // row 1, col 2

// Cleanup: delete EACH row, then the array
for (int i = 0; i < 3; i++)
    delete[] grid[i];
delete[] grid;
⚠️ Cleanup order matters! Delete inner arrays first, then the outer array. Reversing this order = dangling pointers + leaks.
💡 Better alternative: Use vector<vector<int>> — automatic cleanup, no manual new/delete!

🏷️ Challenge: Stack or Heap?

For each declaration, decide where the data lives.

int x = 42;
int* p = new int(10);
string s = "hello";
int arr[5];
int* arr = new int[5];
static int count = 0;

🚰 Memory Leak — The Silent Bug

Forgetting to delete means memory is gone forever (until the program exits).

void leaky() {
    int* p = new int(42);
    // oops, forgot delete!
}  // p is gone, but the int is still on the heap

for(int i=0; i<1000000; i++)
    leaky();  // 💀 4MB leaked!
Click "Call leaky()" to watch memory fill up
🔍 Real-world impact: Leaks cause programs to slow down and eventually crash. Servers running 24/7 are especially vulnerable. Tools like Valgrind and AddressSanitizer help detect leaks.

⚡ Dangling Pointers & Double Free

Three classic mistakes that cause undefined behavior.

💀 Use After Delete

int* p = new int(42);
delete p;
cout << *p; // 💥 dangling!

Click to see explanation

🏚️ Return Local Address

int* bad() {
    int x = 10;
    return &x; // 💥
}

Click to see explanation

🔁 Double Free

int* p = new int(42);
delete p;
delete p; // 💥 double free!

Click to see explanation

🛡️ Prevention: Always set pointers to nullptr after delete. Even better — use smart pointers (coming up next!).

🔬 Debugging Tools: Valgrind & ASan

You can't always see memory bugs. These tools catch them automatically.

Valgrind (Linux/macOS)

$ valgrind ./my_program
==12345== Invalid read of size 4
==12345==    at 0x4005F2: main (test.cpp:8)
==12345==  Address 0x5204040 is 0 bytes
==12345==   inside a block of size 4 free'd

==12345== LEAK SUMMARY:
==12345==    definitely lost: 40 bytes
==12345==    indirectly lost: 0 bytes

Runs your program in a virtual CPU. Slow but thorough.

AddressSanitizer (compile flag)

$ g++ -fsanitize=address -g test.cpp
$ ./a.out
==ERROR: AddressSanitizer: heap-use-after-free
READ of size 4 at 0x602000000010
    #0 main test.cpp:8

freed by thread T0 here:
    #0 operator delete
    #1 main test.cpp:7

Compile-time instrumentation. Faster than Valgrind, catches most bugs.

🛠️ Pro tip: Always compile with -fsanitize=address -g during development. It adds ~2x overhead but catches bugs instantly.

📐 The Rule of Three (Preview)

If your class manages a resource (like heap memory), you probably need all three:

~Destructor

~MyClass() {
    delete[] data;
}

Cleans up when object is destroyed

Copy Constructor

MyClass(const MyClass& o) {
    data = new int[o.size];
    copy(o.data, ...);
}

Deep copy, not shallow

Copy Assignment

MyClass& operator=(const MyClass& o) {
    if(this!=&o) { ... }
    return *this;
}

Handles a = b;

🎓 Coming in the OOP unit: We'll dive deep into the Rule of Three (and the Rule of Five for move semantics). For now, just remember: if you write a destructor, you probably need the other two.
💡 Or even better: Use smart pointers and containers (like std::vector) so you don't need to write any of these!

📋 Shallow vs Deep Copy

Default copy just copies pointer values — both objects share the same heap data!

💀 Shallow Copy (default)

class Bad {
    int* data;
public:
    Bad(int v) { data = new int(v); }
    ~Bad() { delete data; }
};
Bad a(42);
Bad b = a;  // shallow! both point to same int
// ~Bad() called twice → 💥 double free!

✅ Deep Copy (custom)

class Good {
    int* data;
public:
    Good(int v) { data = new int(v); }
    Good(const Good& o) {
        data = new int(*o.data); // copy VALUE
    }
    ~Good() { delete data; }
};

🐛 Challenge B: Spot the Bug

Each snippet has a memory bug. Identify the type!

Bug 1

void process() {
    int* arr = new int[100];
    if (arr[0] < 0) return;
    // ... use arr ...
    delete[] arr;
}

Bug 2

int* p = new int(5);
int* q = p;
delete p;
delete q;

Bug 3

int* arr = new int[10];
delete arr;  // hmm...

Bug 4

int* make() {
    int val = 42;
    return &val;
}
int* p = make();
cout << *p;

✏️ Exercise 2: Dynamic Memory

Practice heap management! (~30 min)

What you'll do:

Open Exercise 2 →

🛡️ Smart Pointers — RAII to the Rescue

RAII = Resource Acquisition Is Initialization. Smart pointers auto-delete when they go out of scope.

❌ Manual (raw pointer)

void risky() {
    int* p = new int(42);
    // if exception here → LEAK!
    delete p;
}

✅ Smart pointer

#include <memory>
void safe() {
    auto p = make_unique<int>(42);
    // auto-deleted, even if exception!
}
Step 0 / 3 — Watch smart pointer lifecycle

🏛️ What is RAII?

Resource Acquisition Is Initialization — the constructor acquires, the destructor releases. Always.

🧠

Memory

class Buffer {
    int* data;
public:
    Buffer(int n)
      : data(new int[n]) {}
    ~Buffer()
      { delete[] data; }
};
📂

File Handles

class FileGuard {
    FILE* f;
public:
    FileGuard(const char* path)
      : f(fopen(path, "r")) {}
    ~FileGuard()
      { if(f) fclose(f); }
};
🔒

Mutex Locks

// std::lock_guard is RAII!
{
    lock_guard<mutex> lk(mtx);
    // critical section
} // auto-unlocks here

// No manual unlock needed
// Exception-safe too!
🎓 Key insight: RAII ties resource lifetime to object lifetime. Since C++ guarantees destructors run when objects leave scope (even during exceptions), resources are always cleaned up.

🔒 unique_ptr — One Owner, No Copies

A unique_ptr has exclusive ownership. It can't be copied — only moved.

#include <memory>

auto p1 = make_unique<int>(42);

// auto p2 = p1;  // ❌ COMPILE ERROR!

auto p2 = move(p1);  // ✅ ownership transferred
// p1 is now nullptr
cout << *p2;         // 42
💡 When to use: Default smart pointer choice. Use for single-owner resources: files, database connections, GUI elements.

🧩 unique_ptr Patterns

Real-world ways to use unique_ptr effectively.

Factory Functions

unique_ptr<Shape> createShape(
    string type) {
  if (type == "circle")
    return make_unique<Circle>();
  return make_unique<Square>();
}
// Caller owns the result
auto s = createShape("circle");

Return unique_ptr from factories — ownership is clear and explicit.

In Containers

vector<unique_ptr<Shape>> shapes;

shapes.push_back(
    make_unique<Circle>(5));
shapes.push_back(
    make_unique<Square>(3));

// All auto-freed when vector dies
for (auto& s : shapes)
    s->draw();

Polymorphic collections without manual delete loops.

Custom Deleters

// For C APIs that need special cleanup
auto deleter = [](FILE* f) {
    fclose(f);
};
unique_ptr<FILE, decltype(deleter)>
    file(fopen("data.txt", "r"),
         deleter);
// fclose called automatically!

Wrap any C resource in RAII with a custom deleter.

🤝 shared_ptr — Reference Counting

Multiple shared_ptrs can own the same object. Deleted when last one goes away.

auto p1 = make_shared<int>(42);  // count: 1
{
    auto p2 = p1;  // count: 2
    auto p3 = p1;  // count: 3
}  // p2,p3 destroyed → count: 1
// p1 destroyed → count: 0 → DELETE
Step 0 / 5 — Watch the reference count
⚠️ Watch out: Circular references (A→B→A) prevent deletion! Use weak_ptr to break cycles.

🔗 weak_ptr — Breaking Cycles

Circular shared_ptr references prevent deletion. weak_ptr breaks the cycle.

💀 The Problem: Circular Reference

struct Node {
    shared_ptr<Node> next;
};
auto a = make_shared<Node>();
auto b = make_shared<Node>();
a->next = b;  // a→b, b count:2
b->next = a;  // b→a, a count:2
// Both go out of scope → count:1
// Never reaches 0 → LEAKED!

✅ The Fix: Use weak_ptr

struct Node {
    weak_ptr<Node> next; // doesn't count!
};
// weak_ptr doesn't increment ref count
// Use .lock() to get a temporary shared_ptr
if (auto p = node->next.lock()) {
    // use p safely
}

🗺️ Smart Pointer Decision Tree

Follow the flowchart to pick the right pointer type.

🎯 Key Takeaways

Everything you need to remember about pointers and dynamic memory.

📍

Pointers Are Addresses

A pointer stores the memory address of another variable. Use & to get an address, * to follow it.

🏗️

new/delete Manage Heap

new allocates, delete frees. Every new needs exactly one delete. Match new[] with delete[].

🐛

Top Bugs

Memory leaks (forgot delete), dangling pointers (use after delete), double free. All cause undefined behavior.

🛡️

Smart Pointers Fix It

unique_ptr for single ownership, shared_ptr for shared. They auto-delete — no leaks, no dangling.

🚀 Modern C++ motto: "No raw new/delete in application code." Use smart pointers and containers!

💼 Common Interview Questions

Click each question to reveal the answer.

Q1: What is the difference between a pointer and a reference?

Q2: What is a memory leak and how do you prevent it?

Q3: What is the Rule of Three/Five?

Q4: When would you use shared_ptr vs unique_ptr?

Q5: What is a dangling pointer? How is it different from a wild pointer?

🔄 Challenge C: Manual vs Smart

Which smart pointer (or alternative) best replaces each raw pointer usage?

Scenario 1

// Single-owner file handle
File* f = new File("data.txt");
// ... use f ...
delete f;

Scenario 2

// Cache shared by multiple components
Cache* c = new Cache();
componentA->setCache(c);
componentB->setCache(c);
// Who deletes it? 🤔

Scenario 3

// Pass an existing object to a function
void print(Widget* w) {
    cout << w->name();
}
Widget w;
print(&w);

Scenario 4

// Factory: create and return to caller
Widget* create() {
    return new Widget();
}
// Caller owns it

📝 Quiz: Pointer Concepts

Q1: What does &x give you?

The value of x
The memory address of x
A reference to x
A copy of x

Q2: What is *p when p is a pointer?

The address p stores
The value at the address p stores
The size of p in bytes
A new pointer to p

Q3: What is nullptr?

Address 0x0000
A special value meaning "points nowhere"
An integer with value 0
A deleted pointer

🔍 Quiz: Memory Trace

Trace through the code. What is the heap state after each line?

int* a = new int(10);  // Line 1
int* b = new int(20);  // Line 2
*a = *b;                // Line 3
delete b;               // Line 4
b = a;                  // Line 5
delete a;               // Line 6
Click Next to step through and verify your trace

🧰 Quiz: Pick the Right Tool

For each scenario, pick the best option.

A function needs to modify the caller's variable

A linked list node pointing to the next node

A texture shared by multiple game objects

A function that creates an object and returns it to the caller

🎯 Bonus Quiz: Mixed Bag

4 harder questions mixing all topics. Good luck!

Q1: What prints?

int x = 5;
int& r = x;
int* p = &r;
*p = 10;
cout << x << " " << r;

Q2: How many bytes leaked?

int* a = new int[10];
int* b = new int[5];
a = b;
delete[] a;

Q3: Is this code safe?

auto p = make_unique<int>(42);
auto q = move(p);
cout << *p;

Q4: What's wrong here?

void foo(unique_ptr<int> p) {
    cout << *p;
}
auto x = make_unique<int>(10);
foo(x);

📑 Table of Contents