Module 3.3

Memory Management in C++

Understand how C++ manages memory with stack and heap allocation. Learn about dynamic memory, avoid memory leaks, and master modern smart pointers for safe and efficient resource management.

45 min read
Intermediate
Hands-on Examples
What You'll Learn
  • Stack vs Heap memory
  • Dynamic allocation (new/delete)
  • Memory leaks & debugging
  • RAII pattern
  • Smart pointers (unique_ptr, shared_ptr)
Contents
01

Introduction to Memory Management

Memory management is a critical skill in C++. Unlike garbage-collected languages, C++ gives you direct control over memory allocation and deallocation. This power comes with responsibility—improper memory management leads to bugs, crashes, and security vulnerabilities.

Concept

Memory Management

Memory management is the process of controlling and coordinating computer memory, assigning portions called blocks to running programs, and freeing them when no longer needed.

Key Principle: Every allocation must have a corresponding deallocation.

Memory Regions in C++

Stack

Fast, automatic allocation for local variables

Heap

Dynamic allocation with manual control

Global/Static

Lives for entire program duration

Code (Text)

Stores compiled program instructions

#include <iostream>
using namespace std;

int globalVar = 100;       // Global/Static segment

int main() {
    int stackVar = 10;     // Stack - automatic management
    static int staticVar = 20;  // Global/Static segment
    
    int* heapVar = new int(30);  // Heap - manual management
    
    cout << "Global: " << globalVar << endl;
    cout << "Stack: " << stackVar << endl;
    cout << "Static: " << staticVar << endl;
    cout << "Heap: " << *heapVar << endl;
    
    delete heapVar;  // Must manually free heap memory!
    
    return 0;
}  // stackVar automatically freed here
02

Stack vs Heap Memory

Understanding the difference between stack and heap memory is fundamental to effective C++ programming. Each has distinct characteristics, advantages, and use cases.

Stack Memory

#include <iostream>
using namespace std;

void stackExample() {
    // All these variables are on the STACK
    int a = 10;
    double b = 3.14;
    char c = 'X';
    int arr[5] = {1, 2, 3, 4, 5};  // Fixed-size array on stack
    
    cout << "Stack variables:" << endl;
    cout << "a = " << a << endl;
    cout << "b = " << b << endl;
    cout << "c = " << c << endl;
    
    // Stack memory layout (grows downward):
    // ┌─────────────────┐ Higher Address
    // │     arr[4]      │
    // │     arr[3]      │
    // │     arr[2]      │
    // │     arr[1]      │
    // │     arr[0]      │
    // │       c         │
    // │       b         │
    // │       a         │
    // └─────────────────┘ Lower Address (Stack Pointer)
    
}  // All stack variables automatically freed here!

int main() {
    stackExample();
    // a, b, c, arr no longer exist - memory reclaimed
    return 0;
}

Heap Memory

#include <iostream>
using namespace std;

int* createOnHeap() {
    // Allocate on HEAP - survives function return
    int* ptr = new int(42);
    return ptr;  // Safe! Memory persists
}

int* dangerousStack() {
    int local = 100;
    return &local;  // DANGER! Returns address of dead variable
}

int main() {
    // Heap allocation
    int* heapPtr = createOnHeap();
    cout << "Heap value: " << *heapPtr << endl;  // 42
    
    // Dynamic array on heap
    int size = 10;  // Can be determined at runtime!
    int* arr = new int[size];
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }
    
    // Must manually free heap memory
    delete heapPtr;
    delete[] arr;
    
    return 0;
}

Comparison Table

Feature Stack Heap
Allocation Speed Very Fast Slower
Deallocation Automatic (scope-based) Manual (delete required)
Size Limited (usually 1-8 MB) Large (limited by RAM)
Size Known At compile time At runtime
Access Speed Faster Slower
Fragmentation No Yes (can occur)
Use Case Local variables, small data Large data, dynamic structures

Practice Questions: Stack vs Heap

Task: For each variable, identify if it's on stack or heap:

int a = 5;
int* b = new int(10);
double arr[3] = {1.0, 2.0, 3.0};
char* str = new char[20];
Show Solution
int a = 5;                      // STACK
int* b = new int(10);           // b (pointer) on STACK, value on HEAP
double arr[3] = {1.0, 2.0, 3.0}; // STACK (fixed-size array)
char* str = new char[20];        // str (pointer) on STACK, array on HEAP

Task: List 3 scenarios where heap allocation is necessary or preferred.

Show Solution
  1. Unknown size at compile time - Array size determined by user input
  2. Large data structures - Data too big for stack (e.g., large arrays)
  3. Data must outlive function - Returning allocated data from function
  4. Dynamic data structures - Linked lists, trees, graphs with varying size
03

Dynamic Memory Allocation

C++ provides new and delete operators for dynamic memory allocation. These give you precise control over when memory is allocated and freed.

#include <iostream>
using namespace std;

int main() {
    // Single variable allocation
    int* num = new int;        // Uninitialized
    *num = 42;
    
    int* num2 = new int(100);  // Initialized to 100
    int* num3 = new int{200};  // C++11 uniform initialization
    
    cout << *num << ", " << *num2 << ", " << *num3 << endl;
    
    // Free single variables with delete
    delete num;
    delete num2;
    delete num3;
    
    // Array allocation
    int size;
    cout << "Enter array size: ";
    cin >> size;
    
    int* arr = new int[size];  // Dynamic array
    
    for (int i = 0; i < size; i++) {
        arr[i] = i * 10;
    }
    
    // Free arrays with delete[]
    delete[] arr;
    
    return 0;
}

new with Classes

#include <iostream>
#include <string>
using namespace std;

class Student {
public:
    string name;
    int age;
    
    Student(string n, int a) : name(n), age(a) {
        cout << "Constructor called for " << name << endl;
    }
    
    ~Student() {
        cout << "Destructor called for " << name << endl;
    }
    
    void display() {
        cout << name << " is " << age << " years old" << endl;
    }
};

int main() {
    // Single object
    Student* s1 = new Student("Alice", 20);
    s1->display();
    delete s1;  // Calls destructor
    
    cout << "---" << endl;
    
    // Array of objects
    Student* students = new Student[3]{
        {"Bob", 21},
        {"Carol", 22},
        {"Dave", 23}
    };
    
    for (int i = 0; i < 3; i++) {
        students[i].display();
    }
    
    delete[] students;  // Calls destructors for all
    
    return 0;
}

Handling Allocation Failure

#include <iostream>
#include <new>  // For nothrow
using namespace std;

int main() {
    // Method 1: Exception handling (default)
    try {
        int* huge = new int[1000000000000];  // Very large
        delete[] huge;
    } catch (bad_alloc& e) {
        cout << "Allocation failed: " << e.what() << endl;
    }
    
    // Method 2: nothrow (returns nullptr on failure)
    int* ptr = new(nothrow) int[1000000000000];
    if (ptr == nullptr) {
        cout << "Allocation failed (nullptr)" << endl;
    } else {
        delete[] ptr;
    }
    
    return 0;
}

Practice Questions: Dynamic Allocation

Task: Ask user for size, create dynamic array, fill with squares (1, 4, 9, ...), print, and free.

Show Solution
int size;
cout << "Enter size: ";
cin >> size;

int* arr = new int[size];

for (int i = 0; i < size; i++) {
    arr[i] = (i + 1) * (i + 1);
}

for (int i = 0; i < size; i++) {
    cout << arr[i] << " ";
}
cout << endl;

delete[] arr;

Task: Create a dynamic 3x4 matrix, initialize with values, print, and properly deallocate.

Show Solution
int rows = 3, cols = 4;

// Allocate
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {
    matrix[i] = new int[cols];
}

// Initialize
int val = 1;
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        matrix[i][j] = val++;
    }
}

// Print
for (int i = 0; i < rows; i++) {
    for (int j = 0; j < cols; j++) {
        cout << matrix[i][j] << "\t";
    }
    cout << endl;
}

// Deallocate (reverse order)
for (int i = 0; i < rows; i++) {
    delete[] matrix[i];
}
delete[] matrix;
04

Memory Leaks & Common Mistakes

Memory leaks occur when dynamically allocated memory is never freed. Over time, leaks consume available memory, causing slowdowns or crashes. Learning to avoid and detect them is essential.

Common Memory Mistakes

#include <iostream>
using namespace std;

// Mistake 1: Forgetting to delete
void leak1() {
    int* ptr = new int(42);
    // Function ends without delete - MEMORY LEAK!
}

// Mistake 2: Losing pointer before delete
void leak2() {
    int* ptr = new int(100);
    ptr = new int(200);  // Lost reference to first allocation!
    delete ptr;          // Only frees second allocation
}

// Mistake 3: Using wrong delete form
void mistake3() {
    int* arr = new int[10];
    // delete arr;       // WRONG! Use delete[] for arrays
    delete[] arr;        // Correct
}

// Mistake 4: Double delete
void mistake4() {
    int* ptr = new int(42);
    delete ptr;
    // delete ptr;  // UNDEFINED BEHAVIOR - double free!
    ptr = nullptr;  // Best practice: set to nullptr after delete
}

// Mistake 5: Using deleted memory (dangling pointer)
void mistake5() {
    int* ptr = new int(42);
    delete ptr;
    // cout << *ptr;  // UNDEFINED BEHAVIOR - use after free!
}

// Mistake 6: Returning address of local variable
int* mistake6() {
    int local = 10;
    return &local;  // DANGER! local dies when function returns
}

int main() {
    // Don't actually run these - they demonstrate bugs!
    return 0;
}
Memory Leak Symptoms:
  • Program uses more and more memory over time
  • System becomes slow or unresponsive
  • Eventually crashes with "out of memory" error
  • Particularly dangerous in long-running servers

Safe Memory Management Practices

#include <iostream>
using namespace std;

// Practice 1: Always match new with delete
void safe1() {
    int* ptr = new int(42);
    // ... use ptr ...
    delete ptr;
    ptr = nullptr;  // Prevent accidental reuse
}

// Practice 2: Use RAII - allocate in constructor, delete in destructor
class SafeArray {
private:
    int* data;
    int size;
public:
    SafeArray(int s) : size(s) {
        data = new int[size];
        cout << "Allocated " << size << " ints" << endl;
    }
    
    ~SafeArray() {
        delete[] data;
        cout << "Freed " << size << " ints" << endl;
    }
    
    int& operator[](int i) { return data[i]; }
};

// Practice 3: Check for nullptr before use
void safe3(int* ptr) {
    if (ptr != nullptr) {
        cout << *ptr << endl;
    }
}

int main() {
    {
        SafeArray arr(5);  // Allocated
        arr[0] = 100;
    }  // Automatically freed when arr goes out of scope
    
    return 0;
}

Practice Questions: Memory Leaks

Task: Identify the memory leak and fix it:

void processData() {
    int* data = new int[100];
    
    if (someCondition()) {
        return;  // Early return
    }
    
    // Process data...
    delete[] data;
}
Show Solution
// Problem: Early return causes leak
// Solution: Delete before all return paths

void processData() {
    int* data = new int[100];
    
    if (someCondition()) {
        delete[] data;  // Free before returning!
        return;
    }
    
    // Process data...
    delete[] data;
}

// Better: Use smart pointers (covered later)
void processDataSmart() {
    unique_ptr data(new int[100]);
    
    if (someCondition()) {
        return;  // Automatically freed!
    }
    
    // Process data...
}  // Automatically freed
05

RAII Pattern

RAII (Resource Acquisition Is Initialization) is a fundamental C++ idiom where resources are tied to object lifetime. Resources are acquired in constructors and released in destructors, ensuring automatic cleanup.

Pattern

RAII

RAII binds resource lifetime to object lifetime. When an object is created, it acquires resources. When it's destroyed (goes out of scope), it automatically releases those resources.

Benefit: Exception-safe, leak-free resource management without manual cleanup.

#include <iostream>
#include <fstream>
using namespace std;

// RAII wrapper for dynamic array
class IntArray {
private:
    int* data;
    size_t size;
    
public:
    // Constructor: Acquire resource
    IntArray(size_t n) : size(n), data(new int[n]) {
        cout << "Acquired " << size << " ints" << endl;
        for (size_t i = 0; i < size; i++) {
            data[i] = 0;
        }
    }
    
    // Destructor: Release resource
    ~IntArray() {
        delete[] data;
        cout << "Released " << size << " ints" << endl;
    }
    
    // Disable copy (Rule of Three/Five)
    IntArray(const IntArray&) = delete;
    IntArray& operator=(const IntArray&) = delete;
    
    // Access
    int& operator[](size_t i) { return data[i]; }
    size_t getSize() const { return size; }
};

void process() {
    IntArray arr(10);  // Resource acquired
    
    arr[0] = 100;
    arr[1] = 200;
    
    cout << "Processing..." << endl;
    
    if (true) {
        throw runtime_error("Oops!");  // Exception thrown
    }
    
    // Even with exception, arr is properly cleaned up!
}

int main() {
    try {
        process();
    } catch (exception& e) {
        cout << "Caught: " << e.what() << endl;
    }
    
    // RAII example with file (fstream already uses RAII)
    {
        ofstream file("test.txt");  // File opened
        file << "Hello, RAII!" << endl;
    }  // File automatically closed here
    
    return 0;
}

RAII Benefits

Exception Safe

Resources freed even if exceptions occur

Automatic

No need to remember manual cleanup

No Leaks

Destructor always runs when scope ends

Practice Questions: RAII

Task: Create a simple RAII class that wraps a FILE* pointer (C-style file) - opens in constructor, closes in destructor.

Show Solution
class FileWrapper {
private:
    FILE* file;
public:
    FileWrapper(const char* name, const char* mode) {
        file = fopen(name, mode);
        if (!file) {
            throw runtime_error("Failed to open file");
        }
    }
    
    ~FileWrapper() {
        if (file) {
            fclose(file);
        }
    }
    
    // Disable copy
    FileWrapper(const FileWrapper&) = delete;
    FileWrapper& operator=(const FileWrapper&) = delete;
    
    FILE* get() { return file; }
};

// Usage:
{
    FileWrapper f("test.txt", "w");
    fprintf(f.get(), "Hello!");
}  // File automatically closed
06

unique_ptr - Exclusive Ownership

std::unique_ptr is a smart pointer that owns and manages a dynamically allocated object, automatically deleting it when the unique_ptr goes out of scope. It enforces exclusive ownership—only one unique_ptr can own an object at a time.

#include <iostream>
#include <memory>  // Required for smart pointers
using namespace std;

class Resource {
public:
    Resource() { cout << "Resource created" << endl; }
    ~Resource() { cout << "Resource destroyed" << endl; }
    void use() { cout << "Resource used" << endl; }
};

int main() {
    // Create unique_ptr
    unique_ptr<Resource> ptr1(new Resource());
    
    // Better: use make_unique (C++14)
    auto ptr2 = make_unique<Resource>();
    
    // Use the resource
    ptr1->use();
    (*ptr2).use();  // Also works
    
    // unique_ptr cannot be copied (exclusive ownership)
    // unique_ptr<Resource> ptr3 = ptr1;  // ERROR!
    
    // But can be moved
    unique_ptr<Resource> ptr3 = move(ptr1);
    // ptr1 is now nullptr
    
    if (ptr1 == nullptr) {
        cout << "ptr1 is null after move" << endl;
    }
    
    ptr3->use();
    
    // Get raw pointer (careful - don't delete it!)
    Resource* raw = ptr3.get();
    raw->use();
    
    // Release ownership (returns raw pointer)
    Resource* released = ptr3.release();
    // Now YOU are responsible for deleting it
    delete released;
    
    return 0;
}  // ptr2 automatically deleted here

unique_ptr with Arrays

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // unique_ptr for arrays
    unique_ptr<int[]> arr(new int[5]);
    
    // Or with make_unique (C++14)
    auto arr2 = make_unique<int[]>(5);
    
    // Use like regular array
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 10;
        arr2[i] = i * 100;
    }
    
    for (int i = 0; i < 5; i++) {
        cout << arr[i] << " " << arr2[i] << endl;
    }
    
    return 0;
}  // Both arrays automatically deleted

unique_ptr in Functions

#include <iostream>
#include <memory>
using namespace std;

// Return unique_ptr from function (ownership transfer)
unique_ptr<int> createNumber(int value) {
    return make_unique<int>(value);
}

// Accept unique_ptr by value (takes ownership)
void consume(unique_ptr<int> ptr) {
    cout << "Consumed: " << *ptr << endl;
}  // ptr deleted here

// Accept by reference (no ownership transfer)
void inspect(const unique_ptr<int>& ptr) {
    cout << "Inspecting: " << *ptr << endl;
}

// Accept raw pointer for read-only access
void readOnly(int* ptr) {
    cout << "Reading: " << *ptr << endl;
}

int main() {
    auto num = createNumber(42);
    
    inspect(num);      // num still valid
    readOnly(num.get()); // Pass raw pointer
    
    consume(move(num)); // Transfer ownership
    // num is now nullptr
    
    return 0;
}

Practice Questions: unique_ptr

Task: Convert this code to use unique_ptr:

int* ptr = new int(42);
cout << *ptr << endl;
delete ptr;
Show Solution
auto ptr = make_unique(42);
cout << *ptr << endl;
// No delete needed!

Task: Create a factory function that creates a string on the heap and returns ownership via unique_ptr.

Show Solution
unique_ptr createGreeting(const string& name) {
    return make_unique("Hello, " + name + "!");
}

// Usage:
auto msg = createGreeting("World");
cout << *msg << endl;  // Hello, World!
07

shared_ptr - Shared Ownership

std::shared_ptr allows multiple pointers to share ownership of an object. It uses reference counting—the object is deleted only when the last shared_ptr pointing to it is destroyed.

#include <iostream>
#include <memory>
using namespace std;

class Resource {
public:
    string name;
    Resource(string n) : name(n) { 
        cout << name << " created" << endl; 
    }
    ~Resource() { 
        cout << name << " destroyed" << endl; 
    }
};

int main() {
    // Create shared_ptr
    shared_ptr<Resource> ptr1 = make_shared<Resource>("Res1");
    cout << "Use count: " << ptr1.use_count() << endl;  // 1
    
    {
        // Share ownership
        shared_ptr<Resource> ptr2 = ptr1;  // Copy is allowed!
        cout << "Use count: " << ptr1.use_count() << endl;  // 2
        
        shared_ptr<Resource> ptr3 = ptr1;
        cout << "Use count: " << ptr1.use_count() << endl;  // 3
        
        ptr2->name = "Renamed";  // All see the change
        cout << ptr1->name << endl;  // Renamed
        
    }  // ptr2 and ptr3 destroyed, count decreases
    
    cout << "Use count: " << ptr1.use_count() << endl;  // 1
    
    return 0;
}  // ptr1 destroyed, count = 0, Resource deleted

shared_ptr vs unique_ptr

Feature unique_ptr shared_ptr
Ownership Exclusive (one owner) Shared (multiple owners)
Copy Not allowed Allowed
Move Allowed Allowed
Overhead Minimal (size of raw pointer) Higher (control block + ref count)
Use Case Clear single owner Shared resources, caches

shared_ptr Best Practices

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

class Node {
public:
    int value;
    vector<shared_ptr<Node>> children;
    
    Node(int v) : value(v) {}
};

int main() {
    // Always use make_shared (more efficient)
    auto node1 = make_shared<Node>(1);
    auto node2 = make_shared<Node>(2);
    auto node3 = make_shared<Node>(3);
    
    // Build tree structure
    node1->children.push_back(node2);
    node1->children.push_back(node3);
    
    // Multiple references to same node
    auto sharedNode = node2;
    cout << "node2 use count: " << node2.use_count() << endl;  // 3
    
    // Pass to functions by reference or value
    auto process = [](shared_ptr<Node> n) {
        cout << "Processing node " << n->value << endl;
    };
    
    process(node1);
    
    return 0;
}

Practice Questions: shared_ptr

Task: What is the output? Track the use_count:

auto p1 = make_shared(10);
cout << p1.use_count() << endl;  // ?

auto p2 = p1;
cout << p1.use_count() << endl;  // ?

p2.reset();
cout << p1.use_count() << endl;  // ?
Show Solution
auto p1 = make_shared(10);
cout << p1.use_count() << endl;  // 1

auto p2 = p1;
cout << p1.use_count() << endl;  // 2

p2.reset();  // p2 releases ownership
cout << p1.use_count() << endl;  // 1

Task: Create a simple cache that stores shared_ptr to strings, allowing multiple parts of code to share the same string data.

Show Solution
class StringCache {
    map> cache;
public:
    shared_ptr get(const string& key) {
        if (cache.find(key) == cache.end()) {
            cache[key] = make_shared("Data for " + key);
        }
        return cache[key];
    }
};

// Usage:
StringCache cache;
auto data1 = cache.get("user1");
auto data2 = cache.get("user1");  // Same object!
cout << (data1 == data2) << endl;  // 1 (true)
08

weak_ptr - Breaking Cycles

std::weak_ptr is a non-owning smart pointer that holds a weak reference to an object managed by shared_ptr. It doesn't affect reference count and is used to break circular references.

The Circular Reference Problem

#include <iostream>
#include <memory>
using namespace std;

// PROBLEM: Circular reference with shared_ptr
class BadNode {
public:
    string name;
    shared_ptr<BadNode> partner;  // Strong reference
    
    BadNode(string n) : name(n) {
        cout << name << " created" << endl;
    }
    ~BadNode() {
        cout << name << " destroyed" << endl;
    }
};

void circularProblem() {
    auto node1 = make_shared<BadNode>("Node1");
    auto node2 = make_shared<BadNode>("Node2");
    
    node1->partner = node2;  // node1 -> node2
    node2->partner = node1;  // node2 -> node1 (circular!)
    
    cout << "node1 use_count: " << node1.use_count() << endl;  // 2
    cout << "node2 use_count: " << node2.use_count() << endl;  // 2
    
}  // Memory leak! Neither node is destroyed

int main() {
    circularProblem();
    cout << "After function - no destructors called!" << endl;
    return 0;
}

Solution: weak_ptr

#include <iostream>
#include <memory>
using namespace std;

// SOLUTION: Use weak_ptr to break cycle
class GoodNode {
public:
    string name;
    weak_ptr<GoodNode> partner;  // Weak reference!
    
    GoodNode(string n) : name(n) {
        cout << name << " created" << endl;
    }
    ~GoodNode() {
        cout << name << " destroyed" << endl;
    }
    
    void greet() {
        // Must convert weak_ptr to shared_ptr to use
        if (auto p = partner.lock()) {
            cout << name << " says hi to " << p->name << endl;
        } else {
            cout << name << "'s partner is gone" << endl;
        }
    }
};

void circularSolved() {
    auto node1 = make_shared<GoodNode>("Node1");
    auto node2 = make_shared<GoodNode>("Node2");
    
    node1->partner = node2;  // Weak reference
    node2->partner = node1;  // Weak reference
    
    cout << "node1 use_count: " << node1.use_count() << endl;  // 1
    cout << "node2 use_count: " << node2.use_count() << endl;  // 1
    
    node1->greet();
    node2->greet();
    
}  // Both nodes properly destroyed!

int main() {
    circularSolved();
    cout << "After function - destructors called!" << endl;
    return 0;
}

weak_ptr Operations

#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> shared = make_shared<int>(42);
    weak_ptr<int> weak = shared;  // Create from shared_ptr
    
    // Check if object still exists
    cout << "expired: " << weak.expired() << endl;  // false
    
    // Get use count
    cout << "use_count: " << weak.use_count() << endl;  // 1
    
    // Convert to shared_ptr (safe access)
    if (auto locked = weak.lock()) {
        cout << "Value: " << *locked << endl;  // 42
    }
    
    // Reset the shared_ptr
    shared.reset();
    
    cout << "After reset:" << endl;
    cout << "expired: " << weak.expired() << endl;  // true
    
    if (auto locked = weak.lock()) {
        cout << "Value: " << *locked << endl;
    } else {
        cout << "Object no longer exists" << endl;
    }
    
    return 0;
}

Practice Questions: weak_ptr

Task: List 3 scenarios where weak_ptr should be used instead of shared_ptr.

Show Solution
  1. Breaking circular references - Parent-child relationships where child references parent
  2. Caching - Cache that doesn't keep objects alive if nothing else uses them
  3. Observer pattern - Observers that shouldn't keep subjects alive
  4. Back-pointers - Any bidirectional relationship where one direction should be weak

Task: Implement a simple Subject that notifies observers using weak_ptr, so dead observers are automatically skipped.

Show Solution
class Observer {
public:
    virtual void notify(const string& msg) = 0;
    virtual ~Observer() = default;
};

class Subject {
    vector> observers;
public:
    void addObserver(shared_ptr obs) {
        observers.push_back(obs);
    }
    
    void notifyAll(const string& msg) {
        for (auto& weak : observers) {
            if (auto obs = weak.lock()) {
                obs->notify(msg);
            }
        }
    }
};

// Observers can be destroyed independently
// Subject won't keep them alive
// Dead observers automatically skipped

Key Takeaways

Stack vs Heap

Stack: fast, automatic. Heap: flexible, manual.

new/delete

Match new with delete, new[] with delete[]

Avoid Leaks

Always free allocated memory

RAII

Tie resources to object lifetime

unique_ptr

Exclusive ownership, prefer by default

shared_ptr & weak_ptr

Shared ownership, break cycles

Knowledge Check

Quick Quiz

Test what you have learned about C++ memory management

1 Which memory region automatically frees variables when they go out of scope?
2 What happens if you use delete instead of delete[] for an array?
3 What does RAII stand for?
4 Which smart pointer should you use by default for single ownership?
5 What is the primary purpose of weak_ptr?
6 How do you safely access the object pointed to by a weak_ptr?
Answer all questions to check your score