No history yet

Memory Management

Beyond Manual Memory

So far, you've learned to manage memory with new and delete. This gives you precise control, but it's also a source of common and frustrating bugs. Forgetting a delete leads to a memory leak. Deleting a pointer twice can crash your program. These issues pushed the C++ community to develop better, safer tools for memory management.

The core idea is simple: use objects to manage resources. When the object is created, it acquires the resource. When the object is destroyed, it releases the resource. This principle is called Resource Acquisition Is Initialization (RAII).

Smart pointers are the perfect embodiment of RAII. They are wrapper classes that hold a raw pointer and automatically handle its deallocation when the smart pointer object goes out of scope. This eliminates the need for manual delete calls and prevents a whole class of bugs.

Smart Pointers in Action

The C++ Standard Library provides two main types of smart pointers, each defining a different kind of ownership policy.

std::unique_ptr for Exclusive Ownership A unique_ptr exclusively owns the object it points to. Think of it as a key to a house. Only one person has the key at a time. You can't copy a unique_ptr, because that would mean two pointers think they have exclusive ownership of the same memory. You can, however, move ownership from one unique_ptr to another, like handing the key to someone else.

#include <memory>
#include <iostream>

class MyObject {
public:
    MyObject() { std::cout << "MyObject created\n"; }
    ~MyObject() { std::cout << "MyObject destroyed\n"; }
    void doSomething() { std::cout << "Doing something!\n"; }
};

int main() {
    // Create a unique_ptr. Memory is allocated here.
    auto ptr1 = std::make_unique<MyObject>();
    ptr1->doSomething();

    // auto ptr2 = ptr1; // This line won't compile! Cannot copy.

    // Move ownership from ptr1 to ptr2
    std::unique_ptr<MyObject> ptr2 = std::move(ptr1);
    
    // Now ptr1 is null, and ptr2 owns the object.
    if (!ptr1) {
        std::cout << "ptr1 is now null.\n";
    }
    ptr2->doSomething();

    // ptr2 goes out of scope, MyObject is automatically destroyed.
    return 0;
}

Notice how delete is never called. The unique_ptr handles it for us when ptr2 goes out of scope at the end of main.

std::shared_ptr for Shared Ownership Sometimes, multiple parts of a program need to share access to the same object, and any of them could be the last one to finish using it. A shared_ptr handles this scenario. It keeps an internal counter of how many shared_ptrs are pointing to the object. Each time a new shared_ptr is copied, the count goes up. When a shared_ptr is destroyed, the count goes down. The memory is only deallocated when the count reaches zero.

#include <memory>
#include <iostream>

class SharedResource {
public:
    SharedResource() { std::cout << "Resource created\n"; }
    ~SharedResource() { std::cout << "Resource destroyed\n"; }
};

void use_resource(std::shared_ptr<SharedResource> ptr) {
    std::cout << "Using resource. Count is: " << ptr.use_count() << std::endl;
} // ptr goes out of scope, count decreases

int main() {
    std::shared_ptr<SharedResource> p1;
    {
        auto p2 = std::make_shared<SharedResource>();
        std::cout << "Initial count: " << p2.use_count() << std::endl;

        p1 = p2; // Copy p2. Count increases.
        std::cout << "After copy: " << p1.use_count() << std::endl;

        use_resource(p1); // Pass by value, count increases temporarily.

        std::cout << "p2 is about to go out of scope.\n";
    } // p2 is destroyed, count decreases.

    std::cout << "Final count before main ends: " << p1.use_count() << std::endl;

    return 0; // p1 is destroyed, count becomes 0, Resource is destroyed.
}

Using smart pointers should be your default choice for managing dynamically allocated memory. They make your code safer, clearer, and easier to maintain.

Optimizing Allocation

While smart pointers solve memory safety issues, they don't change the underlying allocation mechanism. Calls to new (or std::make_unique/std::make_shared) ask the operating system for memory, which can be a relatively slow process. In performance-critical applications, like games or high-frequency trading systems, making thousands of small allocations per second can become a bottleneck.

fragmentation

noun

A condition where memory is broken into small, non-contiguous chunks, making it difficult to allocate larger blocks of memory even if the total free memory is sufficient.

Furthermore, frequent allocations and deallocations can lead to memory fragmentation. This happens when the free memory on the heap is split into many small, disconnected pieces. Your program might fail to allocate a large block of memory, even if there's enough free memory in total, simply because no single piece is large enough.

Using a Memory Pool

A memory pool, also known as an object pool, is a powerful technique to combat these issues. The strategy is to pre-allocate a single, large, contiguous block of memory at the start. Then, your program manages this block itself, handing out small pieces to fulfill allocation requests and taking them back when they are freed. It's like having your own private supply of memory, avoiding the need to ask the OS every time.

Benefits of memory pools include:

  • Speed: Allocating from a pool can be as fast as moving a pointer, which is significantly faster than a system call.
  • Reduced Fragmentation: Since you're dealing with one large block, external fragmentation is eliminated. Internal fragmentation (wasted space within the pool) can still occur but is often more manageable.
  • Improved Locality: Objects allocated from the same pool are often close to each other in memory. This can improve CPU cache performance, as accessing one object may pre-load its neighbors into the cache.

In multithreaded applications, memory pools offer another advantage. A global heap requires synchronization (like mutexes) to prevent race conditions when multiple threads try to allocate memory at once. This synchronization adds overhead. By giving each thread its own private memory pool, you can avoid this contention entirely, allowing threads to allocate memory without blocking each other.

While the C++ Standard Library doesn't provide a ready-made memory pool, many third-party libraries do, such as Boost.Pool. Implementing a simple one for a specific object type is also a great exercise to deepen your understanding.

By moving beyond manual new and delete to embrace smart pointers and specialized allocators like memory pools, you can write C++ code that is not only safer and more robust but also significantly faster.