45% complete
Memory Management in C++
Memory management is one of the most powerful yet challenging aspects of C++. Understanding how to properly allocate, use, and deallocate memory is essential for writing efficient and bug-free C++ programs.
What You'll Learn
- Stack vs heap memory allocation
- Dynamic memory allocation with new and delete
- Common memory issues like memory leaks and dangling pointers
- Smart pointers and RAII (Resource Acquisition Is Initialization)
- Move semantics and perfect forwarding
- Best practices for memory management
Stack vs Heap Memory
In C++, memory is primarily allocated in two ways: on the stack or on the heap. Understanding the difference is crucial for effective memory management.
Stack Memory
- Automatically managed by the system
- Fixed size determined at compile time
- Very fast allocation and deallocation
- Stores local variables, function parameters, and function calls
- Memory is automatically reclaimed when variables go out of scope
- Limited in size (typically a few MB)
Heap Memory
- Manually managed by the programmer
- Size can be determined at runtime
- Slower allocation and deallocation
- Used for dynamic memory allocation
- Memory must be explicitly freed when no longer needed
- Much larger size (limited by physical memory)
1#include <iostream>2using namespace std;34void demonstrateMemoryTypes() {5 // Stack allocation6 int stackArray[5] = {1, 2, 3, 4, 5}; // Array on stack7 int stackVar = 10; // Variable on stack89 cout << "Stack array at address: " << stackArray << endl;10 cout << "Stack variable at address: " << &stackVar << endl;1112 // Heap allocation13 int* heapArray = new int[5]; // Array on heap14 int* heapVar = new int; // Variable on heap1516 *heapVar = 20;17 for (int i = 0; i < 5; i++) {18 heapArray[i] = i + 1;19 }2021 cout << "Heap array at address: " << heapArray << endl;22 cout << "Heap variable at address: " << heapVar << endl;23 cout << "Heap variable value: " << *heapVar << endl;2425 // Must deallocate heap memory to prevent memory leaks26 delete[] heapArray;27 delete heapVar;2829 // After deletion, the pointers become dangling pointers30 // Best practice is to set them to nullptr31 heapArray = nullptr;32 heapVar = nullptr;33}3435int main() {36 demonstrateMemoryTypes();37 return 0;38}
Note: Stack variables have automatic storage duration—they're automatically created when the program enters their scope and destroyed when it leaves. Heap variables have dynamic storage duration—they exist until explicitly deallocated.
Dynamic Memory Allocation
C++ provides operators for dynamic memory allocation and deallocation:
new
- Allocates memory on the heapdelete
- Deallocates memory allocated with newnew[]
- Allocates an array on the heapdelete[]
- Deallocates an array allocated with new[]
1#include <iostream>2#include <string>3using namespace std;45class Person {6private:7 string name;8 int age;910public:11 Person(const string& n, int a) : name(n), age(a) {12 cout << "Person constructor called for " << name << endl;13 }1415 ~Person() {16 cout << "Person destructor called for " << name << endl;17 }1819 void display() const {20 cout << "Name: " << name << ", Age: " << age << endl;21 }22};2324int main() {25 // Allocating a single object26 Person* p1 = new Person("Alice", 30);27 p1->display();2829 // Allocating an array of objects30 Person* people = new Person[3] {31 Person("Bob", 25),32 Person("Charlie", 35),33 Person("Dave", 40)34 };3536 for (int i = 0; i < 3; i++) {37 people[i].display();38 }3940 // Proper deallocation41 delete p1; // For single object42 delete[] people; // For array of objects4344 // Prevent dangling pointers45 p1 = nullptr;46 people = nullptr;4748 return 0;49}
Memory Leaks
A memory leak occurs when allocated memory is not freed, causing the program to consume increasing amounts of memory over time. This can lead to degraded performance or crashes.
1#include <iostream>2using namespace std;34// Function with a memory leak5void leakyFunction() {6 int* p = new int(42);78 // Use the memory9 cout << "Value: " << *p << endl;1011 // Oops! Forgot to delete p12 // delete p; // This line should be here to prevent a leak1314 // p goes out of scope, but the memory it pointed to remains allocated15}1617// Correct function18void properFunction() {19 int* p = new int(42);2021 // Use the memory22 cout << "Value: " << *p << endl;2324 // Properly delete the memory25 delete p;26 p = nullptr; // Good practice to avoid dangling pointer27}2829int main() {30 cout << "Calling leaky function..." << endl;31 leakyFunction(); // Memory leak occurs3233 cout << "Calling proper function..." << endl;34 properFunction(); // No memory leak3536 return 0;37}
Common Memory Issues
- Memory leaks: Forgetting to deallocate memory
- Dangling pointers: Using a pointer after its memory has been freed
- Double deletion: Deleting the same memory twice
- Memory fragmentation: Free memory becomes divided into small, non-contiguous blocks
- Buffer overflows: Writing beyond allocated memory boundaries
RAII: Resource Acquisition Is Initialization
RAII is a C++ programming technique where resource acquisition (like memory allocation) is tied to object initialization, and resource release (like memory deallocation) is tied to object destruction. This ensures resources are properly managed.
1#include <iostream>2#include <string>3using namespace std;45// RAII class for managing a dynamically allocated resource6class ResourceManager {7private:8 int* resource;910public:11 // Constructor acquires the resource12 ResourceManager(int value) {13 cout << "Acquiring resource..." << endl;14 resource = new int(value);15 }1617 // Destructor releases the resource18 ~ResourceManager() {19 cout << "Releasing resource..." << endl;20 delete resource;21 }2223 // Access the resource24 int getValue() const {25 return *resource;26 }2728 void setValue(int value) {29 *resource = value;30 }31};3233void useResource() {34 // Resource is automatically managed35 ResourceManager manager(42);3637 cout << "Resource value: " << manager.getValue() << endl;3839 manager.setValue(100);40 cout << "New value: " << manager.getValue() << endl;4142 // No need to manually delete!43 // When manager goes out of scope, its destructor will be called44 // and the resource will be released45}4647int main() {48 useResource();4950 // Try with exceptions51 try {52 ResourceManager manager(10);53 cout << "Initial value: " << manager.getValue() << endl;5455 // Simulate an exception56 throw runtime_error("Simulated exception");5758 // This code is never reached59 manager.setValue(20);60 } catch (const exception& e) {61 cout << "Exception caught: " << e.what() << endl;62 cout << "But the resource is still properly released!" << endl;63 }6465 return 0;66}
RAII is one of the most important idioms in C++. It allows resources to be managed automatically, even in the presence of exceptions, ensuring proper cleanup.
Smart Pointers
Smart pointers are RAII wrappers around raw pointers that automatically manage memory. The C++ standard library provides several types of smart pointers:
unique_ptr
A unique_ptr
exclusively owns the object it points to. When the unique_ptr is destroyed, it automatically deletes the object it owns.
1#include <iostream>2#include <memory> // For smart pointers3using namespace std;45class Resource {6public:7 Resource(int id) : id_(id) {8 cout << "Resource " << id_ << " created" << endl;9 }1011 ~Resource() {12 cout << "Resource " << id_ << " destroyed" << endl;13 }1415 void use() {16 cout << "Using resource " << id_ << endl;17 }1819private:20 int id_;21};2223void useUniquePtr() {24 // Create a unique_ptr25 unique_ptr<Resource> res1 = make_unique<Resource>(1); // C++1426 // In C++11: unique_ptr<Resource> res1(new Resource(1));2728 res1->use();2930 // Cannot copy a unique_ptr31 // unique_ptr<Resource> res2 = res1; // Compilation error3233 // But can transfer ownership via move34 unique_ptr<Resource> res2 = move(res1);3536 // res1 is now nullptr37 if (res1 == nullptr) {38 cout << "res1 is now nullptr" << endl;39 }4041 res2->use();4243 // Automatic cleanup when res2 goes out of scope44}4546int main() {47 useUniquePtr();4849 // unique_ptr with arrays50 unique_ptr<Resource[]> resources = make_unique<Resource[]>(3);51 // This creates 3 Resource objects5253 // No need for delete[] - it's handled automatically5455 return 0;56}
shared_ptr
A shared_ptr
allows multiple pointers to share ownership of the same object. The object is destroyed when the last shared_ptr owning it is destroyed or reassigned.
1#include <iostream>2#include <memory>3#include <vector>4using namespace std;56class Resource {7public:8 Resource(int id) : id_(id) {9 cout << "Resource " << id_ << " created" << endl;10 }1112 ~Resource() {13 cout << "Resource " << id_ << " destroyed" << endl;14 }1516 void use() {17 cout << "Using resource " << id_ << endl;18 }1920private:21 int id_;22};2324void useSharedPtr() {25 // Create a shared_ptr26 shared_ptr<Resource> res1 = make_shared<Resource>(2);2728 cout << "Reference count: " << res1.use_count() << endl; // 12930 {31 // Create another shared_ptr pointing to the same resource32 shared_ptr<Resource> res2 = res1;3334 cout << "Reference count: " << res1.use_count() << endl; // 23536 res2->use();3738 // Create a vector of shared_ptrs39 vector<shared_ptr<Resource>> resources;40 resources.push_back(res1);41 resources.push_back(res2);4243 cout << "Reference count: " << res1.use_count() << endl; // 44445 // res2 goes out of scope here, but the resource isn't destroyed46 // because res1 still owns it47 }4849 cout << "After inner block, reference count: " << res1.use_count() << endl; // 25051 res1->use();5253 // Resource is destroyed when res1 goes out of scope and reference count becomes 054}5556int main() {57 useSharedPtr();58 return 0;59}
weak_ptr
A weak_ptr
is used in conjunction with shared_ptr. It provides access to an object owned by shared_ptrs without increasing the reference count. This helps avoid circular references.
1#include <iostream>2#include <memory>3using namespace std;45class Person {6public:7 Person(const string& name) : name_(name) {8 cout << "Person " << name_ << " created" << endl;9 }1011 ~Person() {12 cout << "Person " << name_ << " destroyed" << endl;13 }1415 void setFriend(shared_ptr<Person> friend_) {16 friend_ = friend_;17 }1819 // This would cause circular reference and memory leak20 // void setFriend(shared_ptr<Person> friend_) {21 // friend_ = friend_;22 // }2324 string getName() const {25 return name_;26 }2728 void greetFriend() {29 // Try to lock the weak_ptr to get a shared_ptr30 if (auto sp = friend_.lock()) {31 cout << name_ << " greets " << sp->getName() << endl;32 } else {33 cout << name_ << "'s friend is no longer available" << endl;34 }35 }3637private:38 string name_;39 weak_ptr<Person> friend_; // Using weak_ptr to avoid circular reference40};4142void demonstrateWeakPtr() {43 auto alice = make_shared<Person>("Alice");44 auto bob = make_shared<Person>("Bob");4546 alice->setFriend(bob);47 bob->setFriend(alice);4849 alice->greetFriend();50 bob->greetFriend();5152 cout << "Resetting Bob..." << endl;53 bob.reset(); // Bob is destroyed5455 alice->greetFriend(); // Will show that Bob is no longer available56}5758int main() {59 demonstrateWeakPtr();60 return 0;61}
Move Semantics
Move semantics, introduced in C++11, allows resources to be transferred from one object to another instead of copying them. This is especially useful for efficiently managing memory.
1#include <iostream>2#include <vector>3#include <string>4using namespace std;56class MemoryResource {7private:8 int* data;9 size_t size;1011public:12 // Constructor13 MemoryResource(size_t sz) : size(sz) {14 cout << "Allocating " << size << " integers" << endl;15 data = new int[size];16 for (size_t i = 0; i < size; i++) {17 data[i] = i;18 }19 }2021 // Destructor22 ~MemoryResource() {23 cout << "Deallocating memory" << endl;24 delete[] data;25 }2627 // Copy constructor - expensive!28 MemoryResource(const MemoryResource& other) : size(other.size) {29 cout << "Copy constructor - deep copying " << size << " integers" << endl;30 data = new int[size];31 for (size_t i = 0; i < size; i++) {32 data[i] = other.data[i];33 }34 }3536 // Copy assignment operator - expensive!37 MemoryResource& operator=(const MemoryResource& other) {38 cout << "Copy assignment - deep copying " << other.size << " integers" << endl;39 if (this != &other) {40 delete[] data;41 size = other.size;42 data = new int[size];43 for (size_t i = 0; i < size; i++) {44 data[i] = other.data[i];45 }46 }47 return *this;48 }4950 // Move constructor - efficient!51 MemoryResource(MemoryResource&& other) noexcept : data(other.data), size(other.size) {52 cout << "Move constructor - stealing resources" << endl;53 other.data = nullptr;54 other.size = 0;55 }5657 // Move assignment operator - efficient!58 MemoryResource& operator=(MemoryResource&& other) noexcept {59 cout << "Move assignment - stealing resources" << endl;60 if (this != &other) {61 delete[] data;62 data = other.data;63 size = other.size;64 other.data = nullptr;65 other.size = 0;66 }67 return *this;68 }6970 size_t getSize() const {71 return size;72 }7374 void display(size_t n = 5) const {75 if (data == nullptr) {76 cout << "Empty resource" << endl;77 return;78 }7980 cout << "First " << min(n, size) << " elements: ";81 for (size_t i = 0; i < min(n, size); i++) {82 cout << data[i] << " ";83 }84 cout << endl;85 }86};8788int main() {89 // Create a resource90 MemoryResource res1(1000000);91 res1.display();9293 cout << "--- Demonstrating move semantics ---" << endl;9495 // Using move semantics96 MemoryResource res2 = move(res1); // Move constructor9798 cout << "After move:" << endl;99 cout << "res1 size: " << res1.getSize() << endl; // Should be 0100 cout << "res2 size: " << res2.getSize() << endl; // Should be 1000000101102 res1.display(); // Should show empty103 res2.display(); // Should show data104105 // Create another resource and move-assign106 MemoryResource res3(5);107 res3.display();108109 res3 = move(res2); // Move assignment110111 cout << "After move assignment:" << endl;112 cout << "res2 size: " << res2.getSize() << endl; // Should be 0113 cout << "res3 size: " << res3.getSize() << endl; // Should be 1000000114115 res2.display(); // Should show empty116 res3.display(); // Should show data117118 return 0;119}
Best Practices for Memory Management
Best Practices
- Use smart pointers instead of raw pointers whenever possible
- Prefer stack allocation over heap allocation when feasible
- Follow the RAII principle to manage resources
- Use move semantics to transfer ownership efficiently
- Set pointers to nullptr after deletion
- Avoid manual memory management in modern C++
- Use container classes from the STL instead of manual arrays
- Be consistent in how memory is allocated and deallocated
Modern C++ Memory Management
1#include <iostream>2#include <memory>3#include <vector>4#include <string>5using namespace std;67class Widget {8public:9 Widget(const string& name) : name_(name) {10 cout << "Widget " << name_ << " created" << endl;11 }1213 ~Widget() {14 cout << "Widget " << name_ << " destroyed" << endl;15 }1617 void use() {18 cout << "Using widget " << name_ << endl;19 }2021private:22 string name_;23};2425void modernMemoryManagement() {26 // Using smart pointers27 auto w1 = make_shared<Widget>("First");28 auto w2 = make_unique<Widget>("Second");2930 // Using STL containers31 vector<shared_ptr<Widget>> widgets;3233 widgets.push_back(w1);34 widgets.push_back(make_shared<Widget>("Third"));3536 // Using range-based for loop37 for (const auto& w : widgets) {38 w->use();39 }4041 // Using auto and move semantics42 auto w3 = move(w2); // w2 is now nullptr4344 if (w2 == nullptr) {45 cout << "w2 is now nullptr after move" << endl;46 }4748 w3->use();4950 // All memory is automatically cleaned up when variables go out of scope51}5253int main() {54 modernMemoryManagement();5556 cout << "All resources have been properly cleaned up" << endl;57 return 0;58}
Summary
Effective memory management is critical in C++ programming. In this tutorial, you've learned:
- The differences between stack and heap memory
- How to use dynamic memory allocation with new/delete
- Common memory issues and how to avoid them
- The RAII principle for automatic resource management
- Smart pointers (unique_ptr, shared_ptr, weak_ptr)
- Move semantics for efficient resource transfer
- Best practices for modern C++ memory management
By following modern C++ practices, you can write code that is both efficient and safe from memory-related bugs like leaks, dangling pointers, and double deletions.
Practice Exercise
Create a program that manages a collection of objects using smart pointers. Implement a class that contains a large resource (like a dynamically allocated array), and demonstrate proper memory management using RAII, move semantics, and smart pointers.
Related Tutorials
Learn about object-oriented programming in C++.
Learn moreExplore generic programming with C++ templates.
Learn moreMaster the powerful components in the STL.
Learn more