Dynamic Memory & The Heap in C: malloc, calloc, realloc & Memory Leak Prevention

Dynamic Memory & The Heap in C: malloc, calloc, realloc & Memory Leak Prevention
Table of Contents
- The Two Arenas: Stack vs Heap
- How the Heap Actually Works (glibc Internals)
- malloc: Requesting Raw Memory
- calloc: Zero-Initialized Allocation
- realloc: Resizing the Block
- free: The Mandatory Cleanup
- Building a Dynamic Array with realloc
- Memory Leak Detection with Valgrind
- AddressSanitizer: Runtime Bug Detection
- Common Security Vulnerabilities
- Frequently Asked Questions
- Key Takeaway
The Two Arenas: Stack vs Heap
Every C program has two primary memory arenas for data:
The stack is perfect for small, short-lived data. The heap is essential when:
- Size is unknown at compile time (reading a file of unknown length).
- Data must outlive its creating function (building a linked list, returning complex data).
- The size is too large for the stack (processing a 100 MB buffer).
- Data is shared between many threads or distant parts of the system.
How the Heap Actually Works (glibc Internals)
Understanding the heap's internal mechanism helps you understand performance and bugs.
On Linux, the heap is managed by glibc's ptmalloc (pthread malloc). When your program starts, glibc requests a chunk of virtual memory from the OS using the brk() or mmap() system calls. malloc then sub-allocates from this chunk by maintaining a free-list of available blocks.
Each block of allocated memory has a hidden header (typically 16 bytes on 64-bit systems) containing:
- The size of the block
- Whether the previous block is free
- Status flags
When you call free(ptr), glibc does not immediately return the memory to the OS. It marks the block as free and potentially coalesces it with adjacent free blocks (preventing fragmentation). The memory stays in the process's virtual address space, ready for the next malloc call.
This explains why:
malloccan seem very fast — it often doesn't call the OS at all.- Your program's reported memory usage (
RSS) doesn't immediately decrease afterfree. - Heap corruption (overwriting into a block's metadata) can cause bizarre behavior in unrelated future
malloccalls.
malloc: Requesting Raw Memory
malloc(size_t n) requests n bytes on the heap and returns a void* pointer to the start of the block. The memory is uninitialized — it contains whatever bytes were there previously.
The multiplication pattern: malloc(n * sizeof(Type)) is the canonical way to allocate space for n items of a type. Always use sizeof rather than hardcoding byte counts — this makes your code portable across 32-bit and 64-bit systems.
Detecting allocation failure: On modern desktops, malloc rarely returns NULL because the OS uses overcommit — it promises memory it doesn't yet have, hoping the program won't actually use it all. On embedded systems with 64KB of RAM, checking for NULL is not optional. Always check.
calloc: Zero-Initialized Allocation
calloc(size_t nmemb, size_t size) allocates memory for nmemb elements of size bytes each, and zero-initializes all bytes:
When to use calloc vs malloc:
| Scenario | Use |
|---|---|
| About to fill every element from a file/network | malloc (zeroing is wasted work) |
| Allocating a struct with pointer members | calloc (zero-init sets all pointers to 0/NULL) |
| Creating a boolean flag array | calloc (guarantees false state initial) |
| Building hash table buckets | calloc (empty buckets must be NULL/0) |
calloc also provides an implicit overflow check on the nmemb * size multiplication — if the multiplication would overflow size_t, calloc returns NULL safely. malloc(nmemb * size) does not perform this check.
realloc: Resizing the Block
realloc(void *ptr, size_t new_size) attempts to resize an existing block. If there is enough free space after the current block, it expands in place. Otherwise, it allocates a new block, copies the data, and frees the old one.
[!CAUTION] Never assign
realloc's return value directly back to the original pointer.int *data = realloc(data, new_size)is a bug: ifreallocfails and returnsNULL, you've just lost your only reference to the original block (memory leak AND no handle tofreeit). Always use a temporary pointer.
Building a Dynamic Array with realloc
The classic dynamic array (similar to C++'s std::vector) uses realloc with an exponential growth strategy:
The doubling strategy gives amortized O(1) push — even though occasional reallocations are expensive, the average cost per element remains constant.
Memory Leak Detection with Valgrind
Valgrind runs your program in a virtual CPU and intercepts every memory operation, detecting leaks, invalid accesses, and use-after-free errors:
Reading Valgrind output:
- definitely lost: Memory that was allocated but no pointer to it remains — a true leak.
- indirectly lost: Memory reachable only through a lost pointer.
- still reachable: Memory that was never freed but still has a live pointer at exit — technically safe but sloppy.
AddressSanitizer: Runtime Bug Detection
AddressSanitizer (ASan) is built into GCC and Clang and detects memory errors at runtime with ~2× overhead — much faster than Valgrind:
ASan detects:
- Heap buffer overflow: Writing past the end of a
malloc'd block. - Use-after-free: Accessing memory after calling
free. - Double-free: Calling
freetwice on the same pointer. - Stack buffer overflow: Writing past a local array.
- Global buffer overflow: Writing past a global array.
Use ASan on every debug build. It has saved countless hours in production codebases.
Common Security Vulnerabilities
Double Free
Fix: Set p = NULL immediately after free(p).
Heap Buffer Overflow
Fix: Always validate array indices against the allocated size.
Use After Free
Integer Overflow in Allocation Size
Fix: Validate count before multiplication, or use calloc for the implicit overflow check.
Frequently Asked Questions
What happens if I free a NULL pointer?
Nothing — free(NULL) is explicitly defined to be a no-op in the C standard. This is why the pattern free(p); p = NULL; followed by free(p); is safe — the second free(NULL) is harmless.
Why does my program not return memory to the OS after calling free?
free returns memory to glibc's internal free list, not to the OS. The OS only reclaims virtual address space when the process exits or when glibc explicitly calls munmap (which it does for very large allocations). Use malloc_trim(0) to force glibc to release unused heap memory to the OS.
Is there a safer alternative to malloc in 2026?
Libraries like jemalloc and tcmalloc offer better performance and security properties. For C code that needs provable memory safety, consider using a region allocator (arena allocator) pattern — allocate from a large arena, then free the entire arena at once — this eliminates individual free calls entirely and makes leak analysis trivial. We implement this in Module 26 (Heap Memory Allocator Project).
When should I use the heap vs stack? Use the stack for: primitive variables, small structs, buffers under ~1KB. Use the heap for: large buffers, data that outlives its creating function, dynamic collections (linked lists, trees, hash tables), and any data whose size is unknown at compile time.
What is posix_memalign and when do I use it?
malloc aligns memory to at least alignof(max_align_t) (typically 16 bytes on modern systems). SIMD instructions (SSE, AVX) require 32-byte or 64-byte alignment. posix_memalign(&ptr, 64, size) allocates memory with a guaranteed 64-byte alignment, enabling maximum vectorization throughput.
Key Takeaway
Dynamic memory in C is Total Power with Total Responsibility. The heap gives you the ability to build data structures of any size, lasting any duration. The price is that you must manually track every allocation and free it exactly once.
The tools — Valgrind and AddressSanitizer — make this manageable. Use them on every debug build. Set pointers to NULL after free. Always check for NULL return from malloc. Follow these rules consistently, and heap management becomes second nature.
Read next: Pointer Arithmetic: Navigating Memory Buffers →
Part of the C Mastery Course — 30 modules from fundamentals to production-grade systems engineering.
