Course outline · 0% complete

0/27 lessons0%

Course overview →

Stack vs Heap, new and delete

lesson 6-1 · ~12 min · 15/27

Reaching data through an address

Lesson 5-1 introduced the two-line pattern int* p = &x; *p = 99;, which stores x's address in p and then writes 99 into x through that address.

Each piece does one job. &x produces the address of x, p stores it, and *p dereferences that address, so the assignment lands in x itself rather than in p. Pointers are how this unit's heap memory is reached, so that line is worth being comfortable with before continuing.

This unit is the heart of C++'s reputation, good and bad. Where a value lives decides how long it exists, how fast it is to allocate, and who must clean it up. Every segfault war story, every garbage-collector pause in other languages, and a large share of systems-interview questions trace back to the distinction on this page.

Two places your data can live

The stack. Every function call gets a stack frame: a slab of memory holding its parameters and local variables. When the function returns, the frame is popped and every variable in it dies instantly. All the locals you have written so far lived here. Stack memory is automatic and extremely fast, but small (a few MB) and tied to scope.

The heap. A big pool of memory you request at run time. Heap data survives until you explicitly free it, no matter how many functions return in the meantime. The price: you are the garbage collector.

int* makeCounter() {
    int local = 0;          // stack: dies when makeCounter returns
    int* h = new int(0);    // heap: survives the return
    return h;               // caller receives the address
}

new int(0) allocates one int on the heap, initializes it to 0, and returns its address. Later, someone must call delete on that address, or the memory is lost until the program exits: a memory leak.

Python note: every Python object lives on a heap, and the garbage collector frees it for you. C++ gives you the choice, and the responsibility.

stackheapmain() framemakeCounter()newnewframes come and go with callsblocks live until you delete
Stack frames appear on call and vanish on return (watch makeCounter's frame). Heap blocks created with new stay put until delete, even after their creator returned.

The full new, use, delete cycle

Two heap allocations, one for a single int and one for an array, each freed with the matching form of delete.

#include <iostream>

int main() {
    int* score = new int(10);    // one int on the heap
    *score += 5;
    std::cout << "score: " << *score << "\n";
    delete score;                // free the single int

    int* data = new int[3];      // an array of 3 ints on the heap
    data[0] = 1; data[1] = 2; data[2] = 3;
    std::cout << "sum: " << data[0] + data[1] + data[2] << "\n";
    delete[] data;               // arrays need delete[]
    return 0;
}

Output

score: 15
sum: 6

The two delete forms are not interchangeable. delete frees one object and delete[] frees an array, and mismatching them is undefined behavior rather than a diagnosed error, because the runtime needs to know how many destructors to run and how the block was recorded.

Notice that data[0] works on a pointer with no array declaration anywhere in sight. That is lesson 5-2's identity a[i] being *(a + i) doing its job, and it is why heap arrays are indexed exactly like stack arrays.

new int(10) uses parentheses to supply an initial value, while new int[3] leaves its three ints uninitialized, holding whatever bytes were already there. Writing new int[3]{} would zero them.

Fixing a leak, and neutering the dangling pointer

The starting version allocates a heap int and never frees it, so the memory is lost for the life of the process.

#include <iostream>

int main() {
    int* score = new int(10);   // heap allocation
    *score += 5;
    std::cout << *score << "\n";
    std::cout << "freed\n";
    return 0;
}

Two lines fix it, one to release the memory and one to make later misuse detectable:

#include <iostream>

int main() {
    int* score = new int(10);
    *score += 5;
    std::cout << *score << "\n";
    delete score;
    score = nullptr;
    std::cout << "freed\n";
    return 0;
}

Output

15
freed

delete score; releases the single int, and arrays allocated with new T[n] would need delete[] instead.

The second line matters because delete does not change the pointer. After the free, score still holds the old address, which is now a dangling pointer aimed at memory the allocator may hand to someone else. Dereferencing it might appear to work, might return garbage, or might corrupt unrelated data, which is the worst possible combination for debugging. Setting it to nullptr converts that silent hazard into a reliable crash.

Deleting a null pointer is explicitly safe and does nothing, so delete score; a second time after the reset would be harmless. Without the reset, a double delete is undefined behavior.

Allocating without freeing

A function that calls new int[1000] on every invocation and never calls delete[] has a memory leak, and the program's memory use grows until the process exits or runs out.

Heap memory is never reclaimed automatically while the program runs, so each call abandons another 4,000 bytes that nothing points to anymore. The allocator still considers that block in use, and no code can reach it, which is the precise definition of a leak.

In a short script you may never notice, since everything is released when the process exits. In a server that runs for weeks, leaks eventually exhaust RAM and the process is killed by the operating system, often long after the offending call.

Two properties make leaks especially sneaky. The stack is unaffected, so no crash happens at the point of the bug, and the symptom is a slow growth in memory rather than an error message. Tools like valgrind and the address and leak sanitizers built into modern compilers exist because human review is bad at spotting them.

A heap array sized at run time

The size comes from input, which is the situation a stack array cannot handle, since its length has to be a compile-time constant.

#include <iostream>

int main() {
    int n;
    std::cin >> n;

    int* arr = new int[n];
    for (int i = 0; i < n; i++) {
        arr[i] = i * i;
    }
    for (int i = 0; i < n; i++) {
        std::cout << arr[i] << "\n";
    }
    delete[] arr;
    return 0;
}

Input

5

Output

0
1
4
9
16

new int[n] takes a runtime value for its length, which is the whole reason to reach for the heap here. The array was allocated with new[], so it must be freed with delete[] arr;.

The two loops could be merged into one that fills and prints in the same pass, and keeping them separate is a readability choice rather than a correctness one.

There is a lurking problem this code ignores. Nothing validates n, so an input of 0 allocates an empty array, and a negative input is undefined behavior. Nothing here tracks the size alongside the pointer either, which is why std::vector in unit 8 replaces this entire pattern in real code.