Memory Management

Dynamic Memory

Understand C++ dynamic memory — new/delete, heap vs stack, memory leaks, RAII, detecting leaks with Valgrind, and the dangers of raw pointer ownership.

Dynamic Memory

Stack memory is automatic — the compiler manages it. Heap memory is manual — you allocate it with new and must release it with delete. Dynamic allocation lets you create objects whose size or lifetime is not known at compile time. It also introduces the risk of memory leaks and double frees — which is why modern C++ wraps heap allocation in smart pointers.


Stack vs Heap

cpp
void example() {
    // STACK — automatic, freed when function returns
    int x = 42;             // Stack allocation
    int arr[100];           // Stack — must know size at compile time
    // These go away automatically at the closing brace

    // HEAP — manual, exists until explicitly deleted
    int* p    = new int(42);          // Single int on heap
    int* arr2 = new int[100];         // Array of 100 ints on heap
    delete p;                          // Free single object
    delete[] arr2;                     // Free array (use delete[], not delete!)
}
PropertyStackHeap
Allocation speedVery fastSlower (allocator overhead)
LifetimeBlock/function scopeUntil delete is called
Size limitSmall (~1-8 MB)Large (limited by RAM)
Size known at compile timeMust beCan be determined at runtime
Memory managementAutomaticManual (or smart pointers)

new and delete

cpp
#include <iostream>
using namespace std;

int main() {
    // Allocate and initialise a single object
    int* n = new int(99);
    cout << *n << "\n";   // 99
    delete n;
    n = nullptr;          // Good practice — prevents use-after-free

    // Allocate without initialising
    double* d = new double;   // Uninitialized — contains garbage!
    *d = 3.14;
    delete d;

    // Allocate an array
    int size;
    cout << "Size: ";
    cin >> size;
    int* arr = new int[size];   // Runtime-determined size

    for (int i = 0; i < size; i++) arr[i] = i * i;
    for (int i = 0; i < size; i++) cout << arr[i] << " ";
    cout << "\n";

    delete[] arr;   // MUST use delete[] for arrays
    arr = nullptr;

    return 0;
}

Warning: Mixing delete with new[] (or delete[] with new) is undefined behaviour — always match them.


Allocating Objects

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

class Person {
public:
    string name;
    int    age;

    Person(const string& name, int age) : name(name), age(age) {
        cout << "Person created: " << name << "\n";
    }

    ~Person() {
        cout << "Person destroyed: " << name << "\n";
    }

    void greet() const {
        cout << "Hi, I'm " << name << " (" << age << ")\n";
    }
};

int main() {
    // Dynamic allocation — constructor called
    Person* alice = new Person("Alice", 30);
    alice->greet();             // Hi, I'm Alice (30)
    delete alice;               // Destructor called
    alice = nullptr;

    // Array of objects — default constructors required
    // Person* arr = new Person[3];  // ERROR if no default constructor

    // Array of pointers
    Person* people[3];
    people[0] = new Person("Bob",     25);
    people[1] = new Person("Charlie", 35);
    people[2] = new Person("Diana",   28);

    for (auto* p : people) {
        p->greet();
        delete p;
    }

    return 0;
}

Memory Leaks

A memory leak occurs when you allocate memory and never free it:

cpp
// LEAK 1: Forgetting delete
void leaky() {
    int* p = new int(42);
    // Function returns — p (stack) is gone, but heap allocation remains!
    // delete p;  // This was forgotten
}

// LEAK 2: Exception before delete
void leakyOnException() {
    int* p = new int(42);
    riskyOperation();    // If this throws, delete never runs!
    delete p;
}

// LEAK 3: Overwriting pointer without deleting
void overwriteLeak() {
    int* p = new int(1);
    p = new int(2);      // Original allocation is now unreachable — leaked!
    delete p;            // Only deletes the second allocation
}

Detecting Memory Leaks

bash
# Valgrind (Linux/macOS) — detects leaks, invalid reads/writes
g++ -g myprogram.cpp -o myprogram
valgrind --leak-check=full ./myprogram

# AddressSanitizer (all platforms with Clang/GCC)
g++ -fsanitize=address -g myprogram.cpp -o myprogram
./myprogram   # Will report leaks and invalid accesses on exit

# Visual Studio has a built-in memory leak detector on Windows

RAII — The Solution to Manual Memory Management

RAII (Resource Acquisition Is Initialisation) wraps resource management in a class. The constructor acquires; the destructor releases. This ensures cleanup even if exceptions are thrown:

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

// RAII wrapper for a raw array
template <typename T>
class ScopedArray {
    T*     data_;
    size_t size_;

public:
    ScopedArray(size_t n) : data_(new T[n]()), size_(n) {
        cout << "Allocated " << n << " elements\n";
    }

    ~ScopedArray() {
        delete[] data_;
        cout << "Freed " << size_ << " elements\n";
    }

    // Access
    T&       operator[](size_t i)       { return data_[i]; }
    const T& operator[](size_t i) const { return data_[i]; }
    size_t   size()                const { return size_; }

    // Prevent copying (would cause double-free)
    ScopedArray(const ScopedArray&) = delete;
    ScopedArray& operator=(const ScopedArray&) = delete;
};

void process(size_t n) {
    ScopedArray<int> arr(n);   // Allocated on stack (wrapper), data on heap

    for (size_t i = 0; i < arr.size(); i++) arr[i] = i * 10;
    for (size_t i = 0; i < arr.size(); i++) cout << arr[i] << " ";
    cout << "\n";

    // arr is destroyed here — even if an exception is thrown above
}

int main() {
    process(5);
    // Allocated 5 elements
    // 0 10 20 30 40
    // Freed 5 elements
    return 0;
}

placement new

Place an object at a specific memory address (used in memory pools and embedded systems):

cpp
#include <iostream>
#include <new>
using namespace std;

int main() {
    // Pre-allocated buffer
    alignas(int) char buffer[sizeof(int)];

    // Construct an int in the buffer (no heap allocation)
    int* p = new(buffer) int(42);
    cout << *p << "\n";   // 42

    // Must call destructor manually — no delete! (buffer is stack)
    p->~int();   // For trivial types this is a no-op, but good practice

    return 0;
}

Practical Example: Dynamic Stack Data Structure

cpp
#include <iostream>
#include <stdexcept>
using namespace std;

template <typename T>
class DynamicStack {
    T*     data;
    int    capacity;
    int    top_;

    void resize() {
        int    newCap  = capacity * 2;
        T*     newData = new T[newCap];
        for (int i = 0; i <= top_; i++) newData[i] = data[i];
        delete[] data;
        data     = newData;
        capacity = newCap;
        cout << "Resized to " << capacity << "\n";
    }

public:
    DynamicStack(int initialCap = 4)
        : data(new T[initialCap]), capacity(initialCap), top_(-1) {}

    ~DynamicStack() { delete[] data; }

    void push(const T& val) {
        if (top_ + 1 == capacity) resize();
        data[++top_] = val;
    }

    T pop() {
        if (empty()) throw underflow_error("Stack is empty");
        return data[top_--];
    }

    const T& peek() const {
        if (empty()) throw underflow_error("Stack is empty");
        return data[top_];
    }

    bool empty() const { return top_ == -1; }
    int  size()  const { return top_ + 1; }
};

int main() {
    DynamicStack<int> stack;

    for (int i = 1; i <= 6; i++) {
        stack.push(i * 10);
        cout << "Pushed " << i * 10 << " (size=" << stack.size() << ")\n";
    }

    while (!stack.empty()) {
        cout << "Pop: " << stack.pop() << "\n";
    }

    return 0;
}
// Pushed 10 (size=1)
// Pushed 20 (size=2)
// Pushed 30 (size=3)
// Pushed 40 (size=4)
// Resized to 8
// Pushed 50 (size=5)
// Pushed 60 (size=6)
// Pop: 60
// Pop: 50
// ...

Summary

  • new allocates on the heap; delete frees it — always match new with delete and new[] with delete[]
  • Set pointers to nullptr after delete — prevents use-after-free bugs
  • Memory leaks occur when allocated memory is never freed — use Valgrind or AddressSanitizer to detect them
  • RAII is the key pattern: wrap heap allocations in a class whose destructor frees the memory — ensures cleanup even on exceptions
  • Prefer std::vector, std::string, and smart pointers over raw new/delete in modern C++
  • placement new constructs an object at a specific address — used for memory pools and embedded systems
  • The heap is larger but slower than the stack; use heap for large or long-lived objects, stack for small, short-lived ones

Next, we'll learn about smart pointers — unique_ptr, shared_ptr, and weak_ptr — the modern C++ replacement for raw new/delete.