|$ curl https://forge-ai.dev/api/markdown?path=docs/c/memory-management
$cat docs/c-—-memory-management.md
updated Recently·45 min read·published

C — Memory Management

CMemoryStackHeapAdvancedAdvanced🎯Free Tools
Memory Model Overview

C gives programmers direct control over memory through pointers, manual allocation, and an intimate understanding of the process memory layout. Unlike managed languages, there is no garbage collector — every byte you allocate must be explicitly freed. This power comes with responsibility: buffer overflows, dangling pointers, and memory leaks are among the most common and dangerous bugs in C programs.

Understanding the memory model is not optional for serious C programming. It determines performance characteristics, security properties, and correctness guarantees of your code. This page covers every layer — from the operating system's process layout down to individual allocation strategies and debugging tools.

Process Memory Layout

When a C program is loaded into memory by the OS, the process address space is divided into distinct segments. Each segment serves a specific purpose and has different characteristics regarding mutability, lifetime, and access permissions.

memory_layout.c
C
1// Demonstration of memory layout segments
2#include <stdio.h>
3#include <stdlib.h>
4
5// Initialized global variable -> .data segment
6int initialized_global = 42;
7
8// Uninitialized global variable -> .bss segment
9int uninitialized_global;
10
11// Constant string literal -> .rodata (read-only data)
12const char *constant_string = "Hello, Memory Layout";
13
14// Function body -> .text segment (read-only machine code)
15void function_example(void) {
16 // Local variable -> stack
17 int local_variable = 10;
18 printf("local_variable is at: %p\n", (void *)&local_variable);
19}
20
21int main(void) {
22 // Local variable -> stack
23 int stack_var = 100;
24
25 // Dynamically allocated -> heap
26 int *heap_var = malloc(sizeof(int));
27 *heap_var = 200;
28
29 printf("initialized_global: %p (data segment)\n",
30 (void *)&initialized_global);
31 printf("uninitialized_global: %p (bss segment)\n",
32 (void *)&uninitialized_global);
33 printf("constant_string: %p (rodata)\n",
34 (void *)constant_string);
35 printf("function_example: %p (text segment)\n",
36 (void *)function_example);
37 printf("stack_var: %p (stack)\n",
38 (void *)&stack_var);
39 printf("heap_var: %p (heap)\n",
40 (void *)heap_var);
41
42 free(heap_var);
43 return 0;
44}
SegmentContainsWritableLifetime
.textCompiled machine codeNoProgram lifetime
.rodataString literals, constantsNoProgram lifetime
.dataInitialized globals and staticsYesProgram lifetime
.bssUninitialized globals and staticsYesProgram lifetime
Heapmalloc/calloc/realloc allocationsYesUntil free()
StackLocal variables, return addressesYesBlock scope

The visual layout of a process address space from low to high addresses:

memory-layout-diagram.txt
TEXT
1Process Memory Layout (typical Linux x86-64)
2+------------------------------- High Address (0x7FFF...) ---+
3| Stack |
4| +---------------------------------------------------+ |
5| | Function frames (local vars, args, ret) | |
6| | Grows DOWN (toward lower addresses) | |
7| +---------------------------------------------------+ |
8| | |
9| Stack Guard Page (unmapped) |
10| | |
11| +---------------------------------------------------+ |
12| | Memory-mapped regions | |
13| | (libraries, mmap'd files) | |
14| +---------------------------------------------------+ |
15| | |
16| +---------------------------------------------------+ |
17| | Heap | |
18| | +---------------------------------------------+ | |
19| | | malloc/calloc allocations | | |
20| | | Grows UP (toward higher addresses) | | |
21| | +---------------------------------------------+ | |
22| +---------------------------------------------------+ |
23| | |
24| +------------------------------+ |
25| | .bss (uninitialized data) | |
26| +------------------------------+ |
27| +------------------------------+ |
28| | .data (initialized data) | |
29| +------------------------------+ |
30| +------------------------------+ |
31| | .rodata (read-only data) | |
32| +------------------------------+ |
33| +------------------------------+ |
34| | .text (code segment) | |
35| +------------------------------+ |
36+------------------------------- Low Address (0x400000) ----+
The Stack

The stack is a region of memory that operates in a LIFO (Last In, First Out) manner. It is used for automatic storage — local variables, function parameters, and return addresses are all stored on the stack. Allocation is handled implicitly by adjusting the stack pointer, making it extremely fast (typically a single instruction).

Stack Frame Anatomy:

stack-frame-diagram.txt
TEXT
1Stack Frame for function call foo(42, 100):
2
3High Address
4+-----------------------------------+
5| Previous Frame's Stack Pointer | <- Frame Pointer (EBP/RBP)
6+-----------------------------------+
7| Return Address | <- 8 bytes on x86-64
8+-----------------------------------+
9| Saved Frame Pointer | <- Saved EBP/RBP from caller
10+-----------------------------------+
11| Parameter: 100 (arg 2) | <- Passed on stack (or registers)
12+-----------------------------------+
13| Parameter: 42 (arg 1) |
14+-----------------------------------+
15| Local variable: int x | <- Compiler may use registers first
16+-----------------------------------+
17| Local variable: char buf[64] |
18+-----------------------------------+
19| Saved registers (callee-saved) | <- RBX, R12-R15 if used
20+-----------------------------------+
21| (Padding for alignment) | <- Stack must be 16-byte aligned
22+-----------------------------------+
23Low Address <- Stack Pointer (ESP/RSP)
stack_frames.c
C
1#include <stdio.h>
2
3// Each function call creates a new stack frame
4void inner_function(int value) {
5 // This local variable lives in inner_function's stack frame
6 int local_copy = value * 2;
7 printf("inner_function: local_copy = %d, address = %p\n",
8 local_copy, (void *)&local_copy);
9 // When this function returns, local_copy is destroyed
10}
11
12void outer_function(int a, int b) {
13 // Parameters a and b are in outer_function's stack frame
14 int result = a + b;
15 printf("outer_function: result = %d, address = %p\n",
16 result, (void *)&result);
17
18 // Calling inner_function pushes a new frame onto the stack
19 inner_function(result);
20 printf("outer_function resumes after inner_function returns\n");
21}
22
23int main(void) {
24 printf("main: stack of main at %p\n", (void *)&main);
25 outer_function(10, 20);
26 return 0;
27}
📝

note

In x86-64 calling conventions (System V AMD64 ABI), the first six integer/pointer arguments are passed in registers (RDI, RSI, RDX, RCX, R8, R9), not on the stack. Only additional arguments and local variables that don't fit in registers go on the stack.

Stack Overflow: Recursion or deep call chains can exhaust the stack. The default stack size is typically 1-8 MB.

stack_overflow.c
C
1#include <stdio.h>
2
3// WARNING: This will cause a stack overflow!
4void infinite_recursion(int depth) {
5 // Each call allocates a new stack frame
6 // Eventually the stack pointer hits the guard page
7 char waste[1024]; // 1KB per call
8 printf("depth: %d, address: %p\n", depth, (void *)waste);
9 infinite_recursion(depth + 1);
10}
11
12// Safe: Tail-call optimized version (with -O2)
13int countdown_safe(int n) {
14 if (n <= 0) return 0;
15 printf("%d...\n", n);
16 // Compiler may reuse the current stack frame
17 return countdown_safe(n - 1); // tail call
18}
19
20int main(void) {
21 // infinite_recursion(0); // DO NOT RUN - will crash
22 countdown_safe(100000); // Safe with optimization
23 return 0;
24}

warning

Stack overflow causes undefined behavior — typically a segmentation fault. On Linux, you can check stack limits with ulimit -s (default is often 8192 KB). Use recursion judiciously and convert to iteration when call depth is unbounded.
The Heap

The heap is the region of memory used for dynamic allocation. Unlike the stack, heap allocations persist until explicitly freed, and they can be of any size. The heap grows upward (toward higher addresses) on most systems, while the stack grows downward — they meet in the middle.

malloc / calloc / realloc / free:

heap_basics.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5int main(void) {
6 // malloc: allocates uninitialized memory
7 // Contents are indeterminate (could be anything)
8 int *arr_malloc = malloc(5 * sizeof(int));
9 if (!arr_malloc) {
10 perror("malloc failed");
11 return 1;
12 }
13 printf("malloc uninitialized: %d %d %d\n",
14 arr_malloc[0], arr_malloc[1], arr_malloc[2]); // Garbage
15
16 // calloc: allocates zero-initialized memory
17 // All bits set to zero -- safer for most use cases
18 int *arr_calloc = calloc(5, sizeof(int));
19 if (!arr_calloc) {
20 perror("calloc failed");
21 free(arr_malloc);
22 return 1;
23 }
24 printf("calloc zero-initialized: %d %d %d\n",
25 arr_calloc[0], arr_calloc[1], arr_calloc[2]); // All 0
26
27 // realloc: resize a previous allocation
28 // May move the block if growth is not possible in place
29 arr_malloc = realloc(arr_malloc, 10 * sizeof(int));
30 if (!arr_malloc) {
31 perror("realloc failed");
32 free(arr_calloc);
33 return 1;
34 }
35 // Original contents preserved, new area is indeterminate
36 printf("realloc preserved: %d, new slots indeterminate\n",
37 arr_malloc[0]);
38
39 // free: release memory back to the heap
40 // After free(), the pointer is dangling -- set to NULL
41 free(arr_malloc);
42 arr_malloc = NULL;
43 free(arr_calloc);
44 arr_calloc = NULL;
45
46 printf("Memory freed successfully\n");
47 return 0;
48}

info

Always check the return value of malloc/calloc/realloc for NULL. Even on modern systems with overcommit, allocation can fail. Set pointers to NULL after freeing to avoid use-after-free bugs.
Memory Alignment

Memory alignment refers to the requirement that data types be stored at addresses that are multiples of their size. Most CPU architectures require aligned access for performance, and some (like ARM) will fault on misaligned access. Even on x86, which allows unaligned access, misaligned data causes performance penalties of 2-10x.

alignment.c
C
1#include <stdio.h>
2#include <stdalign.h>
3#include <stdint.h>
4
5int main(void) {
6 // Typical alignment requirements on 64-bit systems:
7 // char: 1 byte alignment
8 // short: 2 byte alignment
9 // int: 4 byte alignment
10 // long: 8 byte alignment
11 // pointer: 8 byte alignment
12
13 printf("sizeof(char) = %zu, alignof(char) = %zu\n",
14 sizeof(char), alignof(char));
15 printf("sizeof(short) = %zu, alignof(short) = %zu\n",
16 sizeof(short), alignof(short));
17 printf("sizeof(int) = %zu, alignof(int) = %zu\n",
18 sizeof(int), alignof(int));
19 printf("sizeof(long) = %zu, alignof(long) = %zu\n",
20 sizeof(long), alignof(long));
21 printf("sizeof(void*) = %zu, alignof(void*) = %zu\n",
22 sizeof(void *), alignof(void *));
23
24 // Struct padding due to alignment
25 struct Bad {
26 char a; // 1 byte + 3 padding
27 int b; // 4 bytes
28 char c; // 1 byte + 7 padding
29 };
30
31 struct Good {
32 int b; // 4 bytes
33 char a; // 1 byte
34 char c; // 1 byte + 2 padding
35 };
36
37 printf("sizeof(Bad) = %zu (wasted %zu bytes)\n",
38 sizeof(struct Bad),
39 sizeof(struct Bad) - sizeof(char) - sizeof(int) - sizeof(char));
40 printf("sizeof(Good) = %zu (wasted %zu bytes)\n",
41 sizeof(struct Good),
42 sizeof(struct Good) - sizeof(int) - sizeof(char) - sizeof(char));
43
44 // C11 alignas for explicit control
45 alignas(64) int cache_aligned; // Aligned to cache line boundary
46 printf("cache_aligned address: %p (%%64 == %zu)\n",
47 (void *)&cache_aligned,
48 (uintptr_t)&cache_aligned % 64);
49
50 return 0;
51}
TypeSize (64-bit)AlignmentPacked Layout
char1 byte1 byte0
short2 bytes2 bytes0
int4 bytes4 bytes0
long / pointer8 bytes8 bytes0
{ char; int; char; }12 bytes4 bytes6 wasted

best practice

Order struct members from largest to smallest to minimize padding waste. A struct with fields ordered as long, int, short, char typically uses less memory thanchar, int, short, long due to alignment padding rules.
Buffer Overflow

A buffer overflow occurs when a program writes data beyond the boundaries of a pre-allocated fixed-length buffer. This is one of the most exploited vulnerability classes in history — the Morris Worm (1988) used a buffer overflow in gets(). Modern compilers add mitigations (stack canaries, ASLR, NX bit), but the fundamental problem remains if programmers are careless.

buffer_overflow.c
C
1#include <stdio.h>
2#include <string.h>
3#include <stdlib.h>
4
5// VULNERABLE: no bounds checking
6void vulnerable_copy(const char *input) {
7 char buffer[64];
8 // strcpy does NOT check buffer size -- DANGEROUS
9 strcpy(buffer, input);
10 printf("Copied: %s\n", buffer);
11}
12
13// SAFE: use strncpy or snprintf with explicit bounds
14void safe_copy(const char *input) {
15 char buffer[64];
16 // Always ensure null termination after strncpy
17 strncpy(buffer, input, sizeof(buffer) - 1);
18 buffer[sizeof(buffer) - 1] = '\0';
19 printf("Copied: %s\n", buffer);
20}
21
22// BETTER: use snprintf for automatic bounds checking
23void better_copy(const char *input) {
24 char buffer[64];
25 snprintf(buffer, sizeof(buffer), "%s", input);
26 printf("Copied: %s\n", buffer);
27}
28
29// BEST: use a string library with dynamic sizing
30void best_copy(const char *input) {
31 size_t len = strlen(input);
32 char *buffer = malloc(len + 1);
33 if (!buffer) return;
34 memcpy(buffer, input, len + 1);
35 printf("Copied: %s\n", buffer);
36 free(buffer);
37}
38
39int main(void) {
40 char safe_input[] = "Hello, this is a normal string";
41 char long_input[] = "This string is definitely longer than "
42 "sixty-four bytes and will overflow "
43 "a fixed-size buffer if not handled properly";
44
45 vulnerable_copy(safe_input);
46 vulnerable_copy(long_input); // Buffer overflow!
47
48 safe_copy(safe_input);
49 safe_copy(long_input); // Truncated safely
50
51 better_copy(safe_input);
52 better_copy(long_input); // Truncated safely
53
54 return 0;
55}

warning

Never use gets() (removed in C11), strcpy(), strcat(), or sprintf() with unbounded input. Always use the bounded variants: fgets(), strncpy(), strncat(), snprintf(). Better yet, use safe_string libraries or dynamically sized buffers.
Use-After-Free & Double Free

Use-after-free (UAF) occurs when a program continues to use a pointer after the memory it points to has been freed. The freed memory may be reallocated for different purposes, leading to data corruption, crashes, or exploitable security vulnerabilities. Double free — freeing the same pointer twice — corrupts the heap metadata and can also be exploited.

use_after_free.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5// Use-after-free: the pointer is still used after free()
6void use_after_free_example(void) {
7 int *data = malloc(sizeof(int));
8 *data = 42;
9 printf("Before free: %d\n", *data);
10
11 free(data);
12 // data is now a DANGLING POINTER
13 // The memory may be reused by another allocation
14
15 // BUG: accessing freed memory -- undefined behavior!
16 // printf("After free: %d\n", *data);
17
18 // The safe pattern: set pointer to NULL after freeing
19 data = NULL;
20 // if (data) printf("Safe: %d\n", *data); // This check works
21}
22
23// Double free: freeing the same pointer twice
24void double_free_example(void) {
25 char *str = malloc(32);
26 strcpy(str, "Hello");
27
28 free(str);
29 // BUG: double free corrupts heap metadata
30 // free(str); // DO NOT DO THIS
31
32 // Safe pattern: set to NULL after free
33 str = NULL;
34}
35
36// Correct linked list node removal (no UAF)
37struct Node {
38 int value;
39 struct Node *next;
40};
41
42struct Node *remove_node(struct Node *head, int target) {
43 struct Node *curr = head;
44 struct Node *prev = NULL;
45
46 while (curr != NULL) {
47 if (curr->value == target) {
48 if (prev == NULL) {
49 head = curr->next;
50 } else {
51 prev->next = curr->next;
52 }
53 free(curr);
54 // curr is now dangling, but we return immediately
55 // and don't use it again
56 return head;
57 }
58 prev = curr;
59 curr = curr->next;
60 }
61 return head;
62}
63
64int main(void) {
65 use_after_free_example();
66 double_free_example();
67 printf("Examples completed (UB was avoided)\n");
68 return 0;
69}
🔥

pro tip

Compile with -fsanitize=address to catch use-after-free and double-free bugs at runtime. AddressSanitizer instruments every memory access and maintains shadow memory to detect invalid operations. It adds ~2x overhead but catches bugs that valgrind might miss.
Memory Leaks

A memory leak occurs when allocated memory is no longer reachable (no pointers reference it) but has not been freed. Over time, leaks consume increasing amounts of memory, eventually exhausting available resources. In long-running programs (servers, daemons), even small leaks can cause catastrophic failures.

memory_leaks.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5// LEAK: memory allocated but never freed
6void leak_example(void) {
7 char *buffer = malloc(1024);
8 strcpy(buffer, "This memory is leaked");
9 printf("%s\n", buffer);
10 // buffer goes out of scope -- pointer is lost
11 // Memory is still allocated but unreachable
12}
13
14// LEAK: losing pointer before freeing
15void losing_pointer(void) {
16 int *data = malloc(sizeof(int) * 100);
17 data = malloc(sizeof(int) * 200); // Lost the first allocation!
18 free(data);
19}
20
21// CORRECT: proper allocation and deallocation
22void no_leak(void) {
23 int *data = malloc(sizeof(int) * 100);
24 if (!data) return;
25
26 // ... use data ...
27
28 free(data);
29 data = NULL;
30}
31
32// CORRECT: error path cleanup
33int process_data(const char *filename) {
34 FILE *fp = NULL;
35 char *buffer = NULL;
36 int *results = NULL;
37 int status = -1;
38
39 fp = fopen(filename, "r");
40 if (!fp) goto cleanup;
41
42 buffer = malloc(4096);
43 if (!buffer) goto cleanup;
44
45 results = malloc(sizeof(int) * 1000);
46 if (!results) goto cleanup;
47
48 // ... processing ...
49 status = 0;
50
51cleanup:
52 free(results); // free(NULL) is safe (no-op)
53 free(buffer);
54 if (fp) fclose(fp);
55 return status;
56}
57
58int main(void) {
59 leak_example();
60 losing_pointer();
61 printf("Memory leaks occurred -- use valgrind to detect\n");
62 return 0;
63}
Leak TypeDescriptionSeverity
Lost pointerPointer overwritten without freeingHigh
Missing freeForgot to free at end of scopeMedium
Error path leakEarly return skips cleanup codeHigh
Growing data structureNodes added but never removed/freedHigh
Debugging Tools: Valgrind & ASan

Two essential tools for finding memory bugs are Valgrind (a dynamic analysis framework) and AddressSanitizer (a compiler-based sanitizer). They detect different classes of bugs and are complementary — use both in your development workflow.

Valgrind Memcheck: The most widely used tool for memory debugging. Runs your program in a virtual machine and instruments every memory access.

valgrind_usage.sh
Bash
1# Compile with debug symbols (no optimization)
2gcc -g -O0 -o my_program my_program.c
3
4# Run with Valgrind memcheck
5valgrind --leak-check=full \
6 --show-leak-kinds=all \
7 --track-origins=yes \
8 --verbose \
9 ./my_program
10
11# Common Valgrind output patterns:
12# ==12345== Invalid read of size 4 -> out-of-bounds or use-after-free
13# ==12345== Invalid write of size 4 -> buffer overflow
14# ==12345== Conditional jump depends on -> use of uninitialized value
15# ==12345== 48 bytes in 1 blocks are -> memory leak (definitely lost)
16# ==12345== Mismatched free() / delete -> heap corruption
17# ==12345== Heap summary: -> detailed heap statistics

AddressSanitizer (ASan): A fast compiler-based tool that detects out-of-bounds accesses, use-after-free, and stack/heap buffer overflows. Typically adds only 2x slowdown.

asan_usage.sh
Bash
1# Compile with AddressSanitizer
2gcc -g -fsanitize=address -fno-omit-frame-pointer \
3 -o my_program_asan my_program.c
4
5# Run -- ASan intercepts every memory operation
6./my_program_asan
7
8# ASan output example:
9# ==4567==ERROR: AddressSanitizer: heap-buffer-overflow
10# on address 0x60200000eff8 at pc 0x000000401234
11# WRITE of size 4 at 0x60200000eff8 thread T0
12# #0 0x401233 in overflow_func
13# #1 0x401299 in main
14
15# Also available: ThreadSanitizer (TSan), UndefinedBehaviorSanitizer (UBSan)
16gcc -g -fsanitize=address,undefined -o my_program_ubsan my_program.c

best practice

Use AddressSanitizer during development (fast, catches most bugs). Use Valgrind in CI for thorough analysis (slower but more detailed). Enable UndefinedBehaviorSanitizer alongside ASan to catch signed overflow, null dereference, and other UB.
Undefined Behavior & Memory

Many forms of undefined behavior (UB) in C relate to memory access. The compiler assumes UB never happens, so it may optimize aggressively, making UB bugs particularly hard to diagnose. A program that accesses memory out of bounds, uses a dangling pointer, or dereferences NULL may appear to "work" in debug builds but crash or produce wrong results in release builds.

undefined_behavior.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4int main(void) {
5 // 1. Out-of-bounds array access (UB)
6 int arr[5] = {10, 20, 30, 40, 50};
7 // int bad = arr[10]; // UB: reading past array bounds
8
9 // 2. Dangling pointer (UB)
10 int *ptr;
11 {
12 int local = 42;
13 ptr = &local;
14 }
15 // *ptr; // UB: local is out of scope, ptr is dangling
16
17 // 3. NULL pointer dereference (UB)
18 int *null_ptr = NULL;
19 // *null_ptr = 10; // UB: null pointer dereference
20
21 // 4. Signed integer overflow (UB)
22 int max = __INT_MAX__;
23 // int overflow = max + 1; // UB: signed overflow
24
25 // 5. Using freed memory (UB)
26 int *dynamic = malloc(sizeof(int));
27 *dynamic = 100;
28 free(dynamic);
29 // int val = *dynamic; // UB: use-after-free
30
31 printf("All UB examples are commented out for safety\n");
32 return 0;
33}
UB TypeConsequenceDetection
Buffer overflowData corruption, code executionASan, Valgrind
Use-after-freeCrash, exploitable vulnerabilityASan, Valgrind
Dangling pointerUnpredictable reads/writesASan, static analysis
NULL dereferenceSegfaultUBSan, Valgrind
Uninitialized readNon-deterministic behaviorValgrind, MSAN
Memory Debugging Strategies

Beyond Valgrind and ASan, several practical techniques help prevent and detect memory bugs at the source level. Guard pages, canary values, and red zones are techniques used by both programmers and runtime environments to catch buffer overflows.

debugging_strategies.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5// Stack canary: a value placed before the return address
6// If a buffer overflow overwrites the canary, the program
7// detects corruption before returning from the function
8
9// On GCC/Clang, this is automatic with -fstack-protector-strong
10// But you can implement a manual version for heap allocations:
11
12#define CANARY_VALUE 0xDEADBEEF
13
14typedef struct {
15 unsigned int canary_head;
16 size_t size;
17 void *data;
18 unsigned int canary_tail;
19} ProtectedBlock;
20
21ProtectedBlock *protected_alloc(size_t size) {
22 ProtectedBlock *block = malloc(sizeof(ProtectedBlock));
23 if (!block) return NULL;
24
25 block->canary_head = CANARY_VALUE;
26 block->canary_tail = CANARY_VALUE;
27 block->size = size;
28 block->data = malloc(size);
29 if (!block->data) {
30 free(block);
31 return NULL;
32 }
33 return block;
34}
35
36int protected_check(ProtectedBlock *block) {
37 if (block->canary_head != CANARY_VALUE) {
38 fprintf(stderr, "CORRUPTION: head canary mismatch!\n");
39 return -1;
40 }
41 if (block->canary_tail != CANARY_VALUE) {
42 fprintf(stderr, "CORRUPTION: tail canary mismatch!\n");
43 return -1;
44 }
45 return 0;
46}
47
48void protected_free(ProtectedBlock *block) {
49 if (!block) return;
50 if (protected_check(block) != 0) {
51 fprintf(stderr, "Freeing corrupted block anyway\n");
52 }
53 free(block->data);
54 free(block);
55}
56
57// Red zone: extra padding around allocations to detect
58// out-of-bounds writes that go slightly beyond the buffer
59#define RED_ZONE_SIZE 16
60
61void *redzone_alloc(size_t size) {
62 size_t total = RED_ZONE_SIZE + size + RED_ZONE_SIZE;
63 char *raw = malloc(total);
64 if (!raw) return NULL;
65
66 // Fill red zones with known pattern
67 memset(raw, 0xAA, RED_ZONE_SIZE); // Head red zone
68 memset(raw + RED_ZONE_SIZE + size, 0xAA, RED_ZONE_SIZE); // Tail red zone
69
70 return raw + RED_ZONE_SIZE; // Return pointer past head red zone
71}
72
73int redzone_check(void *ptr, size_t size) {
74 char *raw = (char *)ptr - RED_ZONE_SIZE;
75 for (size_t i = 0; i < RED_ZONE_SIZE; i++) {
76 if ((unsigned char)raw[i] != 0xAA) return -1;
77 }
78 for (size_t i = 0; i < RED_ZONE_SIZE; i++) {
79 if ((unsigned char)raw[RED_ZONE_SIZE + size + i] != 0xAA) return -1;
80 }
81 return 0;
82}
83
84int main(void) {
85 ProtectedBlock *block = protected_alloc(256);
86 if (block) {
87 printf("Block allocated, head canary: 0x%X\n",
88 block->canary_head);
89 protected_check(block);
90 protected_free(block);
91 }
92
93 void *rz = redzone_alloc(128);
94 if (rz) {
95 printf("Redzone allocation OK, check: %d\n",
96 redzone_check(rz, 128));
97 free((char *)rz - RED_ZONE_SIZE);
98 }
99
100 return 0;
101}

info

Compile with -fstack-protector-strong (GCC/Clang) to enable automatic stack canaries. This detects stack buffer overflows by placing a random value between local arrays and the saved return address. The program aborts if the canary is corrupted.
Stack vs Heap Comparison
PropertyStackHeap
Allocation speed~1 instruction (sub rsp)Slower (OS allocator + search)
Maximum size1-8 MB (system limit)Available RAM + swap
LifetimeScope (automatic)Until explicit free()
ManagementAutomatic (compiler)Manual (programmer)
FragmentationNone (LIFO order)Can fragment over time
Variable sizeFixed at compile time (VLAs aside)Resizable with realloc()
Thread safetyPer-thread (no sharing needed)Shared (requires synchronization)
stack_vs_heap.c
C
1#include <stdio.h>
2#include <stdlib.h>
3
4// Stack allocation: fast, automatic, limited
5void stack_heavy(int n) {
6 // VLA (Variable Length Array) -- stack allocated
7 // C99 feature, optional in C11, removed in C23
8 // WARNING: can cause stack overflow for large n
9 int local_arr[n];
10 for (int i = 0; i < n; i++) {
11 local_arr[i] = i;
12 }
13 printf("Stack array sum: %d\n", local_arr[n - 1]);
14}
15
16// Heap allocation: slower, manual, flexible
17void heap_heavy(int n) {
18 int *heap_arr = malloc(n * sizeof(int));
19 if (!heap_arr) {
20 perror("malloc failed");
21 return;
22 }
23 for (int i = 0; i < n; i++) {
24 heap_arr[i] = i;
25 }
26 printf("Heap array sum: %d\n", heap_arr[n - 1]);
27 free(heap_arr);
28}
29
30int main(void) {
31 // For small, fixed-size data: prefer stack
32 int small[100]; // Fast, automatic cleanup
33
34 // For large or dynamic-size data: use heap
35 int *large = malloc(1000000 * sizeof(int)); // Can be huge
36 if (large) {
37 heap_heavy(1000000);
38 free(large);
39 }
40
41 return 0;
42}
Memory-Mapped Files

The mmap() system call maps a file (or anonymous memory) into the process address space. This provides zero-copy file I/O and can be significantly faster than read()/write() for large files. Memory-mapped files appear as if they were loaded into memory, and the OS handles paging data in and out of disk transparently.

mmap_example.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4#include <sys/mman.h>
5#include <sys/stat.h>
6#include <fcntl.h>
7#include <unistd.h>
8
9// Memory-map a file for reading (POSIX)
10int mmap_file(const char *filename, void **addr, size_t *length) {
11 int fd = open(filename, O_RDONLY);
12 if (fd == -1) {
13 perror("open");
14 return -1;
15 }
16
17 struct stat st;
18 if (fstat(fd, &st) == -1) {
19 perror("fstat");
20 close(fd);
21 return -1;
22 }
23
24 *length = st.st_size;
25 *addr = mmap(NULL, *length, PROT_READ, MAP_PRIVATE, fd, 0);
26 close(fd);
27
28 if (*addr == MAP_FAILED) {
29 perror("mmap");
30 return -1;
31 }
32
33 return 0;
34}
35
36// Anonymous mmap: allocate memory without backing file
37// More flexible than malloc for large allocations
38void *mmap_alloc(size_t size) {
39 void *ptr = mmap(NULL, size,
40 PROT_READ | PROT_WRITE,
41 MAP_PRIVATE | MAP_ANONYMOUS,
42 -1, 0);
43 if (ptr == MAP_FAILED) return NULL;
44 return ptr;
45}
46
47void mmap_free(void *ptr, size_t size) {
48 munmap(ptr, size);
49}
50
51int main(int argc, char *argv[]) {
52 // Example: mmap a file
53 void *file_data;
54 size_t file_size;
55 if (mmap_file("/etc/hostname", &file_data, &file_size) == 0) {
56 printf("Mapped %zu bytes: %.*s\n",
57 file_size, (int)file_size, (char *)file_data);
58 munmap(file_data, file_size);
59 }
60
61 // Example: anonymous mmap allocation
62 size_t alloc_size = 1024 * 1024; // 1 MB
63 void *mem = mmap_alloc(alloc_size);
64 if (mem) {
65 memset(mem, 0, alloc_size);
66 printf("mmap allocated %zu bytes\n", alloc_size);
67 mmap_free(mem, alloc_size);
68 }
69
70 return 0;
71}
📝

note

mmap() is POSIX-specific. On Windows, the equivalent is CreateFileMapping() + MapViewOfFile(). For cross-platform code, consider using a compatibility library or conditional compilation. mmap is commonly used for shared memory (MAP_SHARED) between processes, loading shared libraries, and high-performance file I/O.
Memory Pool Allocator

A memory pool pre-allocates a large block of memory and sub-allocates from it, avoiding the overhead of individual malloc/free calls. Pools are ideal for scenarios where many objects of the same size are allocated and freed frequently (e.g., network packets, AST nodes, game objects). They eliminate fragmentation and provide O(1) allocation.

memory_pool.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5#define POOL_BLOCK_SIZE 4096
6
7typedef struct PoolBlock {
8 struct PoolBlock *next;
9 char data[POOL_BLOCK_SIZE];
10} PoolBlock;
11
12typedef struct {
13 PoolBlock *blocks;
14 size_t offset; // Current offset within top block
15 size_t total_allocated;
16 size_t total_used;
17} MemoryPool;
18
19MemoryPool *pool_create(void) {
20 MemoryPool *pool = malloc(sizeof(MemoryPool));
21 if (!pool) return NULL;
22 pool->blocks = NULL;
23 pool->offset = POOL_BLOCK_SIZE; // Force first allocation
24 pool->total_allocated = 0;
25 pool->total_used = 0;
26 return pool;
27}
28
29static PoolBlock *pool_new_block(MemoryPool *pool) {
30 PoolBlock *block = malloc(sizeof(PoolBlock));
31 if (!block) return NULL;
32 block->next = pool->blocks;
33 pool->blocks = block;
34 pool->offset = 0;
35 pool->total_allocated += POOL_BLOCK_SIZE;
36 return block;
37}
38
39void *pool_alloc(MemoryPool *pool, size_t size) {
40 // Align to 8-byte boundary
41 size = (size + 7) & ~(size_t)7;
42
43 // Does current block have enough space?
44 if (pool->offset + size > POOL_BLOCK_SIZE) {
45 if (size > POOL_BLOCK_SIZE) return NULL; // Too large
46 if (!pool_new_block(pool)) return NULL;
47 }
48
49 void *ptr = pool->blocks->data + pool->offset;
50 pool->offset += size;
51 pool->total_used += size;
52 return ptr;
53}
54
55void pool_destroy(MemoryPool *pool) {
56 if (!pool) return;
57 PoolBlock *block = pool->blocks;
58 while (block) {
59 PoolBlock *next = block->next;
60 free(block);
61 block = next;
62 }
63 free(pool);
64}
65
66// Example: allocate many small objects efficiently
67int main(void) {
68 MemoryPool *pool = pool_create();
69 if (!pool) {
70 perror("pool_create");
71 return 1;
72 }
73
74 // Allocate 10000 small structures
75 for (int i = 0; i < 10000; i++) {
76 int *obj = pool_alloc(pool, sizeof(int));
77 if (!obj) {
78 fprintf(stderr, "Pool allocation failed at i=%d\n", i);
79 break;
80 }
81 *obj = i;
82 }
83
84 printf("Pool stats: allocated=%zu bytes, used=%zu bytes\n",
85 pool->total_allocated, pool->total_used);
86 printf("Efficiency: %.1f%%\n",
87 100.0 * pool->total_used / pool->total_allocated);
88
89 // Single free releases everything -- no individual free needed
90 pool_destroy(pool);
91 return 0;
92}
🔥

pro tip

Arena allocators are a variant of pool allocators where you can't free individual objects — you free the entire arena at once. This pattern is used in compilers and game engines where objects share the same lifetime within a frame or compilation unit.
Best Practices

These practices will save you from the most common memory bugs in C. Follow them consistently and your code will be dramatically more reliable.

best_practices.c
C
1#include <stdio.h>
2#include <stdlib.h>
3#include <string.h>
4
5// Practice 1: Always initialize pointers to NULL
6void practice_init(void) {
7 int *ptr = NULL; // Safe: free(NULL) is a no-op
8 // ... conditional allocation ...
9 ptr = malloc(sizeof(int));
10 if (ptr) {
11 *ptr = 42;
12 }
13 free(ptr);
14 ptr = NULL; // Safe: prevents double-free
15}
16
17// Practice 2: Always check return values
18void practice_check(void) {
19 int *arr = calloc(1000, sizeof(int));
20 if (!arr) {
21 fprintf(stderr, "Out of memory\n");
22 return; // Don't continue with NULL pointer
23 }
24 // ... use arr ...
25 free(arr);
26}
27
28// Practice 3: Match allocators -- malloc/free, not malloc/delete
29// Practice 4: Use sizeof(*ptr) not sizeof(type) for portability
30void practice_sizeof(void) {
31 int *arr = malloc(100 * sizeof(*arr)); // Not sizeof(int)
32 if (!arr) return;
33 // ... use arr ...
34 free(arr);
35}
36
37// Practice 5: Write a cleanup macro for goto-based cleanup
38#define CLEANUP_GUARD char *_cleanup_ptr __attribute__((cleanup(_cleanup_free)))
39static void _cleanup_free(void *p) {
40 free(*(void **)p);
41}
42
43void practice_cleanup_pattern(void) {
44 CLEANUP_GUARD = malloc(1024);
45 CLEANUP_GUARD = malloc(2048);
46 // Both are automatically freed when function returns
47 // (even on early return)
48}
49
50// Practice 6: Never use the return value of realloc if the
51// original pointer might leak
52void practice_realloc(void) {
53 int *data = malloc(100 * sizeof(int));
54 if (!data) return;
55
56 int *tmp = realloc(data, 200 * sizeof(int));
57 if (!tmp) {
58 // data is still valid -- don't lose it!
59 fprintf(stderr, "realloc failed, original data preserved\n");
60 free(data);
61 return;
62 }
63 data = tmp; // Only update pointer on success
64 free(data);
65}
RuleWhy
Always initialize pointers to NULLPrevents use-after-free and double-free
Check every allocation returnmalloc/calloc can return NULL
Use sizeof(*ptr) instead of sizeof(type)Automatically matches pointer target type
Don't store realloc result directlyOriginal pointer leaks on failure
Use goto cleanup for error pathsAvoids resource leaks on early return
Run Valgrind/ASan regularlyCatch bugs before they reach production
$Blueprint — Engineering Documentation·Section ID: C-MEMORY-MANAGEMENT·Revision: 1.0