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:
int* ptr = (int*) malloc(sizeof(int));
if (ptr == NULL) {
// Handle error
return ERROR_CODE;
} -
Always free allocated memory when done:
free(ptr);
ptr = NULL; // Set to NULL to prevent use after free -
Avoid memory leaks in conditionals and loops:
int* 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:
typedef 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.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!