C++ Dynamic Memory
C++ Dynamic Memory Management
Section titled “C++ Dynamic Memory Management”Understanding dynamic memory management is crucial in C++. C++ programs typically use two main areas for memory allocation:
- The Stack: Memory used for local variables, function parameters, and return addresses. Allocation and deallocation are automatic and fast (LIFO - Last-In, First-Out). Size is usually limited.
- The Heap (Free Store): A large pool of memory available for dynamic allocation during program execution. Allocation and deallocation are manual (or managed via smart pointers), potentially slower, but necessary when the size or lifetime of data isn’t known at compile time, or when data needs to outlive the function that created it.
Dynamic memory allocation allows you to request memory from the heap at runtime when the exact amount needed isn’t known beforehand.
Raw Pointers: new and delete (Use with Caution!)
Section titled “Raw Pointers: new and delete (Use with Caution!)”Historically, C++ used the new operator to allocate memory on the heap and the delete operator to release it.
// Allocate memory for a single doubledouble* pValue = new double;*pValue = 3.14159; // Use the allocated memory
// ... later ...
delete pValue; // *** CRUCIAL: Release the memory ***pValue = nullptr; // Good practice: Avoid dangling pointerTo allocate an array:
// Allocate memory for an array of 10 integersint* pArray = new int[10];pArray[0] = 100; // Use the allocated array
// ... later ...
delete[] pArray; // *** CRUCIAL: Use delete[] for arrays ***pArray = nullptr;Problems with Raw new/delete:
- Memory Leaks: Forgetting to call
deleteordelete[]leaks memory (it remains allocated but unusable). - Dangling Pointers: Accessing memory via a pointer after
deletehas been called leads to undefined behavior. - Double Deletion: Calling
deleteordelete[]twice on the same pointer leads to undefined behavior. - Exception Safety: If an exception occurs between
newanddelete, thedeletemight be skipped, causing a leak. - Ownership Confusion: It’s often unclear which part of the code is responsible for deleting the memory.
Handling Allocation Failure: By default, if new fails to allocate memory (e.g., out of memory), it throws an exception of type std::bad_alloc (defined in <new>). You should handle this using try...catch blocks.
#include <iostream>#include <new> // Required for std::bad_alloc
int main() { double* pHugeArray = nullptr; try { // Attempt to allocate a huge amount of memory (likely to fail) pHugeArray = new double[1000000000000]; std::cout << "Memory allocated successfully (unlikely!)" << std::endl; // Use pHugeArray here... } catch (const std::bad_alloc& e) { std::cerr << "Memory allocation failed: " << e.what() << std::endl; // Handle the error appropriately return 1; // Exit or recover }
// If allocation succeeded, remember to delete delete[] pHugeArray; pHugeArray = nullptr;
return 0;}There’s also a non-throwing version: new(std::nothrow) type. It returns nullptr on failure instead of throwing, requiring manual null checks. However, the exception-throwing version is generally preferred in modern C++.
Modern C++: Smart Pointers (The Preferred Way)
Section titled “Modern C++: Smart Pointers (The Preferred Way)”Modern C++ strongly advocates using smart pointers (defined in <memory>) to manage dynamic memory. They automate the deallocation process using the RAII (Resource Acquisition Is Initialization) principle: memory is acquired in the smart pointer’s constructor and automatically released in its destructor when the smart pointer goes out of scope.
std::unique_ptr: Represents exclusive ownership. Only one unique_ptr can point to a resource at a time. When the unique_ptr is destroyed (goes out of scope), it automatically deletes the managed object. It’s lightweight and the preferred choice for single ownership.
#include <iostream>#include <memory> // Required for smart pointers#include <vector>
class MyResource {public: MyResource() { std::cout << "MyResource Created\n"; } ~MyResource() { std::cout << "MyResource Destroyed\n"; } void use() { std::cout << "Using MyResource\n"; }};
int main() { // Create a unique_ptr managing a MyResource object // Use std::make_unique (C++14+) for safer allocation auto ptr1 = std::make_unique<MyResource>();
// Use the resource via the unique_ptr (acts like a raw pointer) ptr1->use();
// unique_ptr for an array auto ptrArray = std::make_unique<int[]>(5); // Array of 5 ints ptrArray[0] = 10; std::cout << "ptrArray[0] = " << ptrArray[0] << std::endl;
// No need to manually call delete or delete[]! // Memory is released automatically when ptr1 and ptrArray go out of scope. std::cout << "Exiting main...\n"; return 0;}// Output will show MyResource Created, Using MyResource, ptrArray[0]=10, Exiting main..., MyResource Destroyedstd::shared_ptr: Represents shared ownership. Multiple shared_ptr instances can point to the same resource. The resource is deleted only when the last shared_ptr pointing to it is destroyed or reset. It uses reference counting internally, which adds some overhead compared to unique_ptr.
#include <iostream>#include <memory>#include <vector>
int main() { // Use std::make_shared (preferred way) auto shared_res = std::make_shared<MyResource>(); std::cout << "Initial use count: " << shared_res.use_count() << std::endl; // Typically 1
std::vector<std::shared_ptr<MyResource>> ptr_vec; ptr_vec.push_back(shared_res); // Copy increases use count std::cout << "Use count after vector push_back: " << shared_res.use_count() << std::endl; // Typically 2
auto another_shared = shared_res; // Another copy std::cout << "Use count after assignment: " << another_shared.use_count() << std::endl; // Typically 3
shared_res->use();
// Pointers go out of scope, use count decreases. // Resource is deleted when use count drops to 0. std::cout << "Exiting main...\n"; return 0;}// Output shows use counts changing and resource destroyed at the end.std::weak_ptr: A non-owning companion to shared_ptr. It points to an object managed by shared_ptr but doesn’t affect the reference count. Used to break potential circular references involving shared_ptr.
Summary: Prefer smart pointers (std::unique_ptr, std::shared_ptr) over raw new and delete. Use std::make_unique and std::make_shared for safer and potentially more efficient allocation. This approach largely eliminates memory leaks and makes code safer and easier to manage.
Dynamic Memory for Objects
Section titled “Dynamic Memory for Objects”When you allocate objects dynamically using new or smart pointers, their constructors are called. When they are deallocated (via delete or smart pointer destruction), their destructors are called.
#include <iostream>#include <memory>
class Box {public: Box() { std::cout << "Constructor called!\n"; } ~Box() { std::cout << "Destructor called!\n"; }};
int main() { std::cout << "Creating unique_ptr for single Box:\n"; { auto singleBoxPtr = std::make_unique<Box>(); // Constructor called here } // Destructor called here when singleBoxPtr goes out of scope std::cout << "\nCreating unique_ptr for Box array:\n"; { // Allocate an array of 4 Box objects auto myBoxArray = std::make_unique<Box[]>(4); // 4 Constructor calls happen here } // 4 Destructor calls happen here when myBoxArray goes out of scope std::cout << "\nExiting main.\n"; return 0;}The output demonstrates constructor/destructor calls managed automatically by std::unique_ptr:
Creating unique_ptr for single Box:Constructor called!Destructor called!
Creating unique_ptr for Box array:Constructor called!Constructor called!Constructor called!Constructor called!Destructor called!Destructor called!Destructor called!Destructor called!
Exiting main.