Dynamic Memory Management

Dynamic memory management is a critical concept in C++ that allows programmers to allocate and deallocate memory during program execution (at runtime) rather than at compile time. This enables creating data structures whose size can change during program execution.

Memory Areas in C++ Programs

A C++ program’s memory is divided into several areas:

  1. Code Segment (Text Segment): Contains the compiled program code.
  2. Static/Global Data Segment: Stores global and static variables.
  3. Stack: Manages function calls and local variables.
  4. Heap: Used for dynamic memory allocation.

The Need for Dynamic Memory

  1. Unknown Memory Requirements: When the amount of memory needed isn’t known at compile time.
  2. Efficient Memory Usage: Allocating only what’s needed, when it’s needed.
  3. Data Structures: Creating dynamic data structures like linked lists, trees, and graphs.
  4. Memory Optimization: Avoiding stack overflow for large data sets.

Dynamic Memory Allocation in C++

C-style Memory Management

C++ inherited these functions from C:

  1. malloc(): Allocates memory block of specified size
  2. calloc(): Allocates memory and initializes it to zero
  3. realloc(): Resizes previously allocated memory
  4. free(): Deallocates previously allocated memory
#include <stdlib.h>
#include <iostream>
using namespace std;

int main() {
    // Allocate memory for an integer
    int* ptr = (int*)malloc(sizeof(int));
    
    // Check if memory allocation was successful
    if (ptr == NULL) {
        cout << "Memory allocation failed" << endl;
        return 1;
    }
    
    // Assign a value
    *ptr = 10;
    cout << "Value: " << *ptr << endl;
    
    // Free the allocated memory
    free(ptr);
    
    // Prevent dangling pointer
    ptr = NULL;
    
    return 0;
}

C++ Memory Management Operators

C++ introduced more type-safe memory management operators:

  1. new: Allocates memory for a specific data type
  2. delete: Deallocates memory allocated with new
  3. new[]: Allocates memory for an array
  4. delete[]: Deallocates array memory allocated with new[]
#include <iostream>
using namespace std;

int main() {
    // Allocate memory for a single integer
    int* ptr = new int;
    
    // Assign a value
    *ptr = 10;
    cout << "Value: " << *ptr << endl;
    
    // Deallocate memory
    delete ptr;
    
    // Allocate memory for an array of 5 integers
    int* arr = new int[5];
    
    // Initialize the array
    for (int i = 0; i < 5; i++) {
        arr[i] = i * 10;
    }
    
    // Access and display array elements
    for (int i = 0; i < 5; i++) {
        cout << "arr[" << i << "] = " << arr[i] << endl;
    }
    
    // Deallocate array memory
    delete[] arr;
    
    return 0;
}

Difference Between new/delete and malloc/free

Featurenew/deletemalloc/free
TypeOperatorsFunctions
ReturnsExact data typevoid* (requires casting)
FailureThrows exceptionReturns NULL
Size calculationAutomaticManual (using sizeof)
CustomizationCan be overloadedCannot be overloaded
Constructor/DestructorCalls constructors/destructorsDoes not call constructors/destructors
Memory sourceFree storeHeap

Common Pitfalls in Dynamic Memory Management

1. Memory Leaks

Memory leaks occur when allocated memory is not deallocated, causing the program to consume more and more memory over time.

void memoryLeakExample() {
    int* ptr = new int; // Allocate memory
    *ptr = 10;
    
    // Function ends without delete ptr;
    // The allocated memory is now inaccessible but still allocated
    // This is a memory leak
}

2. Dangling Pointers

Dangling pointers occur when a pointer references memory that has been deallocated.

int* createDanglingPointer() {
    int* ptr = new int;
    *ptr = 10;
    delete ptr; // Memory is deallocated
    return ptr; // Returning a dangling pointer
}

void danglingPointerExample() {
    int* dangling = createDanglingPointer();
    *dangling = 20; // DANGEROUS: Undefined behavior
}

3. Double Deletion

Double deletion happens when delete is called more than once on the same memory location.

void doubleDeletionExample() {
    int* ptr = new int;
    *ptr = 10;
    
    delete ptr; // First deletion
    delete ptr; // Second deletion - Undefined behavior
}

4. Using Incorrect Delete Operator

Using delete instead of delete[] (or vice versa) can lead to undefined behavior.

void incorrectDeleteExample() {
    // Allocate an array
    int* arr = new int[5];
    
    // Initialize
    for (int i = 0; i < 5; i++) {
        arr[i] = i;
    }
    
    // Incorrect: Using delete instead of delete[]
    delete arr; // This may not properly release all the memory
    
    // Correct would be:
    // delete[] arr;
}

Best Practices for Dynamic Memory Management

  1. Always Initialize Pointers: Initialize pointers to either a valid address or nullptr.

    int* ptr = nullptr; // Modern C++ (C++11 onwards)
    int* ptr = NULL;    // Pre-C++11
  2. Always Check for Allocation Failure: When using malloc or pre-C++11 new.

    int* ptr = new (nothrow) int; // C++11 approach
    if (ptr == nullptr) {
        // Handle allocation failure
    }
  3. Always Release Memory: Deallocate all dynamically allocated memory.

    int* ptr = new int;
    // Use ptr
    delete ptr;
    ptr = nullptr; // Avoid dangling pointer
  4. Use Smart Pointers: In modern C++, use smart pointers instead of raw pointers.

    #include <memory>
    
    // Unique pointer - exclusive ownership
    std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);
    
    // Shared pointer - shared ownership with reference counting
    std::shared_ptr<int> sharedPtr = std::make_shared<int>(20);
    
    // No need to manually delete memory with smart pointers
  5. Match new with delete, new[] with delete[]: Always use the correct deallocation operator.

  6. Avoid Explicit New/Delete When Possible: Use containers and smart pointers.

    // Instead of managing arrays manually:
    std::vector<int> vec(5);
    for (int i = 0; i < 5; i++) {
        vec[i] = i * 10;
    }
    // No manual deletion needed

Smart Pointers in Modern C++

Smart pointers, introduced in C++11, provide automatic memory management through RAII (Resource Acquisition Is Initialization).

Types of Smart Pointers

  1. unique_ptr: Exclusive ownership model
  2. shared_ptr: Shared ownership model with reference counting
  3. weak_ptr: Non-owning reference to a shared_ptr
#include <iostream>
#include <memory>
using namespace std;

class Resource {
public:
    Resource() { cout << "Resource acquired" << endl; }
    ~Resource() { cout << "Resource released" << endl; }
    void doSomething() { cout << "Resource used" << endl; }
};

int main() {
    // unique_ptr example
    {
        cout << "unique_ptr example:" << endl;
        unique_ptr<Resource> uniqueResource = make_unique<Resource>();
        uniqueResource->doSomething();
        
        // No need to delete - will be automatically deleted when goes out of scope
    }
    
    // shared_ptr example
    {
        cout << "\nshared_ptr example:" << endl;
        shared_ptr<Resource> sharedResource1 = make_shared<Resource>();
        
        {
            shared_ptr<Resource> sharedResource2 = sharedResource1; // Reference count: 2
            cout << "Reference count: " << sharedResource1.use_count() << endl;
            sharedResource2->doSomething();
        } // sharedResource2 goes out of scope, reference count: 1
        
        cout << "Reference count: " << sharedResource1.use_count() << endl;
        sharedResource1->doSomething();
    } // sharedResource1 goes out of scope, reference count: 0, resource is deleted
    
    // weak_ptr example
    {
        cout << "\nweak_ptr example:" << endl;
        shared_ptr<Resource> sharedResource = make_shared<Resource>();
        weak_ptr<Resource> weakResource = sharedResource;
        
        cout << "Reference count: " << sharedResource.use_count() << endl;
        
        if (auto tempResource = weakResource.lock()) {
            tempResource->doSomething();
        } else {
            cout << "Resource not available" << endl;
        }
        
        sharedResource.reset(); // Release the resource
        
        if (auto tempResource = weakResource.lock()) {
            tempResource->doSomething();
        } else {
            cout << "Resource not available" << endl;
        }
    }
    
    return 0;
}

Custom Memory Management with Operator Overloading

C++ allows overloading the new and delete operators to implement custom memory management.

#include <iostream>
using namespace std;

class CustomMemory {
private:
    int data;
public:
    CustomMemory(int d) : data(d) {
        cout << "Constructor called: " << data << endl;
    }
    
    ~CustomMemory() {
        cout << "Destructor called: " << data << endl;
    }
    
    // Overloaded new operator
    void* operator new(size_t size) {
        cout << "Custom new operator: allocating " << size << " bytes" << endl;
        void* ptr = malloc(size);
        if (!ptr) {
            throw bad_alloc();
        }
        return ptr;
    }
    
    // Overloaded delete operator
    void operator delete(void* ptr) {
        cout << "Custom delete operator: deallocating memory" << endl;
        free(ptr);
    }
};

int main() {
    // Using custom memory management
    CustomMemory* obj = new CustomMemory(100);
    delete obj;
    
    return 0;
}

Dynamic Memory Management in Data Structures

Dynamic memory is essential for implementing flexible data structures like linked lists, trees, and graphs.

Example: Linked List Implementation

#include <iostream>
using namespace std;

// Node class for linked list
class Node {
public:
    int data;
    Node* next;
    
    Node(int val) : data(val), next(nullptr) {}
};

// Linked list class
class LinkedList {
private:
    Node* head;
    
public:
    LinkedList() : head(nullptr) {}
    
    // Destructor to free all dynamically allocated memory
    ~LinkedList() {
        Node* current = head;
        while (current != nullptr) {
            Node* temp = current;
            current = current->next;
            delete temp;
        }
        head = nullptr;
        cout << "All nodes deleted" << endl;
    }
    
    // Add a node to the end of the list
    void append(int val) {
        Node* newNode = new Node(val);
        
        if (head == nullptr) {
            head = newNode;
            return;
        }
        
        Node* current = head;
        while (current->next != nullptr) {
            current = current->next;
        }
        
        current->next = newNode;
    }
    
    // Display all nodes
    void display() {
        Node* current = head;
        
        if (current == nullptr) {
            cout << "List is empty" << endl;
            return;
        }
        
        while (current != nullptr) {
            cout << current->data << " -> ";
            current = current->next;
        }
        cout << "nullptr" << endl;
    }
};

int main() {
    // Create a linked list using dynamic memory
    LinkedList list;
    
    // Add nodes
    list.append(10);
    list.append(20);
    list.append(30);
    
    // Display the list
    list.display();
    
    // List destructor will automatically free all memory when list goes out of scope
    
    return 0;
}

Summary

Dynamic memory management in C++ provides flexibility and control over how memory is allocated and deallocated during program execution. It allows for creating data structures whose size can change at runtime, which is particularly useful for implementing complex data structures.

Key points to remember:

  • Use new and delete for single objects, new[] and delete[] for arrays
  • Always deallocate memory to prevent memory leaks
  • Be careful about dangling pointers and double deletion
  • In modern C++, prefer smart pointers and containers over manual memory management
  • Match the allocation method with the appropriate deallocation method
  • Consider implementing custom memory management for specialized needs

Proper memory management is crucial for writing robust and efficient C++ programs. With the tools provided by the language, especially in modern C++, managing dynamic memory has become safer and more straightforward, though it still requires careful attention from the programmer.