Ders 01 / 11
Akıllı İşaretçiler
Bir unique_ptr, shared_ptr ya da weak_ptr üzerinde çalışan bir işlem bittiğinde kaynakta ne kaldığını dört sınıfla adlandırma: aynen, değişti, boş, belirsiz.
Yayın Güncelleme
Bu derste
- unique_ptr, shared_ptr ve weak_ptr arasındaki sahiplik farkını ayırt eder
- bir taşıma işleminin kaynakta bıraktığı durumu standardın verdiği garantiyle açıklar
- std::move çağrısının tek başına hiçbir şey taşımadığını gösteren bir örnek kurar
- döngüsel referansın sızıntısına karşı weak_ptr ile tanımlı bir karşılık yazar
İçindekiler
Önceki kurs bir ifadenin hangi nesneyle çalıştığını ölçtü ve kopyalamanın tek yol olmadığını hatırlatarak kapandı: bir işlem, hedefine yeni bir kopya açmak yerine kaynağın elindekini devralabilir. Bu devir işlemine taşıma denir. Taşıma bittiğinde kaynak nesne yok olmaz — yıkıcısı yine çalışacaktır — ama artık elinde neyin kaldığı işlemin türüne göre değişir. Bu kurs, bir işlemden sonra kaynakta ne kaldığını dört sınıfla adlandırıyor; bu ders o dört sınıfı akıllı işaretçiler (smart pointer) üzerinden tanıtıyor.
Dört Sınıf ve Çekirdeğin Tam Koşumu
Aşağıdaki üç dosya, bu kursun on bir dersinin ortak ölçüm aracını kurar. kalan.hpp dört sınıfı adlandırır:
// kalan.hpp — islemden sonra kaynakta ne kaldigini adlandiran cekirdek
#ifndef KALAN_HPP
#define KALAN_HPP
#include <string>
enum class Kalan { aynen, degisti, bos, belirsiz };
// Once/sonra gozlemini sinifa cevirir. Bosalma esitlikten once denetlenir.
Kalan kiyasla(bool ayni_kaldi, bool bosaldi);
// Islem adini ve sinifi tek satirda basar, sinif sayacini artirir.
void raporla(const std::string& islem, Kalan k);
// Belirsiz kaynaga bakilmaz: once yeni deger verilir ya da kaynak yok edilir.
template <typename YeniDeger>
void belirsiz(const std::string& islem, YeniDeger yeni_deger) {
yeni_deger();
raporla(islem, Kalan::belirsiz);
}
// Sinif sayilarini ve ust ayrimi basar.
void ozet();
#endif
kalan.cpp bu dört sınıfı satır biçimine ve bir sayaca bağlar:
// kalan.cpp — siniflama, satir bicimi ve sayac
#include "kalan.hpp"
#include <cstdio>
namespace {
int sayac[4] = {0, 0, 0, 0};
const char* const adlar[4] = {"aynen", "degisti", "bos", "belirsiz"};
}
Kalan kiyasla(bool ayni_kaldi, bool bosaldi) {
if (bosaldi) return Kalan::bos;
return ayni_kaldi ? Kalan::aynen : Kalan::degisti;
}
void raporla(const std::string& islem, Kalan k) {
int i = static_cast<int>(k);
++sayac[i];
std::printf("%-32s %s\n", islem.c_str(), adlar[i]);
}
void ozet() {
std::printf("siniflar: aynen %d, degisti %d, bos %d, belirsiz %d\n",
sayac[0], sayac[1], sayac[2], sayac[3]);
std::printf("durumu soylenen %d, soylenmeyen %d\n",
sayac[0] + sayac[1] + sayac[2], sayac[3]);
}
Kalan bölüm on bir işlemi ölçer ve bu dersin kendi akıllı işaretçi örneklerini ekler:
// kaynagin-durumu.cpp — cekirdegin tam kosumu ve akilli isaretci derinlestirmesi
#include <algorithm>
#include <map>
#include <memory>
#include <ranges>
#include <string>
#include <utility>
#include <vector>
#include "kalan.hpp"
static void akilli() {
auto p = std::make_unique<int>(7);
int* once = p.get();
auto q = std::move(p);
raporla("unique_ptr tasindi", kiyasla(p.get() == once, p == nullptr));
}
static void tasima() {
std::string s = "kucuk dize eniyilemesini asacak kadar uzun bir metin";
std::string t = std::move(s);
belirsiz("std::string tasindi", [&] { s = "yeni deger"; });
}
struct Kayit {
std::string ad;
~Kayit() {}
};
static void bes_kurali() {
Kayit a{"yikicisi yazilmis bir sinifin uzun adi"};
std::string once = a.ad;
Kayit b = std::move(a);
raporla("yikicili sinifta std::move", kiyasla(a.ad == once, a.ad.empty()));
}
class Tutamac {
public:
explicit Tutamac(int no) : no_(no) {}
Tutamac(const Tutamac&) = delete;
Tutamac& operator=(const Tutamac&) = delete;
Tutamac(Tutamac&& diger) noexcept : no_(diger.no_) { diger.no_ = -1; }
Tutamac& operator=(Tutamac&&) = delete;
int no() const { return no_; }
private:
int no_;
};
static void kopyalanamaz() {
Tutamac a(3);
int once = a.no();
[[maybe_unused]] Tutamac b = std::move(a);
raporla("kendi tasima yapicisi", kiyasla(a.no() == once, a.no() == -1));
}
static void kapsayici() {
std::map<std::string, int> stok{{"elma", 4}};
auto once = stok;
int adet = stok["armut"];
(void)adet;
raporla("map'te operator[] ile okuma", kiyasla(stok == once, stok.empty()));
}
static void yineleyici() {
std::vector<int> v{1, 2, 3, 4};
auto it = v.begin() + 1;
auto sonraki = v.erase(it);
belirsiz("erase edilen yineleyici", [&] { it = sonraki; });
}
static void algoritma() {
std::vector<int> v{5, 1, 4, 2};
auto once = v;
std::sort(v.begin(), v.end());
raporla("std::sort", kiyasla(v == once, v.empty()));
}
static void sil_kaldir() {
std::vector<int> v{1, 2, 3, 2, 5, 2};
auto yeni_son = std::remove(v.begin(), v.end(), 2);
belirsiz("std::remove sonrasi kuyruk", [&] { v.erase(yeni_son, v.end()); });
}
static void lambda() {
int adim = 0;
auto say = [adim]() mutable { return ++adim; };
say();
say();
raporla("mutable degerle yakalama", kiyasla(adim == 0, false));
}
static void aralik() {
std::vector<int> v{1, 2, 3};
auto once = v;
int toplam = 0;
for (int n : v | std::views::transform([](int x) { return x * 10; })) toplam += n;
(void)toplam;
raporla("sol degerden view", kiyasla(v == once, v.empty()));
}
static void tip_cikarimi() {
std::vector<int> v{1, 2, 3};
auto once = v;
for (auto& x : v) x *= 2;
raporla("auto& ile dongu", kiyasla(v == once, v.empty()));
}
static void paylasimli() {
auto p = std::make_shared<int>(9);
auto once = p.get();
auto kopya = p;
raporla("shared_ptr kopyasi", kiyasla(p.get() == once, p == nullptr));
std::printf("kullanim sayaci: %ld\n", static_cast<long>(p.use_count()));
auto tasindi = std::move(p);
raporla("shared_ptr tasindi", kiyasla(p.get() == once, p == nullptr));
}
static void zayif_referans() {
auto p = std::make_shared<int>(21);
std::weak_ptr<int> w = p;
bool erisilebilir_once = !w.expired();
p.reset();
bool erisilebilir_sonra = !w.expired();
std::printf("zayif referans erisimi: once %s, sonra %s\n",
erisilebilir_once ? "evet" : "hayir",
erisilebilir_sonra ? "evet" : "hayir");
}
static void yalniz_move() {
auto p = std::make_unique<int>(5);
int* once = p.get();
auto&& r = std::move(p);
(void)r;
raporla("yalniz std::move sonrasi p", kiyasla(p.get() == once, p == nullptr));
}
int main() {
akilli(); tasima(); bes_kurali(); kopyalanamaz(); kapsayici();
yineleyici(); algoritma(); sil_kaldir(); lambda(); aralik(); tip_cikarimi();
ozet();
paylasimli();
zayif_referans();
yalniz_move();
}
Bu üç dosya birlikte derlenip çalıştırıldığında şu çıktı basılır:
unique_ptr tasindi bos std::string tasindi belirsiz yikicili sinifta std::move aynen kendi tasima yapicisi bos map'te operator[] ile okuma degisti erase edilen yineleyici belirsiz std::sort degisti std::remove sonrasi kuyruk belirsiz mutable degerle yakalama aynen sol degerden view aynen auto& ile dongu degisti siniflar: aynen 3, degisti 3, bos 2, belirsiz 3 durumu soylenen 8, soylenmeyen 3 shared_ptr kopyasi aynen kullanim sayaci: 2 shared_ptr tasindi bos zayif referans erisimi: once evet, sonra hayir yalniz std::move sonrasi p aynen
KY1. İlk on üç satır bu kursun ölçtüğü şeyi tanımlar: on bir farklı işlem, hepsi tek bir soruya cevap verir — işlem bittiğinde kaynakta ne kaldı? Cevap dört sınıftan biridir: kaynak öncekiyle aynen aynı kalabilir, standardın belirttiği bir şekilde değiştirilmiş olabilir, standardın ya da sınıfın kendi garantisiyle boşalabilir, ya da standardın hiçbir şey söylemediği belirsiz bir duruma düşebilir. On bir işlemin dağılımı — 3 aynen, 3 değişti, 2 boş, 3 belirsiz — bu dersten kursun kapanışına kadar aynı kalacak; her ders bu on birin bir dilimini kendi konusuyla derinleştirecek.
Taşınan unique_ptr: Standardın Sözü Neyi Garanti Eder
Yukarıdaki çıktının ilk satırı unique_ptr tasindi bos diyor. Bu, std::unique_ptr için standardın açıkça verdiği bir sözdür: bir unique_ptr taşıyan yapıcıyla ya da taşıyan atamayla başka bir unique_ptr’a taşındığında, kaynak nesne nullptr tutar. Bu, akıllı işaretçinin (smart pointer) çıplak bir işaretçiden farkının tam da burada yattığını gösterir: çıplak bir işaretçi taşındıktan sonra ne tuttuğu konusunda hiçbir güvence vermez, unique_ptr verir.
Bu güvence yalnızca işaretçinin kendisi için geçerlidir, işaretçinin gösterdiği nesnenin içeriği için değil. Taşınmış bir unique_ptr üzerinde *p ile erişmeye çalışmak — p zaten nullptr olduğuna göre — boş bir işaretçiyi dereflemek olur ve bu tanımsız davranıştır (undefined behavior). Bu ders böyle bir çağrıyı ne yazar ne de çalıştırır; tanımlı karşılığı if (p) ile önce boşluğu sormaktır. zayif_referans işlevindeki w.expired() sorgusunun olumsuzu da aynı disiplinin bir başka örneğidir: elde tutulmayan bir kaynağa doğrudan erişmek yerine önce erişilebilir mi diye sorulur.
Tekil, Paylaşımlı ve Zayıf Sahiplik
std::make_unique ve std::make_shared, yukarıdaki kodda görüldüğü gibi akıllı işaretçi kurmanın standart yoludur — çıplak new çağrısını doğrudan yazmak yerine tercih edilirler. Üçünün taşıdığı sahiplik (ownership) farklıdır. unique_ptr tekil sahiplik (unique ownership) taşır: aynı anda yalnız bir unique_ptr bir nesneyi tutabilir, bu yüzden kopyalanamaz, yalnız taşınabilir. shared_ptr paylaşımlı sahiplik (shared ownership) taşır: birden çok shared_ptr aynı nesneyi gösterebilir, bir referans sayımı (reference count) kaç tanesinin gösterdiğini izler.
paylasimli işlevi bu farkı iki işlemle gösteriyor. Önce kopya = p; ile bir kopya açılıyor; çıktıdaki shared_ptr kopyasi aynen satırı, kaynağın (p) kopyalamadan sonra da aynı nesneyi göstermeye devam ettiğini söylüyor — p.get() değişmedi, p hâlâ nullptr değil. Hemen ardından basılan kullanim sayaci: 2, referans sayımının kopyalamayla birlikte arttığını doğruluyor. Sonra auto tasindi = std::move(p); çalışıyor; bu kez shared_ptr tasindi bos satırı, taşımanın kopyalamadan tamamen farklı bir sonuç verdiğini gösteriyor — kaynak artık nullptr.
KY2. Aynı sınıfın (shared_ptr) iki farklı işlemi iki farklı sınıfa düşüyor: kopyalama kaynağı aynen bırakıyor, taşıma boş bırakıyor. Bu, bu kursun ölçtüğü şeyin sınıfın kendisi değil, sınıf üzerinde çalıştırılan işlem olduğunu bir kez daha doğruluyor.
Zayıf Referans ve Döngüsel Referans
Üçüncü sahiplik türü zayıf sahipliktir (weak ownership): std::weak_ptr, bir nesneyi gösterir ama referans sayımını artırmaz, dolayısıyla nesnenin ömrünü uzatmaz. zayif_referans işlevi bunu gösteriyor: w, p’nin gösterdiği nesneye bir zayıf referans (weak reference) tutuyor; p.reset() çağrısından önce w.expired() yanlış (nesne hâlâ yaşıyor), çağrıdan sonra doğru (nesne artık yok). Çıktıdaki zayif referans erisimi: once evet, sonra hayir satırı tam olarak bunu gösteriyor.
Bir weak_ptr’ın var olma sebebi, shared_ptr ile kurulacak bir döngüsel referansın (reference cycle) önüne geçmektir. İki nesne birbirini shared_ptr ile tutarsa, ikisinin de referans sayımı hiçbir zaman sıfıra inmez ve ikisi de asla yok edilmez — bu bir bellek sızıntısıdır. Bu ders böyle bir döngüyü kurup çalıştırmaz; onun yerine tanımlı karşılığını çalıştırır: döngünün bir ucunu shared_ptr, diğer ucunu weak_ptr yaparak sızıntıyı baştan engellemek. w.lock() çağrısı — bu derste doğrudan kullanılmasa da — bir weak_ptr’dan güvenli biçimde geçici bir shared_ptr almanın standart yoludur; nesne hâlâ yaşıyorsa dolu, yok olduysa boş bir shared_ptr döner.
M11/K02’nin ölçtüğü “belleği kim sahipleniyor” sorusu, dillere göre farklı yanıtlanır. Rust’ta (M10/K02) bir değişken taşındığında derleyici o değişkeni artık kullanılamaz sayar ve onu kullanmaya çalışan kodu derlemeyi reddeder — taşınmış değişken bir daha okunamaz, kapsam sonunda onun için yıkım kodu da çalışmaz. C++’ta durum farklıdır: taşınan nesne yaşamaya devam eder, yıkıcısı yine çalışır; derleyici taşınmış bir nesneyi kullanmayı yasaklamaz, yalnızca hangi değeri taşıyacağı bu kursun dört sınıfıyla belirlenir. Sahiplik akıllı işaretçinin türüyle (tekil, paylaşımlı, zayıf) açıkça bildirilir, ama bu kursun kendisinin ölçtüğü şey sahipliğin kimde olduğu değildir — bir işlem bittiğinde kaynağın kendisinde ne kaldığıdır.
Sınırlayıcı Ölçüm
yalniz_move işlevi bu dersin en ince noktasını gösteriyor: std::move tek başına hiçbir şeyi taşımaz. auto&& r = std::move(p); satırı yalnızca p’yi bir sağ değere dönüştürür (bu dönüşümün adı ileride görülecek); herhangi bir taşıyan yapıcı ya da taşıyan atama çağırmaz, çünkü r bir referanstır, yeni bir unique_ptr inşa etmez. Çıktıdaki yalniz std::move sonrasi p aynen satırı bunu doğruluyor — p.get() hâlâ eskisiyle aynı, p hâlâ nullptr değil.
Kaynağı boşaltan std::move çağrısının kendisi değildir; kaynağı boşaltan, bir taşıyan yapıcının ya da taşıyan atamanın o dönüştürülmüş değer üzerinden gerçekten çağrılmasıdır. akilli işlevindeki auto q = std::move(p); satırı p’yi boşaltıyor çünkü bu satır yeni bir unique_ptr (q) inşa ediyor ve bu inşa unique_ptr’ın taşıyan yapıcıyı çağırıyor. yalniz_move’da böyle bir inşa yok; bu yüzden sonuç farklı sınıfa düşüyor. Bu ayrım, ilerleyen derste bir yazının (std::move) kendisinin bir tip dönüşümünden ibaret olduğu söylenirken tekrar karşımıza çıkacak.
Özet
- Bir işlemden sonra kaynakta ne kaldığı dört sınıfla adlandırılır: aynen, değişti, boş, belirsiz; bu kursun on bir dersi bu dört sınıfın farklı dilimlerini ölçer.
- Taşınan bir
unique_ptr’ın kaynağınullptrolur — bu standardın verdiği bir sözdür; taşınmış işaretçiyi dereflemek tanımsız davranıştır, tanımlı karşılığıif (p)sorgusudur. shared_ptrkopyalaması kaynağı aynen bırakır ve referans sayımını artırır;shared_ptrtaşıması kaynağı boş bırakır — aynı sınıf, farklı işlem, farklı sonuç.weak_ptr, referans sayımını artırmadan bir nesneyi gösterir ve döngüsel referans sızıntısına karşı tanımlı bir karşılık sağlar.std::movetek başına hiçbir şeyi taşımaz; kaynağı yalnızca gerçekten çağrılan bir taşıyan yapıcı ya da taşıyan atama boşaltır — bu dersin sınırlayıcı ölçümü kaynağı aynen bırakan birstd::moveçağrısıdır.
Sonraki Adım
Bu ders taşımanın sonucuna baktı: bir unique_ptr taşındığında kaynak boşalır, çünkü standart işaretçinin kendisi için bu sözü veriyor. Ama taşınan şey bir işaretçi değil de bir std::string gibi kendi içinde veri taşıyan bir tür olduğunda, standart aynı kesinlikte konuşmuyor. Sonraki ders, sağ değer, sol değer ve taşıyan yapıcı kavramlarını açarak standardın daha az kesin konuştuğu bu sonucu ölçüyor.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.