C Memory Management
Memory management is one of the most powerful yet challenging aspects of C programming. Unlike higher-level languages with automatic garbage collection, C requires programmers to explicitly manage memory allocation and deallocation. Understanding how memory works in C is essential for writing efficient and bug-free programs.
Memory Layout in C Programs
When a C program runs, the system allocates memory for it with the following segments:
- Text/Code Segment: Stores the compiled program instructions (read-only)
- Data Segment:
- Initialized Data: Global and static variables with initial values
- Uninitialized Data (BSS): Global and static variables without initial values
- Stack: Manages function calls, local variables, and program flow
- Heap: Area for dynamic memory allocation
High Address
┌───────────────────┐
│ Stack │ ← Local variables, function calls (grows downward)
│ ↓ │
├───────────────────┤
│ ↑ │
│ Heap │ ← Dynamic memory allocation (grows upward)
├───────────────────┤
│ BSS Segment │ ← Uninitialized static/global variables
├───────────────────┤
│ Data Segment │ ← Initialized static/global variables
├───────────────────┤
│ Text Segment │ ← Program instructions
└───────────────────┘
Low Address
Memory Management Functions
C provides several standard library functions for memory management, all defined in <stdlib.h>
:
malloc()
Allocates the specified number of bytes and returns a pointer to the first byte of the allocated space.
void* malloc(size_t size);
calloc()
Allocates space for an array of elements, initializes them to zero, and returns a pointer to the memory.
void* calloc(size_t num_elements, size_t element_size);
realloc()
Changes the size of a previously allocated memory block.
void* realloc(void* ptr, size_t new_size);
free()
Deallocates the memory previously allocated by malloc()
, calloc()
, or realloc()
.
void free(void* ptr);
Basic Memory Allocation
- malloc
- calloc
- realloc
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory for an integer
int* ptr = (int*) malloc(sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Use the allocated memory
*ptr = 42;
printf("Value: %d\n", *ptr);
// Free the allocated memory
free(ptr);
// Avoid using the pointer after freeing (set to NULL)
ptr = NULL;
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory for 5 integers and initialize to 0
int* ptr = (int*) calloc(5, sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// The memory is already initialized to 0
for (int i = 0; i < 5; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}
// Free the allocated memory
free(ptr);
ptr = NULL;
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory for 5 integers
int* ptr = (int*) malloc(5 * sizeof(int));
if (ptr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Initialize the array
for (int i = 0; i < 5; i++) {
ptr[i] = i * 10;
}
// Resize the array to hold 10 integers
int* new_ptr = (int*) realloc(ptr, 10 * sizeof(int));
if (new_ptr == NULL) {
printf("Memory reallocation failed\n");
free(ptr);
return 1;
}
ptr = new_ptr; // Update pointer to new memory block
// Initialize the new elements
for (int i = 5; i < 10; i++) {
ptr[i] = i * 10;
}
// Print all values
for (int i = 0; i < 10; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}
// Free the allocated memory
free(ptr);
ptr = NULL;
return 0;
}
Common Memory Management Issues
Memory Leaks
A memory leak occurs when allocated memory is not freed, causing the program to consume more memory over time.
void memory_leak_example() {
int* ptr = (int*) malloc(sizeof(int));
// Problem: The function ends without freeing ptr
// Solution: Always free dynamically allocated memory when done
// free(ptr);
}
Dangling Pointers
A dangling pointer points to memory that has been freed.
int* create_dangling_pointer() {
int* ptr = (int*) malloc(sizeof(int));
*ptr = 42;
free(ptr); // Memory is freed
return ptr; // Problem: Returning a pointer to freed memory
}
// Usage:
// int* dangerous = create_dangling_pointer();
// *dangerous = 10; // Undefined behavior - may crash or corrupt memory
Buffer Overflows
Writing beyond the bounds of allocated memory.
void buffer_overflow_example() {
int* array = (int*) malloc(5 * sizeof(int));
// Problem: Writing beyond allocated memory
for (int i = 0; i < 10; i++) { // Should be i < 5
array[i] = i; // Writes beyond allocated memory for i >= 5
}
free(array);
}
Double Free
Freeing the same memory block more than once.
void double_free_example() {
int* ptr = (int*) malloc(sizeof(int));
free(ptr); // First free - correct
// free(ptr); // Problem: Second free - undefined behavior
// Solution: Set pointer to NULL after freeing
ptr = NULL;
// Now this check prevents double free
if (ptr != NULL) {
free(ptr);
}
}
Best Practices for Memory Management
-
Always check for allocation failure:
cint* ptr = (int*) malloc(sizeof(int));
if (ptr == NULL) {
// Handle error
return ERROR_CODE;
} -
Always free allocated memory when done:
cfree(ptr);
ptr = NULL; // Set to NULL to prevent use after free -
Avoid memory leaks in conditionals and loops:
cint* ptr = (int*) malloc(sizeof(int));
if (condition) {
free(ptr); // Free before return
return;
}
// Use ptr...
free(ptr); // Free at end -
Use tools to detect memory issues:
- Valgrind (Linux/macOS)
- Address Sanitizer (Clang/GCC)
- Dr. Memory (Windows)
-
Consider encapsulating memory management:
ctypedef struct {
int* data;
size_t size;
} IntArray;
IntArray* create_int_array(size_t size) {
IntArray* array = malloc(sizeof(IntArray));
if (array == NULL) return NULL;
array->data = malloc(size * sizeof(int));
if (array->data == NULL) {
free(array);
return NULL;
}
array->size = size;
return array;
}
void free_int_array(IntArray* array) {
if (array) {
free(array->data);
free(array);
}
}
Advanced Memory Management Techniques
Custom Memory Allocators
For performance-critical applications, you can implement custom memory allocators:
#include <stdio.h>
#include <stdlib.h>
#define POOL_SIZE 1024 // Size of our memory pool
typedef struct {
char buffer[POOL_SIZE];
size_t offset;
} MemoryPool;
MemoryPool* create_memory_pool() {
MemoryPool* pool = malloc(sizeof(MemoryPool));
if (pool) {
pool->offset = 0;
}
return pool;
}
void* pool_alloc(MemoryPool* pool, size_t size) {
// Ensure alignment (simplified)
size_t aligned_size = (size + 7) & ~7;
if (pool->offset + aligned_size > POOL_SIZE) {
return NULL; // Not enough space
}
void* ptr = &pool->buffer[pool->offset];
pool->offset += aligned_size;
return ptr;
}
void destroy_memory_pool(MemoryPool* pool) {
free(pool);
}
// No individual free() - the entire pool is freed at once
Memory Alignment
Memory alignment is crucial for performance and correctness on some architectures:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Standard malloc doesn't guarantee alignment beyond what's
// needed for any basic type
// For specific alignment needs, use aligned_alloc (C11)
// or platform-specific functions like posix_memalign
#ifdef _ISOC11_SOURCE
// Allocate 1024 bytes aligned to 64-byte boundary (C11)
void* aligned_ptr = aligned_alloc(64, 1024);
if (aligned_ptr) {
printf("Address: %p\n", aligned_ptr);
// Check if properly aligned
if (((uintptr_t)aligned_ptr & 63) == 0) {
printf("Properly aligned to 64 bytes\n");
}
free(aligned_ptr);
}
#endif
return 0;
}
Conclusion
Memory management is a critical skill for C programmers. By understanding how memory works and following best practices, you can write more efficient, reliable programs. The power of direct memory management in C allows for highly optimized applications but requires careful attention to prevent bugs and security vulnerabilities.
In the next section, we'll explore dynamic memory allocation in more detail.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)