C++ Memory Management

Managing dynamic memory allocation and deallocation

🧠 What is C++ Memory Management?

C++ memory management involves controlling how programs allocate, use, and free memory during execution. It includes stack memory, heap memory, and techniques to prevent memory leaks.


// Basic memory allocation
int* ptr = new int(42);  // Allocate
delete ptr;              // Deallocate
                                    

Memory Management Concepts

📚

Stack Memory

Automatic memory for local variables

int x = 10;  // Stack allocation
char arr[100];  // Stack array
🏗️

Heap Memory

Dynamic memory allocation

int* ptr = new int(20);
int* arr = new int[100];
🗑️

Memory Deallocation

Free allocated memory

delete ptr;      // Single object
delete[] arr;    // Array
🛡️

Smart Pointers

Smart pointers automate memory management, preventing leaks and simplifying ownership. std::unique_ptr enforces exclusive ownership and is lightweight; std::shared_ptr allows shared ownership via reference counting; std::weak_ptr breaks reference cycles. They call the appropriate destructor automatically when the pointer goes out of scope. Using smart pointers eliminates most manual delete calls, making code exception-safe and easier to reason about, which is foundational for robust modern C++ applications.

unique_ptr<int> ptr = 
    make_unique<int>(42);

🔹 Stack vs Heap Memory

Stack memory is automatically managed, fast, and used for local variables, while heap memory is dynamically allocated, larger, and manually controlled. Stack allocation is deterministic (LIFO) and limited in size, suitable for short-lived data. Heap allocation via new/malloc is flexible for objects with varying lifetimes or large data but requires explicit deallocation to prevent memory leaks, making understanding the distinction crucial for performance and stability.

#include <iostream>
using namespace std;

void demonstrateStack() {
    // Stack allocation - automatic cleanup
    int stackVar = 100;
    int stackArray[5] = {1, 2, 3, 4, 5};
    
    cout << "Stack variable: " << stackVar << endl;
    cout << "Stack array: ";
    for (int i = 0; i < 5; i++) {
        cout << stackArray[i] << " ";
    }
    cout << endl;
    
    // Variables automatically destroyed when function ends
}

void demonstrateHeap() {
    // Heap allocation - manual cleanup required
    int* heapVar = new int(200);
    int* heapArray = new int[5]{10, 20, 30, 40, 50};
    
    cout << "Heap variable: " << *heapVar << endl;
    cout << "Heap array: ";
    for (int i = 0; i < 5; i++) {
        cout << heapArray[i] << " ";
    }
    cout << endl;
    
    // Must manually free memory
    delete heapVar;
    delete[] heapArray;
}

int main() {
    cout << "=== Stack Memory Demo ===" << endl;
    demonstrateStack();
    
    cout << "\n=== Heap Memory Demo ===" << endl;
    demonstrateHeap();
    
    return 0;
}

Output:

=== Stack Memory Demo ===
Stack variable: 100
Stack array: 1 2 3 4 5 

=== Heap Memory Demo ===
Heap variable: 200
Heap array: 10 20 30 40 50 

🔹 Dynamic Memory Allocation

Dynamic memory allocation lets you request memory at runtime using new and release it with delete. Unlike stack variables, dynamically allocated memory persists until explicitly freed, allowing flexible data structures like linked lists or arrays of unknown size. Always pair each new with a delete to prevent memory leaks. For arrays, use new[] and delete[]. Proper management ensures efficient resource use and prevents crashes or performance degradation.

#include <iostream>
using namespace std;

int main() {
    // Allocate single integer
    int* singleInt = new int(42);
    cout << "Single integer: " << *singleInt << endl;
    
    // Allocate array of integers
    int size = 5;
    int* dynamicArray = new int[size];
    
    // Initialize array
    for (int i = 0; i < size; i++) {
        dynamicArray[i] = (i + 1) * 10;
    }
    
    cout << "Dynamic array: ";
    for (int i = 0; i < size; i++) {
        cout << dynamicArray[i] << " ";
    }
    cout << endl;
    
    // Check if allocation was successful
    if (singleInt != nullptr && dynamicArray != nullptr) {
        cout << "Memory allocation successful!" << endl;
    }
    
    // Free allocated memory
    delete singleInt;
    delete[] dynamicArray;
    
    // Set pointers to nullptr to avoid dangling pointers
    singleInt = nullptr;
    dynamicArray = nullptr;
    
    cout << "Memory freed and pointers set to nullptr" << endl;
    
    return 0;
}

Output:

Single integer: 42
Dynamic array: 10 20 30 40 50 
Memory allocation successful!
Memory freed and pointers set to nullptr

🔹 Memory Leaks and Prevention

A memory leak occurs when dynamically allocated memory is not freed after use, gradually consuming available RAM and potentially crashing the program. Prevention strategies include always pairing allocations with deallocations, using RAII (Resource Acquisition Is Initialization) patterns, leveraging scope-bound resource management, and employing tools like valgrind for detection. In modern C++, preferring smart pointers over raw pointers automates this cleanup, dramatically reducing leak risks.

#include <iostream>
using namespace std;

// BAD: Memory leak example
void memoryLeakExample() {
    int* ptr = new int(100);
    cout << "Value: " << *ptr << endl;
    // Missing delete - MEMORY LEAK!
    // delete ptr;  // This line is missing
}

// GOOD: Proper memory management
void properMemoryManagement() {
    int* ptr = new int(200);
    
    try {
        cout << "Value: " << *ptr << endl;
        // Some operations that might throw exceptions
        
        delete ptr;  // Always delete allocated memory
        ptr = nullptr;  // Set to nullptr after deletion
    }
    catch (...) {
        delete ptr;  // Clean up even if exception occurs
        throw;  // Re-throw the exception
    }
}

// BETTER: Using RAII (Resource Acquisition Is Initialization)
class SafeIntWrapper {
private:
    int* ptr;
    
public:
    SafeIntWrapper(int value) : ptr(new int(value)) {}
    
    ~SafeIntWrapper() {  // Destructor automatically called
        delete ptr;
        ptr = nullptr;
    }
    
    int getValue() const { return *ptr; }
    void setValue(int value) { *ptr = value; }
};

int main() {
    cout << "=== Memory Leak Example ===" << endl;
    memoryLeakExample();  // This leaks memory!
    
    cout << "\n=== Proper Memory Management ===" << endl;
    properMemoryManagement();
    
    cout << "\n=== RAII Example ===" << endl;
    {
        SafeIntWrapper wrapper(300);
        cout << "RAII Value: " << wrapper.getValue() << endl;
        // Destructor automatically called when wrapper goes out of scope
    }
    cout << "Memory automatically cleaned up!" << endl;
    
    return 0;
}

Output:

=== Memory Leak Example ===
Value: 100

=== Proper Memory Management ===
Value: 200

=== RAII Example ===
RAII Value: 300
Memory automatically cleaned up!

🔹 Smart Pointers (C++11)

Smart pointers (unique_ptr, shared_ptr, weak_ptr) automatically manage dynamic memory, eliminating the need for manual delete calls. unique_ptr enforces exclusive ownership; shared_ptr allows shared ownership with reference counting; weak_ptr breaks reference cycles. Adopting these from the C++ Standard Library prevents memory leaks and dangling pointers, making code safer, more readable, and exception-safe by ensuring proper cleanup when objects go out of scope.

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // unique_ptr - exclusive ownership
    unique_ptr<int> uniquePtr = make_unique<int>(42);
    cout << "unique_ptr value: " << *uniquePtr << endl;
    
    // Transfer ownership
    unique_ptr<int> anotherPtr = move(uniquePtr);
    // uniquePtr is now nullptr
    
    if (uniquePtr == nullptr) {
        cout << "Original unique_ptr is now null" << endl;
    }
    cout << "Transferred value: " << *anotherPtr << endl;
    
    // shared_ptr - shared ownership
    shared_ptr<int> sharedPtr1 = make_shared<int>(100);
    shared_ptr<int> sharedPtr2 = sharedPtr1;  // Share ownership
    
    cout << "shared_ptr value: " << *sharedPtr1 << endl;
    cout << "Reference count: " << sharedPtr1.use_count() << endl;
    
    // Array with smart pointers
    unique_ptr<int[]> arrayPtr = make_unique<int[]>(5);
    for (int i = 0; i < 5; i++) {
        arrayPtr[i] = (i + 1) * 10;
    }
    
    cout << "Smart pointer array: ";
    for (int i = 0; i < 5; i++) {
        cout << arrayPtr[i] << " ";
    }
    cout << endl;
    
    // No need to call delete - automatic cleanup!
    cout << "Smart pointers will automatically clean up memory!" << endl;
    
    return 0;
}

Output:

unique_ptr value: 42
Original unique_ptr is now null
Transferred value: 42
shared_ptr value: 100
Reference count: 2
Smart pointer array: 10 20 30 40 50 
Smart pointers will automatically clean up memory!

🧠 Test Your Knowledge

Which operator is used to deallocate memory for arrays?