İçindekiler
C++ geliştirirken new ile bellek ayırıp işimiz bitince delete çağırmak ilk bakışta oldukça düzenli görünür. Fakat kodun ortasında bir istisna fırlatıldığında kontrol akışı planladığımız yolu izlemez. Temizlik satırına ulaşılamaz ve ayrılan kaynak sahipsiz kalır. Program çalışmaya devam etse bile bellekte küçük bir hayalet dolaşmaya başlar.
``
Sorun yalnızca delete unutmak değildir
Manuel kaynak yönetiminde genellikle kaynağın yaşam süresini zihnimizde takip ederiz: kaynağı edin, kullan ve serbest bırak. İdeal akış şöyle düşünülebilir:
\[T_{yaşam} = T_{serbest\ bırakma} - T_{edinme}\]Ancak bir exception oluştuğunda fonksiyon normal dönüş noktasına ulaşmaz. C++ çalışma zamanı stack unwinding işlemini başlatır; yani çağrı yığınındaki yerel nesneler geriye doğru yok edilir. Ham işaretçinin kendisi yok olur fakat işaret ettiği dinamik nesne otomatik olarak silinmez.
#include <stdexcept>
void veriyiIsle() {
int* sayilar = new int[1000];
sayilar[0] = 42;
throw std::runtime_error("Dosya okunamadı");
delete[] sayilar; // Bu satıra hiçbir zaman ulaşılamaz.
}
Buradaki sızıntının matematiksel özeti basittir:
\[Ayrılan\ Kaynaklar - Serbest\ Bırakılan\ Kaynaklar > 0\]Fonksiyon her çağrıldığında yeni bir blok ayrılır. Hata tekrarlanırsa kayıp miktarı birikir. Tek çağrıdaki sızıntı $M$ bayt ve hatalı çağrı sayısı $n$ ise toplam kayıp yaklaşık $n \times M$ olur.
try-catch ile temizlik yeterli mi?
Kaynağı catch bloğunda temizlemek mümkündür, fakat çözüm hızla kırılganlaşır:
void veriyiIsle() {
int* sayilar = new int[1000];
try {
riskliIslem(sayilar);
} catch (...) {
delete[] sayilar;
throw; // Orijinal exception yeniden gönderilir.
}
delete[] sayilar;
}
Bu kod sızıntıyı önleyebilir; ancak hem normal hem de hatalı yol için aynı temizlik işlemi yazılmıştır. Fonksiyona dosya tanıtıcısı, kilit veya ikinci bir bellek bloğu eklendiğinde olası çıkış yolları çoğalır. Ayrıca yanlış sırada temizlik yapmak ya da bir kaynağı iki kez serbest bırakmak mümkündür.
| Yaklaşım | Exception güvenliği | Bakım maliyeti | Risk |
|---|---|---|---|
Ham işaretçi ve manuel delete |
Düşük | Yüksek | Sızıntı, çift silme |
try-catch ile manuel temizlik |
Orta | Yüksek | Tekrarlı ve kırılgan kod |
| RAII nesnesi | Yüksek | Düşük | Sahiplik daha nettir |
| Akıllı işaretçi | Yüksek | Düşük | Yanlış işaretçi türü seçimi |
Güvenli çözüm: RAII
RAII, kaynağın yaşam süresini bir nesnenin yaşam süresine bağlar. Kullanıcı manuel olarak yıkıcı çağırmaz; kapsam sona erdiğinde yıkıcı dil tarafından otomatik çağrılır. Exception oluşması da bu davranışı bozmaz.
#include <memory>
#include <stdexcept>
void veriyiIsle() {
auto sayilar = std::make_unique<int[]>(1000);
sayilar[0] = 42;
throw std::runtime_error("İşlem başarısız");
}
std::unique_ptr, dizinin tek sahibidir. Stack unwinding sırasında sayilar nesnesinin yıkıcısı çalışır ve dinamik dizi serbest bırakılır. Açık bir delete[] satırına ihtiyaç kalmaz.
Çoğu durumda dinamik dizi için daha doğal seçenek std::vector kullanmaktır:
#include <vector>
void veriyiIsle() {
std::vector<int> sayilar(1000);
sayilar.at(0) = 42;
riskliIslem(sayilar.data());
}
RAII yalnızca bellek için değildir. Dosyalar, mutex kilitleri, soketler ve veritabanı bağlantıları da aynı ilkeyle kapsüllenebilir. Temel kural şudur: Kaynağı edinen nesne, onu bırakmaktan da sorumlu olmalıdır. Böylece exception geldiğinde temizlik bir dilek değil, kapsam mekanizmasının garantisi olur.
Yorumlar