Dynamic Memory: new and delete

Objects can be created on the heap with new and destroyed with delete, so they live as long as you need rather than until the end of a block. Modern C++ almost never calls these directly — containers and smart pointers do it for you — but knowing how they work explains why those tools exist.

Stack vs. heap

Automatic (stack)Dynamic (heap)
Created byDeclaring a local variablenew (or containers/smart pointers)
DestroyedAutomatically at the end of the blockWhen you delete it
SizeFixed at compile time; limitedDecided at run time; large

new and delete

C++
#include <iostream>
#include <string>

struct Note {
    std::string text;
    Note(std::string t) : text(std::move(t)) { std::cout << "created: " << text << '\n'; }
    ~Note() { std::cout << "destroyed: " << text << '\n'; }
};

int main() {
    Note* n = new Note("buy milk");     // allocate + construct
    std::cout << "using: " << n->text << '\n';
    delete n;                           // destruct + free
    n = nullptr;

    int* numbers = new int[5]{1, 2, 3, 4, 5};   // an array
    std::cout << numbers[4] << '\n';
    delete[] numbers;                   // arrays need delete[]
}
Output
created: buy milk
using: buy milk
destroyed: buy milk
5

new allocates memory and runs the constructor; delete runs the destructor and frees the memory. Arrays allocated with new[] must be freed with delete[].

What goes wrong

Manual memory management has the same pitfalls as in C:

  • Leaks — forgetting delete, or an early return or exception skipping it.
  • Double delete — deleting the same object twice.
  • Use after delete — following a pointer to an object that's already gone.
  • Mismatched forms — delete on something from new[], or vice versa.

Here's how easily a leak happens — an exception thrown between new and delete:

C++
#include <iostream>
#include <stdexcept>

void process(int value) {
    int* buffer = new int[1000];
    if (value < 0) throw std::invalid_argument("negative value");   // delete below is skipped!
    buffer[0] = value;
    delete[] buffer;
}

int main() {
    try {
        process(-1);
    } catch (const std::exception& e) {
        std::cout << "error: " << e.what() << " (and 4000 bytes leaked)\n";
    }
}
Output
error: negative value (and 4000 bytes leaked)

Sanitizers catch these bugs while testing: compile with -fsanitize=address (lesson 51).

The modern alternative

The fix isn't "be more careful" — it's to let objects own the memory and free it in their destructors, which run automatically even when exceptions are thrown. The same function with a std::vector:

C++
#include <iostream>
#include <stdexcept>
#include <vector>

void process(int value) {
    std::vector<int> buffer(1000);             // freed automatically, whatever happens
    if (value < 0) throw std::invalid_argument("negative value");
    buffer[0] = value;
}

int main() {
    try {
        process(-1);
    } catch (const std::exception& e) {
        std::cout << "error: " << e.what() << " (nothing leaked)\n";
    }
}
Output
error: negative value (nothing leaked)
Instead ofUse
new T[n] / delete[]std::vector<T>
new char[n] for textstd::string
new T with one ownerstd::unique_ptr<T> via std::make_unique
new T with shared ownershipstd::shared_ptr<T> via std::make_shared

The C++ Core Guidelines summarise it as: avoid naked new and delete. The next lesson shows smart pointers.

Try it yourself

Rewrite this manual-memory function to use a std::vector, so there is nothing to delete.

Try it yourself · main.cpp
Edit the code, copy it into main.cpp, then compile and run it with c++ -std=c++20 -Wall main.cpp -o main && ./main. How to set up a C++ compiler
Output
1 4 9 16 25 36