SECTION 1
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.
1972 • C and malloc/free
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.1979–1985 • C++ Introduces new/delete
new and delete operators. Unlike malloc, new calls constructors and returns a typed pointer, coupling allocation with initialization.1995–2003 • Java & C# Adopt Garbage Collection
2011–2014 • C++11 Smart Pointers
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.
SECTION 2
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
KEY TAKEAWAY
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.SECTION 3
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.
SECTION 4
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
NEW EXPRESSION
Internally: (1) void* raw = operator new(sizeof(T)); (2) T* p = new(raw) T(args...); — Step 1 allocates raw memory. Step 2 invokes placement new to construct the object in-place.
The delete Expression: Step by Step
DELETE EXPRESSION
Internally: (1) p->~T(); — calls the destructor. (2) operator delete(p); — returns the memory block to the allocator. For arrays: delete[] arr; calls destructors for every element in reverse order, then frees the block. Mismatching new[] with scalar delete is undefined behavior.
Undefined Behavior Warning
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.
SECTION 5
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 |
Summary of common memory risks from manual new/delete usage
SECTION 6
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.
Finding Memory Bugs in a Function
- 1
Step 1 — Read the Code
The function under analysis: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; }Three distinct memory errors are present. - 2
Step 2 — Identify Bug 1: Buffer Overrun
The first loop uses the conditioni <= ninstead ofi < 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.Fix: change condition to i < n. - 3
Step 3 — Identify Bug 2: Memory Leak via Early Return
Whensum > 100, the function returns immediately without freeingarr. The pointerarris 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 leaksn × sizeof(int)bytes.Fix: add delete[] arr; before the early return, or restructure to have a single exit point. - 4
Step 4 — Identify Bug 3: new[]/delete Mismatch
The array was allocated withnew int[n](the array form), but the deallocation uses scalardelete arrinstead ofdelete[] arr. This is undefined behavior—on many implementations it will fail to release the full block or skip destructor calls for elements.Fix: change to delete[] arr;. - 5
Step 5 — Corrected Version
The fixed function: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 usestd::vector<int>, which eliminates all three bugs by managing memory automatically via RAII.All three bugs resolved. In production, prefer std::vector or std::unique_ptr over raw new/delete.
SECTION 7
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 |
Comparing memory management strategies
KEY TAKEAWAY
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.SECTION 8
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++ |
Raw vs. smart pointer comparison
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.
SECTION 9
Practice Problems
Question 1: CONCEPTUAL
Show answer
new, requests memory from the free store, which is a shared pool managed by the runtime. The runtime has no knowledge of scope boundaries, so it cannot determine when the memory is no longer needed. The programmer must explicitly call delete to signal that the memory can be reclaimed. This decoupling of lifetime from scope is both the power (objects can outlive the function that created them) and the danger (if no one calls delete, the memory leaks).Question 2: BASIC CALCULATION
double* data = new double[50];
// ... use data ...
delete data;Show answer
new double[50] (array form), but deallocated with scalar delete data instead of delete[] data. This is undefined behavior. On many implementations, the allocator stores the array element count just before the returned pointer; scalar delete does not read this metadata, so it may free the wrong amount of memory or skip destructor calls. The fix is simply to change the deallocation to delete[] data;. An even better approach is to use std::vector<double> data(50);.Question 3: INTERMEDIATE
std::string* buildMessage(bool urgent) {
std::string* msg = new std::string("Alert");
if (urgent) {
throw std::runtime_error("Cannot process urgent messages");
}
return msg;
}Show answer
urgent is true, the function throws an exception after allocating the string on the heap. Because the exception causes stack unwinding, the local pointer msg is destroyed (it's a stack variable), but the heap-allocated std::string it points to is never deallocated—resulting in a memory leak. The corrected approach uses a smart pointer: auto msg = std::make_unique<std::string>("Alert");. If the exception fires, the unique_ptr's destructor runs during stack unwinding and frees the string. Alternatively, one could catch the exception, delete msg, and rethrow, but this is brittle and not recommended.Question 4: APPLIED
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.Show answer
std::vector<std::unique_ptr<Enemy>> so that removing an element automatically invokes the destructor. For high-frequency allocation, consider an object pool pattern where enemies are recycled rather than destroyed—this avoids heap fragmentation and allocation overhead while keeping ownership clear.Question 5: CRITICAL THINKING
Show answer
SUMMARY
Lesson Summary
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.