Akıllı İşaretçilerin Çözdüğü Sorun
Önceki sayfada belleği new ile ayırdın ve delete ile serbest bıraktın. Bu işe yarar, ama yükü sana yükler: her new'in karşılık gelen bir delete'e ihtiyacı vardır; bu, kodun her yolunda, hatta yarı yolda bir istisnanın fırlatıldığı yollarda bile geçerlidir. Birini kaçırırsan bellek sızdırırsın; delete'i iki kez çalıştırırsan heap'i bozarsın.
Akıllı işaretçiler bunu, heap belleğinin ömrünü yığındaki normal bir nesneye bağlayarak çözer. O nesne kapsam dışına çıktığında, yıkıcısı senin yerine delete'i çalıştırır; bir istisna yığını geri sarsa bile bu garantilidir. Bu fikre RAII (Resource Acquisition Is Initialization) denir ve akıllı işaretçiler <memory> başlığında bulunur.
*p ve p->member'i tıpkı bir ham işaretçide olduğu gibi kullanırsın. Fark, asla delete çağırmamandır; bunu akıllı işaretçi yapar.
unique_ptr: Tek Sahip, Paylaşım Yok
unique_ptr, varsayılan olarak başvurman gereken akıllı işaretçidir. Münhasır sahipliği temsil eder: her an nesneye tam olarak bir unique_ptr sahiptir ve o işaretçi öldüğünde nesne de onunla birlikte ölür. Ham bir işaretçiye kıyasla sıfır çalışma zamanı maliyeti vardır.
Bir tane oluşturmak için make_unique (C++14) kullan. Yapıcının argümanlarını alır ve sana kullanıma hazır bir işaretçi verir:
Yalnızca bir sahip olabileceği için bir unique_ptr kopyalanamaz. Onu kopyalamaya çalışmak bir derleme hatasıdır ve bu hata, dilin seni iki sahibin aynı nesneyi delete etmeye çalışmasından koruyor olmasıdır:
auto a = make_unique<int>(10);
auto b = a; // error: call to deleted copy constructor of unique_ptr
Sahipliği başkasına devretmek için onu std::move ile taşırsın. Taşımadan sonra orijinal işaretçi boş kalır (nullptr tutar):
Çoğu zaman isteyeceğin model budur: her zaman tam olarak bir net sahip vardır ve derleyici bunu zorunlu kılar.
shared_ptr: Referans Sayımıyla Paylaşılan Sahiplik
Bazen programının birkaç parçasının gerçekten aynı nesneyi paylaşması gerekir ve hiçbiri en son hangisinin biteceğini bilmez. İşte shared_ptr bunun içindir. Bir referans sayısı tutar: her kopya sayıyı artırır, her yıkım azaltır ve nesne yalnızca sayı sıfıra ulaştığında serbest bırakılır.
Bunları make_shared ile oluştur:
unique_ptr'in aksine, bir shared_ptr'i kopyalamak sorun değildir; zaten bütün mesele budur. Bunun bedeli maliyettir: referans sayısı heap üzerinde saklanır ve atomik olarak (iş parçacığı güvenli) güncellenir, bu yüzden shared_ptr, unique_ptr'den daha ağırdır. Yalnızca sahiplik gerçekten paylaşıldığında ona başvur; neyin sahibinin kim olduğunu düşünmekten kaçınmak için değil.
make_shared ayrıca shared_ptr<T>(new T(...))'dan daha verimlidir: nesneyi ve kontrol bloğunu iki yerine tek bir ayırmada tahsis eder.
weak_ptr ve Referans Döngülerini Kırmak
shared_ptr'in klasik bir tuzağı vardır: eğer iki nesne birbirine shared_ptr tutuyorsa, referans sayıları asla sıfıra ulaşmaz, dolayısıyla hiçbiri serbest bırakılmaz; akıllı işaretçiler kullanmana rağmen bir bellek sızıntısı.
struct Node {
shared_ptr<Node> next; // iki düğüm birbirini gösterirse,
}; // birbirlerini sonsuza kadar canlı tutarlar
Çözüm weak_ptr'dir: bir shared_ptr'in sahip olmayan bir gözlemcisi. Referans sayısını artırmaz, bu yüzden asla bir nesneyi canlı tutmaz. Nesneyi kullanmak için .lock() çağırırsın; bu, nesne hâlâ varsa sana bir shared_ptr, çoktan gitmişse boş bir tane verir.
weak_ptr'i "geri işaretçiler" ve önbellekler için kullan; bir nesnenin sahipliğini iddia etmeden ona referans vermek istediğin her yer için.
Sık Yapılan Hatalar ve Tuzaklar
Akıllı işaretçiler bellek hatalarının çoğunu ortadan kaldırır, ama birkaç tuzak kalır:
Aynı belleğin akıllı ve ham sahipliğini karıştırma. Aynı ham işaretçiden asla iki akıllı işaretçi oluşturma; her biri onu delete etmeye çalışacaktır:
int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw); // felaket: ikisi de aynı int'i siler (çift serbest bırakma)
İşte bu yüzden make_unique/make_shared'i tercih edersin; yanlış kullanılacak başıboş bir ham işaretçi olmaz.
Bir unique_ptr yalnızca taşınabilir, bu yüzden sahipliği devretmek için onu değere göre geçir. Bir fonksiyon nesneyi kullanmalı ama sahiplenmemeli ise, bunun yerine düz bir referans veya ham bir T* al; yalnızca gözlemleyen ham bir işaretçi gayet uygundur:
void consume(unique_ptr<int> p); // sahipliği alır (içine taşı)
void observe(int* p); // sadece bakar, hiçbir şeye sahip olmaz
Varsayılan olarak shared_ptr'e başvurma. Serbestçe kopyalandığı için cazip gelir, ama atomik referans sayımı gerçek performans maliyetine yol açar ve paylaşılan sahiplik ömürler üzerine akıl yürütmeyi zorlaştırır. Varsayılanın unique_ptr olsun; yalnızca gerçekten birden çok sahip gerektiğinde shared_ptr'e yükselt.
Diziler için unique_ptr'in dizi biçimine ihtiyacı vardır. make_unique<int[]>(n), sana delete[]'i doğru şekilde çağıran bir unique_ptr<int[]> verir. Pratikte, dinamik diziler için std::vector'ı tercih et; belleği senin yerine yönetir ve üstüne boyut takibi sağlar.
Sonraki: Stringler
Artık bellek yönetimi kontrol altında: akıllı işaretçiler sana sızıntılar olmadan heap ayırma verir. Ayırıp ortalıkta dolaştıracağın en yaygın şeylerden biri metindir ve C++ sana ham char* tamponlarından çok daha güvenli bir araç sunar. Sonraki sayfa std::string'i ele alır: kendi kendine nasıl büyüdüğünü, her gün kullanacağın işlemleri ve seni manuel bellek işinden tamamen nasıl kurtardığını.
Sıkça Sorulan Sorular
C++'ta akıllı işaretçiler nedir?
Akıllı işaretçiler, <memory> başlığından gelen ve bir ham işaretçiyi saran nesnelerdir (unique_ptr, shared_ptr, weak_ptr); kapsam dışına çıktıklarında belleği otomatik olarak delete ederler. Sana manuel delete ve onu unutmaktan kaynaklanan sızıntılar olmadan heap üzerinde ayırma imkânı verirler.
unique_ptr ile shared_ptr arasındaki fark nedir?
unique_ptr nesnesinin tek sahibidir; kopyalanamaz, yalnızca taşınabilir ve öldüğü anda belleği serbest bırakır. shared_ptr ise referans sayımı yoluyla paylaşılan sahipliğe izin verir: birden çok shared_ptr aynı nesneyi gösterebilir ve nesne yalnızca sonuncusu yok edildiğinde serbest bırakılır. Gerçekten paylaşılan sahiplik gerekmedikçe unique_ptr'i tercih et.
Modern C++'ta make_unique mi yoksa new mi kullanmalıyım?
make_unique ve make_shared kullan. Nesneyi tek bir adımda ayırıp sararlar, bu yüzden sonucu bir akıllı işaretçiye ulaşmadan sızabilecek ham bir new olmaz. Genel bir kural olarak, modern bir C++ kod tabanında neredeyse hiç açıkta new veya delete bulunmamalıdır.