Skip to main content

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.

c
void* malloc(size_t size);

calloc()

Allocates space for an array of elements, initializes them to zero, and returns a pointer to the memory.

c
void* calloc(size_t num_elements, size_t element_size);

realloc()

Changes the size of a previously allocated memory block.

c
void* realloc(void* ptr, size_t new_size);

free()

Deallocates the memory previously allocated by malloc(), calloc(), or realloc().

c
void free(void* ptr);

Basic Memory Allocation

c
#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;
}

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.

c
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.

c
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.

c
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.

c
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

  1. Always check for allocation failure:

    c
    int* ptr = (int*) malloc(sizeof(int));
    if (ptr == NULL) {
    // Handle error
    return ERROR_CODE;
    }
  2. Always free allocated memory when done:

    c
    free(ptr);
    ptr = NULL; // Set to NULL to prevent use after free
  3. Avoid memory leaks in conditionals and loops:

    c
    int* ptr = (int*) malloc(sizeof(int));
    if (condition) {
    free(ptr); // Free before return
    return;
    }
    // Use ptr...
    free(ptr); // Free at end
  4. Use tools to detect memory issues:

    • Valgrind (Linux/macOS)
    • Address Sanitizer (Clang/GCC)
    • Dr. Memory (Windows)
  5. Consider encapsulating memory management:

    c
    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:

c
#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:

c
#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! :)