Dynamic Memory: new, delete & Their Dangers
Learn how dynamic allocation works with new and delete, understand the dangers of memory leaks, double-free, and use-after-free, and discover why modern C++ should contain zero raw new/delete.
Dynamic Allocation with new and delete
The stack is fast but limited in size and scope. When you need objects that outlive their creating scope, or whose size is unknown at compile time, you allocate on the heap (free store).
new T(args) allocates memory for one T, calls its constructor, and returns a T*. delete p calls the destructor and frees the memory. For arrays: new T[n] and delete[] p. Mismatching new with delete[] or new[] with delete is undefined behavior — the compiler will not catch this.
new, delete, and Their Array Variants
Here is the raw allocation API. Study it so you recognize it in legacy code, but never write this in new code — use smart pointers instead.
#include <iostream>
#include <cstring>
struct Widget {
int id;
Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};
int main() {
// Single object
Widget* w = new Widget(1);
std::cout << "Using widget " << w->id << '\n';
delete w; // destructor called, memory freed
// Array
Widget* arr = new Widget[3]{{10}, {20}, {30}};
for (int i = 0; i < 3; ++i)
std::cout << arr[i].id << ' ';
std::cout << '\n';
delete[] arr; // MUST use delete[] for new[]
// UNDEFINED BEHAVIOR — don't do these:
// delete arr; // wrong: used delete instead of delete[]
// delete[] w; // wrong: used delete[] instead of delete
}Placement new
Placement new constructs an object at a specific memory address without allocating. It is used by allocators, memory pools, and containers like std::vector internally. You must call the destructor explicitly — delete must NOT be used on placement-new'd objects.
#include <iostream>
#include <new> // required for placement new
#include <cstddef> // std::byte
#include <memory> // std::align
struct Sensor {
int id;
double reading;
Sensor(int i, double r) : id(i), reading(r) {
std::cout << "Sensor " << id << " init\n";
}
~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};
int main() {
// Pre-allocate raw storage (aligned)
alignas(Sensor) std::byte buffer[sizeof(Sensor)];
// Construct in pre-allocated memory
Sensor* s = new (buffer) Sensor(42, 3.14);
std::cout << "Reading: " << s->reading << '\n';
// Must destroy explicitly — do NOT use delete
s->~Sensor(); // explicit destructor call
// delete s; // WRONG — memory was not allocated by new
}The Three Deadly Memory Bugs
Raw new/delete exposes you to three categories of bugs:
1. Memory Leak — allocating with new but never calling delete. The memory is never returned to the OS (until process exit). In a long-running server, this eventually consumes all available RAM.
2. Double Free — calling delete on the same pointer twice. The second delete corrupts the heap allocator's internal data structures, often leading to crashes much later in unrelated code. This is extremely hard to debug.
3. Use-After-Free — accessing memory after delete. The memory may have been reused for a different allocation, so you could be reading or writing another object's data. This is a security vulnerability (CVE-class bug).
Consider: f(new A(), new B()). The compiler may evaluate arguments in any order and may interleave them:
1. Allocate memory for A
2. Allocate memory for B
3. Construct A
4. Construct B
If step 4 throws, A is leaked — nobody calls delete on it. Even f(std::shared_ptr(new A()), std::shared_ptr(new B())) had this problem before C++17 (which guarantees that each argument is fully evaluated before the next). The fix: always use std::make_unique or std::make_shared — they perform allocation and construction as a single, exception-safe operation.
Custom Allocators Overview
The default new/delete use the global allocator (typically malloc/free), which is general-purpose but not optimal for all patterns. Custom allocators provide:
- Arena/linear allocators — bump a pointer, free everything at once. Great for per-frame game data.
- Pool allocators — pre-allocate fixed-size blocks. O(1) alloc/free, no fragmentation.
- Stack allocators — LIFO allocation, extremely fast.
C++ containers accept allocator template parameters: std::vector. The std::pmr (polymorphic memory resources) namespace in C++17 provides a runtime-polymorphic allocator framework that avoids the template parameter problem.
In modern C++ (C++14 and later), well-written application code should contain zero raw new and delete:
- Single ownership: use std::make_unique
- Shared ownership: use std::make_shared
- Dynamic arrays: use std::vector
- Strings: use std::string
- Optional objects: use std::optional
Raw new/delete may appear in library code (allocators, custom containers), but application code should never need them. If you see new in a code review, ask why — there is almost always a safer alternative.
- Mismatching
new/delete[]ornew[]/deleteis undefined behavior - Memory leaks, double-free, and use-after-free are the three deadly memory bugs
f(new A(), new B())can leak if a constructor throws (pre-C++17 even with shared_ptr wrappers)- Placement new constructs at a given address — destroy manually, never use
delete - Modern C++ application code should have zero raw
new/delete— use smart pointers and containers
Quiz — Test Your Knowledge
(15 XP)1. What happens if you use `delete` on memory allocated with `new[]`?
2. Why can `f(new A(), new B())` leak memory?
3. How should you destroy an object created with placement new?