
In the world of C++ programming, memory management is a crucial topic that developers cannot avoid. When we use the delete keyword to release memory, an interesting question arises: how does delete accurately complete the memory release task when it does not know the size of the memory being operated on? It’s like trying to clean a warehouse without knowing its size; where do we start?
For C++ developers, the importance of memory management is self-evident. Correctly allocating and releasing memory is key to ensuring stable program operation, avoiding memory leaks, and improving performance. If we compare a program to a building, then memory is the foundation of the building, and memory management is the key technology for constructing and maintaining this building. A slight misstep can lead to the building swaying, causing various hard-to-debug issues in the program. Therefore, delving into the mechanism of how delete releases memory becomes particularly important. Next, let us unveil its mysterious veil!
1. Basic Concepts of Memory Management
1.1 Memory Area Division in Programs
Before we delve into the memory release mechanism of the delete keyword, it is necessary to clarify the logic of memory area division during the execution of a C++ program—this is the core prerequisite for understanding memory management. Typically, the memory space of an executing C++ program is systematically divided into the following key areas:

- Stack: Automatically allocated and released by the compiler, mainly storing function parameter values, local variables, etc. Its characteristics include high efficiency in memory allocation and release, similar to a last-in-first-out stack structure. For example, when we define a local variable int num = 10; inside a function, this num variable is stored in the stack area, and when the function execution ends, the stack memory occupied by this variable will be automatically released. The size of the stack area is determined at compile time and is relatively small; allocating too much memory in the stack area may lead to stack overflow errors.
- Heap: Generally manually allocated and released by the programmer, used to store dynamically allocated objects and data. Unlike the stack area, memory allocation and release in the heap area are more flexible but can also lead to issues such as memory leaks. For example, when we use the new keyword to allocate memory, int* p = new int;, the allocated memory resides in the heap area. The size of the heap memory can be dynamically adjusted during program execution, but since its allocation and release require manual management by the programmer, forgetting to release memory that is no longer in use can lead to memory leaks, causing the program to consume more and more memory, ultimately affecting system performance.
- Data Segment: Used to store initialized global variables and static variables. The data segment can be further divided into initialized data segments and uninitialized data segments (BSS segment). The initialized data segment stores global variables and static variables that have been assigned initial values in the program, such as int globalVar = 10;, which is stored in the initialized data segment; while the uninitialized data segment stores global variables and static variables that have been declared but not initialized, like int uninitGlobalVar;. The memory for the data segment is allocated by the system when the program is loaded and released by the system when the program ends.
- Code Segment: Stores the binary code of function bodies, which are the instructions executed by the program. The code segment is usually read-only to prevent the program from accidentally modifying its own code during execution. The code segment may also contain some read-only constants, such as string constants “Hello, World!”. When multiple processes run the same program, they can share the same code segment, saving memory space.
Among these memory areas, heap memory is the primary target for our delete operations. Since it requires manual management by developers, it is also the most prone to issues related to memory release. Understanding the division of memory areas allows us to better comprehend the role and function of delete in memory management.
1.2 C++ Memory Management Methods
In C++, memory management mainly has two methods:C-style malloc/free and C++ specific new/delete.
malloc and free are functions provided by the C standard library for dynamic memory allocation and release. The malloc function is used to allocate a specified size of memory block from the heap, returning a pointer to the starting address of the allocated memory, and it does not initialize the allocated memory. For example:
int* p = (int*)malloc(sizeof(int));
if (p != nullptr) {
*p = 10; // Manual assignment
}
After using the memory, the free function needs to be called to release the memory:
free(p);
p = nullptr; // Prevent dangling pointer
If you forget to call free to release memory, it will lead to memory leaks.
On the other hand, new and delete are C++ operators that not only handle memory allocation and release but also automatically call the constructor and destructor of objects. This is particularly important for custom types. For example, if we have a custom class MyClass:
class MyClass {
public:
MyClass() {
// Constructor, initialize resources
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
// Destructor, release resources
std::cout << "MyClass destructor" << std::endl;
}
};
Using new to create an object:
MyClass* obj = new MyClass;
Here, new first allocates enough memory and then calls the constructor of MyClass to initialize the object. When we no longer need this object, we use delete to release the memory:
delete obj;
Delete will first call the destructor of MyClass to release the resources occupied by the object and then release the memory.
Comparing malloc/free and new/delete, we find that new/delete is safer and more convenient when dealing with custom types because it can automatically manage the object’s lifecycle. However, it is important to note that new and delete must be used in pairs; if memory is allocated with new but released with free, or vice versa, it will lead to undefined behavior, potentially causing program crashes or other hard-to-debug issues. Additionally, when using new[] to allocate array memory, delete[] must be used to release it to ensure that the destructor of each element in the array is called correctly.
1.3 Classification and Management of Heap Memory
The heap, as an important component of memory management, is like a huge warehouse that provides a region for dynamic memory allocation for programs. In Glibc heap memory management, the heap is mainly divided into two categories: the main Arena heap and the sub Arena heap, each with its unique characteristics and management methods.
/* This data structure is used only in the subArena to record current heap information. */
typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */ // Points to the Arena where this heap is located
struct _heap_info *prev; /* Previous heap. */ // Since a subArena manages multiple heaps,
size_t size; /* Current size in bytes. */ // The size allocated for user use, the remaining part is reserved
size_t mprotect_size; /* Size in bytes that has been mprotected
PROT_READ|PROT_WRITE. */ // From the code, it seems to be no different from size (personal opinion)
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; // For alignment
} heap_info;
The main Arena heap is obtained from the operating system through the brk system call. There is only one, like a large central warehouse, located in a specific area of the process address space, growing from low addresses to high addresses. When initialized, the size of the main Arena heap is usually a small value, but as the program runs and demands more memory, it can dynamically expand through the brk system call. For example, when a program needs to allocate more memory, the brk system call moves the end address of the heap to the end address of the required memory block, thus providing new memory space for the program. This is like a central warehouse that can expand to increase storage capacity when supplies are insufficient. The main Arena heap plays an important role in memory management, serving as the primary source for many small memory allocations. Due to the relatively frequent operations of memory allocation and release, it requires an efficient management mechanism to ensure reasonable memory usage.
The sub Arena heap, on the other hand, is created through the mmap system call. Unlike the main Arena heap, there can be multiple sub Arena heaps, and these heaps are connected through a linked list. This is like multiple scattered small warehouses, each with its own independent management method. The sub Arena heap is usually used to meet specific memory allocation needs or to provide independent memory allocation space for different threads in a multithreaded environment, reducing memory contention between threads. When the space of a sub Arena heap is exhausted, a new heap will be requested and added to the linked list, just like a small warehouse that builds a new warehouse when supplies are insufficient and connects it to the original warehouse.
① Heap Allocation
The first type of heap does not require an application; it only needs to call brk to expand the heap boundary. Here, we mainly explain the application of the second type of heap.
- Heap size and alignment: The second type of heap always requests memory of size HEAP_MAX_SIZE when applying, and the extra part will be reserved to prevent frequent applications. Additionally, its starting address is aligned to HEAP_MAX_SIZE, which facilitates finding the starting address of the heap.
- When to apply for the heap: There are two situations in which the second type of heap will be applied. The first situation is when creating a subArena, a heap will be applied as the first heap of that Arena; the second situation is when the originally applied heap has been fully allocated, a new heap will be requested, and this heap will be linked to the original heap through a linked list.
- Available part of the heap: Only the part needed by the user is allocated, and the size is recorded, while the remaining part is reserved.
② Heap Release
Here, heap release refers to glibc returning the requested heap memory to the kernel.
For the first type of heap, it can be considered that only the heap size is reduced. When the top free memory of the heap meets certain conditions, the heap boundary can be moved down through brk, while the top chunk pointer remains unchanged but its size becomes smaller.
For the second type of heap, when the memory in a heap has been completely released, that heap will be returned to the kernel through munmap, and the top chunk will be pointed back to the available memory address in the previous heap.
It can be understood that the heap consists of two parts: one part is the memory that has been allocated, and the other part is the reserved memory (top, as it always exists in the highest address part). The allocated memory is partially released by free, becoming free memory (memory fragmentation). Thus, aside from the reserved part, it is divided into two types of memory: free memory and used memory.
Whether it is the main Arena heap or the sub Arena heap, during the processes of memory application, release, and management, certain mechanisms are followed. When a program requests memory through the malloc function, the heap manager first looks for suitable free memory blocks in the heap. If a suitable free memory block is found, it will be allocated for the program and marked as allocated; if no suitable free memory block is found, the heap manager will request more memory from the operating system or merge and organize existing memory blocks to meet the program’s memory needs.
When a program releases memory through the free function, the heap manager will mark the released memory block as free and attempt to merge it with adjacent free memory blocks to reduce memory fragmentation and improve memory utilization. This is like in a warehouse, when supplies need to be retrieved, the first step is to check if there are suitable supplies in the warehouse; if not, new supplies will be requested; when returning supplies, they will be placed back in the warehouse and organized to make the supplies more orderly.
1.4 Heap Memory Management Allocation
The basic idea of heap memory allocation in glib is to first find the Arena of the current thread, then prioritize searching for suitable sized memory in the corresponding recycling box of the Arena. If all memory blocks in the memory box are smaller than the required size, it will split the top chunk. However, if the size of the top chunk is also insufficient, it does not necessarily need to expand the top; it checks whether the required memory is greater than 128k. If it is, it directly uses the system call mmap to allocate memory; if it is less, it expands the top chunk, i.e., expands the heap, and after the expansion is completed, allocates memory from the top chunk, with the remaining part becoming the new top chunk.
(1) malloc function
The malloc function is the core function for dynamic memory allocation in the C standard library, with the function prototype: void* malloc(size_t size);. In this prototype, the size parameter indicates the number of bytes of memory to be allocated, which is an unsigned integer type (size_t), meaning we can specify the required memory size precisely according to actual needs.
The main function of malloc is to allocate a specified size of contiguous memory space from the heap and return a pointer to the starting address of that memory block. The returned pointer type is void*, which is a pointer without a type. This is because malloc does not know what type of data will be stored in this memory in the future, so it returns a generic pointer without a type, which needs to be cast to the actual required data type pointer when used. For example, if we need to allocate memory to store integers, we need to cast the pointer returned by malloc to int*; if we want to store characters, we cast it to char*.
When a program calls the malloc function to request memory allocation, the allocation mechanism behind it involves collaboration between the operating system and the program. To effectively manage heap memory, the operating system usually maintains a free memory linked list, which is like a list that records all free “rooms” (memory blocks). Each node in the linked list represents a free memory area, containing information such as the size of the memory block, previous and next pointers, etc., so that the operating system can quickly find and manage these free memory blocks.
When the malloc function is called, the operating system will traverse this free memory linked list according to a certain algorithm, usually the first-fit algorithm, best-fit algorithm, or worst-fit algorithm. Taking the first-fit algorithm as an example, the operating system starts from the head of the linked list, checking each free memory block in turn to find the first memory block that is greater than or equal to the required allocation size. Once such a memory block is found, the operating system will remove it from the free linked list and split it as needed. If the found free memory block is larger than the requested size, the excess part will be reinserted into the free linked list for subsequent memory allocation requests. The memory block that exactly meets the size requirement will be marked as allocated and its starting address returned to the program, which is the return value of the malloc function. Through this method, the malloc function can flexibly allocate the required memory space in the heap memory to meet various dynamic memory needs.
Below is a simple C code example to demonstrate the specific usage of the malloc function. Suppose we want to dynamically allocate an array containing 10 integers and initialize and output it:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n = 10;
// Use malloc to allocate memory for n integers
arr = (int *)malloc(n * sizeof(int));
// Check if memory allocation was successful
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Initialize the array
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// Output the array contents
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Release memory to avoid memory leaks
free(arr);
return 0;
}
(2) free function
The free function is closely related to the malloc function and is the key function for releasing dynamically allocated memory in C. Its function prototype is: void free(void *ptr); here, the ptr parameter is a pointer to the memory block that was previously dynamically allocated through malloc, calloc, or realloc. The main function of the free function is to return the memory block pointed to by ptr to the system, making it available as free memory for subsequent allocation requests.
When a program calls the free function to release memory, the internal release mechanism of the free function is as follows: the free function first finds the corresponding memory block based on the pointer ptr passed in. When malloc allocates memory, in addition to allocating the user-requested size of memory space, it also records some additional management information, such as the size of the memory block, at the head of that memory block or other specific locations. The free function can accurately determine the boundaries and size of the memory block to be released through this management information. Then, the free function will mark this memory block as free and reinsert it into the free memory linked list maintained by the operating system.
If adjacent memory blocks are also free, the free function will usually merge them into a larger free memory block, a process known as memory coalescing. Memory coalescing can effectively reduce memory fragmentation and improve memory utilization. For example, in a program that frequently allocates and releases memory, if memory coalescing is not performed, over time, there may be many scattered small free memory blocks in memory, which cannot satisfy larger memory allocation requests, leading to waste of memory resources. Through memory coalescing, these adjacent small free memory blocks can be merged into a larger free memory block, thus improving memory usage efficiency.
Continuing from the previous malloc function example code, let’s look at the usage of the free function:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *arr;
int n = 10;
// Use malloc to allocate memory for n integers
arr = (int *)malloc(n * sizeof(int));
// Check if memory allocation was successful
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Initialize the array
for (int i = 0; i < n; i++) {
arr[i] = i + 1;
}
// Output the array contents
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Release memory to avoid memory leaks
free(arr);
// Set pointer to NULL to avoid dangling pointer
arr = NULL;
return 0;
}
In this code, when we use the malloc function to allocate memory and complete operations on the array, we call free(arr) to release the previously allocated memory. It is particularly important to note that after calling the free function, we set the pointer arr to NULL. This is a very important operation because if we do not set the pointer to NULL, arr will become a dangling pointer. A dangling pointer points to memory that has already been released, and continuing to use a dangling pointer for memory access can lead to undefined behavior, potentially causing program crashes, data corruption, and other serious issues. By setting the pointer to NULL, we can avoid accidentally accessing released memory, improving the stability and safety of the program.
2. Analysis of the Principle of Memory Release by Delete
2.1 Basic Usage of Delete
In the world of C++, delete is the key operator for releasing memory, like a magical “memory broom” that can sweep away memory space that is no longer needed in the program. However, there are many rules for using this “broom”.
(1) Delete Operation for a Single Object
When we use the new operator to create a single object on the heap, we need to use delete to release the memory it occupies. Let’s look at a simple code example:
#include <iostream>
int main() {
int* ptr = new int;
*ptr = 10;
std::cout << "The value of ptr is: " << *ptr << std::endl;
delete ptr;
return 0;
}
In this code, int* ptr = new int; allocates a memory space of integer size on the heap and assigns its address to the pointer ptr. Next, we assign the value 10 to this memory space. When we no longer need this memory space, we use delete ptr; to release it. This way, this memory can be reallocated by the system for other needs, just like putting an unused item back in the warehouse so that others can use it again.
(2) Delete [] Operation for Array Objects
If we create an array object, the situation is slightly different; we need to use delete[] to release the memory. Delete[] is specifically used to release dynamically allocated memory by new[]. For example:
#include <iostream>
int main() {
int* arr = new int[5];
for (int i = 0; i < 5; ++i) {
arr[i] = i + 1;
}
std::cout << "The elements of the array are: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
delete[] arr;
return 0;
}
In this example, int* arr = new int[5]; creates an array containing 5 integers. We then initialize the array and output its elements. Finally, we use delete[] arr; to release the entire memory occupied by the array. It is very important to use delete[] instead of delete because delete[] will correctly call the destructor of each element in the array (if it is an array of custom types) and release the memory of the entire array.
If delete is incorrectly used to release array memory, although there may not be immediate obvious issues for arrays of basic data types, for arrays of custom data types, only the destructor of the first object in the array will be called, and the resources of other objects may not be released correctly, leading to serious issues such as memory leaks. It is like demolishing a row of houses (the array); delete[] will demolish them one by one (calling the destructor of each object), while delete may only demolish the first house, leaving the rest standing and occupying valuable land resources (memory).
2.2 Principle of Delete for Simple Types
When delete is used to release memory for simple types (such as basic data types int, char, double, etc.), its principle is relatively simple. In this case, delete actually defaults to just calling the free function to release memory. This is because simple types do not have complex resource management needs; they only occupy fixed-size memory space and do not require additional cleanup operations. For example:
int* p = new int;
*p = 10;
delete p;
In this code, new int allocates a memory space of size int and returns a pointer to that space, p. When executing delete p, delete will call the free function to return the memory pointed to by p to the system, thus completing the memory release. Functionally, when releasing memory for simple types, delete and free serve similar purposes. However, it is important to note that delete is a C++ operator, while free is a C standard library function; their usage scenarios and semantics still differ. In C++, memory allocated with new should be released with delete to ensure code consistency and safety.
2.3 Principle of Delete for Complex Types
When dealing with complex types (such as custom classes, structures, etc.), the working principle of delete becomes more complex and refined. For complex types, delete first calls the destructor of the object and then calls operator delete to release the memory. The destructor plays a crucial role in this process, as it is responsible for cleaning up the resources occupied by the object, such as dynamically allocated memory, open file handles, network connections, etc. For example, if we have a custom class MyClass:
class MyClass {
public:
MyClass() {
// Constructor, initialize resources
data = new int[10];
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
// Destructor, release resources
delete[] data;
std::cout << "MyClass destructor" << std::endl;
}
private:
int* data;
};
When we create a MyClass object using new and release it using delete:
MyClass* obj = new MyClass;
delete obj;
When executing delete obj, the destructor ~MyClass() of MyClass will be called first. In the destructor, we see the line delete[] data;, which is responsible for releasing the dynamically allocated array data within the MyClass object. Only after the destructor completes the cleanup of the internal resources of the object will delete call operator delete to release the memory pointed to by obj, returning it to the system heap manager. If the destructor is not called when releasing complex type objects, the internal resources of the object will not be released correctly, leading to memory leaks. This is the biggest difference between delete for complex types and simple types; it manages the complete lifecycle of complex objects through the destructor, ensuring that all resources occupied by the object are properly cleaned up and released.
2.4 Delete [] and Array Memory Release
When we use new[] to allocate array memory, the situation is different. To correctly release the resources of each element in the array and reclaim the entire array memory, C++ employs a clever mechanism. When using new[] to allocate array memory, it actually allocates an additional 4 bytes (in 32-bit systems; 64-bit systems may differ) to record the size of the array. For example:
int* arr = new int[5];
In this example, new int[5] not only allocates the memory space required for 5 int-type elements but also allocates an additional 4 bytes at the front to store the size information of the array.
When using delete[] to release array memory, it will first read the size information stored in these 4 bytes. Then, delete[] will call the destructors of each element in the array in reverse order according to this size information (if it is an array of custom types), cleaning up the resources occupied by each element. Finally, delete[] will call operator delete[] (which internally calls free) to release the entire memory occupied by the array, including the previously allocated 4 bytes. For example:
delete[] arr;
Delete[] arr reads the number stored in the previous 4 bytes, calls the destructors of each of the 3 elements, and finally releases the entire memory block. If delete is incorrectly used instead of delete[] to release array memory, for arrays of custom types, only the destructor of the first element will be called, and the resources of other elements will not be released correctly, leading to memory leaks. Therefore, when releasing array memory, it is essential to use delete[] to ensure correct memory release and effective resource recovery.
3. How Does Delete Know the Size of Memory to Release?
3.1 Special Handling of Memory Allocation by New []
When we use new[] to allocate array memory, the C++ compiler performs some special handling. Before the actual storage of array data in the memory block, it allocates an additional 4 bytes of space. These 4 bytes are not allocated arbitrarily; they serve a crucial purpose: to record the number of elements in the array. For example, when we use int* arr = new int[10]; to allocate an array containing 10 int-type elements, the actual layout in memory is as follows: first, the additional 4 bytes are allocated, storing the value 10, indicating the number of elements in the array; then comes the actual memory space used to store 10 int-type data. This method of reserving space to record the size during memory allocation provides critical information for subsequent delete[] memory release.
3.2 Delete [] Reads Memory Size Information
When we use delete[] to release memory allocated by new[], it will offset 4 bytes from the starting position of the memory block (because when new[] allocated memory, it allocated an additional 4 bytes in front to record the number of elements). By reading the content of these 4 bytes, delete[] can accurately know the number of elements in the array. Knowing the number of elements, delete[] can call the destructors of each element in the array the correct number of times, cleaning up the resources occupied by each element.
For example, for an array of custom type objects, each object may have allocated some resources during creation, such as dynamically allocated memory, open file handles, etc. By calling the destructor, these resources can be properly released. After all element destructors have been called, delete[] will return the entire memory block (including the previously recorded 4 bytes and the memory space used for data) to the system, completing the memory release operation.
3.3 Example Code Demonstration
Below is a specific code example to visually demonstrate the process of new[] allocating memory and delete[] releasing memory:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor" << std::endl;
}
~MyClass() {
std::cout << "MyClass destructor" << std::endl;
}
};
int main() {
MyClass* arr = new MyClass[3]; // Use new[] to allocate an array containing 3 MyClass objects
// Here we can operate on the arr array
delete[] arr; // Use delete[] to release array memory
return 0;
}
In this code, new MyClass[3] allocates a memory block, and before storing the 3 MyClass objects, it allocates 4 additional bytes to record the number of elements, which is 3. When executing delete[] arr, it will first read the number 3 from the 4 bytes, then call the destructor of MyClass three times, outputting “MyClass destructor” three times, and finally release the entire memory block. If we incorrectly use delete arr (instead of delete[] arr) to release this array memory, it will lead to undefined behavior, potentially causing program crashes, because delete will not read the 4 bytes that record the number of elements and will not correctly call the destructors of each element, leading to memory leaks and resources not being properly released. Through this example code, we can clearly see how new[] and delete[] achieve correct memory management by recording and reading memory size information when handling array memory.
4. Common Problems and Solutions in Memory Release by Delete
4.1 Memory Leaks
One of the most common problems when using delete to release memory is memory leaks. When we allocate memory with new but forget to use delete to release it, this unreleased memory will continue to occupy system resources, and as the program runs, the occupied memory will increase, potentially leading to insufficient system memory, slow program execution, or even crashes. For example, in the following code:
void memoryLeakExample() {
int* ptr = new int;
// Some operations
// Forget to release the memory pointed to by ptr
}
In this function, we use new to allocate a memory of size int, but at the end of the function, we do not call delete to release this memory, resulting in a memory leak.
To avoid memory leaks, we can use smart pointers. The C++ standard library provides several smart pointers, such as std::unique_ptr, std::shared_ptr, and std::weak_ptr. For example, using std::unique_ptr, which adopts an exclusive ownership model to manage objects, when the std::unique_ptr object is destroyed, the object it points to will also be automatically destroyed. Using smart pointers can greatly simplify memory management and reduce the risk of forgetting to release memory. The modified code is as follows:
#include <memory>
void properMemoryManagement() {
std::unique_ptr<int> ptr = std::make_unique<int>();
// Some operations
// No need to manually call delete; ptr will automatically release memory when it goes out of scope
}
In modern C++ programming, it is recommended to use smart pointers to manage dynamic memory, which can improve the safety and reliability of the code.
4.2 Dangling Pointers
When we release memory but do not set the pointer to that memory to NULL (nullptr), a dangling pointer occurs. A dangling pointer points to memory that has already been released, and using a dangling pointer again can lead to undefined behavior, potentially causing program crashes or data corruption. For example:
int* ptr = new int;
delete ptr;
// ptr is now a dangling pointer; using it again may lead to undefined behavior
std::cout << *ptr;
To avoid dangling pointer issues, we should immediately set the pointer to nullptr after releasing memory, clearly indicating that this pointer no longer points to valid memory. The modified code is as follows:
int* ptr = new int;
delete ptr;
ptr = nullptr;
Additionally, using smart pointers can effectively avoid dangling pointer issues, as smart pointers automatically release memory at the end of the object’s lifecycle and do not create dangling pointers.
4.3 Misuse of Delete and Delete []
In C++, special care must be taken when using delete and delete[], as their usage scenarios are different. Delete is used to release memory for a single object, while delete[] is used to release memory for an array. If delete is used to release array memory or delete[] is used to release a single object’s memory, it will lead to undefined behavior. For example:
int* arr = new int[5];
delete arr; // Incorrect; should use delete[]
In this code, we use new[] to allocate an array containing 5 int-type elements but use delete to release memory, which is incorrect. The correct approach is to use delete[] to release array memory:
int* arr = new int[5];
delete[] arr;
Similarly, using delete[] to release memory for a single object is also incorrect:
int* num = new int;
delete[] num; // Incorrect; should use delete
To avoid such errors, we must clarify whether we are allocating a single object or an array when allocating memory, and then use the corresponding operator for release.
4.4 Double Freeing Memory
Executing delete or delete[] on the same memory multiple times will lead to serious errors, as this memory may have already been released, and attempting to release it again will lead to undefined behavior, potentially causing program crashes. For example:
int* ptr = new int;
delete ptr;
delete ptr; // Double freeing; error
To avoid double freeing memory, one method is to set the pointer to nullptr after releasing memory. This way, when delete is called again, it will check if the pointer is nullptr; if it is, it will not perform the release operation, thus avoiding errors. The modified code is as follows:
int* ptr = new int;
delete ptr;
pointer = nullptr;
delete ptr; // Will not produce an error because ptr is nullptr
Another better method is to use smart pointers, which automatically manage the memory lifecycle and avoid issues such as double freeing. For example, using std::unique_ptr:
#include <memory>
std::unique_ptr<int> ptr = std::make_unique<int>();
// No need to worry about double freeing issues; ptr will automatically release memory when it goes out of scope
Through the analysis of the above common problems and the introduction of solutions, we hope that everyone can be more cautious when using delete to release memory, avoiding these issues and writing more robust and reliable C++ programs.
5. Linux Memory Management Interview Questions
Question 1:<span>How does delete know the size of the memory to be released?</span>
Answer: The system usually records information such as the size of the allocated memory near the memory’s starting address when allocating memory. When using <span>delete</span>, it can locate the correct range to be released based on these hidden records.
Question 2: Why do we need to use <span>delete[]</span> to release memory allocated with <span>new</span> for an array?
Answer: For arrays allocated with <span>new[]</span>, <span>delete[]</span> can recognize the length information stored in memory, and for custom types, it will correctly call the destructor of each element in the array before releasing the entire memory block. If <span>delete</span> is misused, it may only call the destructor of the first element and release part of the memory, leading to memory leaks or undefined behavior.
Question 3:<span>How do delete and free differ in releasing memory, and why does delete not require manual size specification while free does?</span>
Answer: <span>new</span> and <span>delete</span> are C++ operators that record size information during allocation based on data types. <span>malloc</span> and <span>free</span> are C standard library functions that allocate memory based on the byte size passed by the user, and during release, there is no default mechanism to store size, requiring the developer to ensure logical consistency and match the allocated memory size.
Question 4: If you do not know whether memory was allocated with <span>new</span> or <span>new[]</span>, how should you release it?
Answer: You must determine the allocation method to release it correspondingly; otherwise, the behavior will be undefined. Programming practices should follow clear memory management standards or use automatic memory management tools like smart pointers to avoid such uncertainties.
Question 5:<span>What happens to the memory size record information after delete releases memory?</span>
Answer: <span>delete</span> typically calls a function similar to <span>operator delete</span> (the default implementation may relate to <span>free</span>, etc.), and after release, the area where the memory size record information is located is returned to the free memory management system. It may be overwritten and reused for subsequent allocation operations.
Question 6: Can delete release memory allocated by malloc, and can it correctly obtain its size?
Answer: No. <span>malloc</span> and <span>free</span> require explicit size, and there is no built-in mechanism for delete to obtain size. <span>new</span> and <span>delete</span> manage C++ types with destructors and other logic, which differs from the memory layout and operation logic of <span>malloc</span> and <span>free</span>; mixing them will lead to undefined behavior.
Question 7: Is it safe to release an array of basic data types with delete when the size is unknown?
Answer: Using <span>delete[]</span> can safely release it. Basic types do not have destructors, so both <span>delete</span> and <span>delete[]</span> can release memory space. However, it is always recommended to use <span>delete[]</span> to match <span>new[]</span> to avoid risks if basic types are later replaced with custom types.
Question 8: Will memory fragmentation affect delete when it does not know the size of the memory to be released?
Answer: Generally, it is not affected. Memory fragmentation refers to free memory being scattered into small fragmented areas, which affects the efficiency of finding contiguous available blocks for memory allocation. <span>delete</span> releases memory based on the recorded allocation range, and after release, the memory management system may attempt to merge adjacent free areas to reduce fragmentation.
Question 9: If operator new is overloaded to change the memory allocation logic, can delete still correctly obtain the size and release it?
Answer: If operator new is overloaded, operator delete must also be overloaded to maintain matching logic. If the size is stored in a special way, the overloaded operator delete must implement corresponding reading logic for release. Otherwise, the default handling of delete may not be able to obtain the correct size, triggering undefined behavior.
Question 10: How does delete know the size of memory for releasing custom type objects?
Answer: When new allocates memory for custom types, it calculates the size based on their declared structure. <span>delete</span> relies on the corresponding type memory allocation information stored during the new phase, calling the destructor and then releasing the memory block that matches the size reserved by new.