Dynamic Memory
2 min read
Dynamic Memory
Stack-allocated objects have lifetimes tied to their enclosing scope. When you need an object to outlive its creating function, or when its size is not known at compile time, you allocate it on the heap using new and release it with delete.
new and delete
#include <iostream>
int main() {
int* p = new int(42); // allocate a single int, initialize to 42
std::cout << *p << "\n"; // 42
*p = 100;
std::cout << *p << "\n"; // 100
delete p; // return memory to the heap
p = nullptr; // good practice: prevent use after free
}
After delete, the pointer is dangling — it holds an address that is no longer valid. Writing p = nullptr immediately after helps catch accidental uses.
Arrays with new[] and delete[]
#include <iostream>
int main() {
int n = 10;
int* arr = new int[n]; // allocate array of n ints
for (int i = 0; i < n; i++) arr[i] = i * i;
for (int i = 0; i < n; i++) std::cout << arr[i] << " ";
std::cout << "\n";
delete[] arr; // must use delete[], not delete
arr = nullptr;
}
Mismatching new[] with plain delete (or vice versa) is undefined behavior.
The problems with manual allocation
Manual new/delete is error-prone:
- Memory leak:
newwithout a matchingdelete— memory is never returned. - Double free: calling
deletetwice on the same pointer — undefined behavior, typically a crash or corruption. - Use after free: reading or writing through a pointer after
delete— undefined behavior. - Exception safety: if an exception is thrown between
newanddelete, thedeleteis skipped.
void risky() {
int* p = new int(42);
might_throw(); // if this throws, p is never deleted
delete p;
}
Placement new
Placement new constructs an object at an address you supply, without allocating:
#include <new>
char buffer[sizeof(int)];
int* p = new (buffer) int(99); // construct in buffer
std::cout << *p << "\n"; // 99
p->~int(); // explicitly call destructor (rarely needed for int)
Placement new is used in memory pools, arenas, and embedded systems. You are responsible for managing the memory lifetime.
std::allocator
The standard library's generic allocator separates allocation from construction:
#include <memory>
std::allocator<int> alloc;
int* p = alloc.allocate(5); // raw memory for 5 ints
for (int i = 0; i < 5; i++) std::construct_at(p + i, i * 10);
for (int i = 0; i < 5; i++) std::cout << p[i] << " ";
for (int i = 0; i < 5; i++) std::destroy_at(p + i);
alloc.deallocate(p, 5);
Container implementations use allocators internally. Most application code never touches them directly.
Prefer smart pointers
Raw new/delete should be a last resort in new code. Use std::unique_ptr and std::shared_ptr instead — they automate deletion and are exception-safe. See the Smart Pointers chapter.
Learn modern C++ through working code. Each chapter introduces one concept — variables, functions, classes, templates, smart pointers, concurrency — with a clear example, a line-by-line explanation, and notes on how it applies in real programs.
View book →