Heap and Dynamic Memory Management
Stack memory is automatic but short-lived and limited; static memory lasts forever but its size is fixed when you compile.
Learning Objectives
Use malloc, calloc, realloc and free correctly, with failure checks and overflow-safe size calculations.
Allocate dynamic 1-D and 2-D arrays and resize buffers safely.
Define ownership and pair every allocation with exactly one deallocation.
Recognise use-after-free, double free, leaks, dangling and lost pointers, uninitialised and out-of-bounds use, and zero-size allocation caveats.
Explain internal and external fragmentation and allocator designs: free lists, bins, arenas, pools and slabs.
Compare RAII and smart pointers, reference counting, tracing garbage collection and Rust ownership.
Choose between stack, static, pool and heap storage — including deterministic strategies for embedded systems — and monitor heap usage.
Plain-Language Explanation
Stack memory is automatic but short-lived and limited; static memory lasts forever but its size is fixed when you compile. The heap is the third option: you ask for any amount of memory at run time, keep it as long as you like, and give it back when you are done. The price is responsibility. Someone must eventually return every block — exactly once — and nobody may use a block after it was returned. Languages differ mainly in *who* carries that responsibility: you (C), objects that clean up after themselves (C++), a garbage collector (Java, C#, Python, JavaScript), or the compiler (Rust).
Precise Technical Explanation
Why dynamic allocation is needed The size is known only at run time (a file's contents, user input).
The object must outlive the function that creates it (a node added to a list).
The object is too large for the stack.
The number of objects varies (a growing collection).
Static/automatic versus dynamic storage Allocated (heap) compile time (VLAs aside) zero at run time adjust stack pointer allocator call (tens–hundreds of ns) link error if too big NULL / exception
malloc, calloc, realloc and free in C Allocation failure and null checks
Every allocation can fail. Check for NULL and handle it: report, clean up and return an error. (On Linux with overcommit, large mallocs may succeed and fail later when touched — Module 12 — but small embedded systems and calloc/realloc failures are real.) Allocation size and sizeof
Write p = malloc(n * sizeof *p); — sizeof *p stays correct if the type of p changes. Do not cast the result in C (it hides a missing #include <stdlib.h> in old compilers); C++ requires the cast but should use new/containers anyway.
Arrays allocated dynamically int *a = malloc(n * sizeof *a); creates an array of n ints indexed a[0] … a[n-1]. The pointer does not remember n; you must keep the count yourself.
Dynamic two-dimensional arrays Three common layouts:
One contiguous block, manual indexing — a[r * cols + c]. One allocation, best locality.
Pointer to variable-length array type — double (*m)[cols] = malloc(rows * sizeof *m); then m[r][c] (C99 VLA types; optional in C11+, supported by GCC/Clang).
Array of row pointers — rows + 1 allocations, rows can have different lengths, worse locality, more cleanup code.
Resizing buffers safely realloc may move the block. Always assign the result to a temporary, because on failure it returns NULL *and the old block is still allocated*:
Grow geometrically (×1.5 or ×2) so that *n* appends cost O(n) total copying.
Ownership and responsibility for freeing The owner of a block is the code responsible for freeing it. Document ownership in function names and comments: char *strdup(const char *) returns an *owned* block the caller must free; const char *getenv(const char *) returns a *borrowed* pointer the caller must not free.
Allocation/deallocation pairing malloc/calloc/realloc ↔ free; C++ new ↔ delete; new[] ↔ delete[]; fopen ↔ fclose; a library's x_create ↔ x_destroy. Mixing pairs is undefined behavior.
Accessing a block after free. The memory may already belong to a new allocation, so reads return someone else's data and writes corrupt it. Detected by AddressSanitizer (Module 9).
Freeing the same block twice corrupts the allocator's bookkeeping. glibc detects many cases and aborts ("free(): double free detected in tcache 2").
A block that is never freed although nothing refers to it any more. Short programs leak harmlessly (the OS reclaims everything at exit); long-running servers and firmware run out of memory. LeakSanitizer reports leaks at exit (below).
Any pointer to a freed block is dangling — including copies (aliases) in other variables and data structures. Setting *one* pointer to NULL after free does not fix the others.
Overwriting the only pointer to a block (p = malloc(10); p = malloc(20);) loses the first block forever — a leak.
Uninitialized allocated memory malloc and realloc's new part contain indeterminate bytes, often old data. Use calloc or initialise explicitly.
Buffer overflows and out-of-bounds access Writing past the end of a block corrupts neighbouring blocks or the allocator's metadata; reading past it leaks data. C does no bounds checking.
Integer overflow in allocation-size calculations malloc(count * size) with a large count can wrap around to a small number; the allocation succeeds and later writes overflow it. Check before multiplying: if (count > SIZE_MAX / size) fail; — or use calloc, reallocarray (glibc 2.26+, BSD) or __builtin_mul_overflow.
Zero-size allocation caveats malloc(0) returns either NULL or a unique pointer that must not be dereferenced — implementation-defined. glibc returns a unique pointer. realloc(p, 0) is implementation-defined in C17 and undefined in C23. Avoid zero-size requests or handle them explicitly.
Fragmentation: internal and external Internal fragmentation — wasted space *inside* blocks: a request for 20 bytes rounded up to a 32-byte chunk.
External fragmentation — free space split into pieces too small to satisfy a request, although the total free space would be enough.
Allocator concepts: free lists, bins, arenas and pools Free list — a linked list of free blocks, threaded through the free blocks themselves.
Bins — separate free lists for size classes, so a request finds a fitting block fast. glibc has tcache (per-thread caches), fastbins, small bins, large bins and an unsorted bin.
Arenas — independent heaps, often one per thread or CPU, to reduce lock contention.
Pools — pre-allocated regions for one size or one purpose.
Placement policies: first fit, best fit, next fit; neighbouring free blocks are coalesced.
Allocation overhead and alignment Each block carries a header (glibc: 8 bytes of size and flags, in a 16-byte granularity), and every block is aligned for any type (alignof(max_align_t), 16 on x86-64). malloc(1) therefore uses 32 bytes of heap on glibc. aligned_alloc provides larger alignment (cache lines, pages, SIMD).
Custom allocators and memory pools When objects of one size are allocated and freed very often, a pool (fixed-size block allocator) gives O(1) allocation and free, no fragmentation, and deterministic timing. An arena (bump/region allocator) allocates by moving a pointer and frees everything at once — ideal for per-request or per-frame data.
Object pools and slab allocation An object pool reuses fully constructed objects (connections, particles). The slab allocator (Bonwick, used by the Linux and Solaris kernels) keeps caches of pre-initialised objects of one type in page-sized slabs.
RAII and smart pointers in C++ RAII (Resource Acquisition Is Initialization) ties a resource to an object's lifetime: the constructor acquires, the destructor releases, and destructors run automatically on scope exit and during exception unwinding. Smart pointers apply RAII to heap memory:
exactly one owner; movable, not copyable same as a raw pointer shared; reference-counted control block + atomic counts non-owning observer of a shared_ptr Garbage collection overview
A garbage collector (GC) frees objects automatically once the program can no longer reach them. Programs still decide *when objects become unreachable*; the GC decides *when to reclaim them*.
Reference counting and cycle issues Each object counts the references to it and is freed when the count hits zero (CPython, Swift ARC, shared_ptr, COM). It is prompt and simple, but every pointer copy costs a count update (atomic in multithreaded code), and cycles (A → B → A) never reach zero. Fixes: weak references, or a backup cycle collector (CPython has one).
Tracing collectors, reachability and pauses A tracing collector starts from roots (globals, stack slots, registers), marks everything reachable, and reclaims the rest. Cycles are no problem. The cost is CPU time and pauses; modern collectors are generational, incremental and concurrent to keep pauses short (Modules 15 and 19).
Ownership/borrowing concepts (Rust overview) Rust gives every value exactly one owner; when the owner goes out of scope the value is dropped. References *borrow* without owning, and the compiler's borrow checker proves at compile time that no reference outlives its value and that mutable access is exclusive. Result: no GC, no use-after-free or double free in safe code. Shown for comparison (not run — no Rust toolchain on the build machine):
Choosing stack, static, pool or heap storage Small, short-lived, size known automatic (stack)
Exists for the whole program, size known Many same-size objects, frequent churn, real-time
Variable size or lifetime, general purpose Many objects freed together
Allocation strategies for embedded systems Safety standards (MISRA C Rule 21.3, many automotive and avionics guidelines) restrict or forbid malloc after initialisation, because of fragmentation, non-deterministic timing and failure modes that are hard to test. Firmware typically allocates everything statically or from pools at start-up.
Deterministic allocation and avoiding runtime allocation Allocate once during initialisation ("allocate at boot, never free"), use fixed-size pools with O(1) operations, and size everything from worst-case analysis. Real-time loops then never call a general-purpose allocator.
Heap telemetry and high-water marks Track current and peak (high-water mark) heap use, allocation counts and failures. glibc provides mallinfo2() and malloc_stats(); RTOS heaps provide xPortGetMinimumEverFreeHeapSize() (FreeRTOS) and similar. Peak use, not average, decides whether a device runs out of memory.
void *malloc(size_t size); /* uninitialised block, or NULL */
void *calloc(size_t count, size_t size); /* zeroed block of count*size bytes, overflow-checked, or NULL */
void *realloc(void *ptr, size_t size); /* resize: may move the block; NULL on failure (old block intact) */
void free(void *ptr); /* release a block from these functions; free(NULL) does nothing */int *temporary = realloc(values, new_count * sizeof *values);
if (temporary != NULL) {
values = temporary;
} else {
/* values remains valid and must still be freed */
}fn main() {
let names = vec![String::from("ada"), String::from("linus")]; // heap buffer, owned by `names`
let moved = names; // ownership moves; `names` can no longer be used
// println!("{:?}", names); // compile error: borrow of moved value
let first: &String = &moved[0]; // borrow: no ownership taken
println!("{first} of {}", moved.len());
} // `moved` dropped here: buffer freed exactly onceWhy It Matters in Real Systems
Memory-safety bugs — mostly heap use-after-free and out-of-bounds writes — account for roughly 70% of serious security vulnerabilities reported in large C/C++ code bases such as Chromium and Windows. Leaks crash long-running services; fragmentation crashes firmware after weeks of uptime.
Key Terms
Memory managed by an allocator for allocated-duration objects
The code responsible for freeing a block Unreachable memory that was never freed Use-after-free / double free
Using / freeing a block after it was freed Internal / external fragmentation
Waste inside blocks / free space split into unusable pieces Free list, bin, arena Allocator data structures Fixed-size block allocators
Resource lifetime bound to an object's lifetime Freeing objects when their count of references reaches zero
Freeing objects not reachable from roots Peak memory use observed
Memory Layout and Execution Flow
This is a simplified view of glibc; other allocators (jemalloc, tcmalloc, mimalloc, Windows' segment heap) differ in detail but share the ideas.
flowchart LR
A[malloc request] --> B{tcache / bin has a fitting free chunk?}
B -- yes --> C[return chunk]
B -- no --> D{top chunk large enough?}
D -- yes --> E[split top chunk]
D -- no --> F[grow heap with brk or mmap]
F --> E --> CSyntax
T *p = malloc(n * sizeof *p); /* check n * sizeof *p cannot overflow first */
T *z = calloc(n, sizeof *z); /* zeroed, overflow-checked */
T *tmp = realloc(p, m * sizeof *p); /* keep p until tmp is known to be non-NULL */
free(p); /* exactly once, then stop using p and its aliases */auto u = std::make_unique<Sensor>(id); // unique ownership
auto s = std::make_shared<Config>(); // shared ownership
std::weak_ptr<Config> w = s; // observer
std::vector<int> v(n); // the usual answer: a container owns the bufferComplete Runnable Example
The specification's example plus safe resizing with an overflow check. GCC 11.2, x86-64 Linux.
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
/* returns 0 on success; on failure *values is unchanged and still owned by the caller */
static int grow(int **values, size_t *capacity)
{
size_t new_capacity = *capacity ? *capacity * 2 : 4;
if (new_capacity > SIZE_MAX / sizeof **values) {
return -1; /* the byte count would overflow */
}
int *temporary = realloc(*values, new_capacity * sizeof **values);
if (temporary == NULL) {
return -1;
}
if (temporary != *values) {
printf(" realloc moved the block (capacity %zu -> %zu)\n", *capacity, new_capacity);
} else {
printf(" realloc grew the block in place (capacity %zu -> %zu)\n", *capacity, new_capacity);
}
*values = temporary;
*capacity = new_capacity;
return 0;
}
int main(void)
{
size_t count = 5;
int *values = malloc(count * sizeof *values);
if (values == NULL) {
perror("malloc");
return 1;
}
for (size_t i = 0; i < count; ++i) {
values[i] = (int)(i * 10);
}
for (size_t i = 0; i < count; ++i) {
printf("%d\n", values[i]);
}
free(values);
…Line-by-Line Explanation
grow doubles the capacity, checks new_capacity * sizeof for overflow *before* multiplying, and only overwrites the caller's pointer when realloc succeeded.
Whether realloc moves the block depends on what lies after it in the heap; both outcomes are correct, and code must never keep pointers into the old block.
values = NULL after free prevents accidental reuse of that variable only.
The calloc call asks for 2⁶³ elements of 4 bytes; the product overflows 64 bits, and glibc's calloc detects that and returns NULL instead of a tiny block.
Expected Output
(The moved/in-place lines are what glibc did in this run; another allocator may differ.)
0
10
20
30
40
growing a dynamic array to 40 elements:
realloc moved the block (capacity 0 -> 4)
realloc moved the block (capacity 4 -> 8)
realloc grew the block in place (capacity 8 -> 16)
realloc grew the block in place (capacity 16 -> 32)
realloc grew the block in place (capacity 32 -> 64)
size=40 capacity=64 last=40
calloc(SIZE_MAX/2, 4) returned NULL (overflow detected)Step-by-Step Execution Trace
malloc(20) returns a 20-byte (32-byte chunk) block; the loops write and print five values.
free returns the chunk to the per-thread cache (tcache).
The growth loop calls realloc(NULL, 16) (like malloc), then 32, 64, 128, 256 bytes as the array fills: 4 → 8 → 16 → 32 → 64 elements.
After 40 appends the size is 40 and the capacity 64 — at most 2× wasted space, O(1) amortised append.
The overflow check inside calloc refuses the impossible request.
Interactive Visualization
The heap allocator simulator shows allocated and free blocks with IDs and sizes. Allocate, resize and free; switch between first-fit, best-fit and next-fit; and watch fragmentation, coalescing, allocation failure and leaks. It is a simplified teaching model, not glibc.
Common Mistakes and Failure Modes
A real leak, as reported by LeakSanitizer (part of AddressSanitizer; -fsanitize=address):
Output (one real run):
LeakSanitizer reports 36 bytes in 3 objects — three 12-byte copies of "sensor-node" plus terminator — and the allocation stack: allocated in copy_name, called from main. The fix is free(n) at the end of each iteration.
p = realloc(p, bigger); /* on failure p becomes NULL and the old block leaks */
int *a = malloc(n); /* n bytes, not n ints */
char *s = malloc(strlen(t)); /* no room for the '\0' terminator */
free(p); free(p); /* double free */
free(p); p->next = 0; /* use after free */
p = malloc(10); p = malloc(20);/* first block lost */
int *x = malloc(n * size); /* n * size may overflow */
delete p_array; /* C++: memory from new[] needs delete[] */#include <stdlib.h>
#include <string.h>
char *copy_name(const char *name)
{
char *copy = malloc(strlen(name) + 1); /* caller owns the result */
if (copy != NULL) {
strcpy(copy, name);
}
return copy;
}
int main(void)
{
for (int i = 0; i < 3; ++i) {
char *n = copy_name("sensor-node");
(void)n; /* bug: never freed */
}
return 0;
}=================================================================
==63==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 36 byte(s) in 3 object(s) allocated from:
#0 0x759a8f8b4867 in __interceptor_malloc ../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:145
#1 0x5c222322a230 in copy_name m06_leak.c:6
#2 0x5c222322a278 in main m06_leak.c:16
#3 0x759a8f429d8f (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f)
SUMMARY: AddressSanitizer: 36 byte(s) leaked in 3 allocation(s).
[exit status 1]Corrected / Safe Pattern
A small fixed-size memory pool: O(1) allocate and free, no fragmentation, no system calls, deterministic — the kind of allocator used in firmware and real-time loops.
C++ smart pointers, including a reference cycle and its weak_ptr fix (G++ 11.2, C++20):
#include <stdalign.h>
#include <stddef.h>
#include <stdio.h>
#define BLOCK_SIZE 32
#define BLOCK_COUNT 4
typedef union Block {
union Block *next; /* used while the block is free */
alignas(max_align_t) unsigned char payload[BLOCK_SIZE];
} Block;
static Block storage[BLOCK_COUNT]; /* static: no heap at all */
static Block *free_list;
static size_t in_use, high_water;
static void pool_init(void)
{
for (size_t i = 0; i < BLOCK_COUNT - 1; ++i) {
storage[i].next = &storage[i + 1];
}
storage[BLOCK_COUNT - 1].next = NULL;
free_list = &storage[0];
}
static void *pool_alloc(void)
{
Block *b = free_list;
if (b == NULL) {
return NULL; /* pool exhausted: caller must handle it */
}
free_list = b->next;
if (++in_use > high_water) {
high_water = in_use;
}
return b->payload;
}
static void pool_free(void *p)
{
if (p == NULL) {
return;
}
Block *b = (Block *)p; /* payload is the block's first member */
b->next = free_list;
free_list = b;
--in_use;
}
static long index_of(const void *p) { return p ? (long)((const Blo
…allocated blocks 0 1 2
after freeing block 1, the next allocation reuses block 1
e = block 3, f = NULL (pool exhausted)
in use now 0, high-water mark 4 of 4#include <iostream>
#include <memory>
#include <string>
struct Node {
std::string name;
std::shared_ptr<Node> next; // owning link
std::weak_ptr<Node> back; // non-owning link
explicit Node(std::string n) : name(std::move(n)) { std::cout << " create " << name << '\n'; }
~Node() { std::cout << " destroy " << name << '\n'; }
};
int main()
{
std::cout << "unique_ptr:\n";
{
auto u = std::make_unique<Node>("U");
auto u2 = std::move(u); // ownership transferred
std::cout << " u is " << (u ? "set" : "empty") << " after the move\n";
} // u2 destroyed here
std::cout << "shared_ptr cycle (leaks):\n";
{
auto a = std::make_shared<Node>("A");
auto b = std::make_shared<Node>("B");
a->next = b;
b->next = a; // A -> B -> A
std::cout << " use_count A=" << a.use_count() << " B=" << b.use_count() << '\n';
} // counts drop to 1, never 0: no destructors
std::cout << " (no destroy lines: A and B leaked)\n";
std::cout << "weak_ptr back-link (n
…unique_ptr:
create U
u is empty after the move
destroy U
shared_ptr cycle (leaks):
create A
create B
use_count A=2 B=2
(no destroy lines: A and B leaked)
weak_ptr back-link (no leak):
create C
create D
use_count C=1 D=2
D's back-link reaches C
destroy C
destroy DDebugging and Diagnostic Techniques
AddressSanitizer/LeakSanitizer (-fsanitize=address), Valgrind Memcheck, Dr. Memory (Windows), Visual Studio's CRT debug heap and heap snapshots.
glibc: MALLOC_CHECK_=3, mallinfo2(), malloc_stats(); mtrace() for allocation logs.
Heap profilers: heaptrack, Massif (Valgrind), jemalloc/tcmalloc profiling, Windows Performance Recorder.
Heap telemetry with glibc's mallinfo2 (glibc 2.33+):
Output (one real run):
After freeing every other block, half the memory is free *inside* the heap but split into 50 separate holes between live blocks — external fragmentation in miniature. At the end about 11 KiB still counts as "in use": glibc parks recently freed chunks in its per-thread cache (tcache), which mallinfo2 reports as allocated, and stdout's buffer is a live heap block. malloc_usable_size shows internal fragmentation: a 1-byte request gets 24 usable bytes, a 25-byte request 40.
#include <malloc.h>
#include <stdio.h>
#include <stdlib.h>
static size_t peak;
static void report(const char *when)
{
struct mallinfo2 mi = mallinfo2();
if (mi.uordblks > peak) {
peak = mi.uordblks;
}
printf("%-26s in use %7zu bytes free in heap %7zu bytes peak %7zu\n",
when, mi.uordblks, mi.fordblks, peak);
}
int main(void)
{
report("start");
void *blocks[100];
for (int i = 0; i < 100; ++i) {
blocks[i] = malloc(1000);
}
report("after 100 x malloc(1000)");
for (int i = 0; i < 100; i += 2) {
free(blocks[i]);
}
report("after freeing every other");
for (int i = 1; i < 100; i += 2) {
free(blocks[i]);
}
report("after freeing the rest");
printf("malloc_usable_size: 1 -> %zu, 24 -> %zu, 25 -> %zu, 1000 -> %zu\n",
malloc_usable_size(blocks[0] = malloc(1)), malloc_usable_size(blocks[1] = malloc(24)),
malloc_usable_size(blocks[2] = malloc(25)), malloc_usable_size(blocks[3] = malloc(1000)));
for (int i = 0; i < 4; ++i) {
free(blocks[i]);
}
return 0;
}start in use 0 bytes free in heap 0 bytes peak 0
after 100 x malloc(1000) in use 105568 bytes free in heap 29600 bytes peak 105568
after freeing every other in use 62224 bytes free in heap 72944 bytes peak 105568
after freeing the rest in use 11824 bytes free in heap 123344 bytes peak 105568
malloc_usable_size: 1 -> 24, 24 -> 24, 25 -> 40, 1000 -> 1000Performance Implications
General-purpose malloc costs tens to hundreds of nanoseconds and may take locks; frequent small allocations dominate many profiles. Remedies: allocate less (reuse buffers, reserve capacity), allocate in bulk (arenas), use pools for hot fixed-size objects, choose a scalable allocator (jemalloc, mimalloc, tcmalloc) for many-threaded servers. Contiguous containers beat node-based ones for cache locality (Module 13).
Security and Safety Implications
Heap overflows, use-after-free and double free are the most exploited memory bugs. Defences: bounds-checked APIs, ownership discipline, smart pointers, sanitizers in testing, hardened allocators (glibc tcache checks, Scudo on Android, Windows segment heap), memory tagging hardware (Arm MTE, Module 19), and memory-safe languages for new code.
Platform and Language Caveats
malloc(0), chunk sizes, the mmap threshold and realloc movement are glibc behavior; musl, Windows UCRT, macOS and embedded C libraries (newlib, picolibc) differ.
mallinfo2, malloc_usable_size and reallocarray are glibc extensions; Windows has _msize and HeapWalk.
C++ new throws std::bad_alloc on failure instead of returning null (unless new (std::nothrow)).
Java, C#, Go, JavaScript and Python allocate almost everything on a GC heap, but allocation is usually a fast pointer bump in a thread-local buffer.
Rust's ownership rules are checked at compile time; unsafe code can still violate them.
Real-World Application
Game engines use per-frame arenas and object pools; databases use region allocators per query; the Linux kernel uses slab caches; network stacks use buffer pools; automotive ECUs allocate everything statically at boot.
Best Practices Checklist
[ ] Check every allocation result.
[ ] Use sizeof *p and overflow-checked size arithmetic.
[ ] Assign realloc to a temporary.
[ ] Document ownership; free exactly once with the matching function.
[ ] Prefer containers and smart pointers in C++; unique_ptr by default.
[ ] Break shared_ptr cycles with weak_ptr.
[ ] Run tests under AddressSanitizer/LeakSanitizer.
[ ] In real-time and safety code, allocate at start-up and use pools afterwards.
[ ] Track the heap high-water mark.
Exercises
E1 · Beginner. Fix: char *s = malloc(strlen(t)); strcpy(s, t);
Answer: char *s = malloc(strlen(t) + 1); if (s == NULL) return NULL; memcpy(s, t, strlen(t) + 1); — one byte for the terminator, and a NULL check.
E2 · Intermediate. Why does realloc growth by a constant (+16 elements) make building an *n*-element array O(n²), while doubling makes it O(n)?
Answer: With constant growth, about n/16 reallocations each copy up to n elements: ~n²/32 copies. With doubling, copies total 1 + 2 + 4 + … + n < 2n.
E3 · Advanced challenge. Extend the pool to manage blocks of *two* sizes (32 and 128 bytes) and to detect a double free of the same block in debug builds.
Answer: Keep one free list per size class and choose the class in pool_alloc(size); pool_free finds the class by checking which storage array the pointer lies in. For double-free detection keep a bool allocated[BLOCK_COUNT] per class (or a canary in the free block) and assert it is set before freeing and cleared afterwards.
Quiz
? p = realloc(p, n) fails and returns NULL. What happened to the old block?
- It was freed. :: realloc leaves the original block untouched on failure.
+ It is still allocated, and the assignment just lost the only pointer to it. :: That is why the result must go to a temporary.
- It was moved to a new address. :: Nothing moved; the request failed.
- It became zero-sized. :: The block is unchanged.
? Which tool reported "Direct leak of 36 byte(s) in 3 object(s)" in this module?
+ LeakSanitizer (part of AddressSanitizer) :: Enabled with -fsanitize=address on Linux.
- The compiler's -Wall warnings :: Warnings cannot see run-time leaks.
- glibc's double-free check :: That only detects double frees.
- The kernel's OOM killer :: It kills processes; it does not report leaks.
? Which problems does reference counting alone fail to handle? (select all that apply)
+ Cycles of objects referring to each other :: Their counts never reach zero.
- Freeing objects promptly when the last reference disappears :: That is its strength.
+ Avoiding count-update overhead on every pointer copy :: Every copy and destruction updates a count.
- Running destructors deterministically :: Reference counting doe
…Summary
The heap provides run-time-sized, flexible-lifetime memory at the cost of explicit ownership. Check every allocation, compute sizes safely, assign realloc to a temporary, and free exactly once. Allocators trade speed, fragmentation and determinism; pools and arenas solve specific cases. C++ RAII, garbage collection and Rust ownership automate deallocation in different ways, each with its own costs and caveats.
References and Further Reading
glibc manual, "Memory Allocation"; the glibc wiki "MallocInternals".
Paul R. Wilson et al., "Dynamic Storage Allocation: A Survey and Critical Review" (1995).
Jeff Bonwick, "The Slab Allocator: An Object-Caching Kernel Memory Allocator" (USENIX 1994).
Herb Sutter, "GotW #89/#91: Smart Pointers"; *The Rust Programming Language*, chapter 4 "Understanding Ownership".
MISRA C:2012 Rule 21.3 (overview: the memory allocation functions shall not be used).