Menu

C++ Akıllı İşaretçiler: unique_ptr ve shared_ptr Açıklaması

Akıllı işaretçiler heap belleğinin sahibidir ve onu otomatik olarak serbest bırakır. unique_ptr, shared_ptr, make_unique ve make_shared'i öğren; ayrıca neden bir daha neredeyse hiç new/delete yazmaman gerektiğini.

Bu sayfada çalıştırılabilir editörler var - düzenle, çalıştır ve sonucu anında gör.

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.

Coddy programming languages illustration

Coddy ile kodlamayı öğren

BAŞLA