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/deletepatterns - 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[...]+throwon parse error → many buffers leaked. - LRU cache:
map<Key, Value*>evicts witheraseonly → objects not deleted. - Callbacks:
new Callback()registered, never unregistered → leak.
Table of contents
- How
newanddeletework - Five dangerous patterns
- Real leak cases
- Examples and detection
- Detection tools
- Debugging practice
- Common leak patterns
- Errors and fixes
- Best practices
- Prevention: smart pointers
1. How new and delete work
What new does
- Allocate with
operator new - Call constructor
- Return pointer—you must
deleteexactly once (or use smart pointers).
What delete does
- Call destructor
- 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
- Confirm RSS grows over time (
top,ps) - Run Valgrind/ASan with representative workload
- Jump to reported line, fix ownership
- Re-run until clean
7. Common patterns
- Factory returning
T*→ returnunique_ptr<T> - Exception between
new/delete→ RAII mapof pointers →unique_ptrvalues or explicit delete on eraseshared_ptrcycles →weak_ptr
8. Errors and fixes
definitely lost: add matchingdeleteor smart pointerinvalid free/ double free: one owner, one deleteheap-use-after-free: lifetime bug—fix ordering, useshared_ptr/weak_ptr- Mismatched
free/delete: pairnew↔delete,new[]↔delete[],malloc↔free
9. Best practices
| Principle | Bad | Good |
|---|---|---|
| Allocation | new T | make_unique<T>() |
| Arrays | new T[n] | vector<T> or make_unique<T[]>(n) |
| Ownership | return new T() | return make_unique<T>() |
| Containers | vector<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.
Related posts
- 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/deleteare risky—prefer smart pointers and containers- Valgrind + ASan catch leaks and heap bugs
deletethennullptrhelps 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
newhas an owner; every path frees or uses RAII
References
Related posts
- Stack vs heap — recursion crash
- Valgrind guide
- RAII