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:
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.
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
ormalloc
.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
orfree
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.
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.
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
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
, andrealloc
. Heap memory must be explicitly freed usingfree()
, 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:
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:
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:
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:
free:
free(ptr); // Frees dynamically allocated memory
Purpose: Frees memory allocated by
malloc
,calloc
, orrealloc
. 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.
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 astd::bad_alloc
exception instead of returningNULL
.Syntax:
new Type
allocates memory for a single object,new Type[quantity]
allocates memory for an array.Example:
delete Operator:
Purpose: Frees memory allocated with
new
, preventing memory leaks.Characteristics:
delete
frees a single object, anddelete[]
frees an array.Syntax:
delete pointer
frees a single object;delete[] pointer
frees an array.Note: Always match
new
withdelete
andnew[]
withdelete[]
.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, whilenew[]
/delete[]
allocate and deallocate arrays, i.e., multiple contiguous elements.Exception Handling:
new
throws astd::bad_alloc
exception when memory allocation fails.malloc
returnsNULL
when allocation fails, so you need to manually check whether the return value isNULL
.Automatic Initialization:
new
can initialize memory. For example,new int(5)
initializes the newly allocatedint
to 5, whereasmalloc
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
andmalloc
are similar in memory allocation behavior, butnew
provides exception handling whilemalloc
returnsNULL
.delete
andfree
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
:
Call
operator new
to allocate memory:operator new
allocates memory for the object usingmalloc
or another memory allocation function.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
:
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.).Call
operator delete
to free the memory:
After calling the destructor,operator delete
is invoked to free the memory usingfree
.
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
anddelete
: 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:
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.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, wherenew
invokes the constructor to initialize the object.Memory Size Calculation:
malloc
: The user must manually calculate the size of memory to be allocated and pass it tomalloc
, 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 anint
.Return Type:
malloc
: Returns avoid*
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 anint*
.Error Handling:
malloc
: ReturnsNULL
when memory allocation fails, so the return value must be manually checked forNULL
.new
: Throws astd::bad_alloc
exception when memory allocation fails, so exception handling is required.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.