Bellek Yönetimi Best Practices ve RAII
C++ sana bellek üzerinde tam kontrol verir. Bu büyük bir güçtür, ama büyük güç büyük sorumluluk getirir. Belleği doğru yönetmezsen programın sızıntı yapar (memory leak), çöker (dangling pointer), veya sessizce yanlış çalışır (undefined behavior).
Bu ders, bellek yönetiminin altın kuralı olan RAII prensibini ve profesyonel C++ kodunda kullanılan best practice'leri öğretecek. Eğer bu prensibi içselleştirirsen, C++'ın en zorlu konularından birini fethetmiş olursun.
RAII — Resource Acquisition Is Initialization
RAII, C++'ın en önemli tasarım prensibidir. İsmi karışık görünse de mantığı basittir:
Bir kaynağı (bellek, dosya, mutex, socket...) edindiğin anda bir nesneye bağla. Nesne yok olduğunda kaynak otomatik serbest bırakılsın.
Bunu bir otel odası gibi düşün. Otele giriş yaptığında (constructor) sana bir oda anahtarı verilir. Otelden çıktığında (destructor) anahtarı iade edersin ve oda temizlenir. Anahtarı kaybetme şansın yok çünkü sistem bunu otomatik yapıyor.
RAII Olmadan
#include <iostream>
void tehlikeliFonksiyon() {
int* veri = new int[1000];
// ... bazi islemler ...
if (/* bir hata olursa */) {
return; // BELLEK SIZINTISI! delete[] cagirilmadi
}
// ... daha fazla islem ...
delete[] veri;
}Bu kodda birden fazla return noktası var ve her birinde delete[] çağırmayı hatırlaman gerek. Exception fırlatılırsa durum daha da kötü — delete[] asla çağrılmaz.
RAII ile
#include <iostream>
#include <memory>
#include <vector>
void guvenlifonksiyon() {
auto veri = std::make_unique<int[]>(1000);
// ... bazi islemler ...
if (/* bir hata olursa */) {
return; // SORUN YOK! unique_ptr otomatik temizler
}
// ... daha fazla islem ...
// Fonksiyon sonunda unique_ptr otomatik delete[] cagirir
}unique_ptr bir RAII sarmalayıcıdır (wrapper). Nesne scope'tan çıktığında — normal dönüş, erken return, exception fark etmez — destructor çalışır ve bellek serbest bırakılır.
💡 RAII sadece bellek için değildir. Dosyalar (
fstream), mutex'ler (lock_guard), veritabanı bağlantıları, soketler — hepsi RAII ile yönetilebilir. Herhangi bir "aç-kapat" veya "edin-serbest bırak" çifti RAII adayıdır.
Kendi Resource Management Sınıfını Yazma
Standart kütüphanede her şey için hazır RAII sarmalayıcı yok. Bazen kendi sınıfını yazman gerekir. Örneğin bir C kütüphanesiyle çalışıyorsun ve kaynağı elle serbest bırakman gerekiyor.
#include <iostream>
#include <cstdio>
#include <stdexcept>
class DosyaYonetici {
public:
DosyaYonetici(const char* dosyaAdi, const char* mod)
: dosya_(std::fopen(dosyaAdi, mod)) {
if (!dosya_) {
throw std::runtime_error("Dosya acilamadi!");
}
}
// Destructor — kaynak serbest birakma
~DosyaYonetici() {
if (dosya_) {
std::fclose(dosya_);
}
}
// Kopyalama yasak
DosyaYonetici(const DosyaYonetici&) = delete;
DosyaYonetici& operator=(const DosyaYonetici&) = delete;
// Tasima izinli
DosyaYonetici(DosyaYonetici&& diger) noexcept
: dosya_(diger.dosya_) {
diger.dosya_ = nullptr;
}
void yaz(const char* metin) {
std::fputs(metin, dosya_);
}
private:
FILE* dosya_;
};
int main() {
try {
DosyaYonetici dosya("test.txt", "w");
dosya.yaz("Merhaba RAII!\n");
// dosya otomatik kapanir
}
catch (const std::exception& e) {
std::cerr << e.what() << "\n";
}
return 0;
}Bu sınıfın üç kritik özelliği var:
Constructor'da kaynak edinilir — dosya açılır
Destructor'da kaynak serbest bırakılır — dosya kapanır
Kopyalama yasak, taşıma izinli — Rule of Five uygulanmış
Rule of Five Hatırlatması
Eğer destructor, copy constructor, copy assignment, move constructor veya move assignment operatörlerinden birini özel olarak tanımlıyorsan, muhtemelen hepsini tanımlaman gerekir. Bu "Rule of Five" kuralıdır.
class KaynakYonetici {
public:
KaynakYonetici(); // constructor
~KaynakYonetici(); // destructor
KaynakYonetici(const KaynakYonetici&) = delete; // copy ctor
KaynakYonetici& operator=(const KaynakYonetici&) = delete; // copy assign
KaynakYonetici(KaynakYonetici&&) noexcept; // move ctor
KaynakYonetici& operator=(KaynakYonetici&&) noexcept; // move assign
};⚠️ Rule of Zero daha iyidir. Eğer tüm üye değişkenlerin RAII türleri (smart pointer, string, vector...) ise, hiçbir özel fonksiyon tanımlamana gerek yok. Derleyici hepsini doğru şekilde üretir. Rule of Five'a ancak ham kaynak (raw pointer, FILE* gibi) yönetiyorsan ihtiyaç duyarsın.
Smart Pointer Kullanım Kuralları — Özet
Smart pointer'ları daha önce öğrendin. Burada hangi durumda hangisini kullanacağını netleştirelim.
unique_ptr — Varsayılan Tercih
Tek bir sahip. Kopyalanamaz, taşınabilir. Bellek maliyeti sıfır (raw pointer ile aynı boyut).
#include <memory>
#include <iostream>
class Motor {
public:
Motor(int guc) : guc_(guc) {
std::cout << "Motor olusturuldu: " << guc_ << " HP\n";
}
~Motor() { std::cout << "Motor yok edildi\n"; }
int guc() const { return guc_; }
private:
int guc_;
};
int main() {
auto motor = std::make_unique<Motor>(200);
std::cout << "Guc: " << motor->guc() << " HP\n";
// Sahiplik devri
auto yeniSahip = std::move(motor);
// motor artik nullptr
return 0;
// yeniSahip scope'tan cikinca Motor yok edilir
}Ne zaman kullanılır? Varsayılan olarak her zaman. Paylaşım gerekmiyorsa unique_ptr yeterlidir.
shared_ptr — Paylaşımlı Sahiplik
Birden fazla sahip. Referans sayacı tutar. Son sahip yok olunca kaynak serbest bırakılır.
#include <memory>
#include <iostream>
int main() {
auto veri = std::make_shared<int>(42);
std::cout << "Referans sayisi: " << veri.use_count() << "\n"; // 1
{
auto kopya = veri;
std::cout << "Referans sayisi: " << veri.use_count() << "\n"; // 2
}
// kopya yok oldu
std::cout << "Referans sayisi: " << veri.use_count() << "\n"; // 1
return 0;
}Ne zaman kullanılır? Gerçekten birden fazla sahibin olması gereken durumlarda. Observer pattern, cache sistemleri, paylaşımlı veri yapıları gibi.
weak_ptr — Döngüsel Referans Kırıcı
shared_ptr ile birlikte kullanılır. Referans sayacını artırmaz. Nesneye "zayıf" bir referans tutar.
#include <memory>
#include <iostream>
struct Dugum {
std::string ad;
std::shared_ptr<Dugum> arkadas; // guclu referans
std::weak_ptr<Dugum> en_yakin; // zayif referans
Dugum(const std::string& a) : ad(a) {}
~Dugum() { std::cout << ad << " yok edildi\n"; }
};
int main() {
auto ali = std::make_shared<Dugum>("Ali");
auto veli = std::make_shared<Dugum>("Veli");
ali->en_yakin = veli; // weak_ptr — dongu olusturmaz
veli->en_yakin = ali; // weak_ptr — dongu olusturmaz
return 0;
// Her ikisi de duzgun temizlenir
}Ne zaman kullanılır? shared_ptr döngüsel referans oluşturacaksa, döngüyü kırmak için weak_ptr kullan. Ayrıca cache'lerde "varsa kullan, yoksa yeniden oluştur" mantığında.
Kısa Karar Tablosu
| Durum | Tercih |
|---|---|
| Tek sahip, standart durum | unique_ptr |
| Paylaşımlı sahiplik gerekli | shared_ptr |
| Döngüsel referans riski var | weak_ptr |
| Performans kritik, sahiplik yok | Raw pointer (gözlemci olarak) |
Memory Leak Detection Stratejileri
Bellek sızıntısı sinsi bir hatadır. Program çalışır, doğru sonuç verir ama zamanla bellek tüketimi artar ve sonunda çöker. Bunu tespit etmenin birkaç yolu var.
Valgrind (Kavramsal)
Valgrind, Linux'ta çalışan güçlü bir bellek analiz aracıdır. Programını Valgrind altında çalıştırdığında, her bellek tahsisini ve serbest bırakma işlemini takip eder.
# Derleme (optimizasyon kapali, debug bilgisi acik)
g++ -g -O0 program.cpp -o program
# Valgrind ile calistirma
valgrind --leak-check=full ./programTipik bir Valgrind çıktısı:
==12345== LEAK SUMMARY:
==12345== definitely lost: 40 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks"definitely lost" — kesin sızıntı. Bir yerde new yapıp delete yapmayı unutmuşsun.
AddressSanitizer (ASan)
AddressSanitizer, derleyiciye entegre bir araçtır. Valgrind'den çok daha hızlıdır (2x yavaşlama vs 20x). Derleme zamanında etkinleştirilir.
g++ -g -fsanitize=address -fno-omit-frame-pointer program.cpp -o program
./programASan şunları tespit eder:
Bellek sızıntıları (memory leak)
Buffer overflow (dizinin dışına yazma)
Use-after-free (serbest bırakılmış belleğe erişim)
Double free (aynı belleği iki kez serbest bırakma)
Stack buffer overflow
// ASan bunu yakalar:
int main() {
int* p = new int(42);
delete p;
*p = 10; // USE AFTER FREE! ASan hata verir
return 0;
}💡 CI/CD pipeline'ına ASan'ı entegre et. Her commit'te testlerin ASan ile çalışması, bellek hatalarını erken yakalamanın en etkili yoludur. Üretim (production) build'inde ASan kapalı olmalı — performans maliyeti vardır.
Common Bellek Hataları ve Çözümleri
1. Memory Leak — Bellek Sızıntısı
// HATALI
void sizinti() {
int* veri = new int[100];
// ... islemler ...
// delete[] unutuldu!
}
// DOGRU — RAII kullan
void guvenli() {
auto veri = std::make_unique<int[]>(100);
// otomatik temizlenir
}Kural: new yazma. make_unique veya make_shared kullan.
2. Dangling Pointer — Sarkan İşaretçi
Serbest bırakılmış belleğe hâlâ işaret eden pointer.
// HATALI
int* tehlike() {
int x = 42;
return &x; // x fonksiyon bitince yok olur!
}
// HATALI
int main() {
int* p = new int(42);
int* q = p; // ayni yeri gosteriyor
delete p;
*q = 10; // DANGLING! p'nin gosterdigi yer serbest birakildi
}Çözüm: Smart pointer kullan. unique_ptr ile sahiplik netleşir, shared_ptr ile birden fazla referans güvenlidir.
3. Double Free — Çift Silme
// HATALI
int main() {
int* p = new int(42);
delete p;
delete p; // DOUBLE FREE! tanimsiz davranis
}Çözüm: Smart pointer kullan. Destructor otomatik çalışır ve sadece bir kez çalışır. Raw pointer kullanmak zorundaysan, delete sonrası nullptr ata.
4. Buffer Overflow — Taşma
// HATALI
int main() {
int dizi[5];
for (int i = 0; i <= 5; i++) { // i <= 5 YANLIS, 5 eleman icin i < 5 olmali
dizi[i] = i; // dizi[5] tasiyor!
}
}
// DOGRU — vector kullan
#include <vector>
int main() {
std::vector<int> dizi(5);
for (int i = 0; i < 5; i++) {
dizi.at(i) = i; // at() sinir kontrolu yapar
}
}Kural: C-style diziler yerine std::vector kullan. İndeks erişiminde at() ile sınır kontrolü yap.
5. Uninitialized Memory — Başlatılmamış Bellek
// HATALI
int main() {
int x; // baslatilmamis!
if (x > 0) { // tanimsiz davranis
// ...
}
}
// DOGRU
int main() {
int x = 0; // her zaman baslat
int y{}; // sifir ile baslatir
}Kural: Değişkenleri her zaman tanımlandıkları yerde başlat. {} (uniform initialization) ile sıfır başlatma yapabilirsin.
⚠️ "Bende çalışıyor" tuzağına düşme. Undefined behavior olan kod bazı derleyicilerde, bazı optimizasyon seviyelerinde çalışıyor gibi görünebilir. Bu, kodun doğru olduğu anlamına gelmez. ASan ve Valgrind ile test et.
Bellek Yönetimi Kontrol Listesi
Her kod review'ında şu soruları sor:
Raw `new` / `delete` var mı? Smart pointer ile değiştir.
Fonksiyondan yerel değişkenin adresi döndürülüyor mu? Dangling pointer riski.
Exception fırlatılırsa kaynaklar temizleniyor mu? RAII kullan.
Kopyalama yasak olması gereken sınıfta kopyalama açık mı?
= deleteile kapat.Move constructor `noexcept` mi? STL container performansı için kritik.
Pointer üye değişkeni null olabilir mi? Kullanmadan önce kontrol et.
Özet
RAII, C++'ın en önemli prensibidir: kaynak edinen nesne, kaynak serbest bırakan nesnedir. Constructor'da edin, destructor'da serbest bırak.
Smart pointer'lar RAII'nin en yaygın uygulamasıdır.
unique_ptrvarsayılan tercih,shared_ptrpaylaşımlı sahiplik,weak_ptrdöngü kırıcıdır.Rule of Zero: Mümkünse özel destructor/copy/move tanımlama — RAII türleri kullan ve derleyiciye bırak. Ham kaynak yönetiyorsan Rule of Five uygula.
Valgrind ve AddressSanitizer, bellek hatalarını tespit eden araçlardır. ASan derleme zamanında etkinleşir ve daha hızlıdır.
En yaygın hatalar: memory leak, dangling pointer, double free, buffer overflow, uninitialized memory. Hepsinin çözümü RAII ve smart pointer'lardır.
`new` yazmaktan kaçın.
make_uniquevemake_sharedkullan. C-style diziler yerinevectorkullan. Bu kadar basit.
AI Asistan
Sorularını yanıtlamaya hazır