C++ Dynamic Memory: new, delete & Why RAII Exists
Table of Contents
- Why Dynamic Memory Exists
- Stack vs. Heap — Two Different Worlds
- The
newanddeleteOperators - Dynamic Arrays:
new[]anddelete[] - Creating Objects on the Heap
- Memory Leaks — The Silent Killer
- Dangling Pointers — Use-After-Free
- Double Free — Instant Crash
- RAII Preview — Why Modern C++ Avoids Raw
new - Common Mistakes & Pitfalls
- Key Takeaways
Every variable you have declared so far lives on the stack. The compiler allocates it when execution enters the scope and destroys it the instant that scope ends. This automatic lifetime is convenient — until it is not. What happens when you need an array whose size you only learn at runtime? Or an object that must survive long after the function that created it has returned? That is where dynamic memory enters the picture.
In this lesson you will learn how new allocates memory on the heap, how delete gives it back, and why misusing either one leads to bugs that can crash your program or silently consume every byte of RAM on the machine. If you are comfortable with pointers and references, you are ready. Let’s dig in.
1. Why Dynamic Memory Exists
Stack memory is fast and automatic, but it has hard limits. On most systems the default stack is between 1 and 8 megabytes. Try to allocate a million-element array as a local variable and you will overflow it. Worse, the stack is scope-bound: once a function returns, every local variable inside it is gone. You cannot return a pointer to a local and expect the caller to use it safely.
The heap (also called the free store in C++ terminology) solves both problems. It is a large pool of memory — typically limited only by your operating system’s virtual memory — and objects placed there persist until you explicitly destroy them. The trade-off is responsibility: the compiler will not clean up after you. Every allocation you make, you must eventually free. Forget, and your program leaks memory. Free twice, and it crashes.
2. Stack vs. Heap — Two Different Worlds
| Property | Stack | Heap |
|---|---|---|
| Allocation speed | Extremely fast (pointer bump) | Slower (allocator bookkeeping) |
| Lifetime | Automatic — dies at scope exit | Manual — lives until delete |
| Size limit | Small (1-8 MB typical) | Large (GBs possible) |
| Access pattern | LIFO (last in, first out) | Random — any order |
| Fragmentation | None | Possible over time |
The key mental model: stack variables are owned by the compiler; heap variables are owned by you. And ownership means obligation.
3. The new and delete Operators
C++ provides two operators for heap management. new allocates memory and constructs the object. delete destroys the object and deallocates the memory. This is different from C’s malloc/free, which only handle raw bytes — new and delete respect constructors and destructors (see cppreference: new expression).
#include <iostream>
int main() {
// Allocate a single int on the heap
int* p = new int; // uninitialized
*p = 42;
std::cout << "Value: " << *p << "\n"; // 42
delete p; // free the memory
p = nullptr; // good practice: avoid dangling pointer
// Allocate with an initial value (direct initialization)
int* q = new int(99);
std::cout << "Value: " << *q << "\n"; // 99
delete q;
q = nullptr;
// Value-initialization (zero-initialized)
int* r = new int(); // *r == 0
std::cout << "Value: " << *r << "\n"; // 0
delete r;
return 0;
}
Notice the pattern: every new is paired with exactly one delete. No more, no less. Break this rule and you are in undefined behavior territory.
4. Dynamic Arrays: new[] and delete[]
When you need an array whose size is determined at runtime, new[] allocates a contiguous block on the heap. The critical rule: memory allocated with new[] must be freed with delete[], not plain delete. Mixing them is undefined behavior.
#include <iostream>
int main() {
int n;
std::cout << "How many scores? ";
std::cin >> n;
// Allocate array of n ints on the heap
int* scores = new int[n];
// Fill the array
for (int i = 0; i < n; ++i) {
scores[i] = (i + 1) * 10;
}
// Print
for (int i = 0; i < n; ++i) {
std::cout << "scores[" << i << "] = " << scores[i] << "\n";
}
// Free with delete[] — NOT delete
delete[] scores;
scores = nullptr;
return 0;
}
Why does delete[] exist separately? The runtime stores how many elements were allocated (usually in a hidden header just before the returned pointer). delete[] reads that count and calls the destructor for each element. Plain delete destroys only one object and leaves the rest corrupted. For primitive types like int the crash may not happen immediately — which makes the bug even harder to find.
5. Creating Objects on the Heap
Dynamic memory becomes truly useful when combined with classes and objects. Allocating an object with new calls its constructor; delete calls its destructor before freeing memory.
#include <iostream>
#include <string>
class Sensor {
public:
std::string name;
double reading;
Sensor(const std::string& n, double r)
: name(n), reading(r) {
std::cout << "Sensor " << name << " constructed\n";
}
~Sensor() {
std::cout << "Sensor " << name << " destroyed\n";
}
};
int main() {
// Create object on the heap
Sensor* s = new Sensor("Temperature", 23.5);
std::cout << s->name << ": " << s->reading << "\n";
// Destructor called here, then memory freed
delete s;
s = nullptr;
// Array of objects
Sensor* array = new Sensor[3]{
{"Humidity", 45.0},
{"Pressure", 1013.25},
{"Wind", 12.3}
};
for (int i = 0; i < 3; ++i) {
std::cout << array[i].name << ": " << array[i].reading << "\n";
}
delete[] array; // calls ~Sensor() for each element
return 0;
}
When you run this, watch the constructor and destructor messages — they confirm that new and delete manage the full object lifecycle, not just raw bytes.
6. Memory Leaks — The Silent Killer
A memory leak happens when you allocate memory with new but never call delete. The memory stays reserved for your program but is unreachable — no pointer points to it anymore. In a long-running server, leaks accumulate until the process consumes all available RAM and the operating system kills it.
#include <iostream>
void processData() {
// BUG: allocated memory, but no delete before function returns
int* buffer = new int[1000];
buffer[0] = 42;
std::cout << "Processed: " << buffer[0] << "\n";
// Oops — we forgot delete[] buffer;
// The 4000 bytes (1000 * sizeof(int)) are now leaked.
// Every call to processData() leaks another 4000 bytes.
}
int main() {
for (int i = 0; i < 100000; ++i) {
processData(); // leaks ~400 MB total
}
return 0;
}
Leaks are insidious because the program appears to work. No crash, no error message — just steadily growing memory usage visible in Task Manager or top. Tools like Valgrind and AddressSanitizer can detect leaks automatically during testing.
7. Dangling Pointers — Use-After-Free
A dangling pointer is a pointer that still holds the address of memory that has been freed. Reading or writing through it is undefined behavior — the program might crash, produce garbage, or appear to work perfectly today and explode in production tomorrow.
#include <iostream>
int main() {
int* p = new int(100);
std::cout << *p << "\n"; // 100 — fine
delete p;
// p is now dangling — it still holds the old address
// BUG: use-after-free — undefined behavior!
// std::cout << *p << "\n"; // might print 100, 0, or crash
// The fix: set to nullptr immediately after delete
p = nullptr;
// Now dereferencing p would be a null-pointer dereference,
// which at least crashes predictably instead of silently corrupting data.
// if (p != nullptr) { std::cout << *p; }
return 0;
}
The discipline of setting pointers to nullptr after delete is not a cure — if multiple pointers alias the same memory, you have to null all of them. This is one reason raw pointers are so error-prone and why modern C++ pushes toward smart pointers (see the C++ Core Guidelines on resource management).
8. Double Free — Instant Crash
Calling delete on the same pointer twice corrupts the heap allocator’s internal data structures. The result is usually an immediate crash, but sometimes it manifests as random corruption much later in the program — the worst kind of bug to debug.
#include <iostream>
int main() {
int* p = new int(50);
delete p;
// delete p; // BUG: double free — undefined behavior, likely crash
// Safe pattern: null after delete prevents accidental double-free
p = nullptr;
delete p; // deleting nullptr is guaranteed safe (does nothing)
return 0;
}
The C++ standard explicitly guarantees that delete nullptr is a no-op (cppreference: delete). This is why the nullptr-after-delete pattern works as a safety net.
9. RAII Preview — Why Modern C++ Avoids Raw new
RAII stands for Resource Acquisition Is Initialization. The idea: tie the lifetime of a resource (heap memory, file handle, network socket) to the lifetime of a stack object. When that object’s destructor runs — automatically, at scope exit — it cleans up the resource. No manual delete required. No leaks. No double-frees.
#include <iostream>
class IntBuffer {
int* data;
int size;
public:
// Constructor acquires the resource
IntBuffer(int n) : data(new int[n]), size(n) {
std::cout << "Allocated " << n << " ints\n";
}
// Destructor releases it — automatically called at scope exit
~IntBuffer() {
delete[] data;
std::cout << "Freed " << size << " ints\n";
}
int& operator[](int i) { return data[i]; }
int getSize() const { return size; }
// Prevent copying (we'll cover copy/move semantics in a later lesson)
IntBuffer(const IntBuffer&) = delete;
IntBuffer& operator=(const IntBuffer&) = delete;
};
int main() {
{
IntBuffer buf(5);
buf[0] = 10;
buf[4] = 50;
std::cout << "buf[0]=" << buf[0]
<< " buf[4]=" << buf[4] << "\n";
} // ~IntBuffer() runs here — memory freed automatically
std::cout << "After scope — no leak, no manual delete\n";
return 0;
}
This is exactly how the standard library works. std::vector is an RAII wrapper around a dynamic array. std::unique_ptr is an RAII wrapper around a single heap object. std::string manages its own character buffer. You already use RAII every time you use these types — you just did not know the name for it.
The ISO C++ FAQ on free-store management puts it bluntly: “Avoid calling new and delete directly.” In professional codebases, raw new/delete is a code smell. But you must understand them — they are the foundation that smart pointers and containers are built on, and they appear constantly in legacy code and in interview questions.
10. Common Mistakes & Pitfalls
Mistake 1: Mismatching new/delete and new[]/delete[]
int* arr = new int[100];
delete arr; // BUG — must use delete[]
// Correct:
// delete[] arr;
This is undefined behavior. On some compilers it appears to work for primitive types, lulling you into a false sense of security. With class types that have destructors, it will corrupt memory or crash.
Mistake 2: Losing the only pointer to allocated memory
int* p = new int(10);
p = new int(20); // BUG — the first int is leaked forever
// Fix: delete p before reassigning
Mistake 3: Returning a pointer to a stack variable
int* bad() {
int x = 42;
return &x; // BUG — x is destroyed when bad() returns
}
// Fix: allocate on heap
int* good() {
return new int(42); // caller must delete
}
This is not a new/delete error per se, but it highlights why dynamic memory exists. You will encounter this confusion frequently if you are still building intuition around pointer lifetimes.
Mistake 4: Using delete on memory not from new
int x = 10;
int* p = &x;
// delete p; // BUG — x is on the stack, not the heap. Crash.
Mistake 5: Forgetting that new can throw
If the system cannot allocate the requested memory, new throws std::bad_alloc by default. In memory-constrained environments, this matters. You can use new(std::nothrow) to get a nullptr instead — see cppreference: nothrow.
When Should You Use Raw new/delete?
In modern C++ (C++14 and later), the answer is almost never. Here is a quick decision guide:
| Situation | Use This Instead |
|---|---|
| Dynamic array of known type | std::vector<T> |
| Single heap object with unique ownership | std::unique_ptr<T> |
| Shared ownership across multiple parts of code | std::shared_ptr<T> |
| Dynamic string | std::string |
| Legacy API requires raw pointer | Use smart pointer, pass .get() |
| Implementing your own container or allocator | Raw new/delete (rare, advanced) |
The C++ Core Guidelines rule R.11 states: “Avoid calling new and delete explicitly.” Follow it. We will cover smart pointers in depth in a future lesson, but understanding raw memory management is the prerequisite that makes smart pointers make sense.
Putting It All Together
Here is a slightly larger example that shows proper allocation, use, and cleanup — alongside the RAII alternative using std::vector, so you can see the contrast. If type casting or pointer arithmetic in the code looks unfamiliar, revisit those lessons first.
#include <iostream>
#include <vector>
// Manual approach — error-prone
double* computeAverages(const int* data, int rows, int cols) {
double* averages = new double[rows];
for (int r = 0; r < rows; ++r) {
double sum = 0.0;
for (int c = 0; c < cols; ++c) {
sum += data[r * cols + c];
}
averages[r] = sum / cols;
}
return averages; // caller MUST delete[]
}
int main() {
const int rows = 3, cols = 4;
// --- Raw new/delete approach ---
int* matrix = new int[rows * cols]{
10, 20, 30, 40,
50, 60, 70, 80,
90, 85, 75, 65
};
double* avg = computeAverages(matrix, rows, cols);
for (int i = 0; i < rows; ++i) {
std::cout << "Row " << i << " average: " << avg[i] << "\n";
}
delete[] avg; // caller's responsibility
delete[] matrix; // don't forget this either
// --- Modern approach with std::vector ---
std::vector<int> mat = {10,20,30,40, 50,60,70,80, 90,85,75,65};
std::vector<double> avgs(rows);
for (int r = 0; r < rows; ++r) {
double sum = 0.0;
for (int c = 0; c < cols; ++c) {
sum += mat[r * cols + c];
}
avgs[r] = sum / cols;
}
// No delete needed — vector cleans up automatically
return 0;
}
Both approaches produce identical output. But the vector version has zero leak risk, zero double-free risk, and zero chance of mismatching delete/delete[]. That is the power of RAII.
Key Takeaways
- Stack memory is automatic and fast but limited in size and scope-bound. Heap memory is large and persistent but manually managed.
newallocates and constructs.deletedestroys and deallocates. They are not justmalloc/freewith a different name.new[]must be paired withdelete[]. Mismatching is undefined behavior.- Every
newmust have exactly onedelete. Zero means a leak. Two means a crash. - Set pointers to
nullptrafterdeleteto reduce dangling-pointer risk. - Memory leaks are silent — no crash, no warning, just growing memory usage until the OS intervenes.
- Dangling pointers (use-after-free) are undefined behavior — they may appear to work and then fail unpredictably.
- RAII ties resource cleanup to object destruction. It is the foundation of modern C++ resource management.
- In production code, prefer
std::vector,std::unique_ptr, andstd::shared_ptrover rawnew/delete. Learn the raw operators to understand why those wrappers exist and to read legacy code. - Use tools like Valgrind or AddressSanitizer (ASan) to catch memory bugs during development.
You now understand the mechanics of manual memory management in C++ — and you understand why the language has evolved to avoid it wherever possible. In the next lessons, we will explore smart pointers and move semantics, which build directly on the concepts you learned here. The heap is a powerful tool. Respect it, and it will serve you well. Ignore the rules, and it will find a way to punish you.