Click the blue “One Click Linux” in the upper left corner, and select “Set as Favorite“
Get the latest technical articles first
☞【Essentials】Learning Path for Embedded Driver Engineers
☞【Essentials】Linux Embedded Knowledge Points - Mind Map - Free Access
☞【Employment】A Comprehensive IoT Project Based on Linux for Your Resume
☞【Employment】Resume Template
Dynamic memory management is a powerful yet error-prone aspect of the C language. Unlike languages with automatic memory management (such as Java and Python), C requires programmers to explicitly allocate and free memory.
This control brings the potential for high performance but also introduces risks such as memory leaks, dangling pointers, and double frees.
Understanding the principles of dynamic memory allocation, mastering the correct usage of malloc/free, detecting memory leaks, and advanced memory management techniques (such as memory pools) are crucial for writing robust and efficient C programs.
This article will delve into various aspects of dynamic memory management in C: from the basic functions malloc, calloc, realloc, free to the causes of memory leaks, detection tools, and prevention strategies, concluding with the design concepts and simple implementations of memory pools.
1. Basics of Dynamic Memory Allocation in C
The C standard library <stdlib.h> provides the main dynamic memory management functions.
1.1 malloc – Allocate Uninitialized Memory
void *malloc(size_t size);
- Function: Allocates a block of contiguous memory of the specified size (size bytes) on the heap.
- Return Value:
- Success: Returns a void* pointer to the beginning of the allocated memory block. This pointer is untyped and needs to be cast to the appropriate pointer type before use.
- Failure: Returns NULL if the required size of memory cannot be allocated (e.g., due to insufficient memory).
- Memory Content: The memory block allocated by malloc is uninitialized, which may contain arbitrary garbage values.
#include<stdio.h>
#include<stdlib.h>
int main() {
int n = 10;
int *arr;
// Allocate enough memory to store n integers
arr = (int *)malloc(n * sizeof(int));
// *** Must check the return value of malloc ***
if (arr == NULL) {
fprintf(stderr, "Memory allocation failed!\n");
return 1; // or take other error handling measures
}
printf("Memory allocated successfully at address: %p\n", (void*)arr);
// Use the allocated memory (content is undefined at this point)
for (int i = 0; i < n; ++i) {
arr[i] = i * 10; // Initialize
printf("arr[%d] = %d\n", i, arr[i]);
}
// *** Free the memory ***
free(arr);
arr = NULL; // Good practice: set pointer to NULL after freeing to prevent dangling pointer
return 0;
}
1.2 calloc – Allocate and Zero Memory
void *calloc(size_t num, size_t size);
- Function: Allocates enough memory to store num elements of size size bytes, and automatically initializes all bytes to zero.
- Total Size: The total number of bytes allocated is num * size.
- Return Value: Similar to malloc, returns a void* pointer on success, and NULL on failure.
- Advantages: Convenient for data structures that need to be initialized to zero (e.g., counters, flag arrays), avoiding the risk of forgetting to initialize.
- Disadvantages: May be slightly slower than malloc due to the additional zeroing operation.
#include<stdio.h>
#include<stdlib.h>
typedef struct {
int id;
double value;
} DataItem;
int main() {
int count = 5;
DataItem *items;
// Allocate 5 DataItem structures and initialize to 0
items = (DataItem *)calloc(count, sizeof(DataItem));
if (items == NULL) {
fprintf(stderr, "calloc failed!\n");
return 1;
}
printf("Memory allocated and zeroed by calloc.\n");
// Verify that the content is 0
for (int i = 0; i < count; ++i) {
printf("items[%d]: id=%d, value=%f\n", i, items[i].id, items[i].value);
}
free(items);
items = NULL;
return 0;
}
1.3 realloc – Resize Allocated Memory
void *realloc(void *ptr, size_t new_size);
- Function: Attempts to change the size of the memory block previously allocated by malloc, calloc, or realloc pointed to by ptr to new_size.
- Parameters:
- ptr: A pointer to the previously allocated memory block. If ptr is NULL, realloc behaves as malloc(new_size).
- new_size: The new size of the memory block (in bytes). If new_size is 0, and ptr is not NULL, the behavior is implementation-defined: it may free the memory (equivalent to free(ptr)) and return NULL, or it may return a non-NULL pointer that can be passed to free.It is recommended to avoid the case where new_size is 0, and to directly call free.
- Return Value and Behavior:
- In-place Expansion/Shrinkage Success: If the size can be adjusted at the original location pointed to by ptr (usually shrinking or slightly expanding in place), returns the original ptr.
- Move and Expand Success: If it cannot expand in place, realloc will attempt to allocate a new, sufficiently large memory block (new_size), copy the contents of the original memory block (up to min(old_size, new_size) bytes) to the new memory block, free the original memory block (ptr), and return a pointer to the new memory block.
- Failure: If it cannot allocate new memory, returns NULL.At this point, the original memory block (ptr) remains valid and has not been freed, and its contents remain unchanged.
- Important:It is necessary to use a new pointer variable to receive the return value of realloc and check if it is NULL.Do not directly assign the return value to the original pointer ptr, as this will cause the original pointer to be lost if realloc fails, leading to a memory leak.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main() {
int initial_size = 5;
int *arr = (int *)malloc(initial_size * sizeof(int));
if (arr == NULL) return 1;
printf("Initial allocation (size %d) at %p\n", initial_size, (void*)arr);
for (int i = 0; i < initial_size; ++i) arr[i] = i;
// Attempt to expand the array
int new_size = 10;
int *temp_arr = (int *)realloc(arr, new_size * sizeof(int));
// *** Check the return value of realloc ***
if (temp_arr == NULL) {
fprintf(stderr, "realloc failed! Original memory still valid.\n");
// Can continue using arr, but cannot expand
free(arr); // Still need to free original memory
return 1;
}
// realloc successful, update pointer
arr = temp_arr;
printf("Reallocated to size %d at %p\n", new_size, (void*)arr);
// New allocated part's content is undefined (unless realloc is an in-place expansion)
// Original part's data is preserved
printf("Original data preserved: ");
for (int i = 0; i < initial_size; ++i) printf("%d ", arr[i]);
printf("\n");
// Initialize new allocated part
for (int i = initial_size; i < new_size; ++i) arr[i] = i * 100;
printf("Full array after realloc and init: ");
for (int i = 0; i < new_size; ++i) printf("%d ", arr[i]);
printf("\n");
// Attempt to shrink the array
int smaller_size = 3;
temp_arr = (int *)realloc(arr, smaller_size * sizeof(int));
if (temp_arr == NULL) { // Shrinking generally should not fail, but still need to check
fprintf(stderr, "realloc (shrink) failed!\n");
free(arr);
return 1;
}
arr = temp_arr;
printf("Shrunk to size %d at %p\n", smaller_size, (void*)arr);
printf("Data after shrinking: ");
for (int i = 0; i < smaller_size; ++i) printf("%d ", arr[i]); // Only the first smaller_size are valid
printf("\n");
free(arr);
arr = NULL;
// realloc(NULL, size) is equivalent to malloc(size)
char *str = (char*)realloc(NULL, 50 * sizeof(char));
if (str) {
strcpy(str, "Allocated via realloc(NULL, ...)");
printf("%s\n", str);
free(str);
}
return 0;
}
1.4 free – Free Memory
void free(void *ptr);
- Function: Frees the memory block previously allocated by malloc, calloc, or realloc pointed to by ptr, returning it to the heap memory manager for subsequent reallocation.
- Parameter ptr:
- Must be a pointer to a previously dynamically allocated and not yet freed memory block.
- If ptr is NULL, free(NULL) issafe and does nothing.
- Important Rules:
- Can only free dynamically allocated memory: Cannot free pointers to stack memory (local variables), static memory (global/static variables), or code segments.
- Cannot Double free: Calling free on the same memory block twice will lead to undefined behavior (usually program crash or memory corruption).
- Cannot free Invalid Pointers: Cannot free a pointer that has never pointed to dynamically allocated memory, or a pointer that points to freed memory (dangling pointer).
- Pointer After Free: After calling free(ptr), the value of ptr itself does not change, but the memory area it points to is no longer valid. At this point, ptr becomes a dangling pointer. Accessing a dangling pointer is undefined behavior.
- Good Practice: After calling free(ptr), immediately set ptr to NULL (ptr = NULL;). This prevents accidental use of dangling pointers later and makes subsequent calls to free(ptr) safe (because free(NULL) is a no-op).
2. Memory Leaks
A memory leak occurs when a program dynamically allocates memory but fails to release it when no longer needed, leading to that portion of memory being unavailable for reuse. As the program runs, leaked memory accumulates, potentially exhausting the system’s available memory, resulting in degraded performance or even crashes.
2.1 Common Causes of Memory Leaks
- Forgetting to free: The most common cause. Memory is allocated but never calls free.
void process() {
char *buffer = (char *)malloc(1024);
if (!buffer) return;
// ... use buffer ...
// Forget to call free(buffer);
} // At function return, buffer pointer is lost, memory leak
- Pointer Loss: Overwriting the only pointer to allocated memory, making it impossible to free that memory.
char *p = (char *)malloc(100);
if (!p) return;
// ...
p = (char *)malloc(200); // Overwrites the previous pointer, leaking the first 100 bytes
if (!p) { /* Handle error, but previous leak has occurred */ }
// ...
free(p); // Only frees the second 200 bytes of memory
```c
char *p = (char *)malloc(100);
if (!p) return;
// ...
p = some_other_pointer; // Overwrites pointer, leaking 100 bytes
// ...
// free(p); // At this point free is on memory pointed to by some_other_pointer (if it is dynamically allocated)
```
- Improper Handling of realloc Failure: As mentioned earlier, if realloc fails and returns NULL, but directly assigns NULL to the original pointer, the original memory block’s pointer is lost.
int *arr = (int *)malloc(10 * sizeof(int));
// ...
// Incorrect:
arr = (int *)realloc(arr, 20 * sizeof(int));
if (arr == NULL) { // If realloc fails, original arr's address is lost, memory leak
// ... cannot free original memory ...
}
- Incomplete Release of Complex Data Structures: For structures or linked lists containing dynamically allocated members, when releasing the outer structure, it is necessary to recursively release its internally dynamically allocated memory as well.
typedef struct Node {
char *data; // data points to dynamically allocated string
struct Node *next;
} Node;
void free_list(Node *head) {
Node *current = head;
while (current != NULL) {
Node *next = current->next;
// Incorrect: only frees the node itself, not the data inside the node
// free(current);
// Correct: first free the internally dynamically allocated members, then free the node itself
free(current->data); // Assume data is dynamically allocated
free(current);
current = next;
}
}
2.2 Memory Leak Detection Tools
Manually finding memory leaks is very difficult. Fortunately, there are many tools available to help detect:
- Valgrind (Linux/macOS): A powerful suite of memory debugging, memory leak detection, and performance analysis tools.The memcheck tool is the most commonly used part.
- Compile: Compile the code with the -g option to include debugging information (gcc -g my_program.c -o my_program).
- Run: valgrind –leak-check=full ./my_program
- Output: Valgrind will report detected memory leaks (Definitely lost, Indirectly lost, Possibly lost), memory errors (such as illegal reads/writes, using uninitialized memory, double frees, etc.), and the code locations where issues occurred.
- AddressSanitizer (ASan) (GCC/Clang/MSVC): A fast memory error detector that is part of the compiler. It can detect memory leaks, buffer overflows, use of freed memory, etc.
- Compile (GCC/Clang): Compile and link using -fsanitize=address -g.
- Compile (MSVC): Enable AddressSanitizer in project properties.
- Run: Run the program normally. If errors are detected, the program will terminate and print a detailed report.
- Leak Detection (LSan): AddressSanitizer typically includes LeakSanitizer (LSan). Sometimes it is necessary to set the environment variable ASAN_OPTIONS=detect_leaks=1 to explicitly enable leak detection.
- Visual Studio Debugger (Windows): The Visual Studio debugger has built-in memory leak detection functionality (mainly for memory allocated using the CRT library _malloc_dbg and other debug versions).
- Include #define _CRTDBG_MAP_ALLOC at the beginning of the code and <crtdbg.h>.
- Call _CrtDumpMemoryLeaks(); before program exit.
- Run the program in debug mode, and it will report detected leaks in the output window upon exit.
- Other Tools: Such as Dr. Memory, Purify, Insure++, and other commercial or open-source tools.
Recommendations for Using Tools:
- Regularly use memory detection tools during development.
- Carefully read the reports from the tools to understand where and why leaks occurred.
- After fixing leaks, run the tools again to confirm that the issues have been resolved.
2.3 Strategies to Prevent Memory Leaks
- Who Allocates, Who Frees: Follow clear memory ownership rules. Generally, the function or module that allocates memory should also be responsible for freeing it.
- Pair malloc/calloc/realloc with free: Ensure that every successful dynamic allocation has a corresponding free call.
- Error Handling: Properly handle allocation failures to ensure that pointers to allocated memory are not lost.
- Correct Use of realloc: Always use a temporary pointer to receive the return value of realloc.
- Cleanup Functions for Complex Data Structures: Write dedicated destroy/cleanup functions for structures containing dynamic memory to ensure all internally dynamically allocated resources are released.
- Use free(ptr); ptr = NULL;: Set pointers to NULL after freeing to prevent dangling pointers and double frees (free(NULL) is safe).
- Code Review: Carefully review code involving dynamic memory management.
- Utilize Tools: Actively use Valgrind, ASan, and other tools for detection.
- Consider Alternatives: In some cases, stack memory (if size is known and not large), static memory, or higher-level abstractions (like C++ smart pointers, containers) can simplify memory management.
3. Memory Pools
A memory pool is a memory management technique that pre-allocates a large block of memory and then allocates smaller blocks of memory from it as needed. When small blocks of memory are freed, they are returned to the memory pool instead of the operating system for later reuse.
3.1 Why Use Memory Pools?
Frequent calls to malloc and free may have the following issues:
- Performance Overhead: malloc/free typically require system calls, locking (in multi-threaded environments), searching for suitable free blocks, etc. For a large number of small memory allocations and deallocations, the overhead can be significant.
- Memory Fragmentation: Frequent allocation and deallocation of memory blocks of different sizes can lead to many non-contiguous small free blocks in heap memory (external fragmentation), making it impossible to satisfy larger allocation requests even if the total free memory is sufficient.
- Uncertain Allocation Time: The execution time of malloc may vary depending on the state of the heap, which can be an issue for systems with high real-time requirements.
Memory pools aim to address these issues:
- Improved Performance: Allocating/freeing from a memory pool is usually just simple pointer operations, avoiding system calls and complex heap management algorithms, making it much faster.
- Reduced Fragmentation: Especially for fixed-size memory pools (allocating only blocks of specific sizes), external fragmentation can be completely avoided.
- More Predictable Allocation Times: Allocating from a memory pool typically has more stable and faster execution times.
- Improved Locality: Memory blocks allocated from a memory pool may be closer together in memory, helping to improve cache hit rates.
3.2 Design and Implementation of Memory Pools (Simple Example)
There are various design approaches for memory pools; here is a very simple implementation idea for a fixed-size block memory pool.
Basic Idea:
- Initialization: Allocate a large block of memory (the pool) once using malloc. Split this large memory into multiple fixed-size small blocks (nodes). Link all small blocks into a free list.
- Allocation (pool_alloc): If the free list is not empty, take the next node from the head of the list and return a pointer to its data area.
- Freeing (pool_free): Reinterpret the memory block to be freed (pointer) as a node and insert it at the head of the free list.
#include<stdio.h>
#include<stdlib.h>
#include<stddef.h> // For offsetof
// Memory pool node structure (for linking free blocks)
// Place it at the beginning of each block
typedef struct PoolNode {
struct PoolNode* next;
} PoolNode;
// Memory pool structure
typedef struct {
void* pool_memory; // Pointer to the pre-allocated large block of memory
PoolNode* free_list; // Pointer to the head node of the free block list
size_t block_size; // Size of each small block (must >= sizeof(PoolNode))
size_t num_blocks; // Total number of blocks in the pool
} MemoryPool;
// Initialize memory pool
int pool_init(MemoryPool* pool, size_t block_size, size_t num_blocks) {
if (block_size < sizeof(PoolNode)) {
fprintf(stderr, "Error: Block size too small for pool node.\n");
return -1; // Block size must be able to hold a pointer
}
// 1. Allocate large block of memory
size_t total_size = block_size * num_blocks;
pool->pool_memory = malloc(total_size);
if (pool->pool_memory == NULL) {
fprintf(stderr, "Error: Failed to allocate memory for the pool.\n");
return -1;
}
pool->block_size = block_size;
pool->num_blocks = num_blocks;
pool->free_list = NULL; // Initialize free list to empty
// 2. Split large memory into blocks and link into free list
char* current_block = (char*)pool->pool_memory;
for (size_t i = 0; i < num_blocks; ++i) {
PoolNode* node = (PoolNode*)current_block;
node->next = pool->free_list; // Head insertion
pool->free_list = node;
current_block += block_size;
}
printf("Memory pool initialized: %zu blocks of %zu bytes each.\n", num_blocks, block_size);
return 0;
}
// Allocate a block from the memory pool
void* pool_alloc(MemoryPool* pool) {
if (pool->free_list == NULL) {
// Pool is full, can return NULL, or dynamically expand the pool (more complex)
fprintf(stderr, "Warning: Memory pool is full!\n");
return NULL;
}
// Take the next node from the head of the free list
PoolNode* allocated_node = pool->free_list;
pool->free_list = allocated_node->next; // Update list head
// Return pointer to the data area of the node (skipping PoolNode part)
// This simple implementation returns the entire block; users need to know PoolNode exists
// A more complete design would hide PoolNode and only return data area pointer
// return (void*)allocated_node; // Return entire block
printf("Allocated block at %p\n", (void*)allocated_node);
return (void*)allocated_node; // For simplicity, return entire block address
}
// Free a block back to the memory pool
void pool_free(MemoryPool* pool, void* block) {
if (block == NULL) {
return; // free(NULL) is safe
}
// Check if pointer is within the pool's range (optional but recommended)
char* block_ptr = (char*)block;
char* pool_start = (char*)pool->pool_memory;
char* pool_end = pool_start + pool->num_blocks * pool->block_size;
if (block_ptr < pool_start || block_ptr >= pool_end) {
fprintf(stderr, "Error: Attempting to free memory not belonging to the pool!\n");
// Or directly call system free(block)? Depends on design
return;
}
// Check if pointer is aligned to block boundary (optional)
if ((block_ptr - pool_start) % pool->block_size != 0) {
fprintf(stderr, "Error: Attempting to free misaligned pointer!\n");
return;
}
// Reinterpret the block as PoolNode and insert it at the head of the free list
PoolNode* node = (PoolNode*)block;
node->next = pool->free_list;
pool->free_list = node;
printf("Freed block at %p back to pool\n", block);
}
// Destroy memory pool (free large block of memory)
void pool_destroy(MemoryPool* pool) {
if (pool && pool->pool_memory) {
free(pool->pool_memory);
pool->pool_memory = NULL;
pool->free_list = NULL;
pool->block_size = 0;
pool->num_blocks = 0;
printf("Memory pool destroyed.\n");
}
}
// --- Example Usage ---
typedef struct {
int id;
double data[5]; // Assume this structure needs to be allocated
} MyData;
int main() {
MemoryPool my_pool;
size_t data_block_size = sizeof(MyData); // Or adjust as needed, ensure >= sizeof(PoolNode)
if (data_block_size < sizeof(PoolNode)) data_block_size = sizeof(PoolNode);
size_t num_items = 10;
if (pool_init(&my_pool, data_block_size, num_items) != 0) {
return 1;
}
MyData* items[num_items];
// Allocation
printf("\n--- Allocation Phase ---\n");
for (size_t i = 0; i < num_items; ++i) {
items[i] = (MyData*)pool_alloc(&my_pool);
if (items[i]) {
items[i]->id = i;
// ... initialize data ...
} else {
printf("Allocation failed for item %zu\n", i);
}
}
// Attempt to allocate more (should fail)
printf("\n--- Over Allocation Attempt ---\n");
void* extra = pool_alloc(&my_pool);
if (extra == NULL) {
printf("Expected failure: Pool is full.\n");
}
// Freeing some
printf("\n--- Freeing Phase ---\n");
pool_free(&my_pool, items[3]); items[3] = NULL;
pool_free(&my_pool, items[7]); items[7] = NULL;
pool_free(&my_pool, items[0]); items[0] = NULL;
// Reallocate (should use just freed blocks)
printf("\n--- Re-Allocation Phase ---\n");
MyData* item_new1 = (MyData*)pool_alloc(&my_pool);
MyData* item_new2 = (MyData*)pool_alloc(&my_pool);
if (item_new1) item_new1->id = 1001;
if (item_new2) item_new2->id = 1002;
// Free all remaining
printf("\n--- Final Free Phase ---\n");
for (size_t i = 0; i < num_items; ++i) {
pool_free(&my_pool, items[i]);
}
pool_free(&my_pool, item_new1);
pool_free(&my_pool, item_new2);
// Destroy pool
printf("\n--- Destruction Phase ---\n");
pool_destroy(&my_pool);
return 0;
}
Improvements and Variants of Memory Pools:
- Multi-Size Memory Pools: Maintain multiple memory pools of different fixed block sizes, selecting the appropriate pool based on request size.
- Buddy System: A more complex memory management algorithm that can handle allocation requests of different sizes and strives to reduce internal fragmentation.
- Slab Allocator: A technique used in the Linux kernel optimized for frequent allocations of specific types of objects, reducing initialization overhead and improving cache utilization.
- Thread-Local Memory Pools: Each thread has its own memory pool, avoiding lock contention during multi-threaded allocations.
- Dynamic Expansion: When the pool is full, more large blocks of memory can be allocated to add to the pool.
3.3 Scenarios for Using Memory Pools
- Applications that require frequent allocation and deallocation of a large number of small memory blocks of fixed size or limited size range (e.g., network servers handling requests, game engines managing objects, custom data structure nodes, etc.).
- Systems with high performance and real-time requirements for memory allocation.
- Applications that need to reduce memory fragmentation.
Considerations:
- The memory pool itself requires a one-time allocation of a large amount of memory, which may increase the program’s startup memory usage.
- If the sizes of allocated blocks vary greatly, a fixed-size memory pool may lead to significant internal fragmentation (allocated blocks being larger than actually needed).
- The management logic of the memory pool needs to be carefully designed and implemented; errors may lead to memory corruption.
- Memory allocated from a memory pool cannot be freed using the system’s free, but must use the memory pool’s own pool_free function.
4. Conclusion
- Dynamic memory management in C is implemented through <stdlib.h> with
- malloc, calloc, realloc, free.
- It is essential to check the return values of malloc, calloc, realloc for NULL.
- Special attention is needed when using realloc, using a temporary pointer to receive the return value.
- Memory leaks are common serious issues caused by forgetting to free, pointer loss, improper handling of realloc, and incomplete release of complex structures.
- Valgrind, AddressSanitizer and other tools are powerful for detecting memory leaks and errors.
- Following good programming practices (pairing allocation with deallocation, clear ownership, setting to NULL after freeing) is key to preventing leaks.
- Memory pools improve the performance of small memory allocations/deallocations, reduce fragmentation, and enhance predictability through pre-allocation and custom management, suitable for specific scenarios, but increase implementation complexity.

end
One Click Linux
Follow, reply with 【1024】 to receive a wealth of Linux materials
Collection of Wonderful Articles
Article Recommendations
☞【Collection】ARM☞【Collection】Fan Q&A☞【Collection】All Originals☞【Collection】LinuxIntroduction☞【Collection】Computer Networks☞【Collection】Linux Drivers☞【Essentials】Learning Path for Embedded Driver Engineers☞【Essentials】All Knowledge Points of Linux Embedded – Mind Map