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.

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

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

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

  1. Always check for allocation failure:

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

    free(ptr);
    ptr = NULL; // Set to NULL to prevent use after free
  3. 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
  4. Use tools to detect memory issues:

    • Valgrind (Linux/macOS)
    • Address Sanitizer (Clang/GCC)
    • Dr. Memory (Windows)
  5. 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!