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 by | Declaring a local variable | new (or containers/smart pointers) |
| Destroyed | Automatically at the end of the block | When you delete it |
| Size | Fixed at compile time; limited | Decided at run time; large |
new and delete
#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[] }
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 earlyreturnor 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 —
deleteon something fromnew[], or vice versa.
Here's how easily a leak happens — an exception thrown between new and delete:
#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"; } }
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:
#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"; } }
error: negative value (nothing leaked)
| Instead of | Use |
|---|---|
new T[n] / delete[] | std::vector<T> |
new char[n] for text | std::string |
new T with one owner | std::unique_ptr<T> via std::make_unique |
new T with shared ownership | std::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.
1 4 9 16 25 36