Memory Management

Memory Layout

Understanding how C++ manages memory is crucial for writing efficient programs. Memory is divided into different segments:

Stack

Local variables, function parameters, return addresses

Heap

Dynamically allocated memory (new/delete)

Data Segment

Global and static variables

Code Segment

Compiled program instructions

Memory Allocation Example

#include <iostream>
using namespace std;

int globalVar = 100;        // Data segment
static int staticVar = 200; // Data segment

void demonstrateMemory() {
    int localVar = 300;     // Stack
    static int staticLocal = 400; // Data segment
    
    int* heapVar = new int(500); // Heap
    
    cout << "Global variable address: " << &globalVar << endl;
    cout << "Static variable address: " << &staticVar << endl;
    cout << "Local variable address: " << &localVar << endl;
    cout << "Static local address: " << &staticLocal << endl;
    cout << "Heap variable address: " << heapVar << endl;
    
    delete heapVar; // Clean up heap memory
}

int main() {
    demonstrateMemory();
    return 0;
}

Smart Pointers (C++11)

Smart pointers automatically manage memory and help prevent memory leaks:

Smart Pointers Example

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

class Resource {
public:
    Resource(int id) : id(id) {
        cout << "Resource " << id << " created" << endl;
    }
    
    ~Resource() {
        cout << "Resource " << id << " destroyed" << endl;
    }
    
    void use() {
        cout << "Using resource " << id << endl;
    }

private:
    int id;
};

int main() {
    cout << "=== unique_ptr ===" << endl;
    {
        unique_ptr<Resource> ptr1 = make_unique<Resource>(1);
        ptr1->use();
        
        // Transfer ownership
        unique_ptr<Resource> ptr2 = move(ptr1);
        if (!ptr1) {
            cout << "ptr1 is now null" << endl;
        }
        ptr2->use();
    } // ptr2 automatically deletes the resource
    
    cout << "\n=== shared_ptr ===" << endl;
    {
        shared_ptr<Resource> ptr1 = make_shared<Resource>(2);
        cout << "Reference count: " << ptr1.use_count() << endl;
        
        {
            shared_ptr<Resource> ptr2 = ptr1; // Share ownership
            cout << "Reference count: " << ptr1.use_count() << endl;
            ptr2->use();
        } // ptr2 goes out of scope
        
        cout << "Reference count: " << ptr1.use_count() << endl;
        ptr1->use();
    } // ptr1 goes out of scope, resource is deleted
    
    cout << "\n=== weak_ptr ===" << endl;
    {
        shared_ptr<Resource> shared = make_shared<Resource>(3);
        weak_ptr<Resource> weak = shared;
        
        cout << "Weak pointer expired: " << weak.expired() << endl;
        
        if (auto locked = weak.lock()) {
            locked->use();
        }
        
        shared.reset(); // Release shared ownership
        cout << "Weak pointer expired: " << weak.expired() << endl;
    }
    
    return 0;
}

RAII Principle

Resource Acquisition Is Initialization - acquire resources in constructors and release them in destructors:

RAII Example

#include <iostream>
#include <fstream>
#include <vector>
using namespace std;

class FileManager {
private:
    ofstream file;

public:
    FileManager(const string& filename) : file(filename) {
        if (!file.is_open()) {
            throw runtime_error("Could not open file");
        }
        cout << "File opened: " << filename << endl;
    }
    
    ~FileManager() {
        if (file.is_open()) {
            file.close();
            cout << "File closed automatically" << endl;
        }
    }
    
    void write(const string& data) {
        file << data << endl;
    }
};

class ArrayManager {
private:
    int* data;
    size_t size;

public:
    ArrayManager(size_t s) : size(s) {
        data = new int[size];
        cout << "Array of size " << size << " allocated" << endl;
    }
    
    ~ArrayManager() {
        delete[] data;
        cout << "Array deallocated" << endl;
    }
    
    // Copy constructor (Rule of Three)
    ArrayManager(const ArrayManager& other) : size(other.size) {
        data = new int[size];
        for (size_t i = 0; i < size; ++i) {
            data[i] = other.data[i];
        }
    }
    
    // Assignment operator
    ArrayManager& operator=(const ArrayManager& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            data = new int[size];
            for (size_t i = 0; i < size; ++i) {
                data[i] = other.data[i];
            }
        }
        return *this;
    }
    
    int& operator[](size_t index) {
        return data[index];
    }
};

int main() {
    cout << "=== RAII with File ===" << endl;
    try {
        FileManager fm("example.txt");
        fm.write("Hello, RAII!");
        fm.write("Automatic cleanup!");
        // File automatically closed when fm goes out of scope
    } catch (const exception& e) {
        cout << "Error: " << e.what() << endl;
    }
    
    cout << "\n=== RAII with Array ===" << endl;
    {
        ArrayManager arr(5);
        for (int i = 0; i < 5; ++i) {
            arr[i] = i * 10;
        }
        
        ArrayManager arr2 = arr; // Copy constructor
        // Both arrays automatically cleaned up
    }
    
    return 0;
}

Memory Management Best Practices

Prefer Stack Allocation

Use automatic storage duration when possible

Use Smart Pointers

Prefer smart pointers over raw pointers for dynamic allocation

Follow RAII

Tie resource lifetime to object lifetime

Rule of Three/Five

If you need destructor, copy constructor, or assignment operator, you likely need all

Initialize Pointers

Always initialize pointers to nullptr or valid addresses

Check for Null

Always check pointers before dereferencing