Mastering C++ Memory Management: An Essential Guide from Beginner to Expert

Time: Column:Mobile & Frontend views:190

Memory management in C++ is key to writing efficient and reliable programs. C++ inherits memory management methods from C, while also adding object-oriented memory allocation mechanisms, making memory management both flexible and complex. Learning memory management not only helps improve program efficiency but also aids in understanding the underlying workings of computers and resource allocation strategies.

1. C/C++ Memory Layout

1.1 Memory Distribution

When compiling and executing C++ programs, memory is divided into several regions, each with distinct responsibilities. Below is the basic layout of C++ memory:

  1. Stack:

    • Purpose: Used to store local variables, function parameters, return addresses, etc.

    • Characteristics: Memory allocation and deallocation are automatically managed by the system, following the Last In, First Out (LIFO) principle. When a function is called, a stack frame is pushed onto the stack, and when the function returns, the stack frame is popped.

    • Suitable Scenario: Best for storing small-scale temporary variables and function parameters.

  2. Heap:

    • Purpose: Used for dynamic memory allocation during program execution. Memory on the heap is manually managed by the programmer (allocation and deallocation), such as memory allocated through new or malloc.

    • Characteristics: Heap memory grows upward (from lower to higher addresses), and unlike the stack, it is not automatically freed. The programmer must explicitly call delete or free to release memory, or it will lead to memory leaks.

    • Suitable Scenario: Best for data structures requiring dynamic allocation or larger memory blocks, such as linked lists, trees, etc.

  3. Data Segment:

    • Purpose: Used to store global variables, static variables, etc., which are allocated at the start of the program and released at its end.

    • Characteristics: The data segment is divided into initialized and uninitialized sections. Initialized variables (e.g., int globalVar = 1;) are directly initialized with specified values when the program is loaded, while uninitialized variables (e.g., int globalVar;) are set to zero.

    • Suitable Scenario: Best for storing variables that need to persist throughout the entire program lifecycle.

  4. Text Segment:

    • Purpose: Stores executable code of the program, including function bodies and constant strings.

    • Characteristics: Typically read-only, to prevent accidental modification during execution, thus enhancing program security.

    • Suitable Scenario: Best for storing executable instructions and constant strings.

1.2 Code Example

#include <cstdlib>  // Includes malloc, calloc, realloc, free
#include <iostream>

int globalVar = 1;              // Global variable, stored in the initialized data segment
static int staticGlobalVar = 1; // Static global variable, stored in the initialized data segment

void Test() {
    static int staticVar = 1;   // Static local variable, stored in the initialized data segment
    int localVar = 1;           // Local variable, stored in the stack
    int num1[10] = {1, 2, 3, 4}; // Local array, stored in the stack
    char char2[] = "abcd";      // Character array, stored in the stack (string content copied to stack)
    const char* pChar3 = "abcd"; // Pointer variable in stack, points to string constant "abcd" in the constant section

    int* ptr1 = (int*)malloc(sizeof(int) * 4); // Dynamically allocated memory, stored in heap
    int* ptr2 = (int*)calloc(4, sizeof(int));  // Dynamically allocated and initialized memory, stored in heap
    int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); // Resize memory, stored in heap

    free(ptr1);  // Free heap memory
    free(ptr3);  // Free heap memory
}

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

1.3 Detailed Explanation

Mastering C++ Memory Management: An Essential Guide from Beginner to Expert

  • globalVar and staticGlobalVar: These are global and static global variables, stored in the initialized data segment. This segment stores global and static variables allocated when the program starts and persists throughout the program’s lifecycle.

  • staticVar: This is a static local variable, stored in the initialized data segment. Even though it's a local variable, due to its static keyword, its memory is not freed when the function exits, and it retains its value until the program ends. If the function is called again, staticVar will retain its previous value.

  • localVar and num1: These are local variables and arrays, stored in the stack. Stack memory is automatically allocated during function calls and deallocated when the function returns. The lifecycle of local variables ends when the function execution completes.

  • char2: This is a character array stored in the stack. The string "abcd" is copied to the stack, meaning that its content resides in the stack memory.

  • pChar3: This is a pointer variable stored in the stack, pointing to a string constant "abcd" stored in the constant section. The constant section contains read-only data that cannot be modified during the program's lifecycle.

  • ptr1, ptr2, ptr3: These are pointer variables stored in the stack, but the memory they point to is in the heap. These pointers allocate memory dynamically using functions like malloc, calloc, and realloc. Heap memory must be explicitly freed using free(), or it will cause memory leaks.


2. Dynamic Memory Management in C

For detailed information, please refer to the earlier blog: Dynamic Memory Management in C.

In C, dynamic memory management is achieved through the following functions:

  1. malloc:

    int* ptr = (int*)malloc(sizeof(int) * 5);  // Allocates memory for 5 integers
    • Purpose: Allocates a block of memory of a specified size, with the data inside uninitialized. Returns a void* pointer, which must be cast to the appropriate type.

    • Example:

  2. calloc:

    int* ptr = (int*)calloc(5, sizeof(int));  // Allocates memory for 5 integers, initialized to zero
    • Purpose: Allocates contiguous memory and initializes all bytes to zero. Returns a void* pointer.

    • Example:

  3. realloc:

    int* newPtr = (int*)realloc(ptr, sizeof(int) * 10);  // Expands original memory block to hold 10 integers
    • Purpose: Resizes an already allocated memory block. If resizing is not possible, realloc tries to allocate a new block of memory and copies the old content.

    • Example:

  4. free:

    free(ptr);  // Frees dynamically allocated memory
    • Purpose: Frees memory allocated by malloc, calloc, or realloc. After freeing, the memory is no longer managed by the program, preventing memory leaks.

    • Example:


3. Memory Management in C++

C++ inherits malloc, calloc, realloc, and free from C but provides more flexible memory management through the new and delete operators.

  1. new Operator:

    int* ptr1 = new int;      // Dynamically allocate memory for one int, uninitialized
    int* ptr2 = new int(10);  // Dynamically allocate and initialize with 10
    int* ptr3 = new int[3];   // Dynamically allocate memory for an array of 3 ints
    • Purpose: Dynamically allocates memory and initializes basic types or custom objects.

    • Characteristics: If allocation fails, new throws a std::bad_alloc exception instead of returning NULL.

    • Syntax: new Type allocates memory for a single object, new Type[quantity] allocates memory for an array.

    • Example:

  2. delete Operator:

    • Purpose: Frees memory allocated with new, preventing memory leaks.

    • Characteristics: delete frees a single object, and delete[] frees an array.

    • Syntax: delete pointer frees a single object; delete[] pointer frees an array.

    • Note: Always match new with delete and new[] with delete[].

  3. Advantages of new and delete:

new and delete not only allocate and deallocate memory but also automatically call the constructor and destructor, making them ideal for managing custom types in object-oriented programming.

Code Example: new and delete with Custom Types

#include <iostream>
#include <cstdlib>  // Includes malloc and free

using namespace std;

class A {
public:
    // Constructor
    A(int a = 0) : _a(a) {
        cout << "A() constructor called, object address: " << this << endl;
    }

    // Destructor
    ~A() {
        cout << "~A() destructor called, object address: " << this << endl;
    }

private:
    int _a;  // Member variable
};

int main() {
    // Use malloc to allocate memory but do not call the constructor
    A* p1 = (A*)malloc(sizeof(A));  // Memory is allocated but constructor is not called
    A* p2 = new A(1);               // Memory is allocated and constructor is called

    // Free memory allocated by malloc, destructor is not called
    free(p1);

    // Use delete to free memory, destructor is called
    delete p2;

    // Operations with built-in types: malloc and new behave similarly for built-in types
    int* p3 = (int*)malloc(sizeof(int));  // Memory is allocated, but not initialized
    int* p4 = new int;                    // Memory is allocated but not initialized
    free(p3);                             // Free memory allocated by malloc
    delete p4;                            // Free memory allocated by new

    // Dynamically allocate arrays
    A* p5 = (A*)malloc(sizeof(A) * 10);   // Memory is allocated, but constructor is not called
    A* p6 = new A[10];                    // Memory is allocated and constructor is called

    // Free memory
    free(p5);      // Free memory, destructor is not called
    delete[] p6;   // Free array and call destructor for each object

    return 0;
}

Note: When allocating memory for custom types, new calls the constructor, and delete calls the destructor, whereas malloc and free do not.


4. operator new and operator delete Functions

In C++, the new and delete operators are used for dynamic memory management and automatically invoke constructors and destructors when creating and destroying objects. However, at a lower level, new and delete rely on the global operator new and operator delete functions to perform actual memory allocation and deallocation. By customizing these functions, developers can control memory allocation strategies, particularly when implementing custom memory management (e.g., memory pools).

1. The Implementation of operator new

operator new is a global function in C++ that is responsible for memory allocation. By default, it calls malloc to allocate memory of the specified size. If the allocation fails, it throws a std::bad_alloc exception.

Code Example:

#include <new>   // Includes bad_alloc
#include <cstdlib>  // Includes malloc
#include <iostream>
using namespace std;

// operator new: Memory allocation function
void* operator new(size_t size) _THROW1(std::bad_alloc) {
    void* p;
    // Use malloc to allocate memory, if allocation fails, attempt to call the handler
    while ((p = malloc(size)) == 0) {
        // If the handler returns 0, throw a bad_alloc exception
        if (_callnewh(size) == 0) {
            static const std::bad_alloc nomem;
            _RAISE(nomem);  // Throw memory allocation failure exception
        }
    }
    return p;  // Return pointer to allocated memory
}

2. The Implementation of operator delete

operator delete is the counterpart of operator new and is used to free memory. It calls free to deallocate memory previously allocated by new. Like new, delete can also be customized to implement specific memory management needs.

Code Example:

#include <cstdlib>  // Includes free
#include <iostream>
using namespace std;

// operator delete: Memory deallocation function
void operator delete(void* p) noexcept {
    // If the pointer is null, do not perform deallocation
    if (p == NULL) return;

    // Use free to deallocate memory
    free(p);
}

3. operator new[] and operator delete[]

Similar to operator new and operator delete, C++ also provides operator new[] and operator delete[] for allocating and deallocating arrays. These functions work in the same way as the ones for single objects, but they are specifically for arrays.

Code Example:

// operator new[]: Allocate memory for an array
void* operator new[](size_t size) {
    cout << "Allocating array of size: " << size << endl;
    return malloc(size);  // Use malloc to allocate memory
}

// operator delete[]: Deallocate memory for an array
void operator delete[](void* p) noexcept {
    cout << "Freeing array memory" << endl;
    free(p);  // Use free to deallocate memory
}

5. Implementation Principles of new and delete

1. Implementation of new and delete for Built-in Types

For built-in types (e.g., int, double, etc.), the behavior of new and malloc, as well as delete and free, is quite similar. The main differences between them are:

  • Single Element vs. Arrays: new/delete allocate and deallocate a single object, while new[]/delete[] allocate and deallocate arrays, i.e., multiple contiguous elements.

  • Exception Handling:

    • new throws a std::bad_alloc exception when memory allocation fails.

    • malloc returns NULL when allocation fails, so you need to manually check whether the return value is NULL.

  • Automatic Initialization:

    • new can initialize memory. For example, new int(5) initializes the newly allocated int to 5, whereas malloc only allocates memory and does not initialize it.

Code Example:

#include <iostream>
#include <cstdlib>  // Includes malloc and free
using namespace std;

int main() {
    // Use malloc to allocate memory, no initialization
    int* p1 = (int*)malloc(sizeof(int));

    // Use new to allocate memory, and initialize to 10
    int* p2 = new int(10);

    // Free memory, malloc uses free to deallocate
    free(p1);

    // Free memory, new uses delete to deallocate
    delete p2;

    return 0;
}

Summary:

  • new and malloc are similar in memory allocation behavior, but new provides exception handling while malloc returns NULL.

  • delete and free are essentially the same in terms of memory deallocation; both perform simple deallocation operations.

2. Implementation of new and delete for Custom Types

For custom types (e.g., class objects), the behavior of new and delete is more complex because they not only allocate and deallocate memory but also invoke constructors and destructors. This is the key difference between new/delete and malloc/free.

Workflow of Custom Type new:

  1. Call operator new to allocate memory:
    operator new allocates memory for the object using malloc or another memory allocation function.

  2. Call the constructor to initialize the object:
    After memory allocation, new calls the constructor to initialize the object, ensuring that its member variables are properly initialized.

Workflow of Custom Type delete:

  1. Call the destructor:
    Before freeing the object, delete first calls the destructor to release resources used by the object (e.g., deallocate dynamically allocated memory in member variables, close files, etc.).

  2. Call operator delete to free the memory:
    After calling the destructor, operator delete is invoked to free the memory using free.

Code Example:

#include <iostream>
using namespace std;

class A {
public:
    // Constructor
    A(int a = 0) : _a(a) {
        cout << "A() constructor called, value: " << _a << endl;
    }

    // Destructor
    ~A() {
        cout << "~A() destructor called, value: " << _a << endl;
    }

private:
    int _a;  // Member variable
};

int main() {
    // Use new to allocate an A class object and call the constructor
    A* p1 = new A(10);

    // Use delete to free the A class object and call the destructor
    delete p1;

    return 0;
}

Summary:

  • new and delete: Not only allocate and deallocate memory, but also invoke constructors and destructors to ensure the proper initialization and cleanup of custom type objects.

  • malloc/free: For custom types, they only allocate and deallocate memory, without calling constructors and destructors, so they are not suitable for managing the lifecycle of objects that require automatic initialization and cleanup.


6. Placement new

Placement new is a special syntax of new that allows constructing objects at a specific memory address. This feature is particularly useful in scenarios that require efficient memory allocation, such as memory pools.

Example:

#include <new> // Must include <new> header

class Example {
public:
    Example() { std::cout << "Example Constructor" << std::endl; }
    ~Example() { std::cout << "Example Destructor" << std::endl; }
};

int main() {
    char buffer[sizeof(Example)];      // Allocate a sufficiently large buffer
    Example* p = new(buffer) Example;  // Construct the object in the buffer

    p->~Example();  // Explicitly call the destructor
    return 0;
}

7. Summary of Differences Between malloc/free and new/delete

In C++, both malloc/free and new/delete can be used for heap memory allocation and deallocation. They share similarities in being used for dynamic memory allocation and requiring the user to manually release the memory. However, there are several important differences:

  1. Function vs. Operator:

    • malloc/free: These are functions from C used to allocate and deallocate memory.

    • new/delete: These are operators in C++ used to allocate and deallocate memory, and they also invoke constructors and destructors.

  2. Memory Initialization:

    • malloc: Only allocates memory, does not initialize the allocated memory; the data in the allocated memory is undefined.

    • new: Allocates memory and can initialize the object, especially for custom types, where new invokes the constructor to initialize the object.

  3. Memory Size Calculation:

    • malloc: The user must manually calculate the size of memory to be allocated and pass it to malloc, e.g., malloc(sizeof(int)).

    • new: The user does not need to manually calculate the memory size; new automatically calculates the required size based on the type. For example, new int automatically allocates space for an int.

  4. Return Type:

    • malloc: Returns a void* pointer, which requires explicit type casting, e.g., (int*)malloc(sizeof(int)).

    • new: Returns a pointer of the specified type, so no explicit type casting is required, e.g., new int returns an int*.

  5. Error Handling:

    • malloc: Returns NULL when memory allocation fails, so the return value must be manually checked for NULL.

    • new: Throws a std::bad_alloc exception when memory allocation fails, so exception handling is required.

  6. Constructors and Destructors:

    • malloc/free: Only allocate and deallocate memory; they do not call constructors or destructors. Therefore, malloc/free cannot properly handle custom types.

    • new/delete: Call the constructor when allocating memory and the destructor when deallocating, making them suitable for managing the lifecycle of custom type objects.


Mastering memory management is fundamental to writing efficient C++ programs. By understanding the stack, heap, and various dynamic memory management methods, you can better understand C++'s underlying mechanisms and implement efficient memory management practices.