C++ • MEMORY AND POINTERS

New/Delete & Memory Risks — Use new/delete conceptually and understand why manual memory is risky (intro)

Understanding heap allocation, deallocation, and the subtle dangers of managing memory by hand in C++.

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
Dennis Ritchie's C language provides 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
Bjarne Stroustrup designs C with Classes (later C++) and introduces the 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
Java (1995) and C# (2000) introduce automatic garbage collection, eliminating manual deallocation entirely. The industry begins to recognize that manual memory management is a leading source of security vulnerabilities, including the infamous buffer-overflow exploits.
2011–2014
C++11 Smart Pointers
Modern C++ introduces 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.

1

Allocation with new

The 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.
2

Deallocation with delete

The 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.
3

Ownership Semantics

Whoever calls 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.
4

Lifetime Mismatch

Unlike stack variables, heap objects have no automatic lifetime. They persist until explicitly deleted. If no pointer retains the address, the memory becomes unreachable—a memory leak. If the object is deleted while still referenced, a dangling pointer results.
✦ KEY TAKEAWAY
Think of heap allocation like checking out a library book. The 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

The stack (left, blue) holds local variables with automatic lifetimes. The heap (right, violet) stores objects allocated with 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

NEW EXPRESSION
T* p = new T(args...);
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
delete p;
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
In C++, undefined behavior (UB) means the language standard imposes no requirements on the program's behavior—it may crash, silently corrupt data, appear to work correctly, or behave differently across compilers. Misusing 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.

The three primary categories of memory bugs arising from manual new/delete usage. Each box contains a minimal code snippet demonstrating the defect. The dashed box at the bottom lists additional related risks.
Summary of common memory risks from manual new/delete usage
RiskCauseSymptomDetection Tool
Memory LeakHeap memory allocated but never freed—pointer lost before calling deleteGradual memory growth; eventual out-of-memory crash in long-running programsValgrind (memcheck), LeakSanitizer, Visual Studio CRT Debug
Dangling PointerPointer used after the memory it references has been freedCorrupted data, segfaults, or silently wrong results depending on heap stateAddressSanitizer (ASan), Valgrind
Double DeleteCalling delete on the same pointer twiceHeap metadata corruption; program may crash or allow exploitationASan, custom allocator checks
new[]/delete MismatchUsing delete instead of delete[] for array allocation (or vice versa)Destructors not called for all elements; heap corruptionASan, static analysis (clang-tidy)
Wild PointerUsing an uninitialized pointer (points to arbitrary address)Random crashes or data corruptionCompiler 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.

Finding Memory Bugs in a Function
1
Step 1 — Read the CodeThe 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 OverrunThe first loop uses the condition 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.
Fix: change condition to i < n.
3
Step 3 — Identify Bug 2: Memory Leak via Early ReturnWhen 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.
Fix: add delete[] arr; before the early return, or restructure to have a single exit point.
4
Step 4 — Identify Bug 3: new[]/delete MismatchThe array was allocated with 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.
Fix: change to delete[] arr;.
5
Step 5 — Corrected VersionThe 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 use std::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.

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.

Comparing memory management strategies
AspectRaw new/deleteSmart Pointers (C++11+)Garbage Collection (Java/C#)
SafetyLow — all responsibility on programmerHigh — RAII ensures cleanup on scope exitHigh — runtime tracks all references
Performance OverheadMinimal (raw allocator call)Minimal for unique_ptr; atomic refcount for shared_ptrGC pauses, higher memory footprint
Deterministic DestructionYes — destructor called at point of deleteYes — destructor called when owner goes out of scopeNo — finalization timing is non-deterministic
Risk of LeaksHigh (early returns, exceptions)Very low (possible with circular shared_ptr)Very low (still possible with native interop)
Use CaseCustom allocators, legacy code, embeddedGeneral modern C++ (default choice)Application-level development
✦ KEY TAKEAWAY
Raw 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.

Raw vs. smart pointer comparison
ConceptRaw new/delete (This Lesson)Smart Pointers (Next Lesson)
OwnershipImplicit — must be tracked by convention or documentationExplicit — unique_ptr = sole owner, shared_ptr = shared ownership
Exception SafetyUnsafe — exception between new and delete causes leakSafe — stack unwinding calls destructor automatically
SyntaxT* p = new T(); ... delete p;auto p = std::make_unique<T>();
Recommended?Only in custom allocators or educational contextsYes — 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

PROBLEM 1 — CONCEPTUAL
Explain the difference between stack allocation and heap allocation in C++. Why does heap allocation require the programmer to explicitly manage the object's lifetime, while stack allocation does not?
PROBLEM 2 — BASIC CALCULATION
Consider the following code snippet. Identify the bug and state the correct fix: double* data = new double[50]; // ... use data ... delete data;
PROBLEM 3 — INTERMEDIATE
The following function has a memory leak that occurs only in certain control-flow paths. Identify the condition under which the leak occurs, explain why, and provide a corrected version. std::string* buildMessage(bool urgent) { std::string* msg = new std::string("Alert"); if (urgent) { throw std::runtime_error("Cannot process urgent messages"); } return msg; }
PROBLEM 4 — APPLIED
You are writing a game engine that allocates and deallocates thousands of enemy objects per frame. Each enemy is created with 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.
PROBLEM 5 — CRITICAL THINKING
The C++ Core Guidelines (maintained by Stroustrup and Sutter) state: "No naked new or delete." Yet the language has not deprecated these operators, and they remain available in C++23. Write a reasoned argument for why raw new/delete should remain in the language standard despite the guidelines discouraging their use. Then present the strongest counter-argument.

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.

Varsity Tutors • C++ • New/Delete & Memory Risks