Managing Dynamic Memory
Explore how to handle dynamic memory in C++ through manual allocation with new and delete, and gain practical knowledge of modern smart pointers such as unique_ptr and shared_ptr. Understand ownership models, lifetime management, and how to prevent common memory errors like leaks, dangling pointers, and double frees. This lesson equips you with safe practices for dynamic memory management in complex programs.
The variables we have used so far live on the stack. The stack is fast and managed automatically, but it has strict limitations: variables must have a known size at compile time, and they vanish the moment their function ends. Real-world applications, however, often need data to survive across function calls or grow dynamically based on user input.
To handle this, C++ provides the heap (also called the free store). The heap offers immense flexibility, but it comes with a catch: unlike the stack, the heap does not clean up after itself. If we take memory, we must give it back, or our program will eventually crash. In this lesson, we will learn how to wield this power responsibly using modern tools that automate the cleanup for us.
Manual allocation with new and delete
When we need memory that persists beyond the current scope, we use the new operator. This operator dynamically allocates memory from the free store (commonly backed by the heap) and returns a pointer to that memory.
Because this memory is not on the stack, it is not reclaimed when the function returns. We must explicitly release it using the delete operator.
new T: Allocates a single object of typeTand returnsT*.delete ptr: Destroys the object and frees the memory.new T[n]: Allocates an array ofnobjects of typeT, and returns a pointer to the first element of the array.delete[] ptr: Destroys the array and frees the memory. Note the brackets[]; using the wrong causes undefined behavior.deleter A deleter specifies how allocated memory or resources are destroyed and released, such as using delete for single objects or delete[] for arrays.
Let’s understand this step by step:
Line 5:
new int(42)allocates memory for one integer on the heap and initializes it to42.numPtrpoints to that integer.Line 10:
new int[5]allocates memory for an array of 5 integers on the heap.arrPtrpoints to the first element of the array.Line 15: It frees memory allocated by
new int(...)at line 5.Line 16: It frees memory allocated by
new int[...]at line 10.
The risks of manual management
Manual memory management is powerful but notoriously dangerous. In complex programs, it is easy to lose track of who owns a pointer and who should delete it.
Three common errors plague manual management:
Memory leaks: If we lose the pointer (e.g., the pointer variable goes out of scope) before calling
delete, the memory remains occupied forever.Dangling pointers: If we
deletememory but keep using the pointer, we are accessing invalid memory. This often crashes the program or corrupts data.Double free: If we try to
deletethe same memory address twice, the program will crash or behave unpredictably.
Let’s understand this step by step:
Line 4: We allocate memory on the heap.
Lines 5–7: The function returns without calling
delete. The address is lost, and the memory cannot be recovered. This is an example of memory leak.Line 14: We free the memory pointed to by
ptr.Line 17: Uncommenting this line would write to memory we no longer own, likely causing a crash.
Smart pointers in C++: Exclusive ownership with std::unique_ptr
Memory management is a core pillar of high-performance programming. To avoid memory leaks and dangling pointers, modern development relies heavily on smart pointers in C++. These are class templates that wrap a raw pointer to manage its lifecycle automatically using RAII (Resource Acquisition Is Initialization).
The most common type of smart pointer in C++ is std::unique_ptr, found in the <memory> header. A std::unique_ptr owns a resource exclusively. There can only be one unique pointer pointing to a specific resource at a time. When the unique_ptr goes out of scope, it automatically deletes the resource.
How to use std::unique_ptr
Because ownership is exclusive, you cannot copy a unique pointer. You can only move it, which transfers ownership from one variable to another. We typically use std::make_unique<T>() to create one, this is considered a best practice for smart pointers in C++ because it is safer and cleaner than using the new keyword.
Let’s understand this step by step:
Line 6:
std::make_uniqueis the recommended way to create smart pointers in C++ because it is exception-safe and removes the need for thenewkeyword.Line 9: We dereference the pointer using
*. One of the best features of smart pointers in C++ is that they behave just like raw pointers but with automatic memory management.Line 12: Copying is disabled for
std::unique_ptr. This ensures that only one "owner" exists for the memory at any time.Line 15: We use
std::moveto transfer ownership. After this, the original pointeruPtrbecomes null.Line 22: No
deletekeyword is needed; the memory is cleaned up automatically when the pointer goes out of scope.
Shared ownership with std::shared_ptr
Sometimes, multiple parts of a program need access to the same resource, and we don't know which part will finish last. For this, we use std::shared_ptr. std::shared_ptr uses reference counting as follows:
When you copy a shared pointer, the internal counter increments.
When a shared pointer is destroyed, the counter decrements.
The memory is deleted only when the counter reaches zero (i.e., the last owner is gone).
Look at the code below.
Let’s break this down step by step:
Line 6:
std::make_sharedallocates the integer and the control block for reference counting.Line 8:
.use_count()shows how many pointers share this resource. Initially, it's 1.Line 12: We copy
sPtr1tosPtr2. Both now points to the same data. So, the count becomes 2.Line 16: The inner scope ends.
sPtr2is destroyed. The count drops back to 1, but the memory is not deleted yet.Line 19:
.use_count()shows that the count dropped back to 1.Line 22:
sPtr1is destroyed. Count drops back to 0. The memory is finally freed.
Breaking cycles with std::weak_ptr
When two objects store std::shared_ptr instances pointing to each other, they form a circular reference. Because each object keeps the other’s reference count above zero, neither object is ever destroyed; even when the rest of the program no longer needs them. This prevents automatic cleanup and results in a memory leak.
C++ solves this problem with std::weak_ptr. A weak_ptr can observe an object managed by a shared_ptr, but it does not represent ownership. In other words, it does not increase the reference count, so it cannot keep an object alive by itself.
Since a weak_ptr does not own the object, the object may be destroyed while the weak_ptr still exists. For that reason, we must first convert it into a shared_ptr using .lock(). If the object is still alive, .lock() returns a valid shared_ptr. If it has already been destroyed, .lock() returns an empty shared_ptr, allowing us to safely detect that the memory is no longer available before using it.
Let’s understand this step by step:
Line 8:
observerwatchesownerbut does not increase the reference count.Line 11:
.lock()creates a temporaryshared_ptr. If the object is alive, it returns a valid pointer; otherwise, it returnsnullptr(or empty).Line 18:
owner.reset()destroys the shared pointer manually. The integer 99 is deleted because the count hits zero.Line 21:
.lock()now fails because the managed object no longer exists.
We have now moved from the risky world of manual allocation to the safety of modern C++. While new and delete are essential for understanding how memory works, modern C++ code relies almost exclusively on std::unique_ptr and std::shared_ptr. These tools ensure that no matter how complex your program logic becomes, your memory is always cleaned up correctly.