← Kursa Dön
📄 Text · 15 min

Smart Pointers

Önceki derste gördük: raw pointer'larla new/delete yönetimi ciddi sorunlara yol açabiliyor. Unutulan delete, çift delete, exception'da sızan bellek... Hepsi birer mayın.

C++11 bu sorunu kökten çözdü: smart pointer'lar. Bunlar, bellek yönetimini otomatikleştiren özel sınıflardır. Bellek ayırırsın, kullanırsın, işin bitince smart pointer kendisi temizler. Manuel delete yok, memory leak yok.


Raw Pointer'ların Sorunları

Neden smart pointer'lara ihtiyacımız olduğunu hatırlayalım:

void sorunluFonksiyon() {
    int* ptr = new int(42);

    if (hataKontrol()) {
        return;  // ptr silinmeden fonksiyon bitiyor — LEAK!
    }

    riskliFonksiyon();  // Exception firlatirsa? — LEAK!

    delete ptr;  // Buraya ulasma garantisi yok
}

Sorunlar listesi:

  • Memory leak — delete'i unutmak veya erişememek

  • Dangling pointer — delete sonrası pointer'ı kullanmak

  • Double delete — aynı belleği iki kez silmek

  • Sahiplik belirsizliği — bu pointer'ı kim silecek?

Smart pointer'lar tüm bu sorunları çözer.


RAII Prensibi

Smart pointer'ları anlamak için önce RAII (Resource Acquisition Is Initialization) prensibini bilmek gerekir.

Otel Odası Analojisi

Bir otele girdiğinde oda kartını alırsın (kaynak edinme). Otelden çıkarken kartı teslim edersin (kaynak bırakma). Oda kartın olduğu sürece oda senindir. Kartı teslim ettiğinde oda temizlenir.

RAII de böyle çalışır: bir nesne oluşturulduğunda kaynak edinilir (constructor), nesne yok edildiğinde kaynak bırakılır (destructor). C++'ın scope kuralları sayesinde bu otomatik olur.

{
    unique_ptr<int> ptr = make_unique<int>(42);
    // ptr kullanilir...
}  // Blok bitince ptr yok olur → destructor calısır → bellek serbest

Hiçbir yerde delete yazmadık. Smart pointer, destructor'ında belleği otomatik olarak geri veriyor. İşte RAII'nin gücü bu.


unique_ptr — Tek Sahiplik

unique_ptr, bir kaynağa tek bir sahip olmasını garanti eder. O pointer yok olduğunda, gösterdiği bellek otomatik silinir. Kopyalanamaz, sadece taşınabilir.

Temel Kullanım

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // make_unique ile olustur (tercih edilen yol)
    unique_ptr<int> ptr = make_unique<int>(42);

    cout << "Deger: " << *ptr << endl;      // 42
    cout << "Adres: " << ptr.get() << endl;  // Ham adres

    // ptr scope sonunda otomatik silinir
    return 0;
}

make_unique<int>(42) heap'te bir int oluşturur ve onu yöneten bir unique_ptr döndürür. delete yazmana gerek yok.

Neden make_unique?

// Yol 1: Dogrudan new ile (eski usul)
unique_ptr<int> p1(new int(42));

// Yol 2: make_unique ile (modern, guvenli)
auto p2 = make_unique<int>(42);

make_unique tercih edilir çünkü:

  • Exception güvenlidir

  • Daha okunabilir

  • new yazmandan kurtarır

Kopyalanamaz, Taşınabilir

unique_ptr'nin "tek sahiplik" demek olduğunu söyledik. Bu, kopyalanamaz demek:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto ptr1 = make_unique<int>(42);

    // auto ptr2 = ptr1;           // HATA! Kopyalama yasak
    auto ptr2 = move(ptr1);        // OK — sahiplik aktarıldı

    if (ptr1 == nullptr) {
        cout << "ptr1 artik bos" << endl;
    }
    cout << "ptr2: " << *ptr2 << endl;

    return 0;
}
ptr1 artik bos
ptr2: 42

move ile sahiplik ptr1'den ptr2'ye geçti. Artık ptr1 nullptr. Tek sahiplik prensibi korundu — aynı anda sadece bir unique_ptr o belleğe sahip.

unique_ptr ve Diziler

#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto dizi = make_unique<int[]>(5);

    for (int i = 0; i < 5; i++) {
        dizi[i] = (i + 1) * 10;
    }

    for (int i = 0; i < 5; i++) {
        cout << dizi[i] << " ";
    }
    cout << endl;

    return 0;  // Otomatik temizlenir
}

Fonksiyonlarla unique_ptr

#include <iostream>
#include <memory>
using namespace std;

// Sahipligi alir (move gerekir)
void sahiplenVeYazdir(unique_ptr<int> ptr) {
    cout << "Deger: " << *ptr << endl;
}  // ptr burada yok olur, bellek serbest

// Sadece degerine bakar (raw pointer veya referans ile)
void sadeceBak(const int& deger) {
    cout << "Deger: " << deger << endl;
}

int main() {
    auto ptr = make_unique<int>(42);

    sadeceBak(*ptr);                  // Kopyalamadan bak
    sahiplenVeYazdir(move(ptr));      // Sahipligi aktar
    // ptr artik nullptr

    return 0;
}

💡 İpucu: Fonksiyon sadece değere bakacaksa, unique_ptr geçirme — const T& veya raw pointer (T*) ile geçir. Sahipliği aktarmak istiyorsan unique_ptr parametresi kullan.


shared_ptr — Paylaşımlı Sahiplik

shared_ptr, bir kaynağı birden fazla pointer'ın paylaşmasını sağlar. Her paylaşımda bir sayaç artar, her pointer yok olduğunda sayaç azalır. Sayaç sıfıra düşünce bellek otomatik silinir.

Temel Kullanım

#include <iostream>
#include <memory>
using namespace std;

int main() {
    auto ptr1 = make_shared<int>(42);
    cout << "Sayac: " << ptr1.use_count() << endl;  // 1

    {
        auto ptr2 = ptr1;  // Kopyalama — sayac artar
        cout << "Sayac: " << ptr1.use_count() << endl;  // 2

        auto ptr3 = ptr1;  // Bir daha kopyala
        cout << "Sayac: " << ptr1.use_count() << endl;  // 3
    }  // ptr2 ve ptr3 yok oldu — sayac azalir

    cout << "Sayac: " << ptr1.use_count() << endl;  // 1

    return 0;
}  // ptr1 yok olur, sayac 0 → bellek serbest
Sayac: 1
Sayac: 2
Sayac: 3
Sayac: 1

use_count() kaç tane shared_ptr'nin aynı kaynağa sahip olduğunu gösterir. Son sahip yok olunca bellek temizlenir.

make_shared Neden Tercih Edilir?

// Yol 1: new ile (eski)
shared_ptr<int> p1(new int(42));

// Yol 2: make_shared ile (modern, verimli)
auto p2 = make_shared<int>(42);

make_shared tek bir bellek ayırmasıyla hem nesneyi hem kontrol bloğunu (sayaç) oluşturur. new ile oluşturma ise iki ayrı ayırma yapar — daha yavaş ve exception'a açık.

shared_ptr ile Nesne Paylaşma

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

struct Belge {
    string baslik;
    Belge(string b) : baslik(b) {
        cout << baslik << " olusturuldu" << endl;
    }
    ~Belge() {
        cout << baslik << " silindi" << endl;
    }
};

int main() {
    vector<shared_ptr<Belge>> editorler;

    auto belge = make_shared<Belge>("Rapor.txt");
    cout << "Sahip sayisi: " << belge.use_count() << endl;

    editorler.push_back(belge);
    editorler.push_back(belge);
    cout << "Sahip sayisi: " << belge.use_count() << endl;

    editorler.clear();
    cout << "Sahip sayisi: " << belge.use_count() << endl;

    return 0;
}
Rapor.txt olusturuldu
Sahip sayisi: 1
Sahip sayisi: 3
Sahip sayisi: 1
Rapor.txt silindi

Belge, tüm editörler onu bırakıp son sahip de scope'tan çıkınca siliniyor. Kimsenin manuel delete yapmasına gerek yok.

⚠️ Dikkat: shared_ptr, unique_ptr'den daha yavaştır — referans sayacı (reference count) yönetimi ek maliyet getirir. Paylaşımlı sahipliğe gerçekten ihtiyacın yoksa unique_ptr kullan.


weak_ptr — Zayıf Referans

weak_ptr, shared_ptr'nin gösterdiği kaynağa sahiplik iddia etmeden erişim sağlar. Referans sayacını artırmaz. Asıl amacı: circular reference (döngüsel referans) sorununu çözmek.

Circular Reference Sorunu

İki nesne birbirine shared_ptr ile bağlandığında, ikisinin de referans sayacı hiçbir zaman sıfıra düşmez:

#include <iostream>
#include <memory>
using namespace std;

struct B;  // Ileriye bildirim

struct A {
    shared_ptr<B> bPtr;
    ~A() { cout << "A silindi" << endl; }
};

struct B {
    shared_ptr<A> aPtr;  // SORUN: A'ya shared_ptr
    ~B() { cout << "B silindi" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();

    a->bPtr = b;  // A, B'yi gosteriyor
    b->aPtr = a;  // B, A'yi gosteriyor — DONGU!

    // main bittiginde:
    // a yok olur → A'nin sayaci 1 (b hala tutuyor)
    // b yok olur → B'nin sayaci 1 (a hala tutuyor)
    // Ikisi de silinmez! MEMORY LEAK!

    return 0;
}

Bu programı çalıştırırsan "A silindi" veya "B silindi" mesajını görmezsin. İkisi birbirini tuttuğu için hiçbiri silinmez.

Çözüm: weak_ptr

Döngüdeki bağlantılardan birini weak_ptr yaparak çözüyoruz:

#include <iostream>
#include <memory>
using namespace std;

struct B;

struct A {
    shared_ptr<B> bPtr;
    ~A() { cout << "A silindi" << endl; }
};

struct B {
    weak_ptr<A> aPtr;  // COZUM: weak_ptr sayaci artirmaz
    ~B() { cout << "B silindi" << endl; }
};

int main() {
    auto a = make_shared<A>();
    auto b = make_shared<B>();

    a->bPtr = b;
    b->aPtr = a;  // weak_ptr — sayaci artirmaz

    return 0;
}
A silindi
B silindi

Artık ikisi de düzgün siliniyor! weak_ptr, A'nın referans sayacını artırmadığı için döngü kırıldı.

weak_ptr Kullanımı

weak_ptr doğrudan dereference edilemez. Önce lock() ile geçici bir shared_ptr elde etmen gerekir:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    weak_ptr<int> zayif;

    {
        auto guclu = make_shared<int>(42);
        zayif = guclu;

        // lock() ile gecici shared_ptr olustur
        if (auto ptr = zayif.lock()) {
            cout << "Deger: " << *ptr << endl;  // 42
        }
    }  // guclu yok oldu — bellek serbest

    // Artık nesne yok
    if (zayif.expired()) {
        cout << "Nesne artik yok!" << endl;
    }

    if (auto ptr = zayif.lock()) {
        cout << *ptr << endl;  // Buraya girilmez
    } else {
        cout << "lock() nullptr dondu" << endl;
    }

    return 0;
}
Deger: 42
Nesne artik yok!
lock() nullptr dondu

expired() nesnenin hâlâ var olup olmadığını kontrol eder. lock() eğer nesne varsa shared_ptr döndürür, yoksa boş shared_ptr döndürür.


unique_ptr vs shared_ptr vs weak_ptr

Özellikunique_ptrshared_ptrweak_ptr
SahiplikTek sahipPaylaşımlıSahiplik yok
Kopyalanabilir mi?HayırEvetEvet
Taşınabilir mi?EvetEvetEvet
Referans sayacıYokVarArtırmaz
OverheadNeredeyse sıfırSayaç maliyetiSayaç maliyeti
Doğrudan erişimEvet (*ptr)Evet (*ptr)Hayır (lock() gerekir)
Kullanım alanıÇoğu durumPaylaşımlı sahiplikDöngüsel referans kırma

Karar Ağacı: Hangi Smart Pointer?

Bir kaynağı yönetmen gerektiğinde şu soruları sor:

1. Bu kaynağın tek bir sahibi mi olacak? → Evet → unique_ptr kullan

2. Birden fazla nesne bu kaynağa sahip olacak mı? → Evet → shared_ptr kullan

3. Kaynağa erişmek istiyorsun ama sahiplik istemiyorsun? → Evet → weak_ptr kullan (veya raw pointer / referans)

4. Kaynak opsiyonel mi? (bazen nullptr olabilir) → Evet, tek sahip → unique_ptr → Evet, paylaşımlı → shared_ptr

5. C API'siyle mi çalışıyorsun? → Raw pointer (zorunlu), ama sahipliği smart pointer ile yönet

Kaynağı yönetmen gerekiyor mu?
├── Evet
│   ├── Tek sahip mi?
│   │   ├── Evet → unique_ptr ✅
│   │   └── Hayır → shared_ptr
│   │       └── Döngüsel referans riski var mı?
│   │           ├── Evet → Bir tarafı weak_ptr yap
│   │           └── Hayır → shared_ptr ✅
│   └── Sahiplik gerekmiyor, sadece gözlem?
│       └── weak_ptr veya raw pointer/referans
└── Hayır → Raw pointer veya referans kullan

💡 İpucu: Şüpheye düştüğünde unique_ptr ile başla. İhtiyaç ortaya çıkarsa shared_ptr'ye geç. Çoğu durumda unique_ptr yeterlidir.


Pratik Örnek: Oyuncu Yönetimi

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

struct Oyuncu {
    string isim;
    int skor;

    Oyuncu(string i, int s) : isim(i), skor(s) {
        cout << isim << " oyuna girdi" << endl;
    }
    ~Oyuncu() {
        cout << isim << " oyundan cikti" << endl;
    }
};

int main() {
    // Oyuncu listesi — unique_ptr ile tek sahiplik
    vector<unique_ptr<Oyuncu>> oyuncular;

    oyuncular.push_back(make_unique<Oyuncu>("Ali", 100));
    oyuncular.push_back(make_unique<Oyuncu>("Veli", 85));
    oyuncular.push_back(make_unique<Oyuncu>("Ayse", 92));

    // Skoru en yuksek oyuncuyu bul
    Oyuncu* enIyi = nullptr;
    for (const auto& o : oyuncular) {
        if (!enIyi || o->skor > enIyi->skor) {
            enIyi = o.get();  // Raw pointer ile gozlem
        }
    }

    if (enIyi) {
        cout << "En iyi: " << enIyi->isim
             << " (" << enIyi->skor << ")" << endl;
    }

    return 0;
}  // Tum oyuncular otomatik silinir
Ali oyuna girdi
Veli oyuna girdi
Ayse oyuna girdi
En iyi: Ali (100)
Ali oyundan cikti
Ayse oyundan cikti
Veli oyundan cikti

Dikkat et: hiçbir yerde delete yazmadık. vector yok olurken içindeki unique_ptr'ler yok olur, onlar da Oyuncu nesnelerini siler.

enIyi ham pointer — gözlem amaçlı. Sahiplik talep etmiyor, sadece en iyi oyuncuya bakıyor. Bu, smart pointer ile raw pointer'ın birlikte kullanımının doğru örneği.


Smart Pointer ile Polimorfizm

Smart pointer'lar, kalıtım hiyerarşisinde de mükemmel çalışır:

#include <iostream>
#include <memory>
#include <vector>
using namespace std;

struct Sekil {
    virtual void ciz() const = 0;
    virtual ~Sekil() = default;
};

struct Daire : Sekil {
    void ciz() const override { cout << "Daire cizildi" << endl; }
};

struct Kare : Sekil {
    void ciz() const override { cout << "Kare cizildi" << endl; }
};

int main() {
    vector<unique_ptr<Sekil>> sekiller;

    sekiller.push_back(make_unique<Daire>());
    sekiller.push_back(make_unique<Kare>());
    sekiller.push_back(make_unique<Daire>());

    for (const auto& s : sekiller) {
        s->ciz();
    }

    return 0;
}
Daire cizildi
Kare cizildi
Daire cizildi

Raw pointer ile yapsan delete yazmayı unutabilirdin. unique_ptr ile bellek yönetimi tamamen otomatik.


Custom Deleter

Bazen standart delete yetmez — dosya kapatmak, bağlantı kesmek gibi özel temizlik işlemleri gerekebilir:

#include <iostream>
#include <memory>
#include <cstdio>
using namespace std;

int main() {
    // C tarzı dosya islemleri icin custom deleter
    auto dosyaSil = [](FILE* f) {
        if (f) {
            cout << "Dosya kapatiliyor..." << endl;
            fclose(f);
        }
    };

    {
        unique_ptr<FILE, decltype(dosyaSil)> dosya(
            fopen("test.txt", "w"), dosyaSil
        );

        if (dosya) {
            fprintf(dosya.get(), "Merhaba Dunya!\n");
        }
    }  // Dosya otomatik kapatilir

    cout << "Dosya kapatildi." << endl;
    return 0;
}

Bu, C API'lerini RAII ile sarmanın güzel bir yolu. Dosya açıldıysa, scope sonunda ne olursa olsun kapatılır.


Sık Yapılan Hatalar

1. Aynı Raw Pointer'dan İki shared_ptr Oluşturmak

int* raw = new int(42);
shared_ptr<int> sp1(raw);
shared_ptr<int> sp2(raw);  // TEHLIKE! Iki ayri sayac, ayni bellek
// Ikisi de delete yapacak → double delete!

Çözüm: make_shared kullan veya ilk shared_ptr'den kopyala.

2. unique_ptr'yi Kopyalamaya Çalışmak

auto p1 = make_unique<int>(42);
// auto p2 = p1;            // HATA! Kopyalama yasak
auto p2 = move(p1);         // OK — sahiplik aktarimi

3. get() ile Alınan Raw Pointer'ı delete Etmek

auto sp = make_shared<int>(42);
int* raw = sp.get();
// delete raw;  // TEHLIKE! shared_ptr da silecek → double delete

get() gözlem içindir — sahiplik almaz, delete yapma.

⚠️ Dikkat: Smart pointer'ın get() metodu ile aldığın raw pointer'ı asla delete etme. Sahiplik hâlâ smart pointer'da.


Modern C++ Bellek Yönetimi Özeti

// ESKI C++ (C++03 ve oncesi)
int* ptr = new int(42);
// ... kullan ...
delete ptr;  // Unutursan leak, exception olursa leak

// MODERN C++ (C++14 ve sonrası)
auto ptr = make_unique<int>(42);
// ... kullan ...
// Otomatik temizlenir — leak imkansiz

Modern C++ kodunda new ve delete kelimelerini neredeyse hiç görmemen gerekir. Eğer görüyorsan, muhtemelen smart pointer kullanılmalıdır.


Özet

  • Raw pointer ile new/delete yönetimi hatalara açıktır — smart pointer'lar bu sorunları RAII prensibiyle çözer.

  • `unique_ptr`: Tek sahiplik, sıfır overhead, kopyalanamaz ama taşınabilir. Varsayılan tercihin bu olsun.

  • `shared_ptr`: Paylaşımlı sahiplik, referans sayacı ile otomatik temizlik. Birden fazla sahibin olduğu durumlarda kullan.

  • `weak_ptr`: Sahiplik iddia etmeden gözlem yapar, shared_ptr'deki circular reference sorununu çözer.

  • `make_unique` ve `make_shared` kullan — daha güvenli, daha verimli, daha okunabilir.

  • Karar kuralı: şüpheye düşersen unique_ptr ile başla, gerekirse shared_ptr'ye geç, döngü riski varsa weak_ptr ekle.