Dynamic Memory in C: Why Your malloc Checks Still Matter
Unchecked malloc returns still crash production C code. This post covers malloc, calloc, realloc, and free with a worked string-buffer example, the realloc trap, and a verification checklist using AddressSanitizer and Valgrind.
29 Jul 2025, 06:20 UTC

The Problem That Never Goes Away
Every C programmer learns to check malloc returns. Yet null-pointer dereferences from unchecked allocations remain one of the most common crash causes in production C code. The pattern is familiar: a request for a large buffer succeeds in testing but fails under load, and the code proceeds to write through a NULL pointer. The fix isn't clever—it's discipline.
What Each Function Actually Does
malloc(size_t size) requests size bytes from the heap. It returns a void* suitably aligned for any object type, or NULL if the request cannot be satisfied. The memory is uninitialized—reading it before writing is undefined behavior.
calloc(size_t nmemb, size_t size) allocates nmemb * size bytes and zero-initializes the block. It also checks for integer overflow in the multiplication, returning NULL if nmemb * size would wrap. This makes calloc safer than malloc + memset for arrays.
realloc(void* ptr, size_t size) resizes an existing allocation. It may return the same pointer or a new one. If it returns NULL, the original block is unchanged and still valid—you must not overwrite your only reference to it. This is the most common realloc bug.
free(void* ptr) returns memory to the allocator. Passing NULL is safe (a no-op). Passing a pointer not returned by malloc, calloc, or realloc, or passing a pointer already freed, corrupts the heap.
A Worked Example: Growing a String Buffer
Consider reading lines of unknown length from a file. A fixed buffer either truncates or wastes space. Dynamic growth handles both.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* read_line(FILE* fp) {
size_t capacity = 64;
size_t length = 0;
char* buffer = malloc(capacity);
if (!buffer) return NULL;
int ch;
while ((ch = fgetc(fp)) != EOF && ch != '\n') {
if (length + 1 >= capacity) { // +1 for NUL
capacity *= 2;
char* new_buf = realloc(buffer, capacity);
if (!new_buf) {
free(buffer); // original still valid
return NULL;
}
buffer = new_buf;
}
buffer[length++] = (char)ch;
}
if (length == 0 && ch == EOF) {
free(buffer);
return NULL;
}
buffer[length] = '\0';
return buffer; // caller owns this memory
}
Key points in this example:
- The
reallocresult is stored in a temporary variable. If it fails,bufferstill points to the original block, which we free before returning. - Growth factor of 2× gives amortized O(1) append cost.
- The function returns ownership to the caller—documented in the comment.
Where Things Go Wrong
Missing Return Checks
Omitting the if (!ptr) check is the single biggest source of crashes. In low-memory conditions (containers, embedded systems, heavy load), allocations fail. Code that assumes success will dereference NULL.
The realloc Trap
// WRONG: if realloc fails, ptr is leaked and we lose the only reference
ptr = realloc(ptr, new_size);
// RIGHT: use a temporary
void* tmp = realloc(ptr, new_size);
if (!tmp) { /* handle error, ptr still valid */ }
ptr = tmp;
Double Free and Use-After-Free
Freeing the same pointer twice, or accessing memory after free, corrupts allocator metadata. Modern allocators (glibc, jemalloc) detect some cases and abort, but the behavior is undefined. Set pointers to NULL after freeing if they remain in scope:
free(ptr);
ptr = NULL; // prevents accidental reuse
Integer Overflow in Size Calculations
malloc(n * sizeof *ptr) overflows if n * sizeof *ptr wraps size_t. calloc(n, sizeof *ptr) checks this for you. If you must use malloc, validate first:
if (n > SIZE_MAX / sizeof *ptr) return NULL;
void* p = malloc(n * sizeof *ptr);
Trade-offs and Limitations
Dynamic allocation has costs. Each call involves a system call or allocator bookkeeping overhead. Fragmentation can increase RSS even when logical memory use is low. For short-lived, small objects, arena or pool allocators (or stack allocation with VLAs where supported) avoid these costs. malloc is not real-time safe—avoid it in signal handlers or hard real-time paths.
C11's <stdalign.h> and aligned_alloc (C17) address alignment needs for SIMD or hardware registers, but malloc already returns max-aligned memory for most purposes.
Verification Checklist
- Compile with
-fsanitize=address -fno-omit-frame-pointer(GCC/Clang) and run your test suite. AddressSanitizer catches use-after-free, double-free, and out-of-bounds access. - Run
valgrind --leak-check=full --show-leak-kinds=all ./your_programto confirm no leaks and no invalid frees. - Run
cppcheck --enable=all --std=c11 .to flag missing return-value checks and suspicious pointer arithmetic.
These tools don't replace code review, but they catch the class of bugs that human eyes miss under deadline pressure.
Closing Action
Pick one file in your codebase that uses malloc/realloc. Verify every allocation has a return check, every realloc uses a temporary, and every allocation has a matching free on all exit paths. Run AddressSanitizer on that file's tests. Fix what it finds. Repeat. The discipline compounds.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.