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 serbestHiç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
newyazmandan 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: 42move 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 serbestSayac: 1
Sayac: 2
Sayac: 3
Sayac: 1use_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 silindiBelge, 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 silindiArtı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 donduexpired() 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
| Özellik | unique_ptr | shared_ptr | weak_ptr |
|---|---|---|---|
| Sahiplik | Tek sahip | Paylaşımlı | Sahiplik yok |
| Kopyalanabilir mi? | Hayır | Evet | Evet |
| Taşınabilir mi? | Evet | Evet | Evet |
| Referans sayacı | Yok | Var | Artırmaz |
| Overhead | Neredeyse sıfır | Sayaç maliyeti | Sayaç maliyeti |
| Doğrudan erişim | Evet (*ptr) | Evet (*ptr) | Hayır (lock() gerekir) |
| Kullanım alanı | Çoğu durum | Paylaşımlı sahiplik | Dö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 silinirAli oyuna girdi
Veli oyuna girdi
Ayse oyuna girdi
En iyi: Ali (100)
Ali oyundan cikti
Ayse oyundan cikti
Veli oyundan ciktiDikkat 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 cizildiRaw 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 aktarimi3. 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 deleteget() 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 imkansizModern 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/deleteyö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_ptrile başla, gerekirseshared_ptr'ye geç, döngü riski varsaweak_ptrekle.
AI Asistan
Sorularını yanıtlamaya hazır