Historical Context & Motivation
Before high-level languages with automatic garbage collection became mainstream, programmers had to manage every byte of memory themselves. Languages like assembly and C gave developers direct control over hardware resources, including the ability to request and release memory from the operating system at will. This power enabled extremely efficient programs—but at a cost. Bugs arising from incorrect memory handling became one of the most persistent and dangerous classes of software defects, responsible for crashes, security vulnerabilities, and unpredictable behavior that could remain hidden for years. When Bjarne Stroustrup designed C++ in the early 1980s, he inherited C's manual memory model but introduced the new and delete operators to pair allocation with construction and deallocation with destruction in a type-safe manner. Understanding why these operators exist—and why raw usage is considered risky in modern C++—requires a brief tour through the evolution of memory management.
malloc and free for raw heap allocation. These functions return untyped memory and require the programmer to track sizes manually, creating fertile ground for buffer overflows and use-after-free bugs.new and delete operators. Unlike malloc, new calls constructors and returns a typed pointer, coupling allocation with initialization.std::unique_ptr and std::shared_ptr, providing deterministic, RAII-based alternatives to raw new/delete. The C++ Core Guidelines later recommend avoiding raw owning pointers altogether.The central question this lesson addresses is deceptively simple: if new gives us memory and delete gives it back, what can possibly go wrong? As we will see, the answer is quite a lot—and understanding these risks is essential before moving on to the modern alternatives that mitigate them.
Core Principles of Dynamic Memory
Every running C++ program divides its address space into several regions, but two are critical for this discussion: the stack and the heap (also called the free store). Stack memory is managed automatically—local variables are created when a function is entered and destroyed when it returns. Heap memory, by contrast, is allocated and freed explicitly by the programmer using new and delete. This manual control is what makes dynamic memory both powerful and dangerous.
Allocation with new
new operator requests a block of memory from the heap, calls the constructor for the specified type, and returns a typed pointer. If allocation fails, it throws std::bad_alloc by default.Deallocation with delete
delete operator calls the destructor of the object pointed to, then returns the memory block to the heap. For arrays allocated with new[], the matching delete[] must be used.Ownership Semantics
new "owns" the memory and is responsible for eventually calling delete. When ownership is ambiguous—passed across function boundaries or stored in multiple locations—bugs inevitably follow.Lifetime Mismatch
new operator is checking the book out; delete is returning it. If you never return it, no one else can use it (memory leak). If you return it and then try to read your copy, you're reading a book that might have been given to someone else (dangling pointer). And if you return the same book twice, the librarian's records become corrupted (double delete). The library's catalog has no automatic tracking—the burden falls entirely on you.Visualizing Stack vs. Heap Allocation
new. The dashed arrow shows how a stack-based pointer (hp) references data living on the heap. The red block illustrates a leaked allocation—memory with no remaining pointer to it.In the diagram above, notice the fundamental asymmetry between the two regions. Stack variables have deterministic lifetimes tied to lexical scope—when the enclosing block ends, the compiler automatically reclaims that memory. Heap objects, on the other hand, persist indefinitely until a corresponding delete call frees them. The pointer hp itself lives on the stack and will be destroyed when the function returns, but the int(99) it points to remains on the heap. If no other pointer copied the address before hp is destroyed, that four-byte block becomes unreachable—a classic memory leak.
How new and delete Work Under the Hood
When you write int* p = new int(42);, the compiler translates this into a two-phase operation. First, the runtime calls operator new, which is the C++ analogue of C's malloc. This function requests a block of at least sizeof(int) bytes from the operating system's memory allocator. If the request succeeds, the runtime receives a raw memory address. Second, the compiler generates code to invoke the type's constructor at that address—in this case, storing the value 42 into the allocated integer. The combined pointer is then returned to p.
The new Expression: Step by Step
The delete Expression: Step by Step
delete[] arr; calls destructors for every element in reverse order, then frees the block. Mismatching new[] with scalar delete is undefined behavior.new/delete (e.g., double delete, delete-on-stack-pointer, new/delete mismatch with new[]/delete[]) all trigger UB. Compilers are not required to warn you.It is worth noting that operator new can be overloaded at global or class scope, allowing custom allocators for performance-critical applications (e.g., memory pools in game engines). However, the fundamental responsibility remains unchanged: every allocation must eventually be paired with exactly one deallocation, and the deallocation must use the matching form.
A Taxonomy of Memory Risks
Manual memory management introduces several distinct failure modes. Each represents a different way the invariant "every allocation has exactly one matching deallocation" can be violated. These bugs are notoriously difficult to diagnose because their symptoms often manifest far from the actual defect—a corrupted heap may not cause a crash until thousands of instructions later, in a completely unrelated function.
new/delete usage. Each box contains a minimal code snippet demonstrating the defect. The dashed box at the bottom lists additional related risks.| Risk | Cause | Symptom | Detection Tool |
|---|---|---|---|
| Memory Leak | Heap memory allocated but never freed—pointer lost before calling delete | Gradual memory growth; eventual out-of-memory crash in long-running programs | Valgrind (memcheck), LeakSanitizer, Visual Studio CRT Debug |
| Dangling Pointer | Pointer used after the memory it references has been freed | Corrupted data, segfaults, or silently wrong results depending on heap state | AddressSanitizer (ASan), Valgrind |
| Double Delete | Calling delete on the same pointer twice | Heap metadata corruption; program may crash or allow exploitation | ASan, custom allocator checks |
| new[]/delete Mismatch | Using delete instead of delete[] for array allocation (or vice versa) | Destructors not called for all elements; heap corruption | ASan, static analysis (clang-tidy) |
| Wild Pointer | Using an uninitialized pointer (points to arbitrary address) | Random crashes or data corruption | Compiler warnings (-Wall), MSan |
Worked Example: Tracing a Memory Bug
Consider the following function that is intended to create a dynamic array, populate it, compute a sum, and return the result. We will trace through the code to identify every memory management issue.
int computeSum(int n) {
int* arr = new int[n];
for (int i = 0; i <= n; ++i) // Bug 1
arr[i] = i * 2;
int sum = 0;
for (int i = 0; i < n; ++i)
sum += arr[i];
if (sum > 100)
return sum; // Bug 2
delete arr; // Bug 3
return sum;
}
i <= n instead of i < n. This means on the final iteration, arr[n] writes one element past the allocated array. This is a classic heap buffer overrun — undefined behavior that corrupts heap metadata or adjacent objects.sum > 100, the function returns immediately without freeing arr. The pointer arr is a local variable on the stack; when the function exits, the pointer is destroyed but the heap memory it referenced is never deallocated. This is a memory leak. Every call where sum exceeds 100 leaks n × sizeof(int) bytes.new int[n] (the array form), but the deallocation uses scalar delete arr instead of delete[] arr. This is undefined behavior—on many implementations it will fail to release the full block or skip destructor calls for elements.
int computeSum(int n) {
int* arr = new int[n];
for (int i = 0; i < n; ++i)
arr[i] = i * 2;
int sum = 0;
for (int i = 0; i < n; ++i)
sum += arr[i];
delete[] arr;
return sum;
}
Even better would be to use std::vector<int>, which eliminates all three bugs by managing memory automatically via RAII.Manual vs. Automatic Memory Management
If manual memory management is so error-prone, why does C++ keep it at all? The answer lies in the language's philosophy of zero-overhead abstraction—you should not pay (in runtime cost) for features you don't use. Garbage-collected languages like Java trade deterministic deallocation for safety, incurring pauses and higher memory overhead. C++ instead offers a spectrum: raw new/delete for maximum control, smart pointers for safety with near-zero overhead, and containers like std::vector that handle allocation internally.
| Aspect | Raw new/delete | Smart Pointers (C++11+) | Garbage Collection (Java/C#) |
|---|---|---|---|
| Safety | Low — all responsibility on programmer | High — RAII ensures cleanup on scope exit | High — runtime tracks all references |
| Performance Overhead | Minimal (raw allocator call) | Minimal for unique_ptr; atomic refcount for shared_ptr | GC pauses, higher memory footprint |
| Deterministic Destruction | Yes — destructor called at point of delete | Yes — destructor called when owner goes out of scope | No — finalization timing is non-deterministic |
| Risk of Leaks | High (early returns, exceptions) | Very low (possible with circular shared_ptr) | Very low (still possible with native interop) |
| Use Case | Custom allocators, legacy code, embedded | General modern C++ (default choice) | Application-level development |
new and delete are like driving a car with no seatbelt, no airbag, and a manual transmission. You can go fast and have complete control, but one mistake can be catastrophic. Smart pointers are the modern equivalent of adding safety features—you retain the performance of driving yourself, but the car (the compiler) handles the dangerous parts. Garbage collection is like taking a taxi: extremely safe, but you lose control over the route and there's a fare.Connection to RAII and Smart Pointers
The C++ community has converged on a powerful idiom to tame manual memory management: RAII (Resource Acquisition Is Initialization). The principle is simple—bind every resource's lifetime to the lifetime of a stack-allocated object. When that object's destructor runs (automatically, at scope exit), the resource is released. Smart pointers are the canonical application of RAII to heap memory. std::unique_ptr<T> wraps a raw pointer and calls delete (or delete[]) in its destructor, making leaks structurally impossible as long as the smart pointer itself has a well-defined scope. std::shared_ptr<T> adds reference counting for shared ownership scenarios, at the cost of a small atomic increment/decrement overhead.
| Concept | Raw new/delete (This Lesson) | Smart Pointers (Next Lesson) |
|---|---|---|
| Ownership | Implicit — must be tracked by convention or documentation | Explicit — unique_ptr = sole owner, shared_ptr = shared ownership |
| Exception Safety | Unsafe — exception between new and delete causes leak | Safe — stack unwinding calls destructor automatically |
| Syntax | T* p = new T(); ... delete p; | auto p = std::make_unique<T>(); |
| Recommended? | Only in custom allocators or educational contexts | Yes — default choice in modern C++ |
In subsequent lessons, you will explore smart pointers and the Rule of Five in depth. For now, the essential insight is that understanding raw new/delete is foundational—not because you should use them everywhere, but because smart pointers are built on top of them. You cannot understand why unique_ptr is safe without first understanding why raw pointers are dangerous.
Practice Problems
double* data = new double[50];
// ... use data ...
delete data;std::string* buildMessage(bool urgent) {
std::string* msg = new std::string("Alert");
if (urgent) {
throw std::runtime_error("Cannot process urgent messages");
}
return msg;
}new Enemy() and later freed with delete. During playtesting, you notice memory usage steadily climbs over a 30-minute session, and Valgrind reports 'definitely lost' blocks. Describe a systematic strategy for finding and fixing the leak, and explain what data structures or design patterns could prevent the issue.Lesson Summary
C++ provides the new operator to allocate typed memory on the heap and call the object's constructor, and the delete operator to call the destructor and return memory to the free store. Unlike stack variables, whose lifetimes are bound to lexical scope, heap objects persist until explicitly freed—placing the burden of correct lifetime management entirely on the programmer.
This manual responsibility creates three primary risk categories: memory leaks (forgotten deallocation), dangling pointers (use after delete), and double deletes (freeing the same address twice)—all of which cause undefined behavior. Modern C++ addresses these risks through RAII and smart pointers (unique_ptr, shared_ptr), which automate deallocation while preserving the deterministic destruction guarantees that distinguish C++ from garbage-collected languages. Understanding raw new/delete is essential groundwork for mastering these safer abstractions.