Finding memory leaks with Valgrind and Top

c++ memory leaks valgrind top debugging linux

In this post, I’m going to cover my go-to workflow for tracking down memory leaks in C++. If your program’s memory usage is climbing and it shouldn’t be, two tools will get you to the answer fast: Top tells you if you’re leaking, and Valgrind tells you where.

Every new or malloc in C++ needs a matching delete/delete[] or free. Miss one and memory silently accumulates — your program works fine until it doesn’t. These are my notes to myself for the next time I need to debug one.

Table of Contents


Spotting the Problem with Top

Top shows running Linux processes and their resource usage. By itself it’s noisy — processes jump around and it’s hard to keep track of the one you want:

Top displaying Linux Processes

We can use the -p switch and pass Top the PID to watch just our process:

Top displaying just one

Finding the PID each time is repetitive, so we can extract it from the process list automatically:

top -p $(ps -aux | grep mem_leak | head -n 1 | awk '{print $2}')

Now press E to cycle the memory display units (KB, MB, GB). Watch the RES column — if it’s growing over time, you have a leak.

Top displaying different memory format

Top confirms a leak exists, but it can’t tell you where in the code it is. That’s where Valgrind comes in.


Pinpointing the Leak with Valgrind

Valgrind instruments your binary at runtime to track every allocation and deallocation. Its default tool, memcheck, reports what was allocated but never freed.

There are two levels of detail. Running valgrind on its own gives you a summary — how many bytes leaked:

valgrind ./mem_leak missing_delete

Valgrind displaying memory leak

Running with --leak-check=full reveals the exact file and line where the leaked memory was allocated, plus the stack trace:

valgrind --leak-check=full ./mem_leak missing_delete

Valgrind displaying culprit and stack trace

After fixing the leak, running Valgrind again displays the happy news:

Valgrind no leaks

Reading the Output

The key sections to look at:

==12345== HEAP SUMMARY:
==12345==   in use at exit: 1,025 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, ...

The stack trace under definitely lost is the gold — it shows the exact function and line that allocated the leaked memory.


Common Leak Patterns

Once Valgrind points you to a line, you still need to understand why it leaked. These are the patterns that come up over and over.

Missing delete — The Obvious One

The simplest case. You new something and never free it:

// Leak
int main() {
    bool* arr = new bool[1025];
    // ... use arr ...
    return EXIT_SUCCESS;  // arr leaked — no delete[]
}
// Fix — just add the matching delete
int main() {
    bool* arr = new bool[1025];
    // ... use arr ...
    delete[] arr;
    return EXIT_SUCCESS;
}

Prevention: Prefer std::unique_ptr or std::make_unique — the destructor handles cleanup automatically:

auto arr = std::make_unique<bool[]>(1025);
// ... use arr.get() ...
// unique_ptr frees on scope exit

Lost Pointers — Reassignment Without Freeing

You overwrite a pointer with a new allocation, and the old one becomes unreachable:

// Leak — original 256 bytes are orphaned
char* buffer = new char[256];
buffer = new char[512];   // old pointer is gone
delete[] buffer;           // only frees the 512 allocation
// Fix — free before reassigning
char* buffer = new char[256];
delete[] buffer;
buffer = new char[512];
// ...
delete[] buffer;

Prevention: Reassigning a unique_ptr automatically frees the previous value:

auto buffer = std::make_unique<char[]>(256);
// old allocation freed automatically
buffer = std::make_unique<char[]>(512);

Early Return in Error Paths

The allocation and the delete are both there, but an early return skips the cleanup. This one is sneaky because the happy path looks correct:

// Leak — if fopen fails, buffer is never freed
bool processFile(const std::string& filename) {
    char* buffer = new char[4096];
    FILE* file = fopen(filename.c_str(), "r");

    if (!file) {
        return false;  // buffer leaked!
    }

    // ... process file ...
    delete[] buffer;
    fclose(file);
    return true;
}
// Fix — RAII ensures cleanup regardless of return path
bool processFile(const std::string& filename) {
    auto buffer = std::make_unique<char[]>(4096);
    FILE* file = fopen(filename.c_str(), "r");

    if (!file) {
        return false;  // unique_ptr cleans up automatically
    }

    // ... process file ...
    fclose(file);
    return true;
}

Prevention: This is the strongest argument for RAII and smart pointers — they handle every exit path, not just the one you remembered.


Containers of Raw Pointers

std::vector::clear() destroys the pointers, not what they point to. The objects are leaked:

// Leak — clear() removes the pointers but doesn't delete the objects
std::vector<Widget*> widgets;
widgets.push_back(new Widget());
widgets.push_back(new Widget());
widgets.push_back(new Widget());
widgets.clear();  // 3 Widgets leaked
// Fix — manually delete before clearing
for (auto* w : widgets) {
    delete w;
}
widgets.clear();

Prevention: Use std::vector<std::unique_ptr<Widget>> — the smart pointers free the objects when the vector is cleared or goes out of scope:

std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());
widgets.push_back(std::make_unique<Widget>());
widgets.clear();  // all Widgets freed automatically

Circular References with shared_ptr

Smart pointers don’t eliminate leaks entirely. If two shared_ptrs point at each other, neither reference count ever reaches zero:

// Leak — A and B keep each other alive forever
struct Node {
    std::shared_ptr<Node> next;
};

auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // circular reference — neither is ever freed
// Fix — break the cycle with weak_ptr
struct Node {
    std::weak_ptr<Node> next;  // doesn't contribute to reference count
};

Prevention: Use std::weak_ptr for back-references or any relationship that shouldn’t keep the object alive.


Valgrind catches all of these. The pattern doesn’t change — run valgrind --leak-check=full, read the stack trace, find the allocation, figure out which category it falls into, and apply the fix.


Caveats and Tips


Check out the companion playground on GitHub — it has runnable examples of each leak pattern, ready for Valgrind.

© 2026 Tessa Power   •  Theme  Moonwalk