aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Game Server

C++ Memory Management: How to Prevent Leaks

If you write a game server or any service meant to stay up for days, C++ memory management will sooner or later catch up with you. A process that runs flawlessly for hours quietly crashes at midnight because of a slowly growing memory leak. The good news: with modern C++ you can eliminate most of these problems while you write the code, never leaving them to runtime. In this article I walk through hunting leaks with RAII, smart pointers and Valgrind, step by step.

The problem: why manual new/delete hurts

In classic C++ you manage memory by hand: allocate with new, return it with delete. Simple in theory, dangerous in practice, because for every new you must guarantee a matching delete on every path — early returns, thrown exceptions and nested conditions make that almost impossible to keep straight.

void process() {
    Connection* c = new Connection();
    if (!c->open()) {
        return; // LEAK: delete c was never called
    }
    c->send();
    delete c;
}

If the connection isn't open the function returns early and the allocation is never freed. A single return is enough; a thrown exception makes it worse. The insidious part is that such bugs don't crash immediately — memory usage climbs slowly and you only notice in production.

RAII: tie the resource to an object's lifetime

The core idea of modern C++ is RAII (Resource Acquisition Is Initialization). You acquire a resource (memory, file, socket, lock) in an object's constructor and release it in its destructor. When the object leaves scope — normally, via return, or by exception — the destructor runs automatically and cleanup is guaranteed.

  • Deterministic: cleanup happens exactly when the object is destroyed, with no garbage collector to wait for.
  • Exception safe: destructors run during stack unwinding, so resources don't escape even when code throws.
  • Local reasoning: you don't have to hunt for where a resource is freed; you only inspect the lifetime of its owner.

Most of the standard library already relies on RAII: std::vector, std::string and std::lock_guard all clean up their resources in their destructors. You should follow the same pattern for your own resources.

Smart pointers: encode ownership in the code

Instead of raw new/delete, use the standard smart pointers. They apply RAII to pointers and make your ownership intent explicit in the type.

std::unique_ptr expresses sole ownership: only it owns the pointed-to object and automatically deletes it when it leaves scope. It cannot be copied, only moved. In most cases this is the right default.

#include <memory>

void process() {
    auto c = std::make_unique<Connection>();
    if (!c->open()) {
        return; // fine: the memory is freed when c is destroyed
    }
    c->send();
} // c is cleaned up automatically here

std::shared_ptr is for shared ownership; it keeps a reference count and frees the memory once the last owner is gone. It has a cost (an atomic counter) and should be used only when you truly need multiple owners. std::weak_ptr observes a shared_ptr without taking ownership and solves the cyclic reference problem: if two objects hold each other through shared_ptr the count never reaches zero and memory leaks — making one a weak_ptr breaks the cycle.

Prefer std::make_unique and std::make_shared when creating objects: they're shorter, exception safe, and in the make_shared case combine the control block and the object into a single allocation, making it a bit faster.

Hunting leaks with Valgrind

Even with rigorous RAII, third-party C libraries, legacy code or subtle bugs can leave leaks. On Linux the best-known way to catch them is Valgrind's memcheck tool. It runs your program on a virtual machine and tracks every memory access.

First compile with debug symbols (-g) and optimizations off so line numbers stay accurate:

g++ -g -O0 -o server main.cpp
valgrind --leak-check=full --show-leak-kinds=all ./server

The headings to watch in the output:

  • definitely lost: a real leak — no pointer to this memory exists anymore. Fix these first.
  • indirectly lost: memory inside a leaked structure; fixing the root usually clears these too.
  • still reachable: memory not freed but still reachable at exit. Often harmless (e.g. globals that live for the program's lifetime) but still worth a look.

Valgrind also reports reads of uninitialized memory and access to freed memory (use-after-free); these are even more dangerous than leaks because they cause crashes and security holes. Adding a Valgrind test run to continuous integration catches leaks before they reach production.

Sanitizers: a fast, development-time alternative

Valgrind is thorough but slow. In the daily development loop the compiler-based AddressSanitizer (ASan) and LeakSanitizer are much faster. They're enabled with a single flag in GCC and Clang:

g++ -g -fsanitize=address -fno-omit-frame-pointer -o server main.cpp
./server

ASan reports use-after-free, buffer overflows and leaks at runtime, with far less slowdown than Valgrind. The two don't fully replace each other: sanitizers for everyday testing and Valgrind for deep auditing make a good combination.

Practical habits

  • In new code, almost never write new/delete directly; leave ownership to unique_ptr/shared_ptr and containers.
  • Express ownership in parameter types: a function taking a unique_ptr says it takes ownership; a raw pointer should mean only a "borrowed" view.
  • Use std::vector instead of manual allocation for arrays; it manages size and lifetime.
  • Don't lock and unlock by hand; leave it to RAII with std::lock_guard or std::scoped_lock.

Frequently Asked Questions

If I use smart pointers, do I still need Valgrind?

Yes. Smart pointers prevent most ownership bugs but don't cover C APIs, manual resources and logic errors. Valgrind or AddressSanitizer measure the code's actual behavior and catch what slips through.

Is shared_ptr always safe?

No. Cyclic references leak memory, and because counter updates are atomic there's a performance cost. Use unique_ptr when sole ownership is enough, and weak_ptr to break cycles.

Does unique_ptr have a runtime cost?

In practice it's negligible. With the default deleter, unique_ptr is usually as efficient as a raw pointer and uses no extra memory; it only buys you compile-time safety.

Need a stable, leak-free C++ server? I can help with memory safety and profiling for game servers and performance-critical services. Get in touch and let's talk about your project.

Bu kategorideki tüm yazılar →

Devamı için