C Memory Management¶
Learning Outcomes
-
Learn characteristics of the C memory layout.
-
Differentiate between storage allocation and variable declaration.
-
Know where variables are stored in C, i.e., which memory segment data is allocated in.
-
Understand how the stack pointer automatically allocates and deallocates stack frames.
-
Understand when it is safe to pass pointers into the stack between functions.
-
Contrast the heap’s dynamic memory with stack memory.
-
freeevery block allocated withmalloc,realloc, orcalloc -
Practice reading Linux
manpages to determine specific behavior of Cstdlibfunctions.
Memory Layout¶
So far we have talked about how much space types occupy (sizeof), but not where things live in memory.
Memory Allocation¶
In C, storage can be allocated in three ways:
| Kind | Where you declare / allocate it | When | Notes |
|---|---|---|---|
| Local variable | Inside a function | Compile time | Visible only in that function’s namespace |
| Global variable | Outside any function | Compile time | Visible to the whole program; use sparingly |
| Dynamic allocation | Via library calls like malloc | Runtime | Size can depend on input (e.g. file length) |
-
Storage allocation means setting aside a block of memory.
-
Variable declaration means allocating a block and giving it a name (and a type, which implies size).
Declaration always implies allocation, but allocation does not always imply a named variable (e.g. malloc returns an address with no name attached).
Warning
Avoid overusing globals. They make accidental sharing and hard-to-track edits easy, and weaken abstraction between functions.
Four Regions of Memory¶
A C program’s address space typically has four segments:
| Segment | Contents | Management |
|---|---|---|
| Stack | Local variables, parameters, return addresses | Automatic; grows downward |
| Heap | Dynamically allocated storage (malloc / free) | On demand; grows upward |
| Data (Static) | Global variables | Fixed size for the whole run |
| Text (Code) | Program instructions | Fixed size; loaded when the program starts |
Rough layout from low to high addresses:
Name traps
-
The stack behaves like the stack data structure; the heap here is not a heap data structure—just a “heap of memory.”
-
The data segment means global/static data, not “all data.”
-
Text means code (often treated as read-only), like written text you don’t rewrite.
In C you must know which segment your data lives in, because each region is managed differently. Wrong assumptions about memory are a major source of C bugs. Later sections focus on the stack and the heap.
Stack and Heap¶
Stack¶
The stack starts at high addresses and grows downward. Each function call gets a new stack frame containing:
-
local variables
-
return address (where to resume after the function returns)
-
function arguments (when not kept in CPU registers)
The stack pointer (sp) tracks the top of the current frame. Allocation is fast: push a frame by moving sp down; pop it by moving sp back up when the function returns. This is LIFO.
Warning
When a function returns, the stack frame is popped, but the memory is not erased. Old values may still sit there until a later call overwrites them.
Passing Pointers into the Stack¶
It is safe to pass a pointer to a caller’s local variable into a callee while the caller is still active:
void load_buf(char *ptr, size_t len) { /* write into ptr */ }
int main() {
char buf[BUFLEN];
load_buf(buf, BUFLEN); /* OK: main's frame still exists */
}
It is catastrophic to return a pointer to a local variable:
char *make_buf(size_t len) {
char buf[len];
return buf; /* dangling pointer after return */
}
After make_buf returns, buf is deallocated. Any pointer to it becomes a dangling reference—later function calls may overwrite that stack memory.
Local arrays live on the stack, so their size must be known at compile time (or use a variable-length array where supported).
Heap¶
The heap is a large pool of memory for dynamic allocation at runtime (malloc, free, realloc). Unlike the stack:
-
sizes need not be known at compile time
-
blocks are not necessarily adjacent
-
the allocator decides where each block goes (fragmentation is possible)
stdlib Heap Functions¶
-
malloc(size_t n)— allocatenuninitialized bytes; returnsvoid *, orNULLon failure.uint32_t *ptr = malloc(20 * sizeof(uint32_t)); if (!ptr) { /* handle failure */ } -
free(void *ptr)— release memory previously returned bymalloc/realloc. Always pair everymallocwith afree.free(ptr); -
realloc(void *ptr, size_t size)— resize a block; may copy data to a new address. Always assign the return value:ptr = realloc(ptr, 40 * sizeof(uint32_t));
Common Heap Bugs¶
| Bug | What goes wrong |
|---|---|
| Memory leak | Forget to free; heap usage grows until the OS refuses more memory |
| Use after free | Read/write through a pointer after free |
| Double free | Call free twice on the same block |
Wrong free | free a stack pointer, or free(ptr + 1) instead of the original address |
Stale pointer after realloc | Other pointers to the old block are invalid if realloc moves the data |
Valgrind
Use tools like Valgrind to catch leaks, invalid free, and out-of-bounds writes. It is slow, but very helpful for debugging C memory bugs.
Debug a memory leak
1. Write a program with a leak
#include <stdlib.h>
int main(void) {
int *p = malloc(4 * sizeof(int));
p[0] = 42;
return 0; /* forgot free(p) */
}
2. Compile with debug symbols
gcc -g leak.c -o leak
3. Run under Valgrind
valgrind --leak-check=full ./leak
4. Read the report
Valgrind will point to the malloc line and report bytes as definitely lost because free was never called. Fix by adding free(p); before return 0;, then rerun until the summary says All heap blocks were freed:
$ valgrind --leak-check=full ./leak
==54905== Memcheck, a memory error detector
==54905== Copyright (C) 2002-2024, and GNU GPL'd, by Julian Seward et al.
==54905== Using Valgrind-3.25.1 and LibVEX; rerun with -h for copyright info
==54905== Command: ./leak
==54905==
==54905==
==54905== HEAP SUMMARY:
==54905== in use at exit: 16 bytes in 1 blocks
==54905== total heap usage: 1 allocs, 0 frees, 16 bytes allocated
==54905==
==54905== 16 bytes in 1 blocks are definitely lost in loss record 1 of 1
==54905== at 0x484F8A8: malloc (vg_replace_malloc.c:446)
==54905== by 0x400114A: main (in /home/virtualguard/vg101/vglab/csa/cs61c/scratch/leak)
==54905==
==54905== LEAK SUMMARY:
==54905== definitely lost: 16 bytes in 1 blocks
==54905== indirectly lost: 0 bytes in 0 blocks
==54905== possibly lost: 0 bytes in 0 blocks
==54905== still reachable: 0 bytes in 0 blocks
==54905== suppressed: 0 bytes in 0 blocks
==54905==
==54905== For lists of detected and suppressed errors, rerun with: -s
==54905== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
Implementing Heap Memory¶
A user-space heap allocator sits on top of memory the OS already mapped into your process. These OS notes give the bigger picture:
| Topic | Why it matters for malloc | OS notes |
|---|---|---|
| Load | How a program is brought into memory | 装入方式 |
| Heap in the address space | Where the heap segment lives | 进程的内存映像 |
| Dynamic partition allocation | OS-level strategies parallel heap block selection | 动态分区分配算法 |
| Context switch | Saving/restoring execution state when switching tasks | 进程控制块 |
| Virtual memory | OS may map more virtual pages on demand when the heap grows | 虚拟内存管理 |
Designing a Heap Allocator¶
The OS does not move your heap blocks around (unlike a compacting GC), but your allocator must:
-
satisfy
malloc/freerequests quickly -
keep bookkeeping overhead small
-
limit fragmentation—many small free gaps that cannot satisfy a large request
External fragmentation: free memory exists, but not in one contiguous block. Example: allocate 100 B (R1), then 1 B (R2); free R1. A later 50 B request (R3) may not fit in the 100 B hole if the allocator’s policy is poor.
K&R malloc / free¶
A classic teaching implementation:
Bookkeeping: each free block has a header with size and a pointer to the next free block, forming a circular linked list of free blocks.
-
malloc: walk the free list for a block large enough. If none fits, request more memory from the OS (e.g.sbrk). If the OS cannot satisfy the request, returnNULL. -
free: if adjacent blocks are also free, coalesce them into one larger block; otherwise insert the block into the free list.
Unlike stack allocation (roughly one instruction), malloc is a function call plus a list walk—worst case, it scans the whole list and still returns NULL.
Choosing a Free Block in malloc¶
When several free blocks are large enough, common policies are:
| Policy | Idea | Trade-off |
|---|---|---|
| Best fit | Pick the smallest block that still fits | Tight fit, but leaves tiny “slivers” |
| First fit | Take the first block that fits | Fast, but fragments the front of the list |
| Next fit | Like first fit, but resume from the last search position | Spreads small gaps more evenly |
These mirror OS 动态分区分配算法 (首次适应 / 最佳适应 / 邻近适应).
flowchart TD
A["malloc request"] --> B["search free list"]
B --> C{"block large enough?"}
C -->|yes| D["split / hand out block"]
C -->|no| E["ask OS for more pages"]
E --> F{"OS OK?"}
F -->|yes| D
F -->|no| G["return NULL"]
H["free(ptr)"] --> I{"neighbors free?"}
I -->|yes| J["coalesce"]
I -->|no| K["insert into free list"] 

