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

C++ Bellek Yönetimi: Sızıntıları Nasıl Önlersiniz

Bir oyun sunucusu veya uzun süre ayakta kalması gereken herhangi bir servis yazıyorsanız, C++ bellek yönetimi er ya da geç karşınıza çıkar. Saatlerce sorunsuz çalışan bir süreç, yavaş yavaş büyüyen bir bellek sızıntısı yüzünden gece yarısı çöker. İyi haber şu: modern C++ ile bu sorunların büyük kısmını daha kod yazarken, çalışma zamanına hiç bırakmadan ortadan kaldırabilirsiniz. Bu yazıda RAII, smart pointer'lar ve Valgrind ile sızıntı avını adım adım anlatıyorum.

Sorun: manuel new/delete neden başını ağrıtır

Klasik C++ kodunda belleği elle yönetirsiniz: new ile ayırır, delete ile iade edersiniz. Teoride basit, pratikte tehlikeli. Çünkü her new için doğru yolda bir delete çağrısı garanti etmek zorundasınız — fonksiyondan erken dönüşler, atılan istisnalar ve iç içe koşullar bunu hızla imkânsızlaştırır.

void islem() {
    Baglanti* c = new Baglanti();
    if (!c->acik()) {
        return; // SIZINTI: delete c çağrılmadı
    }
    c->gonder();
    delete c;
}

Burada bağlantı açık değilse fonksiyon erken döner ve ayrılan bellek asla iade edilmez. Tek bir return bile yeter. İstisna fırlatıldığında durum daha da kötüdür. Bu tür hataların sinsiliği, anında çökmemeleridir; bellek kullanımı yavaşça tırmanır ve sorunu üretimde fark edersiniz.

RAII: kaynağı nesnenin ömrüne bağlamak

Modern C++'ın temel fikri RAII'dir (Resource Acquisition Is Initialization). Mantık şu: bir kaynağı (bellek, dosya, soket, kilit) bir nesnenin yapıcısında edinir, yıkıcısında serbest bırakırsınız. Nesne kapsam dışına çıktığında — normal yolla, return ile ya da istisnayla — yıkıcı otomatik çağrılır ve temizlik garanti edilir.

  • Deterministik: Temizlik tam olarak nesne yok edildiğinde olur, çöp toplayıcı beklemeden.
  • İstisna güvenli: Yığın çözülürken (stack unwinding) yıkıcılar çalışır, yani kod istisna fırlatsa bile kaynak kaçmaz.
  • Yerel akıl yürütme: Bir kaynağın nerede serbest kaldığını aramaya gerek kalmaz; sahibinin ömrüne bakmanız yeter.

Standart kütüphanenin büyük kısmı zaten RAII'ye dayanır: std::vector, std::string, std::lock_guard hepsi kaynaklarını yıkıcılarında temizler. Kendi kaynaklarınız için de aynı deseni izlemelisiniz.

Smart pointer'lar: sahipliği koda yazmak

Çıplak new/delete yerine standart akıllı işaretçileri kullanın. Bunlar RAII'yi işaretçilere uygular ve sahiplik niyetinizi tipte açıkça belirtir.

std::unique_ptr tek sahipliği ifade eder: işaret ettiği nesneyi yalnızca o sahiplenir ve kapsam dışına çıktığında otomatik delete eder. Kopyalanamaz, yalnızca taşınır (move). Çoğu durumda doğru varsayılan budur.

#include <memory>

void islem() {
    auto c = std::make_unique<Baglanti>();
    if (!c->acik()) {
        return; // sorun yok: c yok edilirken bellek iade edilir
    }
    c->gonder();
} // c burada otomatik temizlenir

std::shared_ptr paylaşımlı sahiplik içindir; bir referans sayacı tutar ve son sahip de gidince belleği serbest bırakır. Maliyeti vardır (atomik sayaç) ve gerçekten birden çok sahip gerektiğinde kullanılmalıdır. std::weak_ptr ise sahiplik almadan bir shared_ptr'ı gözlemler ve döngüsel referans sorununu çözer: iki nesne birbirini shared_ptr ile tutarsa sayaç asla sıfıra inmez ve bellek sızar — birini weak_ptr yapmak bu döngüyü kırar.

Nesne oluştururken std::make_unique ve std::make_shared tercih edin: daha kısa, istisna güvenli ve make_shared durumunda kontrol bloğuyla nesneyi tek ayırmada birleştirip biraz daha hızlıdır.

Valgrind ile sızıntı avı

RAII'yi titizlikle uygulasanız bile, üçüncü taraf C kütüphaneleri, eski kod veya ince hatalar sızıntı bırakabilir. Linux'ta bunları yakalamanın en bilinen yolu Valgrind'in memcheck aracıdır. Programınızı bir sanal makinede çalıştırıp her bellek erişimini izler.

Önce hata ayıklama sembolleriyle derleyin (-g), optimizasyonu kapatın ki satır numaraları doğru çıksın:

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

Çıktıda dikkat edilecek başlıklar:

  • definitely lost: Kesin sızıntı — bu belleğe artık hiçbir işaretçi yok. Önce bunları düzeltin.
  • indirectly lost: Sızan bir yapının içindeki bellek; kökü düzeltince genelde bunlar da kaybolur.
  • still reachable: Program sonunda hâlâ erişilebilir olan, serbest bırakılmamış bellek. Çoğu zaman zararsızdır (örneğin program ömrü boyunca yaşayan globaller) ama yine de bakmaya değer.

Valgrind ayrıca tanımsız bellek okumalarını ve serbest bırakılmış belleğe erişimi (use-after-free) de gösterir; bunlar sızıntıdan da tehlikelidir çünkü çökme ve güvenlik açığı doğururlar. Sürekli entegrasyona Valgrind'li bir test koşusu eklemek, sızıntıları üretime ulaşmadan yakalar.

Sanitizer'lar: hızlı, geliştirme zamanı alternatifi

Valgrind kapsamlıdır ama yavaştır. Günlük geliştirme döngüsünde derleyici tabanlı AddressSanitizer (ASan) ve LeakSanitizer çok daha hızlıdır. GCC ve Clang'de bir bayrakla açılır:

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

ASan, use-after-free, tampon taşmaları ve sızıntıları çalışırken bildirir, üstelik Valgrind'den çok daha az yavaşlamayla. İkisi birbirinin yerini tam tutmaz: günlük test için sanitizer, derin denetim için Valgrind iyi bir kombinasyondur.

Pratik alışkanlıklar

  • Yeni kodda new/delete'i neredeyse hiç doğrudan yazmayın; sahipliği unique_ptr/shared_ptr ve konteynerlere bırakın.
  • Sahipliği parametre tiplerinde belli edin: bir unique_ptr alan fonksiyon sahipliği devraldığını söyler; ham işaretçi yalnızca "ödünç" bakış anlamına gelmeli.
  • Diziler için elle ayırma yerine std::vector kullanın; boyut ve ömrü o yönetir.
  • Kilitleri elle açıp kapatmayın; std::lock_guard veya std::scoped_lock ile RAII'ye bırakın.

Sık Sorulan Sorular

Smart pointer kullanırsam Valgrind'e hâlâ gerek var mı?

Evet. Smart pointer'lar sahiplik hatalarının büyük kısmını önler ama C API'leri, manuel kaynaklar ve mantık hatalarını kapsamaz. Valgrind veya AddressSanitizer, kodun gerçek davranışını ölçer ve gözden kaçanları yakalar.

shared_ptr her zaman güvenli midir?

Hayır. Döngüsel referanslar bellek sızdırır ve sayaç güncellemeleri atomik olduğundan performans maliyeti vardır. Tek sahiplik yeterliyse unique_ptr, döngüleri kırmak için weak_ptr kullanın.

unique_ptr'ın çalışma zamanı maliyeti var mı?

Pratikte yok denecek kadar azdır. Varsayılan silici ile unique_ptr genellikle ham işaretçi kadar verimlidir ve ek bellek kullanmaz; size yalnızca derleme zamanında güvenlik kazandırır.

Kararlı, sızıntısız bir C++ sunucusuna mı ihtiyacınız var? Oyun sunucuları ve performans kritik servislerde bellek güvenliği ve profil çıkarma konusunda yardımcı olabilirim. Benimle iletişime geçin ve projenizi konuşalım.

Bu kategorideki tüm yazılar →

Devamı için