Dynamic Memory Management
Introduction
Dynamic memory management is a crucial aspect of C programming that allows programs to allocate and deallocate memory at runtime. Unlike static memory allocation where the size and lifetime of variables are determined at compile time, dynamic memory allocation provides flexibility for handling data structures whose size is unknown until the program runs. This capability is essential for implementing complex data structures like linked lists, trees, and dynamic arrays.
Memory Layout in C Programs
Program Memory Sections
A C program’s memory is typically organized into several sections:
- Text Segment: Contains executable code
- Data Segment: Contains global and static variables
- Stack: Contains local variables and function call information
- Heap: Contains dynamically allocated memory
Stack vs. Heap
// Stack allocation - automatic memory management
void stack_example() {
int local_array[100]; // Allocated on stack
// Automatically deallocated when function returns
}
// Heap allocation - manual memory management
void heap_example() {
int *dynamic_array = malloc(100 * sizeof(int)); // Allocated on heap
// Must be manually deallocated with free()
free(dynamic_array);
}Dynamic Memory Allocation Functions
malloc()
The malloc() function allocates a block of memory of specified size:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory for 10 integers
int *arr = malloc(10 * sizeof(int));
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Use the allocated memory
for (int i = 0; i < 10; i++) {
arr[i] = i * i;
printf("arr[%d] = %d\n", i, arr[i]);
}
// Free the allocated memory
free(arr);
arr = NULL; // Prevent dangling pointer
return 0;
}calloc()
The calloc() function allocates memory and initializes it to zero:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate and zero-initialize memory for 10 integers
int *arr = calloc(10, sizeof(int));
if (arr == NULL) {
printf("Memory allocation failed\n");
return 1;
}
// Memory is already zero-initialized
for (int i = 0; i < 10; i++) {
printf("arr[%d] = %d\n", i, arr[i]); // All zeros
}
// Free the allocated memory
free(arr);
arr = NULL;
return 0;
}realloc()
The realloc() function changes the size of previously allocated memory:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Initial allocation
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
printf("Initial allocation failed\n");
return 1;
}
// Initialize array
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
printf("Original array: ");
for (int i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Resize array to hold 10 elements
arr = realloc(arr, 10 * sizeof(int));
if (arr == NULL) {
printf("Reallocation failed\n");
return 1;
}
// Initialize new elements
for (int i = 5; i < 10; i++) {
arr[i] = (i + 1) * 2;
}
printf("Resized array: ");
for (int i = 0; i < 10; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Free memory
free(arr);
return 0;
}free()
The free() function deallocates previously allocated memory:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate memory
int *ptr = malloc(100 * sizeof(int));
if (ptr == NULL) {
printf("Allocation failed\n");
return 1;
}
// Use memory...
for (int i = 0; i < 100; i++) {
ptr[i] = i;
}
// Deallocate memory
free(ptr);
ptr = NULL; // Prevent dangling pointer
return 0;
}Memory Allocation Strategies
Single Element Allocation
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int id;
char name[50];
} Person;
int main() {
// Allocate single structure
Person *person = malloc(sizeof(Person));
if (person == NULL) {
printf("Allocation failed\n");
return 1;
}
// Initialize structure
person->id = 1;
strcpy(person->name, "John Doe");
printf("ID: %d, Name: %s\n", person->id, person->name);
// Free memory
free(person);
person = NULL;
return 0;
}Array Allocation
#include <stdio.h>
#include <stdlib.h>
int main() {
int size;
printf("Enter array size: ");
scanf("%d", &size);
// Allocate dynamic array
int *arr = malloc(size * sizeof(int));
if (arr == NULL) {
printf("Allocation failed\n");
return 1;
}
// Initialize array
for (int i = 0; i < size; i++) {
arr[i] = i * i;
}
// Use array
printf("Array elements: ");
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Free memory
free(arr);
arr = NULL;
return 0;
}2D Array Allocation
#include <stdio.h>
#include <stdlib.h>
int** create_2d_array(int rows, int cols) {
// Allocate array of row pointers
int **matrix = malloc(rows * sizeof(int*));
if (matrix == NULL) return NULL;
// Allocate each row
for (int i = 0; i < rows; i++) {
matrix[i] = malloc(cols * sizeof(int));
if (matrix[i] == NULL) {
// Clean up on failure
for (int j = 0; j < i; j++) {
free(matrix[j]);
}
free(matrix);
return NULL;
}
}
return matrix;
}
void free_2d_array(int **matrix, int rows) {
for (int i = 0; i < rows; i++) {
free(matrix[i]);
}
free(matrix);
}
int main() {
int rows = 3, cols = 4;
int **matrix = create_2d_array(rows, cols);
if (matrix == NULL) {
printf("Matrix allocation failed\n");
return 1;
}
// Initialize matrix
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
matrix[i][j] = i * cols + j;
}
}
// Print matrix
printf("Matrix:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
// Free memory
free_2d_array(matrix, rows);
return 0;
}Memory Management Best Practices
Error Checking
Always check if memory allocation was successful:
int *ptr = malloc(100 * sizeof(int));
if (ptr == NULL) {
// Handle allocation failure
fprintf(stderr, "Memory allocation failed\n");
exit(EXIT_FAILURE);
}Preventing Dangling Pointers
Set pointers to NULL after freeing:
free(ptr);
ptr = NULL; // Prevent dangling pointerAvoiding Memory Leaks
Ensure every malloc/calloc/realloc has a corresponding free:
// Good practice
int *arr = malloc(10 * sizeof(int));
if (arr != NULL) {
// Use array...
free(arr);
arr = NULL;
}Memory Reallocation Safety
Handle realloc failure properly:
int *temp = realloc(arr, new_size * sizeof(int));
if (temp == NULL) {
// realloc failed, original array still valid
// Handle error without losing original data
fprintf(stderr, "Reallocation failed\n");
// arr is still valid
} else {
// realloc succeeded
arr = temp;
}Common Memory Management Patterns
Dynamic String Handling
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* duplicate_string(const char *source) {
if (source == NULL) return NULL;
size_t len = strlen(source);
char *copy = malloc((len + 1) * sizeof(char));
if (copy == NULL) return NULL;
strcpy(copy, source);
return copy;
}
char* append_strings(const char *str1, const char *str2) {
if (str1 == NULL || str2 == NULL) return NULL;
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
char *result = malloc((len1 + len2 + 1) * sizeof(char));
if (result == NULL) return NULL;
strcpy(result, str1);
strcat(result, str2);
return result;
}
int main() {
char *original = "Hello, ";
char *append = "World!";
char *copy = duplicate_string(original);
char *combined = append_strings(original, append);
if (copy != NULL) {
printf("Copy: %s\n", copy);
free(copy);
}
if (combined != NULL) {
printf("Combined: %s\n", combined);
free(combined);
}
return 0;
}Dynamic Array with Resizing
#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *data;
size_t size;
size_t capacity;
} DynamicArray;
DynamicArray* create_array(size_t initial_capacity) {
DynamicArray *arr = malloc(sizeof(DynamicArray));
if (arr == NULL) return NULL;
arr->data = malloc(initial_capacity * sizeof(int));
if (arr->data == NULL) {
free(arr);
return NULL;
}
arr->size = 0;
arr->capacity = initial_capacity;
return arr;
}
void push_back(DynamicArray *arr, int value) {
if (arr->size >= arr->capacity) {
// Double the capacity
size_t new_capacity = arr->capacity * 2;
int *temp = realloc(arr->data, new_capacity * sizeof(int));
if (temp == NULL) return; // Handle allocation failure
arr->data = temp;
arr->capacity = new_capacity;
}
arr->data[arr->size] = value;
arr->size++;
}
void free_array(DynamicArray *arr) {
if (arr != NULL) {
free(arr->data);
free(arr);
}
}
int main() {
DynamicArray *arr = create_array(2);
if (arr == NULL) {
printf("Array creation failed\n");
return 1;
}
// Add elements (will trigger reallocation)
for (int i = 0; i < 10; i++) {
push_back(arr, i * i);
}
// Print elements
printf("Array elements: ");
for (size_t i = 0; i < arr->size; i++) {
printf("%d ", arr->data[i]);
}
printf("\n");
// Clean up
free_array(arr);
return 0;
}Memory Debugging Tools
Valgrind (Linux/Unix)
Valgrind is a powerful tool for detecting memory errors:
# Compile with debugging information
gcc -g -Wall program.c -o program
# Run with Valgrind
valgrind --leak-check=full ./programAddressSanitizer (GCC/Clang)
AddressSanitizer detects memory errors at runtime:
# Compile with AddressSanitizer
gcc -fsanitize=address -g program.c -o program
# Run program (errors will be reported automatically)
./programStatic Analysis Tools
Tools like cppcheck can detect potential memory issues:
# Static analysis
cppcheck --enable=all program.cCommon Memory Errors and Solutions
Memory Leaks
// Problem: Memory leak
void memory_leak() {
int *ptr = malloc(100 * sizeof(int));
// Missing free(ptr) - memory leak!
}
// Solution: Always free allocated memory
void no_memory_leak() {
int *ptr = malloc(100 * sizeof(int));
if (ptr != NULL) {
// Use memory...
free(ptr);
ptr = NULL;
}
}Dangling Pointers
// Problem: Dangling pointer
void dangling_pointer() {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
printf("%d\n", *ptr); // Undefined behavior!
}
// Solution: Set pointer to NULL after freeing
void no_dangling_pointer() {
int *ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
ptr = NULL;
// if (ptr != NULL) printf("%d\n", *ptr); // Safe check
}Buffer Overflows
// Problem: Buffer overflow
void buffer_overflow() {
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i < 10; i++) {
arr[i] = i; // Writing beyond allocated memory
}
free(arr);
}
// Solution: Always check bounds
void no_buffer_overflow() {
int *arr = malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) { // Correct bound
arr[i] = i;
}
free(arr);
}Advanced Memory Management (C11 and Later)
aligned_alloc()
The aligned_alloc() function allocates aligned memory (C11):
#include <stdio.h>
#include <stdlib.h>
int main() {
// Allocate 100 bytes aligned to 16-byte boundary
void *ptr = aligned_alloc(16, 100);
if (ptr == NULL) {
printf("Aligned allocation failed\n");
return 1;
}
printf("Allocated address: %p\n", ptr);
printf("Alignment check: %s\n",
((uintptr_t)ptr % 16 == 0) ? "OK" : "FAIL");
free(ptr);
return 0;
}Practical Examples
Memory Pool Implementation
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
void *memory;
size_t block_size;
size_t total_blocks;
size_t free_blocks;
char *free_list;
} MemoryPool;
MemoryPool* create_pool(size_t block_size, size_t num_blocks) {
MemoryPool *pool = malloc(sizeof(MemoryPool));
if (pool == NULL) return NULL;
pool->memory = malloc(block_size * num_blocks);
if (pool->memory == NULL) {
free(pool);
return NULL;
}
pool->block_size = block_size;
pool->total_blocks = num_blocks;
pool->free_blocks = num_blocks;
// Initialize free list
pool->free_list = malloc(num_blocks);
if (pool->free_list == NULL) {
free(pool->memory);
free(pool);
return NULL;
}
for (size_t i = 0; i < num_blocks; i++) {
pool->free_list[i] = 1; // 1 = free, 0 = allocated
}
return pool;
}
void* pool_alloc(MemoryPool *pool) {
if (pool->free_blocks == 0) return NULL;
for (size_t i = 0; i < pool->total_blocks; i++) {
if (pool->free_list[i]) {
pool->free_list[i] = 0;
pool->free_blocks--;
return (char*)pool->memory + (i * pool->block_size);
}
}
return NULL;
}
void pool_free(MemoryPool *pool, void *ptr) {
if (ptr == NULL || ptr < pool->memory ||
ptr >= (char*)pool->memory + (pool->total_blocks * pool->block_size)) {
return;
}
size_t index = ((char*)ptr - (char*)pool->memory) / pool->block_size;
if (index < pool->total_blocks && !pool->free_list[index]) {
pool->free_list[index] = 1;
pool->free_blocks++;
}
}
void destroy_pool(MemoryPool *pool) {
if (pool != NULL) {
free(pool->memory);
free(pool->free_list);
free(pool);
}
}
int main() {
// Create pool for 10 integers
MemoryPool *pool = create_pool(sizeof(int), 10);
if (pool == NULL) {
printf("Pool creation failed\n");
return 1;
}
// Allocate from pool
int *numbers[5];
for (int i = 0; i < 5; i++) {
numbers[i] = (int*)pool_alloc(pool);
if (numbers[i] != NULL) {
*numbers[i] = i * 10;
printf("Allocated %d at %p\n", *numbers[i], (void*)numbers[i]);
}
}
// Free some allocations
pool_free(pool, numbers[2]);
pool_free(pool, numbers[4]);
// Allocate again (should reuse freed blocks)
int *new_num = (int*)pool_alloc(pool);
if (new_num != NULL) {
*new_num = 999;
printf("Reallocated %d at %p\n", *new_num, (void*)new_num);
}
// Clean up
destroy_pool(pool);
return 0;
}Smart Pointer-like Implementation
#include <stdio.h>
#include <stdlib.h>
typedef struct {
void *ptr;
size_t ref_count;
void (*destructor)(void*);
} SmartPointer;
SmartPointer* create_smart_pointer(size_t size, void (*destructor)(void*)) {
SmartPointer *sp = malloc(sizeof(SmartPointer));
if (sp == NULL) return NULL;
sp->ptr = malloc(size);
if (sp->ptr == NULL) {
free(sp);
return NULL;
}
sp->ref_count = 1;
sp->destructor = destructor;
return sp;
}
SmartPointer* smart_pointer_retain(SmartPointer *sp) {
if (sp != NULL) {
sp->ref_count++;
}
return sp;
}
void smart_pointer_release(SmartPointer *sp) {
if (sp != NULL) {
sp->ref_count--;
if (sp->ref_count == 0) {
if (sp->destructor != NULL) {
sp->destructor(sp->ptr);
} else {
free(sp->ptr);
}
free(sp);
}
}
}
int main() {
// Create smart pointer for integer
SmartPointer *sp = create_smart_pointer(sizeof(int), NULL);
if (sp == NULL) {
printf("Smart pointer creation failed\n");
return 1;
}
// Use the smart pointer
*(int*)sp->ptr = 42;
printf("Value: %d\n", *(int*)sp->ptr);
// Create another reference
SmartPointer *sp2 = smart_pointer_retain(sp);
printf("Reference count: %zu\n", sp->ref_count);
// Release first reference
smart_pointer_release(sp);
printf("Reference count after release: %zu\n", sp2->ref_count);
// Release second reference (triggers deallocation)
smart_pointer_release(sp2);
return 0;
}Summary
Dynamic memory management is a powerful feature of C that provides flexibility for handling data structures whose size is unknown at compile time. Key points to remember:
- Memory Allocation Functions: malloc, calloc, realloc, and free for memory management
- Memory Layout: Understanding stack vs. heap allocation
- Error Handling: Always check for allocation failures
- Memory Safety: Prevent memory leaks, dangling pointers, and buffer overflows
- Best Practices: Set pointers to NULL after freeing, check bounds, and use debugging tools
- Advanced Features: aligned_alloc for aligned memory (C11+)
- Debugging Tools: Valgrind, AddressSanitizer, and static analysis tools
Proper dynamic memory management is essential for writing robust, efficient C programs and avoiding common memory-related bugs.