Introduction to Memory Management
Memory management is a critical skill in C++. Unlike garbage-collected languages, C++ gives you direct control over memory allocation and deallocation. This power comes with responsibility—improper memory management leads to bugs, crashes, and security vulnerabilities.
Memory Management
Memory management is the process of controlling and coordinating computer memory, assigning portions called blocks to running programs, and freeing them when no longer needed.
Key Principle: Every allocation must have a corresponding deallocation.
Memory Regions in C++
Stack
Fast, automatic allocation for local variables
Heap
Dynamic allocation with manual control
Global/Static
Lives for entire program duration
Code (Text)
Stores compiled program instructions
#include <iostream>
using namespace std;
int globalVar = 100; // Global/Static segment
int main() {
int stackVar = 10; // Stack - automatic management
static int staticVar = 20; // Global/Static segment
int* heapVar = new int(30); // Heap - manual management
cout << "Global: " << globalVar << endl;
cout << "Stack: " << stackVar << endl;
cout << "Static: " << staticVar << endl;
cout << "Heap: " << *heapVar << endl;
delete heapVar; // Must manually free heap memory!
return 0;
} // stackVar automatically freed here
Stack vs Heap Memory
Understanding the difference between stack and heap memory is fundamental to effective C++ programming. Each has distinct characteristics, advantages, and use cases.
Stack Memory
#include <iostream>
using namespace std;
void stackExample() {
// All these variables are on the STACK
int a = 10;
double b = 3.14;
char c = 'X';
int arr[5] = {1, 2, 3, 4, 5}; // Fixed-size array on stack
cout << "Stack variables:" << endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;
// Stack memory layout (grows downward):
// ┌─────────────────┐ Higher Address
// │ arr[4] │
// │ arr[3] │
// │ arr[2] │
// │ arr[1] │
// │ arr[0] │
// │ c │
// │ b │
// │ a │
// └─────────────────┘ Lower Address (Stack Pointer)
} // All stack variables automatically freed here!
int main() {
stackExample();
// a, b, c, arr no longer exist - memory reclaimed
return 0;
}
Heap Memory
#include <iostream>
using namespace std;
int* createOnHeap() {
// Allocate on HEAP - survives function return
int* ptr = new int(42);
return ptr; // Safe! Memory persists
}
int* dangerousStack() {
int local = 100;
return &local; // DANGER! Returns address of dead variable
}
int main() {
// Heap allocation
int* heapPtr = createOnHeap();
cout << "Heap value: " << *heapPtr << endl; // 42
// Dynamic array on heap
int size = 10; // Can be determined at runtime!
int* arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
// Must manually free heap memory
delete heapPtr;
delete[] arr;
return 0;
}
Comparison Table
| Feature | Stack | Heap |
|---|---|---|
| Allocation Speed | Very Fast | Slower |
| Deallocation | Automatic (scope-based) | Manual (delete required) |
| Size | Limited (usually 1-8 MB) | Large (limited by RAM) |
| Size Known | At compile time | At runtime |
| Access Speed | Faster | Slower |
| Fragmentation | No | Yes (can occur) |
| Use Case | Local variables, small data | Large data, dynamic structures |
Practice Questions: Stack vs Heap
Task: For each variable, identify if it's on stack or heap:
int a = 5;
int* b = new int(10);
double arr[3] = {1.0, 2.0, 3.0};
char* str = new char[20];
Show Solution
int a = 5; // STACK
int* b = new int(10); // b (pointer) on STACK, value on HEAP
double arr[3] = {1.0, 2.0, 3.0}; // STACK (fixed-size array)
char* str = new char[20]; // str (pointer) on STACK, array on HEAP
Task: List 3 scenarios where heap allocation is necessary or preferred.
Show Solution
- Unknown size at compile time - Array size determined by user input
- Large data structures - Data too big for stack (e.g., large arrays)
- Data must outlive function - Returning allocated data from function
- Dynamic data structures - Linked lists, trees, graphs with varying size
Dynamic Memory Allocation
C++ provides new and delete operators for dynamic
memory allocation. These give you precise control over when memory is
allocated and freed.
#include <iostream>
using namespace std;
int main() {
// Single variable allocation
int* num = new int; // Uninitialized
*num = 42;
int* num2 = new int(100); // Initialized to 100
int* num3 = new int{200}; // C++11 uniform initialization
cout << *num << ", " << *num2 << ", " << *num3 << endl;
// Free single variables with delete
delete num;
delete num2;
delete num3;
// Array allocation
int size;
cout << "Enter array size: ";
cin >> size;
int* arr = new int[size]; // Dynamic array
for (int i = 0; i < size; i++) {
arr[i] = i * 10;
}
// Free arrays with delete[]
delete[] arr;
return 0;
}
new with Classes
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
Student(string n, int a) : name(n), age(a) {
cout << "Constructor called for " << name << endl;
}
~Student() {
cout << "Destructor called for " << name << endl;
}
void display() {
cout << name << " is " << age << " years old" << endl;
}
};
int main() {
// Single object
Student* s1 = new Student("Alice", 20);
s1->display();
delete s1; // Calls destructor
cout << "---" << endl;
// Array of objects
Student* students = new Student[3]{
{"Bob", 21},
{"Carol", 22},
{"Dave", 23}
};
for (int i = 0; i < 3; i++) {
students[i].display();
}
delete[] students; // Calls destructors for all
return 0;
}
Handling Allocation Failure
#include <iostream>
#include <new> // For nothrow
using namespace std;
int main() {
// Method 1: Exception handling (default)
try {
int* huge = new int[1000000000000]; // Very large
delete[] huge;
} catch (bad_alloc& e) {
cout << "Allocation failed: " << e.what() << endl;
}
// Method 2: nothrow (returns nullptr on failure)
int* ptr = new(nothrow) int[1000000000000];
if (ptr == nullptr) {
cout << "Allocation failed (nullptr)" << endl;
} else {
delete[] ptr;
}
return 0;
}
Practice Questions: Dynamic Allocation
Task: Ask user for size, create dynamic array, fill with squares (1, 4, 9, ...), print, and free.
Show Solution
int size;
cout << "Enter size: ";
cin >> size;
int* arr = new int[size];
for (int i = 0; i < size; i++) {
arr[i] = (i + 1) * (i + 1);
}
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
delete[] arr;
Task: Create a dynamic 3x4 matrix, initialize with values, print, and properly deallocate.
Show Solution
int rows = 3, cols = 4;
// Allocate
int** matrix = new int*[rows];
for (int i = 0; i < rows; i++) {
matrix[i] = new int[cols];
}
// Initialize
int val = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = val++;
}
}
// Print
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
cout << matrix[i][j] << "\t";
}
cout << endl;
}
// Deallocate (reverse order)
for (int i = 0; i < rows; i++) {
delete[] matrix[i];
}
delete[] matrix;
Memory Leaks & Common Mistakes
Memory leaks occur when dynamically allocated memory is never freed. Over time, leaks consume available memory, causing slowdowns or crashes. Learning to avoid and detect them is essential.
Common Memory Mistakes
#include <iostream>
using namespace std;
// Mistake 1: Forgetting to delete
void leak1() {
int* ptr = new int(42);
// Function ends without delete - MEMORY LEAK!
}
// Mistake 2: Losing pointer before delete
void leak2() {
int* ptr = new int(100);
ptr = new int(200); // Lost reference to first allocation!
delete ptr; // Only frees second allocation
}
// Mistake 3: Using wrong delete form
void mistake3() {
int* arr = new int[10];
// delete arr; // WRONG! Use delete[] for arrays
delete[] arr; // Correct
}
// Mistake 4: Double delete
void mistake4() {
int* ptr = new int(42);
delete ptr;
// delete ptr; // UNDEFINED BEHAVIOR - double free!
ptr = nullptr; // Best practice: set to nullptr after delete
}
// Mistake 5: Using deleted memory (dangling pointer)
void mistake5() {
int* ptr = new int(42);
delete ptr;
// cout << *ptr; // UNDEFINED BEHAVIOR - use after free!
}
// Mistake 6: Returning address of local variable
int* mistake6() {
int local = 10;
return &local; // DANGER! local dies when function returns
}
int main() {
// Don't actually run these - they demonstrate bugs!
return 0;
}
- Program uses more and more memory over time
- System becomes slow or unresponsive
- Eventually crashes with "out of memory" error
- Particularly dangerous in long-running servers
Safe Memory Management Practices
#include <iostream>
using namespace std;
// Practice 1: Always match new with delete
void safe1() {
int* ptr = new int(42);
// ... use ptr ...
delete ptr;
ptr = nullptr; // Prevent accidental reuse
}
// Practice 2: Use RAII - allocate in constructor, delete in destructor
class SafeArray {
private:
int* data;
int size;
public:
SafeArray(int s) : size(s) {
data = new int[size];
cout << "Allocated " << size << " ints" << endl;
}
~SafeArray() {
delete[] data;
cout << "Freed " << size << " ints" << endl;
}
int& operator[](int i) { return data[i]; }
};
// Practice 3: Check for nullptr before use
void safe3(int* ptr) {
if (ptr != nullptr) {
cout << *ptr << endl;
}
}
int main() {
{
SafeArray arr(5); // Allocated
arr[0] = 100;
} // Automatically freed when arr goes out of scope
return 0;
}
Practice Questions: Memory Leaks
Task: Identify the memory leak and fix it:
void processData() {
int* data = new int[100];
if (someCondition()) {
return; // Early return
}
// Process data...
delete[] data;
}
Show Solution
// Problem: Early return causes leak
// Solution: Delete before all return paths
void processData() {
int* data = new int[100];
if (someCondition()) {
delete[] data; // Free before returning!
return;
}
// Process data...
delete[] data;
}
// Better: Use smart pointers (covered later)
void processDataSmart() {
unique_ptr data(new int[100]);
if (someCondition()) {
return; // Automatically freed!
}
// Process data...
} // Automatically freed
RAII Pattern
RAII (Resource Acquisition Is Initialization) is a fundamental C++ idiom where resources are tied to object lifetime. Resources are acquired in constructors and released in destructors, ensuring automatic cleanup.
RAII
RAII binds resource lifetime to object lifetime. When an object is created, it acquires resources. When it's destroyed (goes out of scope), it automatically releases those resources.
Benefit: Exception-safe, leak-free resource management without manual cleanup.
#include <iostream>
#include <fstream>
using namespace std;
// RAII wrapper for dynamic array
class IntArray {
private:
int* data;
size_t size;
public:
// Constructor: Acquire resource
IntArray(size_t n) : size(n), data(new int[n]) {
cout << "Acquired " << size << " ints" << endl;
for (size_t i = 0; i < size; i++) {
data[i] = 0;
}
}
// Destructor: Release resource
~IntArray() {
delete[] data;
cout << "Released " << size << " ints" << endl;
}
// Disable copy (Rule of Three/Five)
IntArray(const IntArray&) = delete;
IntArray& operator=(const IntArray&) = delete;
// Access
int& operator[](size_t i) { return data[i]; }
size_t getSize() const { return size; }
};
void process() {
IntArray arr(10); // Resource acquired
arr[0] = 100;
arr[1] = 200;
cout << "Processing..." << endl;
if (true) {
throw runtime_error("Oops!"); // Exception thrown
}
// Even with exception, arr is properly cleaned up!
}
int main() {
try {
process();
} catch (exception& e) {
cout << "Caught: " << e.what() << endl;
}
// RAII example with file (fstream already uses RAII)
{
ofstream file("test.txt"); // File opened
file << "Hello, RAII!" << endl;
} // File automatically closed here
return 0;
}
RAII Benefits
Exception Safe
Resources freed even if exceptions occur
Automatic
No need to remember manual cleanup
No Leaks
Destructor always runs when scope ends
Practice Questions: RAII
Task: Create a simple RAII class that wraps a FILE* pointer (C-style file) - opens in constructor, closes in destructor.
Show Solution
class FileWrapper {
private:
FILE* file;
public:
FileWrapper(const char* name, const char* mode) {
file = fopen(name, mode);
if (!file) {
throw runtime_error("Failed to open file");
}
}
~FileWrapper() {
if (file) {
fclose(file);
}
}
// Disable copy
FileWrapper(const FileWrapper&) = delete;
FileWrapper& operator=(const FileWrapper&) = delete;
FILE* get() { return file; }
};
// Usage:
{
FileWrapper f("test.txt", "w");
fprintf(f.get(), "Hello!");
} // File automatically closed
unique_ptr - Exclusive Ownership
std::unique_ptr is a smart pointer that owns and manages a dynamically
allocated object, automatically deleting it when the unique_ptr goes out of scope.
It enforces exclusive ownership—only one unique_ptr can own an object at a time.
#include <iostream>
#include <memory> // Required for smart pointers
using namespace std;
class Resource {
public:
Resource() { cout << "Resource created" << endl; }
~Resource() { cout << "Resource destroyed" << endl; }
void use() { cout << "Resource used" << endl; }
};
int main() {
// Create unique_ptr
unique_ptr<Resource> ptr1(new Resource());
// Better: use make_unique (C++14)
auto ptr2 = make_unique<Resource>();
// Use the resource
ptr1->use();
(*ptr2).use(); // Also works
// unique_ptr cannot be copied (exclusive ownership)
// unique_ptr<Resource> ptr3 = ptr1; // ERROR!
// But can be moved
unique_ptr<Resource> ptr3 = move(ptr1);
// ptr1 is now nullptr
if (ptr1 == nullptr) {
cout << "ptr1 is null after move" << endl;
}
ptr3->use();
// Get raw pointer (careful - don't delete it!)
Resource* raw = ptr3.get();
raw->use();
// Release ownership (returns raw pointer)
Resource* released = ptr3.release();
// Now YOU are responsible for deleting it
delete released;
return 0;
} // ptr2 automatically deleted here
unique_ptr with Arrays
#include <iostream>
#include <memory>
using namespace std;
int main() {
// unique_ptr for arrays
unique_ptr<int[]> arr(new int[5]);
// Or with make_unique (C++14)
auto arr2 = make_unique<int[]>(5);
// Use like regular array
for (int i = 0; i < 5; i++) {
arr[i] = i * 10;
arr2[i] = i * 100;
}
for (int i = 0; i < 5; i++) {
cout << arr[i] << " " << arr2[i] << endl;
}
return 0;
} // Both arrays automatically deleted
unique_ptr in Functions
#include <iostream>
#include <memory>
using namespace std;
// Return unique_ptr from function (ownership transfer)
unique_ptr<int> createNumber(int value) {
return make_unique<int>(value);
}
// Accept unique_ptr by value (takes ownership)
void consume(unique_ptr<int> ptr) {
cout << "Consumed: " << *ptr << endl;
} // ptr deleted here
// Accept by reference (no ownership transfer)
void inspect(const unique_ptr<int>& ptr) {
cout << "Inspecting: " << *ptr << endl;
}
// Accept raw pointer for read-only access
void readOnly(int* ptr) {
cout << "Reading: " << *ptr << endl;
}
int main() {
auto num = createNumber(42);
inspect(num); // num still valid
readOnly(num.get()); // Pass raw pointer
consume(move(num)); // Transfer ownership
// num is now nullptr
return 0;
}
Practice Questions: unique_ptr
Task: Convert this code to use unique_ptr:
int* ptr = new int(42);
cout << *ptr << endl;
delete ptr;
Show Solution
auto ptr = make_unique(42);
cout << *ptr << endl;
// No delete needed!
Task: Create a factory function that creates a string on the heap and returns ownership via unique_ptr.
Show Solution
unique_ptr createGreeting(const string& name) {
return make_unique("Hello, " + name + "!");
}
// Usage:
auto msg = createGreeting("World");
cout << *msg << endl; // Hello, World!
weak_ptr - Breaking Cycles
std::weak_ptr is a non-owning smart pointer that holds a weak
reference to an object managed by shared_ptr. It doesn't affect reference
count and is used to break circular references.
The Circular Reference Problem
#include <iostream>
#include <memory>
using namespace std;
// PROBLEM: Circular reference with shared_ptr
class BadNode {
public:
string name;
shared_ptr<BadNode> partner; // Strong reference
BadNode(string n) : name(n) {
cout << name << " created" << endl;
}
~BadNode() {
cout << name << " destroyed" << endl;
}
};
void circularProblem() {
auto node1 = make_shared<BadNode>("Node1");
auto node2 = make_shared<BadNode>("Node2");
node1->partner = node2; // node1 -> node2
node2->partner = node1; // node2 -> node1 (circular!)
cout << "node1 use_count: " << node1.use_count() << endl; // 2
cout << "node2 use_count: " << node2.use_count() << endl; // 2
} // Memory leak! Neither node is destroyed
int main() {
circularProblem();
cout << "After function - no destructors called!" << endl;
return 0;
}
Solution: weak_ptr
#include <iostream>
#include <memory>
using namespace std;
// SOLUTION: Use weak_ptr to break cycle
class GoodNode {
public:
string name;
weak_ptr<GoodNode> partner; // Weak reference!
GoodNode(string n) : name(n) {
cout << name << " created" << endl;
}
~GoodNode() {
cout << name << " destroyed" << endl;
}
void greet() {
// Must convert weak_ptr to shared_ptr to use
if (auto p = partner.lock()) {
cout << name << " says hi to " << p->name << endl;
} else {
cout << name << "'s partner is gone" << endl;
}
}
};
void circularSolved() {
auto node1 = make_shared<GoodNode>("Node1");
auto node2 = make_shared<GoodNode>("Node2");
node1->partner = node2; // Weak reference
node2->partner = node1; // Weak reference
cout << "node1 use_count: " << node1.use_count() << endl; // 1
cout << "node2 use_count: " << node2.use_count() << endl; // 1
node1->greet();
node2->greet();
} // Both nodes properly destroyed!
int main() {
circularSolved();
cout << "After function - destructors called!" << endl;
return 0;
}
weak_ptr Operations
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> shared = make_shared<int>(42);
weak_ptr<int> weak = shared; // Create from shared_ptr
// Check if object still exists
cout << "expired: " << weak.expired() << endl; // false
// Get use count
cout << "use_count: " << weak.use_count() << endl; // 1
// Convert to shared_ptr (safe access)
if (auto locked = weak.lock()) {
cout << "Value: " << *locked << endl; // 42
}
// Reset the shared_ptr
shared.reset();
cout << "After reset:" << endl;
cout << "expired: " << weak.expired() << endl; // true
if (auto locked = weak.lock()) {
cout << "Value: " << *locked << endl;
} else {
cout << "Object no longer exists" << endl;
}
return 0;
}
Practice Questions: weak_ptr
Task: List 3 scenarios where weak_ptr should be used instead of shared_ptr.
Show Solution
- Breaking circular references - Parent-child relationships where child references parent
- Caching - Cache that doesn't keep objects alive if nothing else uses them
- Observer pattern - Observers that shouldn't keep subjects alive
- Back-pointers - Any bidirectional relationship where one direction should be weak
Task: Implement a simple Subject that notifies observers using weak_ptr, so dead observers are automatically skipped.
Show Solution
class Observer {
public:
virtual void notify(const string& msg) = 0;
virtual ~Observer() = default;
};
class Subject {
vector> observers;
public:
void addObserver(shared_ptr obs) {
observers.push_back(obs);
}
void notifyAll(const string& msg) {
for (auto& weak : observers) {
if (auto obs = weak.lock()) {
obs->notify(msg);
}
}
}
};
// Observers can be destroyed independently
// Subject won't keep them alive
// Dead observers automatically skipped
Key Takeaways
Stack vs Heap
Stack: fast, automatic. Heap: flexible, manual.
new/delete
Match new with delete, new[] with delete[]
Avoid Leaks
Always free allocated memory
RAII
Tie resources to object lifetime
unique_ptr
Exclusive ownership, prefer by default
shared_ptr & weak_ptr
Shared ownership, break cycles
Knowledge Check
Quick Quiz
Test what you have learned about C++ memory management