Progress9 of 20 topics

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)
cpp
1#include <iostream>
2using namespace std;
3
4void demonstrateMemoryTypes() {
5 // Stack allocation
6 int stackArray[5] = {1, 2, 3, 4, 5}; // Array on stack
7 int stackVar = 10; // Variable on stack
8
9 cout << "Stack array at address: " << stackArray << endl;
10 cout << "Stack variable at address: " << &stackVar << endl;
11
12 // Heap allocation
13 int* heapArray = new int[5]; // Array on heap
14 int* heapVar = new int; // Variable on heap
15
16 *heapVar = 20;
17 for (int i = 0; i < 5; i++) {
18 heapArray[i] = i + 1;
19 }
20
21 cout << "Heap array at address: " << heapArray << endl;
22 cout << "Heap variable at address: " << heapVar << endl;
23 cout << "Heap variable value: " << *heapVar << endl;
24
25 // Must deallocate heap memory to prevent memory leaks
26 delete[] heapArray;
27 delete heapVar;
28
29 // After deletion, the pointers become dangling pointers
30 // Best practice is to set them to nullptr
31 heapArray = nullptr;
32 heapVar = nullptr;
33}
34
35int 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 heap
  • delete - Deallocates memory allocated with new
  • new[] - Allocates an array on the heap
  • delete[] - Deallocates an array allocated with new[]
cpp
1#include <iostream>
2#include <string>
3using namespace std;
4
5class Person {
6private:
7 string name;
8 int age;
9
10public:
11 Person(const string& n, int a) : name(n), age(a) {
12 cout << "Person constructor called for " << name << endl;
13 }
14
15 ~Person() {
16 cout << "Person destructor called for " << name << endl;
17 }
18
19 void display() const {
20 cout << "Name: " << name << ", Age: " << age << endl;
21 }
22};
23
24int main() {
25 // Allocating a single object
26 Person* p1 = new Person("Alice", 30);
27 p1->display();
28
29 // Allocating an array of objects
30 Person* people = new Person[3] {
31 Person("Bob", 25),
32 Person("Charlie", 35),
33 Person("Dave", 40)
34 };
35
36 for (int i = 0; i < 3; i++) {
37 people[i].display();
38 }
39
40 // Proper deallocation
41 delete p1; // For single object
42 delete[] people; // For array of objects
43
44 // Prevent dangling pointers
45 p1 = nullptr;
46 people = nullptr;
47
48 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.

cpp
1#include <iostream>
2using namespace std;
3
4// Function with a memory leak
5void leakyFunction() {
6 int* p = new int(42);
7
8 // Use the memory
9 cout << "Value: " << *p << endl;
10
11 // Oops! Forgot to delete p
12 // delete p; // This line should be here to prevent a leak
13
14 // p goes out of scope, but the memory it pointed to remains allocated
15}
16
17// Correct function
18void properFunction() {
19 int* p = new int(42);
20
21 // Use the memory
22 cout << "Value: " << *p << endl;
23
24 // Properly delete the memory
25 delete p;
26 p = nullptr; // Good practice to avoid dangling pointer
27}
28
29int main() {
30 cout << "Calling leaky function..." << endl;
31 leakyFunction(); // Memory leak occurs
32
33 cout << "Calling proper function..." << endl;
34 properFunction(); // No memory leak
35
36 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.

cpp
1#include <iostream>
2#include <string>
3using namespace std;
4
5// RAII class for managing a dynamically allocated resource
6class ResourceManager {
7private:
8 int* resource;
9
10public:
11 // Constructor acquires the resource
12 ResourceManager(int value) {
13 cout << "Acquiring resource..." << endl;
14 resource = new int(value);
15 }
16
17 // Destructor releases the resource
18 ~ResourceManager() {
19 cout << "Releasing resource..." << endl;
20 delete resource;
21 }
22
23 // Access the resource
24 int getValue() const {
25 return *resource;
26 }
27
28 void setValue(int value) {
29 *resource = value;
30 }
31};
32
33void useResource() {
34 // Resource is automatically managed
35 ResourceManager manager(42);
36
37 cout << "Resource value: " << manager.getValue() << endl;
38
39 manager.setValue(100);
40 cout << "New value: " << manager.getValue() << endl;
41
42 // No need to manually delete!
43 // When manager goes out of scope, its destructor will be called
44 // and the resource will be released
45}
46
47int main() {
48 useResource();
49
50 // Try with exceptions
51 try {
52 ResourceManager manager(10);
53 cout << "Initial value: " << manager.getValue() << endl;
54
55 // Simulate an exception
56 throw runtime_error("Simulated exception");
57
58 // This code is never reached
59 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 }
64
65 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.

cpp
1#include <iostream>
2#include <memory> // For smart pointers
3using namespace std;
4
5class Resource {
6public:
7 Resource(int id) : id_(id) {
8 cout << "Resource " << id_ << " created" << endl;
9 }
10
11 ~Resource() {
12 cout << "Resource " << id_ << " destroyed" << endl;
13 }
14
15 void use() {
16 cout << "Using resource " << id_ << endl;
17 }
18
19private:
20 int id_;
21};
22
23void useUniquePtr() {
24 // Create a unique_ptr
25 unique_ptr<Resource> res1 = make_unique<Resource>(1); // C++14
26 // In C++11: unique_ptr<Resource> res1(new Resource(1));
27
28 res1->use();
29
30 // Cannot copy a unique_ptr
31 // unique_ptr<Resource> res2 = res1; // Compilation error
32
33 // But can transfer ownership via move
34 unique_ptr<Resource> res2 = move(res1);
35
36 // res1 is now nullptr
37 if (res1 == nullptr) {
38 cout << "res1 is now nullptr" << endl;
39 }
40
41 res2->use();
42
43 // Automatic cleanup when res2 goes out of scope
44}
45
46int main() {
47 useUniquePtr();
48
49 // unique_ptr with arrays
50 unique_ptr<Resource[]> resources = make_unique<Resource[]>(3);
51 // This creates 3 Resource objects
52
53 // No need for delete[] - it's handled automatically
54
55 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.

cpp
1#include <iostream>
2#include <memory>
3#include <vector>
4using namespace std;
5
6class Resource {
7public:
8 Resource(int id) : id_(id) {
9 cout << "Resource " << id_ << " created" << endl;
10 }
11
12 ~Resource() {
13 cout << "Resource " << id_ << " destroyed" << endl;
14 }
15
16 void use() {
17 cout << "Using resource " << id_ << endl;
18 }
19
20private:
21 int id_;
22};
23
24void useSharedPtr() {
25 // Create a shared_ptr
26 shared_ptr<Resource> res1 = make_shared<Resource>(2);
27
28 cout << "Reference count: " << res1.use_count() << endl; // 1
29
30 {
31 // Create another shared_ptr pointing to the same resource
32 shared_ptr<Resource> res2 = res1;
33
34 cout << "Reference count: " << res1.use_count() << endl; // 2
35
36 res2->use();
37
38 // Create a vector of shared_ptrs
39 vector<shared_ptr<Resource>> resources;
40 resources.push_back(res1);
41 resources.push_back(res2);
42
43 cout << "Reference count: " << res1.use_count() << endl; // 4
44
45 // res2 goes out of scope here, but the resource isn't destroyed
46 // because res1 still owns it
47 }
48
49 cout << "After inner block, reference count: " << res1.use_count() << endl; // 2
50
51 res1->use();
52
53 // Resource is destroyed when res1 goes out of scope and reference count becomes 0
54}
55
56int 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.

cpp
1#include <iostream>
2#include <memory>
3using namespace std;
4
5class Person {
6public:
7 Person(const string& name) : name_(name) {
8 cout << "Person " << name_ << " created" << endl;
9 }
10
11 ~Person() {
12 cout << "Person " << name_ << " destroyed" << endl;
13 }
14
15 void setFriend(shared_ptr<Person> friend_) {
16 friend_ = friend_;
17 }
18
19 // This would cause circular reference and memory leak
20 // void setFriend(shared_ptr<Person> friend_) {
21 // friend_ = friend_;
22 // }
23
24 string getName() const {
25 return name_;
26 }
27
28 void greetFriend() {
29 // Try to lock the weak_ptr to get a shared_ptr
30 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 }
36
37private:
38 string name_;
39 weak_ptr<Person> friend_; // Using weak_ptr to avoid circular reference
40};
41
42void demonstrateWeakPtr() {
43 auto alice = make_shared<Person>("Alice");
44 auto bob = make_shared<Person>("Bob");
45
46 alice->setFriend(bob);
47 bob->setFriend(alice);
48
49 alice->greetFriend();
50 bob->greetFriend();
51
52 cout << "Resetting Bob..." << endl;
53 bob.reset(); // Bob is destroyed
54
55 alice->greetFriend(); // Will show that Bob is no longer available
56}
57
58int 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.

cpp
1#include <iostream>
2#include <vector>
3#include <string>
4using namespace std;
5
6class MemoryResource {
7private:
8 int* data;
9 size_t size;
10
11public:
12 // Constructor
13 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 }
20
21 // Destructor
22 ~MemoryResource() {
23 cout << "Deallocating memory" << endl;
24 delete[] data;
25 }
26
27 // 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 }
35
36 // 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 }
49
50 // 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 }
56
57 // 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 }
69
70 size_t getSize() const {
71 return size;
72 }
73
74 void display(size_t n = 5) const {
75 if (data == nullptr) {
76 cout << "Empty resource" << endl;
77 return;
78 }
79
80 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};
87
88int main() {
89 // Create a resource
90 MemoryResource res1(1000000);
91 res1.display();
92
93 cout << "--- Demonstrating move semantics ---" << endl;
94
95 // Using move semantics
96 MemoryResource res2 = move(res1); // Move constructor
97
98 cout << "After move:" << endl;
99 cout << "res1 size: " << res1.getSize() << endl; // Should be 0
100 cout << "res2 size: " << res2.getSize() << endl; // Should be 1000000
101
102 res1.display(); // Should show empty
103 res2.display(); // Should show data
104
105 // Create another resource and move-assign
106 MemoryResource res3(5);
107 res3.display();
108
109 res3 = move(res2); // Move assignment
110
111 cout << "After move assignment:" << endl;
112 cout << "res2 size: " << res2.getSize() << endl; // Should be 0
113 cout << "res3 size: " << res3.getSize() << endl; // Should be 1000000
114
115 res2.display(); // Should show empty
116 res3.display(); // Should show data
117
118 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

cpp
1#include <iostream>
2#include <memory>
3#include <vector>
4#include <string>
5using namespace std;
6
7class Widget {
8public:
9 Widget(const string& name) : name_(name) {
10 cout << "Widget " << name_ << " created" << endl;
11 }
12
13 ~Widget() {
14 cout << "Widget " << name_ << " destroyed" << endl;
15 }
16
17 void use() {
18 cout << "Using widget " << name_ << endl;
19 }
20
21private:
22 string name_;
23};
24
25void modernMemoryManagement() {
26 // Using smart pointers
27 auto w1 = make_shared<Widget>("First");
28 auto w2 = make_unique<Widget>("Second");
29
30 // Using STL containers
31 vector<shared_ptr<Widget>> widgets;
32
33 widgets.push_back(w1);
34 widgets.push_back(make_shared<Widget>("Third"));
35
36 // Using range-based for loop
37 for (const auto& w : widgets) {
38 w->use();
39 }
40
41 // Using auto and move semantics
42 auto w3 = move(w2); // w2 is now nullptr
43
44 if (w2 == nullptr) {
45 cout << "w2 is now nullptr after move" << endl;
46 }
47
48 w3->use();
49
50 // All memory is automatically cleaned up when variables go out of scope
51}
52
53int main() {
54 modernMemoryManagement();
55
56 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 more

Explore generic programming with C++ templates.

Learn more

Master the powerful components in the STL.

Learn more