Skip to content
Home C / C++ C Memory Leak — 30 Bytes/Req Crashed at 12h

C Memory Leak — 30 Bytes/Req Crashed at 12h

Where developers are forged. · Structured learning · Free forever.
📍 Part of: C Basics → Topic 11 of 17
A 30-byte C leak at 1000 req/s OOM'd a payment gateway in 12 hours.
⚙️ Intermediate — basic C / C++ knowledge assumed
In this tutorial, you'll learn
A 30-byte C leak at 1000 req/s OOM'd a payment gateway in 12 hours.
  • malloc is for raw allocation — fast but uninitialised. Always initialise before reading, or use calloc if you need guaranteed zeroes.
  • calloc's two-argument form isn't just stylistic — it performs an overflow-safe size calculation internally, making it safer than malloc(count * size) when count is large.
  • realloc must be assigned to a temporary pointer first — assigning directly back to the original loses your data if it fails and returns NULL.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer
  • malloc allocates uninitialised memory; calloc zeroes it and checks overflow
  • realloc grows or shrinks existing allocations; always use a temp pointer
  • free returns memory to the heap; set pointer to NULL after to prevent use-after-free
  • Stack is fixed size (~1-8 MB); heap scales to available RAM
  • Every allocation must have a matching free — leaks crash long-running processes
  • Valgrind and AddressSanitizer catch leaks, double-frees, and buffer overflows
🚨 START HERE

Quick Memory Debug Cheat Sheet

Diagnose and fix C memory issues in under 60 seconds
🟡

Detect memory leaks during development

Immediate ActionCompile with -fsanitize=address and run the program with typical input.
Commands
gcc -fsanitize=address -g -o program program.c && ./program
For detailed leak summary at exit: export ASAN_OPTIONS=detect_leaks=1
Fix NowRun again and fix every 'direct leak' reported — often a missing free in a path not tested.
🟡

Find where heap memory is allocated but never freed

Immediate ActionRun with Valgrind in memcheck mode.
Commands
valgrind --leak-check=full --show-leak-kinds=all ./program
valgrind --tool=memcheck --track-origins=yes ./program (slower but more detail)
Fix NowFocus on 'definitely lost' blocks. The stack trace shows exactly which line malloc'd.
🟡

Double free or invalid free detected

Immediate ActionRun with AddressSanitizer to get a detailed stack trace of the second free call.
Commands
gcc -fsanitize=address -fno-omit-frame-pointer -g -o program program.c
Run program and look for 'AddressSanitizer: attempting double-free'
Fix NowSet pointer to NULL immediately after free. Add conditional: if (ptr) { free(ptr); ptr = NULL; }
🟡

Use-after-free: reading freed memory

Immediate ActionEnable AddressSanitizer and run the program.
Commands
Same compile flags as above. ASAN catches use-after-free with full stack trace.
For non-ASAN environments: use valgrind --tool=memcheck --track-origins=yes
Fix NowReview the code around the reported freed site. Ensure pointer is NULL after free and no other references survive.
Production Incident

The Silent Leak That Took Down a Payment Gateway

A 30-byte leak per request brought down a critical payment service after 12 hours. The root cause? A forgotten free after a string duplication.
SymptomPayment gateway started timing out after ~12 hours of uptime. Memory usage in 'top' climbed steadily from 50 MB to 3.8 GB before OOM killer killed the process.
AssumptionThe team assumed the leak was in a third-party library or the database connection pool — both common culprits in production.
Root causeA helper function strdup() was used to copy request data, but the caller never freed the duplicated string. Each request leaked exactly the length of the transaction ID plus overhead (average 30 bytes). At ~1000 requests/second, that's 30 KB/s — 1.8 MB/min — 108 MB/hour — crash at 3.8 GB after ~35 hours. The crash happened at 12 hours because of concurrent threads and fragmentation.
FixAdded free() after each use of the duplicated string. Introduced a code review checklist: every malloc/strdup must have a corresponding free identified within the same function or clearly documented ownership.
Key Lesson
Small leaks kill: 30 bytes per request × 10 million requests = 300 MB.Ownership must be explicit: if function A returns a heap pointer, document that caller must free.Monitor memory growth trends, not just absolute usage — a linear climb is a reliable leak indicator.Valgrind and AddressSanitizer would have caught this in the first test run.
Production Debug Guide

Common symptoms and targeted actions for malloc/free failures

Segmentation fault (SIGSEGV) when reading a pointerCheck if pointer was freed (use-after-free) or never allocated. Use AddressSanitizer (-fsanitize=address -g). If not available, add assert(ptr != NULL) before dereference.
Program crashes with 'double free or corruption'Run under Valgrind (valgrind --tool=memcheck). Look for 'Invalid free' or 'double free' in output. Fix by ensuring each free is called exactly once — set pointer to NULL after free.
Memory usage grows linearly over time with no plateauUse Valgrind's --leak-check=full to identify allocation sites without matching free. In production, use mtrace or library interposition to log allocations. Consider heap profiling with perf.
Heap corruption: data gets overwritten unexpectedlyEnable debugging heap: link with -lmcheck on Linux or MALLOC_CHECK_=3 environment variable. Run under AddressSanitizer to catch buffer overflows.
realloc returns NULL, but program continues with old pointer lostImmediately check for the anti-pattern ptr = realloc(ptr, new_size). Replace with temporary pointer pattern. Always preserve original until realloc succeeds.

Every real-world C program — from embedded firmware in your car to the Linux kernel — has one thing in common: they can't always know at compile time how much memory they'll need. A web server doesn't know how many simultaneous connections it'll handle. A text editor doesn't know how long the document will be. If you hardcode array sizes, you either waste memory or crash when input exceeds your guess. This is the gap dynamic memory management was built to fill.

C gives you direct control over the heap — a large pool of memory your program can borrow from at runtime. The standard library functions malloc, calloc, realloc, and free are your tools for doing that. Unlike stack memory, which is automatically managed as functions are called and return, heap memory lives until YOU explicitly release it. That power is also the responsibility: C won't clean up after you.

By the end of this article you'll understand not just the syntax of these four functions, but WHY each one exists, when to choose one over another, how to detect and prevent memory leaks, and what interviewers are really testing when they ask about dynamic allocation. You'll have runnable code patterns you can drop straight into real projects.

Why the Stack Isn't Enough — The Case for Heap Allocation

When you declare int scores[100]; inside a function, it lives on the stack. The stack is fast, automatically managed, and perfect for small, fixed-size data. But it has two brutal limitations: its size is fixed (typically 1–8 MB), and every variable must have a known size at compile time.

Think about reading a CSV file with an unknown number of rows, or building a linked list that grows as users add items. You can't express that with stack arrays. You need memory you can size at runtime and control the lifetime of explicitly.

The heap is that place. It's a large region of memory (limited by your RAM and OS) that your program can request chunks from dynamically. The heap doesn't auto-clean — a chunk stays allocated until your code calls free on it. This is where malloc and its siblings come in: they're the formal interface between your program and the heap allocator.

One more thing: stack memory dies when the function returns. If you need data to outlive the function that created it — like returning a dynamically built string to a caller — the heap is the only option.

stack_vs_heap.c · C
1234567891011121314151617181920212223242526272829303132333435363738
#include <stdio.h>
#include <stdlib.h>

// Demonstrates WHY stack allocation fails for runtime-sized data

// This function CANNOT work — you can't use a runtime value as a stack array size
// in C89/C90 (VLAs in C99 help but have their own problems — stack overflow risk)
void demonstrate_stack_limit(int user_count) {
    // VLA — risky for large counts, stack overflow if user_count is huge
    // int user_ids[user_count]; // C99 only, and dangerous for large values

    // The heap-based approach scales safely to millions:
    int *user_ids = malloc(user_count * sizeof(int));
    if (user_ids == NULL) {
        // Always check: malloc returns NULL if the OS can't satisfy the request
        fprintf(stderr, "Failed to allocate memory for %d users\n", user_count);
        return;
    }

    // Populate with demo data
    for (int i = 0; i < user_count; i++) {
        user_ids[i] = 1000 + i;  // Simulating real user IDs starting at 1000
    }

    printf("Allocated %d user IDs on the heap:\n", user_count);
    for (int i = 0; i < user_count; i++) {
        printf("  user_ids[%d] = %d\n", i, user_ids[i]);
    }

    free(user_ids);  // Return the memory — critical. Without this, it's a leak.
    user_ids = NULL; // Null the pointer so it can't be accidentally used after free
}

int main(void) {
    int count = 5;  // In a real app, this comes from user input or a config file
    demonstrate_stack_limit(count);
    return 0;
}
▶ Output
Allocated 5 user IDs on the heap:
user_ids[0] = 1000
user_ids[1] = 1001
user_ids[2] = 1002
user_ids[3] = 1003
user_ids[4] = 1004
⚠ Watch Out: Stack Overflow from Large Local Arrays
Declaring int buffer[1000000]; on the stack will silently corrupt your program or crash it — the stack is typically only 1-8 MB. Any buffer whose size depends on runtime input or exceeds a few kilobytes belongs on the heap.
📊 Production Insight
Stack overflow is silent — the program just corrupts local variables before the crash.
Valgrind doesn't catch stack overflow; AddressSanitizer with -fsanitize=address does.
Rule: If it's larger than a few KB or runtime-sized, allocate on the heap.
🎯 Key Takeaway
Stack is for small, fixed-size, short-lived data.
Heap is for everything else.
Never guess sizes when you can allocate dynamically.
Stack vs Heap: Quick Decision
IfSize known at compile time and small (< 1 KB)
UseUse stack — faster, automatic lifetime
IfSize depends on runtime input or large
UseUse heap — malloc/calloc/realloc
IfData must survive function return
UseUse heap — stack memory dies with function

malloc vs calloc — Same Job, Different Guarantee

Both malloc and calloc allocate heap memory, but they differ in one critical way: calloc zeroes out the memory it allocates, while malloc leaves whatever garbage was there before.

malloc(n) takes a single argument — the number of bytes to allocate. It's the faster choice when you're about to overwrite every byte anyway (like reading from a file or socket into a buffer). The garbage contents don't matter because you'll replace them immediately.

calloc(count, size) takes two arguments — the number of elements and the size of each. It's designed specifically for arrays. Beyond the zero-initialization, it also handles the multiplication safely: if count * size would overflow a size_t, calloc can detect it. malloc with manual multiplication cannot.

Choose calloc when you need a guaranteed-zero starting state — like a boolean flags array, a counter array, or any structure where zero is a valid 'empty' sentinel. Choose malloc when you're going to initialize the memory yourself immediately, and you want the marginal performance gain of skipping the zero-fill.

Neither function initialises memory to any particular value beyond calloc's zero guarantee — use memset if you need a non-zero fill after malloc.

malloc_vs_calloc.c · C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
#include <stdio.h>
#include <stdlib.h>
#include <string.h>  // for memset

// Struct representing a simple task in a to-do list
typedef struct {
    int  task_id;
    int  is_complete;   // 0 = pending, 1 = done
    int  priority;      // 1-5
} Task;

void demonstrate_malloc(void) {
    printf("--- malloc demo ---\n");

    // malloc: fast, but contents are UNDEFINED (garbage)
    Task *task = malloc(sizeof(Task));
    if (task == NULL) {
        fprintf(stderr, "malloc failed\n");
        return;
    }

    // We MUST initialise every field ourselves — there's no zero guarantee
    task->task_id    = 42;
    task->is_complete = 0;  // Explicitly set to pending
    task->priority   = 3;

    printf("Task %d: complete=%d, priority=%d\n",
           task->task_id, task->is_complete, task->priority);

    free(task);
    task = NULL;
}

void demonstrate_calloc(void) {
    printf("--- calloc demo ---\n");

    int task_count = 4;

    // calloc: allocates an array of 4 Task structs, ALL bytes zeroed
    // is_complete will be 0 (pending) for every task — no explicit init needed
    Task *task_list = calloc(task_count, sizeof(Task));
    if (task_list == NULL) {
        fprintf(stderr, "calloc failed\n");
        return;
    }

    // Only set the fields that differ from zero
    for (int i = 0; i < task_count; i++) {
        task_list[i].task_id  = 100 + i;
        task_list[i].priority = i + 1;   // priorities 1 through 4
        // is_complete is already 0 thanks to calloc — nothing to do
    }

    for (int i = 0; i < task_count; i++) {
        printf("Task %d: complete=%d, priority=%d\n",
               task_list[i].task_id,
               task_list[i].is_complete,
               task_list[i].priority);
    }

    free(task_list);
    task_list = NULL;
}

int main(void) {
    demonstrate_malloc();
    demonstrate_calloc();
    return 0;
}
▶ Output
--- malloc demo ---
Task 42: complete=0, priority=3
--- calloc demo ---
Task 100: complete=0, priority=1
Task 101: complete=0, priority=2
Task 102: complete=0, priority=3
Task 103: complete=0, priority=4
💡Pro Tip: Prefer calloc for Arrays of Structs
When allocating an array of structs with calloc, zero-initialization means every integer field starts at 0, every pointer starts at NULL, and every boolean starts false — that's often exactly the 'empty' state you want, saving you an explicit memset or init loop.
📊 Production Insight
calloc's overflow check saved a medical device from allocating 0 bytes when count was enormous.
malloc is faster but leaves sensitive data (passwords!) from previous allocations in memory.
Rule: Use calloc if zero is a valid initial state; use malloc only when you'll overwrite everything.
🎯 Key Takeaway
calloc gives you zeroed memory and overflow safety.
malloc gives you speed and garbage.
Don't pay for zeroing if you'll overwrite it anyway.
malloc vs calloc Decision
IfNeed zero-initialized memory
UseUse calloc — also safer against overflow
IfWill immediately overwrite all bytes
UseUse malloc — no need to zero first
IfAllocating array with large element count
UseUse calloc — built-in overflow check

realloc and free — Growing Memory and Giving It Back

Once you've allocated memory, real programs often need to grow it. A dynamic array that starts at 10 elements might need 10,000 by the time the user is done. That's realloc's job: resize an existing allocation without you having to malloc a new block, memcpy the old data, and free the old pointer manually.

realloc takes your existing pointer and a new size. It tries to extend the block in place; if it can't, it allocates a new block, copies your data, and frees the old one automatically. The key danger: realloc returns a new pointer (which may differ from the old one), so you must assign the return value to a pointer variable. If you ignore it and keep using the old pointer, you're in undefined behaviour territory.

The classic growth pattern is to double capacity each time you run out — this gives amortised O(1) appends and is exactly how C++ std::vector works under the hood.

free is straightforward but has strict rules: only free pointers returned by malloc/calloc/realloc, never free the same pointer twice, and never use a pointer after freeing it. After calling free, immediately set the pointer to NULL — it costs nothing and prevents a whole class of bugs called use-after-free.

dynamic_array.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// A simple dynamic array of strings (like a growing log buffer)
typedef struct {
    char  **entries;       // Pointer to array of string pointers
    int     count;         // Current number of entries
    int     capacity;      // Total slots currently allocated
} LogBuffer;

// Initialise the log buffer with a small starting capacity
LogBuffer *log_buffer_create(void) {
    LogBuffer *log = malloc(sizeof(LogBuffer));
    if (log == NULL) return NULL;

    log->capacity = 4;   // Start small — we'll grow as needed
    log->count    = 0;
    log->entries  = malloc(log->capacity * sizeof(char *));
    if (log->entries == NULL) {
        free(log);
        return NULL;
    }
    return log;
}

// Append a message — grows the buffer if full
int log_buffer_append(LogBuffer *log, const char *message) {
    // Check if we need to grow
    if (log->count == log->capacity) {
        int new_capacity = log->capacity * 2;  // Double the capacity

        // CRITICAL: use a temp pointer — if realloc fails, we still have the original
        char **resized = realloc(log->entries, new_capacity * sizeof(char *));
        if (resized == NULL) {
            fprintf(stderr, "realloc failed — log buffer could not grow\n");
            return -1;  // Original log->entries is still valid here
        }

        log->entries  = resized;       // Now safe to update the pointer
        log->capacity = new_capacity;
        printf("[Buffer grew to capacity: %d]\n", new_capacity);
    }

    // Duplicate the string onto the heap so the caller's memory doesn't matter
    log->entries[log->count] = malloc(strlen(message) + 1);  // +1 for null terminator
    if (log->entries[log->count] == NULL) return -1;

    strcpy(log->entries[log->count], message);
    log->count++;
    return 0;
}

// Free everything — the strings, the array, then the struct itself
void log_buffer_destroy(LogBuffer *log) {
    for (int i = 0; i < log->count; i++) {
        free(log->entries[i]);   // Free each individual string first
        log->entries[i] = NULL;
    }
    free(log->entries);   // Free the array of pointers
    log->entries = NULL;
    free(log);            // Free the struct itself
    // Caller should set their LogBuffer* to NULL after this
}

int main(void) {
    LogBuffer *app_log = log_buffer_create();
    if (app_log == NULL) {
        fprintf(stderr, "Failed to create log buffer\n");
        return 1;
    }

    // Appending 6 entries will trigger one realloc (capacity 4 -> 8)
    log_buffer_append(app_log, "Server started on port 8080");
    log_buffer_append(app_log, "Database connection established");
    log_buffer_append(app_log, "Config file loaded");
    log_buffer_append(app_log, "Worker threads initialised");  // Triggers realloc here
    log_buffer_append(app_log, "First request received");
    log_buffer_append(app_log, "Health check passed");

    printf("\nLog entries (%d total):\n", app_log->count);
    for (int i = 0; i < app_log->count; i++) {
        printf("  [%d] %s\n", i + 1, app_log->entries[i]);
    }

    log_buffer_destroy(app_log);
    app_log = NULL;  // Prevent any accidental use after destroy
    return 0;
}
▶ Output
[Buffer grew to capacity: 8]

Log entries (6 total):
[1] Server started on port 8080
[2] Database connection established
[3] Config file loaded
[4] Worker threads initialised
[5] First request received
[6] Health check passed
⚠ Watch Out: The realloc Pointer Trap
Never do ptr = realloc(ptr, new_size) directly. If realloc fails it returns NULL and your original pointer is overwritten — you've now lost the only reference to your allocated memory, creating an instant leak. Always assign to a temporary pointer, check for NULL, then update the original.
📊 Production Insight
realloc failure is rare but deadly — losing the original pointer means data loss and a leak.
Doubling capacity is the standard growth factor; powers of two keep allocations aligned.
Rule: Always use a temp pointer for realloc, and always NULL after free.
🎯 Key Takeaway
realloc is powerful but dangerous — never reassign directly to the original pointer.
free must be paired with every malloc/calloc/realloc.
NULL after free prevents use-after-free.
When to Use realloc
IfNeed to grow an existing allocation
UseUse realloc with temp pointer — safest
IfNeed to shrink an allocation to exact size
Userealloc can shrink; check if you can trust the new pointer
IfStarting from NULL (first allocation)
Userealloc(NULL, size) works like malloc — but prefer malloc for clarity

Detecting and Preventing Memory Leaks — Real Tools, Real Habits

A memory leak happens when you allocate heap memory and never free it. The program keeps running, keeps allocating, and eventually either runs out of memory or gets killed by the OS. In long-running processes — servers, daemons, embedded systems — even a tiny leak per request will eventually bring the system down.

The most effective tool for catching leaks is Valgrind (Linux/macOS). Run your program with valgrind --leak-check=full ./your_program and it'll report every byte that was allocated but not freed, including the exact line where the allocation happened. On Windows, use AddressSanitizer (built into MSVC and Clang with -fsanitize=address).

Beyond tools, the best defence is clean ownership patterns: every allocation should have a clear owner (the code responsible for freeing it), allocations and their matching frees should live in the same layer of abstraction (don't malloc in one module and free in another without a documented contract), and every pointer should be set to NULL after free.

The code below demonstrates a real leak, what Valgrind reports, and the fix.

leak_detection.c · C
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Simulates loading configuration values — a common real-world pattern

// BAD VERSION — leaks the allocated string
char *load_config_value_leaky(const char *key) {
    // Pretend we read this from a file; we allocate a new string
    char *value = malloc(64);
    if (value == NULL) return NULL;
    snprintf(value, 64, "value_for_%s", key);  // Populate with demo data
    return value;  // Caller receives ownership — must free this
}

// GOOD VERSION — same function, but caller is reminded of ownership via comment
// Returns heap-allocated string. CALLER MUST FREE the returned pointer.
char *load_config_value(const char *key) {
    char *value = malloc(64);
    if (value == NULL) return NULL;
    snprintf(value, 64, "value_for_%s", key);
    return value;
}

void process_config_leaky(void) {
    // This leaks: we get a heap pointer but never free it
    char *db_host = load_config_value_leaky("db_host");
    printf("Leaky path — db_host: %s\n", db_host);
    // Forgot to call free(db_host) — 64 bytes leaked every call
}

void process_config_correct(void) {
    char *db_host = load_config_value("db_host");
    if (db_host == NULL) {
        fprintf(stderr, "Failed to load db_host config\n");
        return;
    }
    printf("Correct path — db_host: %s\n", db_host);

    free(db_host);   // We own it — we free it
    db_host = NULL;  // Nullify immediately
}

int main(void) {
    process_config_leaky();   // Run with Valgrind to see this reported
    process_config_correct(); // Valgrind will show zero leaks from this path

    // Valgrind command: valgrind --leak-check=full ./leak_detection
    // Leaky path output:
    //   LEAK SUMMARY: definitely lost: 64 bytes in 1 blocks
    //   at 0x...: malloc (in /usr/lib/valgrind/...)
    //   by 0x...: load_config_value_leaky (leak_detection.c:12)
    //   by 0x...: process_config_leaky (leak_detection.c:26)

    return 0;
}
▶ Output
Leaky path — db_host: value_for_db_host
Correct path — db_host: value_for_db_host
💡Pro Tip: Compile with AddressSanitizer During Development
Add -fsanitize=address -g to your GCC/Clang compile flags during development. It catches leaks, use-after-free, and buffer overflows at runtime with almost zero setup — faster than Valgrind and no separate tool to install. Remove it for production builds.
📊 Production Insight
Leaks compound: 64 bytes per request × 1000 req/s = 5 GB/day.
AddressSanitizer catches use-after-free that Valgrind may miss in multi-threaded code.
Rule: Use both Valgrind (leak detection) and ASan (buffer overflows) in CI.
🎯 Key Takeaway
Every allocation must have a matching free.
Use Valgrind for leaks, ASan for corruption.
NULL after free is free insurance against use-after-free.
Leak Detection Tool Decision
IfNeed fast, daily checks
UseUse AddressSanitizer (-fsanitize=address)
IfNeed detailed leak summary with stack traces
UseUse Valgrind --leak-check=full
IfRunning on Windows or embedded target
UseUse AddressSanitizer (MSVC, Clang) or custom heap tracker

Common Mistakes with Dynamic Memory and How to Debug Them

Even experienced C programmers make the same three mistakes: not checking malloc's return, double-freeing, and use-after-free. Each has a distinct symptom and a straightforward fix once you know what to look for.

Not checking malloc's return: If malloc returns NULL, dereferencing it is undefined behaviour — your program crashes with a segfault, but not consistently, because NULL may map to a valid address on some systems. Always check if (ptr == NULL) after every allocation.

Double-freeing: Calling free twice on the same pointer corrupts the heap's internal bookkeeping. The next malloc/calloc/realloc may crash, or data may be silently corrupted. The fix: set pointer to NULL after free, and guard with if (ptr) before calling free.

Use-after-free: Accessing memory after calling free on it. The memory may have been reallocated to another part of the program, so reading it gives garbage or writes corrupt other data. This is hard to reproduce. AddressSanitizer catches it deterministically.

Here's a code example showing all three mistakes and how to write them correctly.

common_mistakes.c · C
1234567891011121314151617181920212223242526272829303132
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // Mistake 1: Not checking malloc's return
    int *data = malloc(100 * sizeof(int));
    // Should check: if (data == NULL) { ... }
    data[0] = 42; // If malloc failed, this dereferences NULL -> crash

    // Mistake 2: Double-free
    free(data);
    free(data); // Crash: double free or corruption

    // Mistake 3: Use-after-free
    // data was freed, but pointer still points to freed memory
    // printf("%d\n", data[0]); // Undefined behaviour

    // CORRECT PATTERN:
    int *safe_data = malloc(100 * sizeof(int));
    if (safe_data == NULL) {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }
    safe_data[0] = 42;
    free(safe_data);
    safe_data = NULL; // Prevent double-free and use-after-free

    // Later accidental free(safe_data) is safe because free(NULL) is no-op
    free(safe_data); // Harmless

    return 0;
}
▶ Output
No output on success. Run with AddressSanitizer to see errors from the first part.
⚠ The Triple Threat: Check, Don't Double, Null After Free
Three rules to live by: 1) Always check malloc/calloc/realloc return for NULL. 2) Never call free twice on the same pointer without setting it to NULL first. 3) Set the pointer to NULL immediately after every free. These three habits eliminate the vast majority of C memory bugs.
📊 Production Insight
A null dereference can corrupt data silently on systems where virtual address 0 maps to a page.
Double-free is often triggered by error-handling paths that free a pointer already freed in the success path.
Rule: Adopt a strict ownership and NULL-after-free discipline; it's cheap and saves hours of debugging.
🎯 Key Takeaway
Check malloc returns, never double-free, and NULL after free.
These three rules prevent 90% of C memory bugs.
Use AddressSanitizer in development to catch the rest.
Debugging Memory Corruption
IfSegfault on dereference
UseCheck if pointer is NULL or already freed (use-after-free)
IfDouble free or corruption error message
UseFind where pointer is freed more than once — use AddressSanitizer for stack trace
IfInconsistent crashes or data corruption
UseEnable AddressSanitizer or Valgrind to catch overflows/underflows
🗂 malloc vs calloc vs realloc vs free
Quick reference for C dynamic memory functions
Feature / Aspectmalloccallocreallocfree
Primary purposeAllocate a raw block of bytesAllocate zeroed arrayResize an existing allocationRelease heap memory
Argumentsmalloc(bytes)calloc(count, size)realloc(ptr, new_bytes)free(ptr)
Memory initialised?No — contains garbageYes — all bytes set to 0New bytes are NOT zeroedN/A
Returnsvoid* or NULL on failurevoid* or NULL on failureNew void* or NULL on failurevoid (nothing)
Overflow-safe sizing?No — you multiply manuallyYes — calloc checks internallyNo — you compute size manuallyN/A
Best use caseRead buffers, strings, structs you'll init immediatelyArrays of counters, flags, structs needing zero stateDynamic arrays, growing buffersEvery malloc/calloc/realloc — no exceptions
PerformanceFastest — no zero-fillSlightly slower — zero-fill passVaries — may copy data to new blockGenerally O(1)
Null pointer behaviourmalloc(0) — implementation definedcalloc(0, x) — implementation definedrealloc(NULL, n) == malloc(n)free(NULL) — safe no-op

🎯 Key Takeaways

  • malloc is for raw allocation — fast but uninitialised. Always initialise before reading, or use calloc if you need guaranteed zeroes.
  • calloc's two-argument form isn't just stylistic — it performs an overflow-safe size calculation internally, making it safer than malloc(count * size) when count is large.
  • realloc must be assigned to a temporary pointer first — assigning directly back to the original loses your data if it fails and returns NULL.
  • Setting a pointer to NULL immediately after free costs nothing and prevents an entire class of use-after-free bugs — make it muscle memory.
  • Every dynamic memory allocation must be paired with a matching free — in long-running processes, even small leaks are catastrophic.

⚠ Common Mistakes to Avoid

    Not checking malloc/calloc/realloc return value
    Symptom

    Program crashes with segmentation fault when memory is low, but only in production when memory pressure is high — impossible to reproduce in test.

    Fix

    Always check if (ptr == NULL) immediately after every allocation. Print an error message and handle gracefully (return error, clean up any already-allocated resources, exit if unrecoverable).

    Directly assigning realloc return to original pointer
    Symptom

    Occasional memory corruption after realloc fails — the original pointer is overwritten with NULL, causing both a leak and a crash when trying to free the NULL pointer later.

    Fix

    Always use a temporary pointer: void tmp = realloc(ptr, new_size); if (tmp != NULL) { ptr = tmp; } else { / handle error */ }

    Double-free or use-after-free
    Symptom

    Heisenbugs: program works fine for hours, then crashes with 'double free or corruption' or data becomes corrupted without any clear cause. Symptoms appear only under specific timing conditions.

    Fix

    Immediately set ptr = NULL after every free(ptr). This makes a second free safe (free(NULL) is a no-op) and prevents accidental use of the stale pointer.

    Shrinking allocation with realloc and losing data
    Symptom

    Portion of data beyond new size is lost — but only if realloc actually moves the block. Subtle bugs where the program assumes the old data remains accessible past the new size.

    Fix

    Understand that after a shrink, bytes beyond the new size are invalid. Use realloc only to truncate if you don't need the data beyond that point. If you need to keep data, allocate a new block and copy.

Interview Questions on This Topic

  • QWhat is the difference between malloc and calloc, and when would you choose one over the other?JuniorReveal
    malloc allocates a block of uninitialized memory; calloc allocates memory for an array of elements and zero-initializes every byte. Choose calloc when you need guaranteed zero initial state (e.g., counter arrays, flag arrays) or when the allocation could overflow a size_t multiplication — calloc checks internally. Choose malloc when performance matters and you'll overwrite the memory immediately (e.g., reading into a buffer). Also, calloc's two-argument form is cleaner for arrays and avoids manual multiplication mistakes.
  • QWhat happens if you call free() twice on the same pointer, and how do you protect against it?SeniorReveal
    Double-free invokes undefined behavior: it can corrupt the heap's bookkeeping data, leading to crashes, memory corruption, or security vulnerabilities. The heap manager's metadata is damaged, so subsequent allocations may return overlapping memory or crash. Protection: always set the pointer to NULL immediately after free. Then a second free(NULL) is a safe no-op per the C standard. A common pattern is to define a macro like #define SAFE_FREE(p) do { free(p); (p) = NULL; } while(0).
  • QIf realloc fails and returns NULL, what happens to the original memory block — and how does that affect how you should write the call?Mid-levelReveal
    If realloc fails, it returns NULL and the original memory block remains valid and unchanged. This is why you must never assign the result of realloc directly to the original pointer: ptr = realloc(ptr, new_size) — if realloc fails, ptr becomes NULL and you lose the original allocation permanently. Correct pattern: void tmp = realloc(ptr, new_size); if (tmp != NULL) { ptr = tmp; } else { / handle error, ptr still points to old block */ }. This preserves the old data on failure.

Frequently Asked Questions

What is the difference between malloc and calloc in C?

malloc(bytes) allocates a single block of the given size and leaves the contents uninitialised — they contain whatever garbage was in that memory before. calloc(count, size) allocates space for count elements of the given size and zero-initialises every byte. calloc also performs the size multiplication internally with overflow checking, which malloc does not. Use malloc when you'll overwrite the memory immediately; use calloc when you need a guaranteed zero starting state.

Is it safe to call free() on a NULL pointer in C?

Yes — the C standard explicitly guarantees that free(NULL) is a safe no-op. It does nothing and causes no errors. This is why the pattern of setting a pointer to NULL after freeing it works well: any accidental second free() call is harmless. The danger is double-freeing a non-NULL pointer — that's undefined behaviour.

What is a memory leak in C and how do I detect one?

A memory leak occurs when you allocate heap memory with malloc, calloc, or realloc and never call free on it before the pointer goes out of scope. The memory remains reserved until the process exits, which can cause a long-running program to consume more and more RAM over time. The most effective detection tools are Valgrind on Linux/macOS (valgrind --leak-check=full ./program) and AddressSanitizer (-fsanitize=address compiler flag), both of which report the exact allocation site of any leaked memory.

How do I safely grow a dynamically allocated array in C?

Use realloc with a temporary pointer. For example: int tmp = realloc(arr, new_capacity sizeof(int)); if (tmp != NULL) { arr = tmp; capacity = new_capacity; } else { / handle error, arr still points to old data / }. The typical growth factor is doubling the capacity to achieve amortised O(1) inserts. Never assign realloc's return directly to the original pointer, as a failure would lose the original allocation.

What are the most common memory management bugs in C and how do I avoid them?

The top three are: (1) Not checking if malloc/calloc/realloc returned NULL — always check and handle. (2) Double-free — always set pointer to NULL after free. (3) Use-after-free — don't access memory after freeing; nullifying the pointer prevents accidental access. Additionally, always match every allocation with a corresponding free, and document ownership of heap pointers in comments.

🔥
Naren Founder & Author

Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.

← PreviousStructures and Unions in CNext →File Handling in C
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged