C++ Memory Leaks | Real Server Outage Cases and Five Patterns Valgrind Catches

C++ Memory Leaks | Real Server Outage Cases and Five Patterns Valgrind Catches

이 글의 핵심

Practical guide to C++ memory leaks: production stories, dangerous new/delete patterns, and how Valgrind and ASan find and fix them.

Introduction: Friday 5 PM—the server stopped responding

A memory leak took down our server

Two weeks after launch, Friday 5 PM, the server stopped responding. Restart fixed it until every 2–3 hours it froze again.

What we checked:

  • CPU: ~10% (fine)
  • Disk: 50GB free (fine)
  • Memory: 500MB at start → 7.8GB in 3 hours → crash

Cause: memory leak—allocated memory never freed, like a dripping pipe until the tank overflows.

Flow: allocate → miss delete on error paths → leak → OOM.

Fix mindset: RAII ties allocation to object lifetime—like an automatic door that closes when you leave the room. std::unique_ptr and containers own their memory and release on scope exit, so you do not hunt every return for a matching delete.

flowchart LR
  subgraph cause["Cause"]
    N[new without]
    R[return/exception]
    N --> R
    R --> L[missed delete]
  end
  subgraph detect["Detect"]
    V[Valgrind]
    A[AddressSanitizer]
  end
  subgraph fix["Fix"]
    U[unique_ptr]
    RAII[RAII]
  end
  cause --> detect --> fix

Somewhere we new’d and never delete’d; memory grew until the process died.

Buggy pattern (found after 3 days):

void processRequest(const std::string& data) {
    User* user = new User(data);  // heap allocation

    if (!user->isValid()) {
        return;  // no delete — leak!
    }

    user->process();
    delete user;  // only on happy path
}

Fix (paste and build: g++ -std=c++17 -o leak_fix leak_fix.cpp && ./leak_fix):

// Paste after: g++ -std=c++17 -o leak_fix leak_fix.cpp && ./leak_fix
#include <memory>
#include <iostream>
#include <string>

struct User {
    std::string data;
    explicit User(const std::string& d) : data(d) {}
    bool isValid() const { return !data.empty() && data != "invalid"; }
    void process() { std::cout << "processed " << data << "\n"; }
};

void processRequest(const std::string& data) {
    auto user = std::make_unique<User>(data);
    if (!user->isValid()) {
        return;  // automatic cleanup
    }
    user->process();
}

int main() {
    processRequest("hello");
    processRequest("invalid");
    return 0;
}

Output: processed hello only (invalid returns early with no extra output).

Takeaway: Prefer std::unique_ptr / std::make_unique so ownership is clear and every path frees memory. See smart pointers.

After reading:

  • Understand risky new/delete patterns
  • Detect and fix leaks
  • Learn production-style bug stories
  • Use Valgrind and AddressSanitizer

More scenarios

  • Game server: removePlayer() skipped → Player* leaks → OOM hours later.
  • Image batch: new unsigned char[...] + throw on parse error → many buffers leaked.
  • LRU cache: map<Key, Value*> evicts with erase only → objects not deleted.
  • Callbacks: new Callback() registered, never unregistered → leak.

Table of contents

  1. How new and delete work
  2. Five dangerous patterns
  3. Real leak cases
  4. Examples and detection
  5. Detection tools
  6. Debugging practice
  7. Common leak patterns
  8. Errors and fixes
  9. Best practices
  10. Prevention: smart pointers

1. How new and delete work

What new does

  1. Allocate with operator new
  2. Call constructor
  3. Return pointer—you must delete exactly once (or use smart pointers).

What delete does

  1. Call destructor
  2. Release memory with operator delete

Never mix malloc/free with C++ objects

Use new/delete so constructors/destructors run—avoid malloc/free for C++ objects.


2. Five dangerous patterns

1. Double delete

int* ptr = new int(42);
delete ptr;
delete ptr;  // undefined behavior

Fix: delete ptr; ptr = nullptr; or use smart pointers.

2. Dangling pointer

int* ptr1 = new int(42);
int* ptr2 = ptr1;
delete ptr1;
ptr1 = nullptr;
std::cout << *ptr2;  // use-after-free

Fix: shared_ptr or clear ownership rules.

3. Leak on early return

void function() {
    int* ptr = new int(42);
    if (someCondition) {
        return;  // leak
    }
    delete ptr;
}

Fix: std::unique_ptr.

4. delete vs delete[]

int* arr = new int[100];
delete arr;  // wrong — use delete[]

Correct: delete[] array; or std::vector / make_unique<int[]>(n).

5. Exception safety

void processFile(const std::string& filename) {
    char* buffer = new char[1024];
    std::ifstream file(filename);
    if (!file) {
        throw std::runtime_error("File not found");
        // delete[] never runs
    }
    file.read(buffer, 1024);
    delete[] buffer;
}

Fix:

auto buffer = std::make_unique<char[]>(1024);
// ...

3. Real cases

Vector of raw pointers

vector.clear() does not delete pointed-to objects—only vector<unique_ptr<T>> or manual loop.

Conditional returns

Every path that allocates must free—or use RAII/unique_ptr.

Exceptions between new and delete

Use smart pointers or vector so unwinding always calls destructors.


4. Examples and detection

Early-return leak + Valgrind

Compile with -g, run:

valgrind --leak-check=full --show-leak-kinds=all ./leak_early_return

Look for definitely lost and the stack trace.

delete vs delete[] + LeakSanitizer

g++ -fsanitize=address,leak -g -std=c++17 -o leak_array leak_array.cpp
./leak_array

Container of pointers

clear() without deleting each Item* → Valgrind reports many lost blocks.


5. Detection tools

Valgrind

g++ -g program.cpp -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

Interpret definitely lost, Invalid read, etc.

AddressSanitizer (+ LeakSanitizer)

g++ -fsanitize=address,leak -g program.cpp -o program
./program

VS CRT debug heap (Windows)

_CrtSetDbgFlag / leak check on exit—see MSVC docs.


6. Debugging practice

  1. Confirm RSS grows over time (top, ps)
  2. Run Valgrind/ASan with representative workload
  3. Jump to reported line, fix ownership
  4. Re-run until clean

7. Common patterns

  • Factory returning T* → return unique_ptr<T>
  • Exception between new/delete → RAII
  • map of pointers → unique_ptr values or explicit delete on erase
  • shared_ptr cycles → weak_ptr

8. Errors and fixes

  • definitely lost: add matching delete or smart pointer
  • invalid free / double free: one owner, one delete
  • heap-use-after-free: lifetime bug—fix ordering, use shared_ptr/weak_ptr
  • Mismatched free/delete: pair newdelete, new[]delete[], mallocfree

9. Best practices

PrincipleBadGood
Allocationnew Tmake_unique<T>()
Arraysnew T[n]vector<T> or make_unique<T[]>(n)
Ownershipreturn new T()return make_unique<T>()
Containersvector<T*>vector<unique_ptr<T>>

CI with ASan (-fsanitize=address,leak) on PRs is highly recommended.


10. Prevention: smart pointers

unique_ptr fixes leaks, exception paths, and most double-delete issues on single ownership. See next article.


  • Smart pointers
  • Segmentation fault
  • Interview: pointers vs references

Keywords

C++ memory leak, new delete, Valgrind, AddressSanitizer, dangling pointer, double free, delete[], leak detection

Closing

  • new/delete are risky—prefer smart pointers and containers
  • Valgrind + ASan catch leaks and heap bugs
  • delete then nullptr helps avoid double delete (raw pointers)
  • delete[] for arrays
  • Exception safety: RAII

Next: C++ practical guide #6-3: smart pointers

FAQ

When is this useful?

A. Whenever you manage heap memory in C++—servers, games, long-running tools. Use the examples and tool guide above.

What to read first?

A. Stack vs heap and the series index.

Go deeper?

A. cppreference memory, Valgrind and ASan docs.

One-line summary: Replace raw new/delete with smart pointers; use Valgrind and ASan to prove leaks are gone.

Practical tips

Debugging

  • Enable warnings; reproduce with a tiny test case

Performance

  • Profile before optimizing; define metrics

Code review

  • Every new has an owner; every path frees or uses RAII

References


  • Stack vs heap — recursion crash
  • Valgrind guide
  • RAII