CMD Simulator
Memory Managementmemory leak

Memory Leaks in C - Complete Guide to Detection and Prevention

Learn what memory leaks are in C, why they occur, how to detect them with Valgrind and MemC, and best practices to prevent leaks. Every malloc needs a matching free.

Rojan Acharya··Updated Mar 20, 2026
Share

A memory leak in C occurs when you allocate heap memory with malloc, calloc, or realloc but never call free() to release it. The program loses the ability to access that memory, and the operating system cannot reclaim it until the process exits. In long-running programs—servers, daemons, embedded systems—memory leaks cause gradual memory exhaustion, slowdowns, and eventual crashes.

Whether you're learning C for systems programming, preparing for technical interviews, or debugging a production service, understanding memory leaks is essential. This guide covers what causes leaks, how to detect them with Valgrind and MemC, practical prevention patterns, troubleshooting common mistakes, and frequently asked questions.

This comprehensive guide covers memory leak fundamentals, detection tools, prevention strategies, real-world examples, troubleshooting tips, and best practices. By the end, you'll confidently identify and fix memory leaks in C code.

What Is a Memory Leak in C?

In C, the heap is a region of memory used for dynamic allocation. When you call malloc(100), the allocator reserves 100 bytes on the heap and returns a pointer. That memory remains allocated until you call free(ptr). A memory leak happens when you allocate memory but never free it—either because you forget, lose the pointer, or exit early without cleaning up.

Unlike garbage-collected languages (Java, Python, Go), C does not automatically reclaim heap memory. The programmer is responsible for every malloc having a matching free. Leaks are invisible at runtime: the program continues to run, but available memory shrinks over time. In servers or long-running processes, this leads to out-of-memory (OOM) kills, swap thrashing, and degraded performance.

Memory leaks differ from use-after-free (accessing freed memory) and double-free (calling free() twice on the same pointer). Those are undefined behavior and often cause immediate crashes. Leaks are silent and cumulative—they waste resources without obvious symptoms until the system runs out of memory.

How Memory Allocation Works in C

malloc, calloc, realloc

FunctionPurposeInitialization
malloc(size)Allocate size bytesUninitialized (garbage)
calloc(count, size)Allocate count * size bytesZero-initialized
realloc(ptr, size)Resize existing allocationPreserves old data, new bytes uninitialized

Each allocation returns a pointer. You must store that pointer to call free() later. If you overwrite the pointer without freeing first, the original block is leaked.

The Golden Rule

Every malloc (or calloc, or realloc) must have exactly one matching free. No more, no less. The pointer passed to free() must be the exact value returned by the allocator—not an offset, not a copy, not NULL.

Common Causes of Memory Leaks

1. Forgetting to Call free()

The most direct cause: you allocate memory and never free it.

void process_data() {
    int* arr = malloc(100 * sizeof(int));
    // ... use arr ...
    return;  // Leak: arr never freed
}

Fix: Call free(arr) before every return path.

2. Losing the Pointer

You overwrite the pointer before freeing, so the original block becomes unreachable.

int* p = malloc(100);
p = malloc(200);  // Leak: first 100 bytes lost
free(p);          // Only frees second block

Fix: Free the first block before reassigning: free(p); p = malloc(200);

3. Early Return or Break

You allocate, then hit an early return or break without freeing.

int* buf = malloc(1024);
if (error) return -1;  // Leak
// ...
free(buf);

Fix: Use goto cleanup, or restructure so every path reaches free(buf).

4. Exception-like Control Flow

In code that uses setjmp/longjmp or similar, you might jump over free() calls. Ensure cleanup runs on all exit paths.

5. Circular References or Complex Ownership

In data structures with shared ownership (e.g., reference counting), missing a release can leak entire subgraphs. Design ownership clearly and document who frees what.

Detecting Memory Leaks

Valgrind (Linux, macOS with limitations)

Valgrind's Memcheck tool detects leaks when your program exits:

valgrind --leak-check=full ./your_program

Output includes:

  • Definitely lost: Blocks with no pointer to them (direct leak)
  • Indirectly lost: Blocks reachable only from definitely lost blocks
  • Still reachable: Blocks with valid pointers at exit (often acceptable)

Focus on "definitely lost" and "indirectly lost" first.

MemC – Visual Memory Leak Detection

MemC is an educational C interpreter that visualizes memory. It shows the heap after each statement and reports leaks when the program ends with unfreed blocks. Use it to see exactly which allocations remain and why—ideal for learning and debugging small programs.

// Try this in MemC - it will report a leak
int main() {
    int* p = malloc(4 * sizeof(int));
    p[0] = 42;
    return 0;  // p never freed
}

AddressSanitizer (ASan)

Compile with -fsanitize=address for runtime checks. ASan primarily catches use-after-free and buffer overflows, but some configurations help with leak detection. Combine with Valgrind for comprehensive analysis.

Static Analysis

Tools like Clang's static analyzer (scan-build) and Coverity can flag potential leaks by analyzing control flow. They may produce false positives but help catch obvious mistakes early.

Examples: Correct vs Leaky Code

Example 1: Simple Array

Leaky:

int main() {
    int* arr = malloc(10 * sizeof(int));
    arr[0] = 1;
    return 0;
}

Correct:

int main() {
    int* arr = malloc(10 * sizeof(int));
    if (!arr) return -1;
    arr[0] = 1;
    free(arr);
    return 0;
}

Example 2: Early Return

Leaky:

int load_config(const char* path) {
    char* buf = malloc(4096);
    FILE* f = fopen(path, "r");
    if (!f) return -1;  // Leak: buf not freed
    size_t n = fread(buf, 1, 4096, f);
    fclose(f);
    // ... process buf ...
    free(buf);
    return 0;
}

Correct:

int load_config(const char* path) {
    char* buf = malloc(4096);
    if (!buf) return -1;
    FILE* f = fopen(path, "r");
    if (!f) {
        free(buf);
        return -1;
    }
    size_t n = fread(buf, 1, 4096, f);
    fclose(f);
    // ... process buf ...
    free(buf);
    return 0;
}

Example 3: Reallocation

Leaky:

int* p = malloc(100);
p = realloc(p, 200);  // If realloc fails, p is lost!

Correct:

int* p = malloc(100);
int* q = realloc(p, 200);
if (!q) {
    free(p);  // Original block still valid
    return -1;
}
p = q;

Example 4: Loop Allocation

Leaky:

for (int i = 0; i < 1000; i++) {
    int* block = malloc(1024);
    // use block
}  // 1000 leaks

Correct:

for (int i = 0; i < 1000; i++) {
    int* block = malloc(1024);
    if (!block) break;
    // use block
    free(block);
}

Example 5: Function Returning Allocated Memory

Document ownership: The caller must free.

char* read_line(FILE* f) {
    char* buf = malloc(256);
    if (!buf) return NULL;
    if (!fgets(buf, 256, f)) {
        free(buf);
        return NULL;
    }
    return buf;  // Caller owns it, must free
}

// Usage:
char* line = read_line(stdin);
if (line) {
    printf("%s", line);
    free(line);
}

Example 6: Linked List Node

Leaky:

void free_list(Node* head) {
    while (head) {
        head = head->next;  // Leak: current node not freed
    }
}

Correct:

void free_list(Node* head) {
    while (head) {
        Node* next = head->next;
        free(head);
        head = next;
    }
}

Common Use Cases and Patterns

  1. Temporary buffers – Allocate at start of function, free before every return. Use goto cleanup for complex control flow.

  2. Dynamic arrays – When growing with realloc, always check return value and free original on failure.

  3. String duplicationstrdup returns malloc'd memory; caller must free.

  4. Data structures – Each node/block allocated must be freed. Implement destroy or free_* functions that recursively free children.

  5. Error paths – Every if (error) return must free what was allocated so far.

  6. Long-running servers – Leaks are critical. Use Valgrind in CI, run leak tests, and monitor process memory over time.

  7. Libraries – Document ownership: "Caller must free the returned pointer" or "Library owns it, do not free."

  8. Plugins or callbacks – If your code allocates and passes to a callback, clarify who frees. Avoid double-free and leaks.

  9. Caches – Eviction logic must free evicted entries. Test cache growth under load.

  10. Parsers – Each parsed object (AST node, config entry) needs a corresponding free. Use a single cleanup function.

  11. Networking – Buffers for each request/response must be freed after use. Watch for leaks in connection handlers.

  12. GUI or event loops – Objects created per event must be freed when the event is done. Avoid accumulating allocations across events.

Tips and Best Practices

  1. Initialize pointers to NULLint* p = NULL; makes it easier to check before free and avoids double-free of uninitialized pointers.

  2. Set pointer to NULL after freefree(p); p = NULL; prevents use-after-free if you accidentally use p again.

  3. Use a single cleanup pathgoto cleanup with one free per resource reduces mistakes.

  4. Allocate and free in the same scope – Prefer allocating at the start of a function and freeing before every return.

  5. Document ownership – In function comments, state who frees returned pointers.

  6. Use static analysis – Run scan-build or similar in CI to catch leaks early.

  7. Test with Valgrind – Add a Valgrind leak check to your test suite for critical code paths.

  8. Avoid unnecessary allocation – Use stack or static buffers when size is known and small.

  9. Consider memory pools – For many small same-sized allocations, a pool can reduce overhead and simplify cleanup (free pool once).

  10. RAII-like patterns – In C, use "init" and "cleanup" function pairs; always call cleanup.

  11. Check malloc returnif (!p) { handle_error(); return; } before using p.

  12. Realloc safety – Never do p = realloc(p, size) without checking for NULL and preserving the original pointer for free.

Troubleshooting Common Issues

Valgrind Reports "Still Reachable"

Problem: Valgrind reports blocks "still reachable" at exit.

Cause: Your program exits with valid pointers to heap blocks. This might be intentional (e.g., global caches) or a leak if those blocks should have been freed.

Solution: If the blocks should be freed, add cleanup code (e.g., atexit handler) to free them. If intentional, you can suppress "still reachable" in Valgrind, but prefer freeing when possible.

Leak in Third-Party Library

Problem: Valgrind reports leaks in code you didn't write.

Cause: The library may have intentional "still reachable" allocations, or it may have real leaks.

Solution: Check the library's documentation. Some libraries expect the process to exit without freeing (e.g., for speed). If it's a real leak, report it upstream or wrap the library with your own cleanup.

Intermittent Leaks Under Load

Problem: Leaks appear only when handling many requests or under stress.

Cause: A code path that allocates but doesn't free is only exercised under load (e.g., error handling, rare branches).

Solution: Write stress tests that trigger those paths. Run under Valgrind with the test. Use --leak-check=full --show-leak-kinds=all for details.

Realloc Causing Confusion

Problem: After realloc, you're unsure whether to free the old or new pointer.

Cause: realloc can return a new pointer (and free the old block) or the same pointer (in-place resize). You must use the return value.

Solution: Always use the return value: new_ptr = realloc(old_ptr, size). If new_ptr is NULL, old_ptr is still valid—free it. If new_ptr is non-NULL, do not free old_ptr (it may already be freed). Use new_ptr from then on.

Double-Free When Freeing Struct with Pointer Members

Problem: You free a struct, then free a member. Or you free the member, then the struct, and the member was already freed elsewhere.

Cause: Unclear ownership. Who frees the nested pointer—the struct owner or someone else?

Solution: Define ownership. Typically the struct owner frees its members, then frees itself. Document this. Avoid sharing pointers between structs without clear ownership.

Related Concepts

Use-After-Free

Use-after-free is accessing memory after free(). It's undefined behavior and often causes crashes. Memory leaks are the opposite—never freeing. Both stem from poor pointer discipline. Fix leaks without introducing use-after-free by setting pointers to NULL after free.

Double-Free

Calling free() twice on the same pointer is undefined behavior. It can corrupt the allocator's metadata. Always set the pointer to NULL after free and check for NULL before free if the same pointer might be freed from multiple places.

Stack vs Heap

Stack memory is automatic; heap memory is manual. Leaks only apply to heap allocations (malloc, calloc, realloc). Understanding the difference helps you choose when to use each.

Smart Pointers (C++)

In C++, std::unique_ptr and std::shared_ptr automate freeing. C has no equivalent; you must manage manually. The discipline you learn in C transfers to understanding smart pointer semantics.

Garbage Collection

Languages like Go, Java, and Python use GC to reclaim unreachable memory. C does not. If you're used to GC, C requires explicit free for every malloc.

Frequently Asked Questions

What is a memory leak in C?

A memory leak in C occurs when you allocate heap memory with malloc, calloc, or realloc but never call free() to release it. The memory remains allocated for the life of the process and cannot be reused.

How do I find memory leaks in C?

Use Valgrind (valgrind --leak-check=full ./program) on Linux. It reports definitely lost, indirectly lost, and still reachable blocks at exit. For learning and small programs, use MemC to visualize heap allocations and see leaks as they occur.

Is it OK to not free memory before exit?

For short-lived programs that exit immediately, the OS reclaims all process memory. For long-running programs (servers, daemons), not freeing causes gradual memory exhaustion and must be fixed.

Does free() set the pointer to NULL?

No. free(ptr) releases the memory but does not change ptr. The pointer still holds the old address (dangling). Set ptr = NULL after free to avoid use-after-free.

What happens if I free the same pointer twice?

Double-free is undefined behavior. The program may crash, corrupt the heap, or create security vulnerabilities. Always set the pointer to NULL after free.

Can I free a NULL pointer?

Yes. free(NULL) is a no-op and safe. It's common to write if (p) free(p); p = NULL; for safety.

Who should free memory returned by a function?

The function's documentation should specify. Typically, if a function returns a pointer to allocated memory, the caller must free it. The function transfers ownership to the caller.

How do I prevent memory leaks in C?

Ensure every malloc has a matching free on all code paths. Use goto cleanup for complex control flow. Set pointers to NULL after free. Run Valgrind regularly. Consider static analysis in CI.

What is the difference between a memory leak and use-after-free?

A memory leak is never freeing allocated memory. Use-after-free is accessing memory after it has been freed. Leaks waste memory; use-after-free causes undefined behavior and often crashes.

Does Valgrind work on macOS?

Valgrind support on macOS is limited (especially on Apple Silicon). Use Linux or WSL for full Valgrind support. On macOS, consider AddressSanitizer and static analysis.

How do I free a linked list?

Traverse the list, save the next pointer, free the current node, then advance. Example: while (head) { Node* next = head->next; free(head); head = next; }

What about realloc and memory leaks?

If realloc fails, it returns NULL and the original block remains allocated. You must free it. If realloc succeeds, do not free the original pointer—use the new one. Always assign the return value to a temporary, check for NULL, then update your pointer.

Quick Reference Card

ScenarioAction
malloc at start of functionfree before every return
realloc failsfree original pointer
realloc succeedsUse new pointer, do not free old
After free(p)Set p = NULL
Function returns allocated pointerCaller must free
Linked list/node structureFree each node, save next before free
Early return with resourcesUse goto cleanup or free in each branch
free(NULL)Safe, no-op
Double-freeUndefined behavior—never do it
Valgrind "definitely lost"Fix: add missing free
MemC leak reportUnfreed blocks at program end

Try MemC to Visualize Memory Leaks

See memory leaks in action with MemC, an interactive C memory visualizer. Type C code, step through execution, and watch the heap. MemC reports leaks when your program ends with unfreed blocks—perfect for learning and debugging.

Explore the Leak Demo example to see a classic malloc-without-free leak. Then try fixing it by adding the missing free().

Visit the MemC Learn section for a quick overview of memory leaks, or browse all MemC concepts including stack vs heap, pointers, use-after-free, and double-free.

Summary

Memory leaks in C happen when you allocate heap memory with malloc, calloc, or realloc but never call free(). Unlike garbage-collected languages, C requires manual memory management. Every allocation needs exactly one matching free on all code paths.

Common causes include forgetting to free, losing the pointer by overwriting it, early returns that skip cleanup, and unclear ownership in data structures. Detect leaks with Valgrind (--leak-check=full) and visualize them with MemC. Prevent leaks by using a single cleanup path (e.g., goto cleanup), setting pointers to NULL after free, and documenting ownership of returned pointers.

For long-running programs, leaks are critical—they cause gradual memory exhaustion and crashes. Master the malloc-free discipline, run Valgrind in your test suite, and apply these patterns consistently. Your future self (and your production systems) will thank you.