Mid-level 16 min · March 06, 2026
Memory Management in C — malloc calloc free

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

A 30-byte C leak at 1000 req/s OOM'd a payment gateway in 12 hours.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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
✦ Definition~90s read
What is Memory Management in C?

Memory management in C is the manual process of allocating, using, and deallocating memory at runtime. Unlike garbage-collected languages (Java, Go) or those with RAII (C++, Rust), C gives you direct control over every byte via functions like malloc, calloc, realloc, and free.

Imagine you're planning a dinner party but you don't know how many guests are coming.

This power is a double-edged sword: you can optimize memory layout for performance-critical systems (embedded devices, game engines, databases like Redis or SQLite), but you also bear full responsibility for every allocation. A single missing free or dangling pointer can silently leak memory until your process OOM-kills itself—often hours later, as the 30 bytes/request leak in this article demonstrates.

The core tension is between stack and heap. Stack allocation is automatic, fast, and deterministic—local variables vanish when their scope exits. But stack size is limited (typically 1–8 MB per thread on Linux), and you can't return stack memory to a caller.

Heap allocation via malloc lives until explicitly freed, supports dynamic sizes, and persists across function calls. This is why any non-trivial C program—web servers, audio pipelines, kernel modules—must use the heap. The tradeoff: heap allocation is slower (syscall overhead, fragmentation), and every malloc must eventually pair with a free.

Real-world C memory bugs aren't academic. A 2019 study of the Linux kernel found that memory leaks accounted for ~15% of all reported bugs. Tools like Valgrind, AddressSanitizer (ASan), and mtrace exist precisely because manual management is error-prone at scale.

The calloc vs malloc distinction matters here: calloc zeroes memory (preventing uninitialized-read leaks) but costs extra cycles. realloc can grow or shrink blocks, but misuse (e.g., not checking the return value) creates dangling pointers. The bottom line: in C, you are the garbage collector—and when you forget, your process pays the price in bytes that compound into crashes.

Plain-English First

Imagine you're planning a dinner party but you don't know how many guests are coming. You could set up 1,000 chairs just in case — wasteful — or you could wait until people RSVP and then grab exactly the right number of chairs from a storage room. That storage room is the heap, and malloc/calloc are how you request chairs. When the party's over, you return the chairs with free so someone else can use them. That's dynamic memory management in one paragraph.

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 Manual Memory Management in C Is a Double-Edged Sword

Memory management in C is the explicit allocation and deallocation of heap memory via malloc(), calloc(), realloc(), and free(). Unlike garbage-collected languages, C gives you full control — and full responsibility. Every malloc() returns a pointer to a block of memory; every successful allocation must eventually be matched with a free(). Fail to free, and you leak memory. Free too early, and you get a dangling pointer. Free twice, and you corrupt the heap. The core mechanic is simple: you own every byte you allocate, and the runtime will not clean up after you.

In practice, C's memory model is flat and contiguous. malloc() returns a void* to a block from the heap, typically managed by the OS via brk() or mmap(). The allocator tracks free chunks; fragmentation grows over time if you allocate and free irregularly. There is no bounds checking — writing past the end of an allocated buffer corrupts adjacent memory silently. This is why tools like Valgrind and AddressSanitizer exist: they catch what the language itself does not enforce.

Use manual memory management when you need predictable performance, minimal overhead, or are working on embedded systems, kernels, or real-time applications. It matters because a single leak in a long-running server can accumulate into a crash — as in the 30 bytes/request leak that brings down a service after 12 hours. Understanding C's memory model is not optional if you write systems software; it is the difference between a reliable daemon and a ticking time bomb.

Ownership Is Not Optional
Every malloc() creates a debt. If you don't track who frees it, you will either leak or double-free — there is no third option.
Production Insight
A high-throughput HTTP gateway leaked 30 bytes per request via a missed free() in a rarely-taken error path. After 12 hours at 10k req/s, the process RSS hit 12 GB and OOM killer terminated it. Rule: always free in the same function scope that allocated, or use a clear ownership transfer contract.
Key Takeaway
Every malloc() must have a matching free() — no exceptions.
Use static analysis and runtime sanitizers (Valgrind, ASan) in CI, not just during debugging.
Prefer stack allocation when possible; heap is for dynamic lifetimes only.
C Memory Leak — 30 Bytes/Req Crashed at 12h THECODEFORGE.IO C Memory Leak — 30 Bytes/Req Crashed at 12h Flow from heap allocation to leak detection and prevention Heap Allocation malloc/calloc/realloc for dynamic memory Memory Growth realloc expands; free must match each alloc Leak Accumulation 30 bytes/req → crash after 12 hours Detection Tools Valgrind, AddressSanitizer, static analysis Prevention free after use, RAII patterns, code review ⚠ Forgetting to free after realloc loses old pointer Always store realloc result in a temp pointer, check for NULL THECODEFORGE.IO
thecodeforge.io
C Memory Leak — 30 Bytes/Req Crashed at 12h
Memory Management C

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#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

Stack vs Heap — Quick Comparison Table

When deciding where to place your data, it helps to see the key characteristics side by side. This table distills the most important differences between stack memory and heap memory in C.

FeatureStackHeap
Allocation/DeallocationAutomatic — done by the compiler when functions are called and returnExplicit — you must call malloc/calloc/realloc and free
SizeFixed at program start (typically 1–8 MB per thread)Limited by available RAM (can grow to gigabytes)
SpeedFast — simple pointer manipulationSlower — requires system call overhead and allocation bookkeeping
LifetimeTied to function scope — dies when the function exitsLives until explicitly freed, or until program exit
FlexibilitySize must be known at compile time (or use C99 VLAs, which still live on stack)Size can be determined at runtime (user input, file contents, etc.)
Data persistenceCannot return a pointer to local data — it's invalid after function returnsPointers to heap data remain valid as long as you don't free them
FragmentationNone — stack is contiguousExternal fragmentation possible — allocator may waste space
Thread safetyEach thread has its own stack — inherently thread-safeHeap is shared — allocations need synchronization (mutex) in multithreaded code
RiskStack overflow (silent corruption or crash)Memory leaks, double-free, use-after-free, out-of-memory

When to use each: - Use the stack for small, fixed-size local variables, function parameters, and anything that doesn't need to outlive its function. It's fast, safe, and automatic. - Use the heap for large buffers, data structures that grow dynamically (linked lists, dynamic arrays, trees), and any data that must survive the function that created it.

The code below shows a concrete example of each allocation type and how to handle them correctly.

stack_vs_heap_table.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>

// A simple function that uses both stack and heap

void demonstrate_both() {
    // Stack allocation: small, fixed size, automatic
    int local_array[5] = {10, 20, 30, 40, 50};
    printf("Stack array at %p: ", (void*)local_array);
    for(int i = 0; i < 5; i++) printf("%d ", local_array[i]);
    printf("\n");

    // Heap allocation: runtime size, explicit lifetime
    size_t count = 5;
    int *heap_array = malloc(count * sizeof(int));
    if (heap_array == NULL) {
        fprintf(stderr, "Heap allocation failed");
        return;
    }
    for(size_t i = 0; i < count; i++) {
        heap_array[i] = (int)(i * 100);
    }
    printf("Heap array at %p: ", (void*)heap_array);
    for(size_t i = 0; i < count; i++) printf("%d ", heap_array[i]);
    printf("\n");

    free(heap_array);
    heap_array = NULL;
}

int main() {
    demonstrate_both();
    return 0;
}
Output
Stack array at 0x7ffd5a1b3c40: 10 20 30 40 50
Heap array at 0x55c8f6e3a2a0: 0 100 200 300 400
Remember: Stack Dies With Function
If you return a pointer to a stack variable from a function, the pointer becomes dangling the moment the function returns. The heap is the only way to return dynamically-sized or long-lived data safely.
Production Insight
In production services, stack size is rarely a concern unless you use deep recursion or large VLAs. Heap fragmentation, on the other hand, can silently degrade performance over weeks — monitor with mallinfo() or malloc_stats().
Key Takeaway
Stack is fast, automatic, and limited. Heap is slower, manual, and scalable. Choose based on lifetime, size predictability, and performance needs.

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#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

Memory Function Signatures Reference Table

Every C programmer should have the exact prototypes of the four dynamic memory functions committed to muscle memory. Here they are, with their required header and a quick explanation of each argument.

FunctionPrototypeHeaderDescription
mallocvoid *malloc(size_t size);<stdlib.h>Allocates size bytes of uninitialised memory. Returns pointer to allocated memory, or NULL on failure.
callocvoid *calloc(size_t nmemb, size_t size);<stdlib.h>Allocates memory for an array of nmemb elements, each of size bytes. All bytes are zero-initialised. Returns pointer or NULL. Also checks for integer overflow in nmemb * size.
reallocvoid realloc(void ptr, size_t new_size);<stdlib.h>Changes the size of the memory block pointed to by ptr to new_size bytes. The contents up to the minimum of old and new size are preserved. Returns pointer to new block (may differ from old) or NULL on failure. If ptr is NULL, behaves like malloc. If new_size is 0, behaviour is implementation-defined (commonly like free).
freevoid free(void *ptr);<stdlib.h>Deallocates the memory block pointed to by ptr that was previously allocated by malloc, calloc, or realloc. If ptr is NULL, no operation is performed.

Important details to remember: - All functions return void *, which means they can be assigned to any pointer type without an explicit cast in C (unlike C++). However, always assign to a pointer of the correct type after checking for NULL. - The size_t type is an unsigned integer defined in <stddef.h>. Guaranteeing that sizes fit in size_t is your responsibility — calloc's overflow check is a safety net, not a guarantee of success. - realloc(ptr, 0) is implementation-defined: it may free the block and return NULL, or it may return a unique pointer (C standard allows both). Avoid relying on this behaviour — use free(ptr) explicitly. - free(NULL) is safe and does nothing. This is why setting pointers to NULL after free is so effective: later calls to free become harmless.

The code below shows a complete example that uses all four signatures correctly, including error handling.

function_signatures.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    // 1. malloc — allocate 10 integers, no initialisation
    int *arr = malloc(10 * sizeof(int));
    if (arr == NULL) {
        fprintf(stderr, "malloc failed\n");
        return 1;
    }

    // 2. calloc — allocate 10 integers, zero-initialised
    int *zero_arr = calloc(10, sizeof(int));
    if (zero_arr == NULL) {
        fprintf(stderr, "calloc failed\n");
        free(arr);
        return 1;
    }
    // zero_arr[0] is guaranteed 0

    // 3. realloc — grow arr to 20 integers, preserving first 10
    int *tmp = realloc(arr, 20 * sizeof(int));
    if (tmp == NULL) {
        // arr still valid; handle error
        fprintf(stderr, "realloc failed, original arr preserved\n");
        free(arr);
        free(zero_arr);
        return 1;
    }
    arr = tmp;  // safe assignment

    // 4. free — release both allocations
    free(arr);
    free(zero_arr);
    arr = NULL;
    zero_arr = NULL;

    return 0;
}
Output
(No output — all allocations and deallocations succeeded)
Memory Functions Are in
Always include <stdlib.h> for malloc, calloc, realloc, and free. Including <malloc.h> is non-standard and not portable — stick to <stdlib.h>.
Production Insight
Knowing the exact signatures is crucial when you need to wrap these functions for custom memory tracking. For example, you can interpose malloc with dlsym(RTLD_NEXT, "malloc") to log every allocation — but you must match the exact prototype to avoid ABI mismatch.
Key Takeaway
Memorize the four prototypes: malloc(size), calloc(count, size), realloc(ptr, new_size), free(ptr). Always check for NULL after allocation and use the temporary pointer pattern with realloc.

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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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

The Cost of Allocation — Why Every malloc Is a System Call Gamble

Memory allocation in C isn't free. Every call to malloc or calloc is a system call that requests memory from the kernel. That call costs CPU cycles, context switches, and cache misses. In hot code paths—like game loops, network request handlers, or real-time audio buffers—a single malloc can tank your latency budget.

For example, a classic production pipeline for a video game spawns enemy objects each frame. Calling malloc per enemy is a rookie mistake. The kernel has to service the request via mmap or sbrk, which may zero memory or acquire a mutex in the heap's free list. That's microseconds you don't have per frame. Java and Python hide this with garbage collection pauses; C gives you the control to avoid it entirely.

Why does this matter? Because the alternative is memory pools—pre-allocate a slab of memory once and carve it into fixed-size chunks. That turns allocation into a simple pointer increment. No syscall, no lock, no surprise. You want speed and determinism? That's how you get it.

MemoryPoolExample.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — c-cpp tutorial

#include <stdlib.h>
#include <stdio.h>

#define POOL_SIZE 1000
#define BLOCK_SIZE 64

static char memory_pool[POOL_SIZE * BLOCK_SIZE];
static int next_free = 0;

void* pool_alloc() {
    if (next_free >= POOL_SIZE) {
        fprintf(stderr, "Pool exhausted!\n");
        return NULL;
    }
    return &memory_pool[next_free++ * BLOCK_SIZE];
}

void pool_free_all() {
    next_free = 0;  // Reset, not free()
}

int main() {
    void* p1 = pool_alloc();
    void* p2 = pool_alloc();
    printf("Pool blocks: %p, %p\n", p1, p2);
    pool_free_all();
    return 0;
}
Output
Pool blocks: 0x601060, 0x6010a0
Production Trap:
A malloc inside a hot loop can cause thread contention and heap fragmentation. Profile with perf or Valgrind before assuming it's fast. Memory pools are your friend.
Key Takeaway
Each malloc is a potential syscall; pre-allocate with pools for predictable performance.

Alignment — The Hidden Landmine That Corrupts Your Data

Every type has an alignment requirement. An int wants a 4-byte boundary; a double wants an 8-byte boundary; a 16-byte SSE vector wants 16. When you allocate with malloc, it guarantees the pointer is aligned for the largest scalar type on your platform (usually 8 or 16 bytes). But custom allocators often skip this guarantee.

How does this bite you? Say you implement a slab allocator that returns offsets from a char array. On x86, unaligned access might work but cost 2x to 3x the CPU cycles. On ARM, it triggers a trap and your program crashes. Now your production server reboots every hour. Congrats, you've just learned why posix_memalign exists.

Real-world fix: align your pools to at least 16 bytes. Use alignas(16) in C++ or __attribute__((aligned(16))) in C. Check the pointer value after every custom alloc: if ((uintptr_t)ptr % 16 != 0) abort();. That abort will surface in your crash logs faster than silent data corruption.

AlignmentCheck.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// io.thecodeforge — c-cpp tutorial

#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>

void* aligned_malloc(size_t size, size_t alignment) {
    void* raw = malloc(size + alignment + sizeof(void*));
    if (!raw) return NULL;
    uintptr_t aligned = ((uintptr_t)raw + alignment + sizeof(void*)) & ~(alignment - 1);
    ((void**)aligned)[-1] = raw;  // store offset for free
    return (void*)aligned;
}

void aligned_free(void* ptr) {
    free(((void**)ptr)[-1]);
}

int main() {
    void* buf = aligned_malloc(1024, 16);
    printf("Aligned buffer: %p (mod 16 = %lu)\n", buf, (uintptr_t)buf % 16);
    aligned_free(buf);
    return 0;
}
Output
Aligned buffer: 0x1b72010 (mod 16 = 0)
Senior Shortcut:
Test alignment with ((uintptr_t)ptr % alignof(max_align_t) == 0) inside an assertion. Ship a debug build with that—it will catch custom allocator bugs the minute they happen.
Key Takeaway
Alignment isn't optional; always align to at least 16 bytes in custom allocators.

Stop Using malloc/free in C++ — Here's Why

You're writing C++. Stop pretending it's C with classes. malloc and free have no business in modern C++ code. They don't call constructors or destructors. That means your objects start life uninitialized and die without cleanup. Leaked resources, dangling pointers, undefined behavior — the whole mess.

malloc returns void* that you manually cast. One wrong cast and you're corrupting memory. free doesn't run destructors — your std::vector never releases its heap buffer, your mutex stays locked. Production systems crash on this daily.

Use new/delete for single objects, new[]/delete[] for arrays. Better yet, skip all of it. std::make_unique and std::make_shared allocate and construct in one shot. No casts, no leaks, no forgotten destructors. The compiler enforces correctness. Your future self — and your on-call rotation — will thank you.

MallocVsNew.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// io.thecodeforge — c-cpp tutorial

#include <iostream>
#include <string>

struct Config {
    std::string name;
    int timeout;
    Config() : name("default"), timeout(30) {
        std::cout << "Config constructed\n";
    }
    ~Config() { std::cout << "Config destroyed\n"; }
};

int main() {
    // malloc — constructor never called
    Config* cfg1 = (Config*)malloc(sizeof(Config));
    std::cout << cfg1->name << "\n";  // UB: garbage string

    // new — constructor called
    Config* cfg2 = new Config();
    std::cout << cfg2->name << "\n";  // "default"

    delete cfg2;   // destructor runs
    free(cfg1);    // destructor skipped — leak
    return 0;
}
Output
Config constructed
default
Config destroyed
Production Trap:
malloc + std::string = silent corruption. The string's internal pointer is garbage until the constructor runs. You won't crash in testing — only at 3 AM under load.
Key Takeaway
Never use malloc/free in C++. Use new/delete, or better, std::make_unique and std::make_shared.

new/delete Correctness — One Rule That Saves You

Here's the only rule you need: match new with delete, new[] with delete[]. Mix them and you get undefined behavior — corrupted heap metadata, crashes at free, silent memory corruption that surfaces three sprints later.

new[] allocates extra bytes to store the array count. delete[] reads that count to run destructors on every element. delete just frees the block without calling destructors. That's a leak. Worse, delete[] on a single object reads garbage as the count — it will try to destroy 100 million random objects before crashing.

The fix is trivial — never use new or new[] directly. Use std::vector for arrays. Use std::make_unique for single objects. Raw new/delete belong in legacy code and interview questions. Production code deserves RAII wrappers that make mismatched deletes impossible at compile time.

NewDeleteMismatch.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// io.thecodeforge — c-cpp tutorial

#include <iostream>

struct Sensor {
    int id;
    ~Sensor() { std::cout << "Sensor " << id << " destroyed\n"; }
};

int main() {
    // WRONG: new[] but delete (not delete[])
    Sensor* arr = new Sensor[3];
    arr[0].id = 1;
    arr[1].id = 2;
    arr[2].id = 3;
    delete arr;  // UB: only first destructor might run

    // RIGHT: use std::vector
    // std::vector<Sensor> sensors(3);
    // RAII handles everything

    return 0;
}
Output
Sensor 1 destroyed
(undefined, may crash or leak)
Senior Shortcut:
Enable -Wmismatched-new-delete in GCC/Clang. It catches these at compile time. Pair it with AddressSanitizer for runtime verification.
Key Takeaway
Match new[] with delete[] — or eliminate both by using std::vector and std::make_unique.

Placement new — Constructing Objects on Pre-Allocated Memory

Placement new is not an allocator. It constructs an object at a specific memory address you already own, bypassing heap allocation entirely. This is critical when you need deterministic construction in shared memory, memory-mapped I/O, or custom pools. The syntax is new (address) Type(args). You must manually call the destructor to clean up, never use delete on a placement new address because delete also calls operator delete which frees memory you didn't allocate from the heap. Placement new separates memory allocation from object construction. Use it when allocation overhead is unacceptable or memory is externally managed. The hidden cost is increased manual lifetime control. You must know exactly when to call destructors, or you leak resources inside the object. It's powerful, but one wrong destructor call corrupts your entire memory pool. Reserve placement new for performance-critical systems where every allocation counts.

PlacementNew.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — c-cpp tutorial

#include <new>
#include <cstdlib>

struct Sensor {
    int id;
    Sensor(int i) : id(i) {}
    ~Sensor() { /* cleanup */ }
};

int main() {
    void* raw = std::malloc(sizeof(Sensor));
    Sensor* s = new (raw) Sensor(42);
    // use s...
    s->~Sensor();  // manual destructor
    std::free(raw);
    return 0;
}
Production Trap:
Never call delete on a placement new pointer. delete calls operator delete, which frees heap memory. You already own that memory — it will double-free and corrupt your allocator.
Key Takeaway
Placement new constructs objects on pre-existing memory. You must call the destructor manually and free the underlying memory separately.

Mixing new/delete with malloc()/free() — Undefined Behavior Waiting to Happen

You cannot mix malloc/free with new/delete on the same memory block. It's undefined behavior. malloc allocates raw bytes, new allocates memory and constructs objects. free releases raw memory with no destructor call, delete calls destructors then releases memory. If you malloc then delete, the object's destructor runs on memory that was never constructed, corrupting internal state. If you new then free, the destructor never runs, leaking resources inside the object. Both patterns crash or silently corrupt data. Even new and delete[] must match arrays correctly — use delete[] for array allocations. The rule is simple: pair malloc with free, new with delete, new[] with delete[]. Cross-pairing breaks the C++ object model and memory manager assumptions. Production code reviews catch this immediately. Treat any mix as an instant bug.

BadMix.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — c-cpp tutorial

#include <cstdlib>

struct Data {
    int* buffer;
    Data() { buffer = new int[100]; }
    ~Data() { delete[] buffer; }
};

int main() {
    Data* d = (Data*)std::malloc(sizeof(Data));
    // d->Data();   // constructor NOT called
    // std::free(d); // okay, but leak buffer

    Data* e = new Data;
    std::free(e);   // BUG: destructor never runs
    return 0;
}
Output
undefined behavior — memory leak and possible corruption
Production Trap:
Mixing malloc/free with new/delete on the same pointer is undefined behavior. The C++ standard does not require any diagnostic. It silently corrupts memory or crashes later.
Key Takeaway
Never cross-use malloc/free and new/delete. Always pair allocation and deallocation functions exactly.

C++ new Expression — How It Differs from malloc

The new expression in C++ is not a function call but a language-level operator that integrates memory allocation with object construction. Unlike malloc, which only allocates raw bytes, new calls the constructor of the class immediately after allocating memory. This guarantees that objects are initialized to a valid state before use. Internally, new invokes operator new (which may call malloc) and then the constructor. If the constructor throws, new automatically deallocates the memory, preventing leaks. The array form new[] also tracks the number of elements to call the destructor for each one. This is fundamentally safer than malloc because it ensures construction and destruction happen correctly. When you write auto p = new MyClass(), you get a fully initialized object. With malloc, you would need placement new to achieve the same, inviting mistakes. Always prefer new over malloc in C++ code to leverage RAII and exception safety from the start.

ConstructionSafety.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// io.thecodeforge — c-cpp tutorial
#include <new>
#include <cstdlib>
#include <iostream>

struct Resource {
    Resource() { std::cout << "ctor\n"; }
    ~Resource() { std::cout << "dtor\n"; }
};

int main() {
    auto* p = new Resource();  // alloc + ctor
    delete p;                  // dtor + dealloc

    void* raw = std::malloc(sizeof(Resource));
    Resource* q = new(raw) Resource();  // placement new
    q->~Resource();                     // manual dtor
    std::free(raw);                     // manual dealloc
    return 0;
}
Output
ctor
dtor
ctor
dtor
Production Trap:
Never mix new with free() or malloc with delete. Either pair results in undefined behavior because the memory allocator metadata differs. Stick to new/delete in C++.
Key Takeaway
Use new in C++ to guarantee construction and automatic cleanup—never fall back to malloc.

Practical Examples — Real-World Memory Management Patterns

Consider a video processing pipeline that reads frames and applies filters. Using raw new/delete leads to manual cleanup and leaks when exceptions occur. A safer pattern uses RAII wrappers like std::vector for contiguous buffers and std::unique_ptr for single objects. For example, a FilterPipeline class stores a heap-allocated kernel via std::unique_ptr<Kernel> and a frame buffer via std::vector<uint8_t>. When the pipeline goes out of scope, destructors automatically free the memory. Another pattern is custom memory pools: for thousands of small allocations (e.g., particle systems), pre-allocate a large block via operator new[] and hand out slices with placement new. This avoids repeated system calls and fragmentation. Always prefer standard containers and smart pointers over raw pointers unless performance profiling proves a bottleneck. Debug builds with sanitizers (ASan, UBSan) catch mismatches early. Write unit tests that exercise allocation-heavy paths to verify no leaks with tools like Valgrind.

FilterPipeline.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — c-cpp tutorial
#include <memory>
#include <vector>
#include <cstdint>

struct Kernel { float data[9]; };

class FilterPipeline {
    std::unique_ptr<Kernel> kernel_;
    std::vector<uint8_t> framebuf_;
public:
    FilterPipeline() 
        : kernel_(std::make_unique<Kernel>())
        , framebuf_(1024 * 768 * 3) {}
    // Destructor auto-frees memory
};

int main() {
    FilterPipeline pipe;  // no risk of leak
    return 0;
}
Output
(no output — all memory freed on exit)
Production Trap:
For high-frequency allocations, never use per-object new. Instead, pool memory in a vector or use a custom allocator to reduce fragmentation and performance spikes.
Key Takeaway
Wrap dynamic allocations in RAII objects like smart pointers and containers—they eliminate manual leaks and exception safety issues.
● Production incidentPOST-MORTEMseverity: high

The Silent Leak That Took Down a Payment Gateway

Symptom
Payment 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.
Assumption
The team assumed the leak was in a third-party library or the database connection pool — both common culprits in production.
Root cause
A 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.
Fix
Added 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 guideCommon symptoms and targeted actions for malloc/free failures5 entries
Symptom · 01
Segmentation fault (SIGSEGV) when reading a pointer
Fix
Check if pointer was freed (use-after-free) or never allocated. Use AddressSanitizer (-fsanitize=address -g). If not available, add assert(ptr != NULL) before dereference.
Symptom · 02
Program crashes with 'double free or corruption'
Fix
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.
Symptom · 03
Memory usage grows linearly over time with no plateau
Fix
Use 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.
Symptom · 04
Heap corruption: data gets overwritten unexpectedly
Fix
Enable debugging heap: link with -lmcheck on Linux or MALLOC_CHECK_=3 environment variable. Run under AddressSanitizer to catch buffer overflows.
Symptom · 05
realloc returns NULL, but program continues with old pointer lost
Fix
Immediately check for the anti-pattern ptr = realloc(ptr, new_size). Replace with temporary pointer pattern. Always preserve original until realloc succeeds.
★ Quick Memory Debug Cheat SheetDiagnose and fix C memory issues in under 60 seconds
Detect memory leaks during development
Immediate action
Compile 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 now
Run 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 action
Run 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 now
Focus on 'definitely lost' blocks. The stack trace shows exactly which line malloc'd.
Double free or invalid free detected+
Immediate action
Run 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 now
Set pointer to NULL immediately after free. Add conditional: if (ptr) { free(ptr); ptr = NULL; }
Use-after-free: reading freed memory+
Immediate action
Enable 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 now
Review the code around the reported freed site. Ensure pointer is NULL after free and no other references survive.
malloc vs calloc vs realloc vs free
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

1
malloc is for raw allocation
fast but uninitialised. Always initialise before reading, or use calloc if you need guaranteed zeroes.
2
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.
3
realloc must be assigned to a temporary pointer first
assigning directly back to the original loses your data if it fails and returns NULL.
4
Setting a pointer to NULL immediately after free costs nothing and prevents an entire class of use-after-free bugs
make it muscle memory.
5
Every dynamic memory allocation must be paired with a matching free
in long-running processes, even small leaks are catastrophic.

Common mistakes to avoid

4 patterns
×

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 PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What is the difference between malloc and calloc, and when would you cho...
Q02SENIOR
What happens if you call free() twice on the same pointer, and how do yo...
Q03SENIOR
If realloc fails and returns NULL, what happens to the original memory b...
Q01 of 03JUNIOR

What is the difference between malloc and calloc, and when would you choose one over the other?

ANSWER
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.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between malloc and calloc in C?
02
Is it safe to call free() on a NULL pointer in C?
03
What is a memory leak in C and how do I detect one?
04
How do I safely grow a dynamically allocated array in C?
05
What are the most common memory management bugs in C and how do I avoid them?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's C Basics. Mark it forged?

16 min read · try the examples if you haven't

Previous
Structures and Unions in C
11 / 17 · C Basics
Next
File Handling in C