Constructor, Destructor ve Rule of Three/Five
Bir önceki derste sınıf tanımladık ve nesnelerin üye değişkenlerini tek tek elle atadık. Ama bu hem zahmetli hem de hata yapmaya açık — ya bir değişkeni atamayı unutursak? İşte constructor (yapıcı fonksiyon) tam bu sorunu çözer: nesne oluşturulurken otomatik çağrılır ve nesneyi kullanıma hazır hale getirir.
Bu derste constructor'ların türlerini, destructor kavramını ve C++'ın ünlü Rule of Three / Rule of Five kurallarını öğreneceğiz.
Constructor Nedir?
Constructor, nesne oluşturulduğu an otomatik olarak çağrılan özel bir fonksiyondur. Adı sınıfla aynıdır ve dönüş tipi yoktur (void bile değil).
Bunu bir fabrika bandı gibi düşün. Nesne fabrikadan çıkarken constructor onu hazırlar — parçalarını takar, boyasını yapar, kalite kontrolünü geçirir. Sen nesneyi aldığında kullanıma hazır.
Default Constructor
Parametre almayan constructor'a default constructor denir. Sen yazmazsan derleyici otomatik bir tane üretir (ama üye değişkenleri başlatmaz!).
#include <iostream>
#include <string>
using namespace std;
class Ogrenci {
public:
string isim;
int numara;
double ortalama;
// Default constructor
Ogrenci() {
isim = "Bilinmiyor";
numara = 0;
ortalama = 0.0;
cout << "Default constructor çağrıldı" << endl;
}
void bilgi() {
cout << isim << " (" << numara << ") - " << ortalama << endl;
}
};
int main() {
Ogrenci ogr; // Default constructor otomatik çağrılır
ogr.bilgi(); // Bilinmiyor (0) - 0
return 0;
}Derleyicinin Ürettiği Default Constructor
class Basit {
public:
int x;
double y;
// Constructor yazmadık — derleyici boş bir default constructor üretir
// Ama x ve y başlatılmaz — çöp değer olur!
};
int main() {
Basit b;
// b.x ve b.y çöp değer — KULLANMA!
}⚠️ Dikkat: Derleyicinin ürettiği default constructor üye değişkenlere değer atamaz.
int,doublegibi basit türler çöp değer olarak kalır.stringgibi sınıf türleri kendi default constructor'ları sayesinde boş olarak başlatılır.
Parametreli Constructor
Nesneyi oluştururken değer geçmeni sağlar:
#include <iostream>
#include <string>
using namespace std;
class Ogrenci {
public:
string isim;
int numara;
double ortalama;
// Default constructor
Ogrenci() : isim("Bilinmiyor"), numara(0), ortalama(0.0) {}
// Parametreli constructor
Ogrenci(string i, int n, double o)
: isim(i), numara(n), ortalama(o) {
cout << isim << " oluşturuldu" << endl;
}
void bilgi() {
cout << isim << " (" << numara << ") - " << ortalama << endl;
}
};
int main() {
Ogrenci ali("Ali Yılmaz", 1001, 85.5); // Parametreli
Ogrenci bos; // Default
ali.bilgi(); // Ali Yılmaz (1001) - 85.5
bos.bilgi(); // Bilinmiyor (0) - 0
// C++11 uniform initialization
Ogrenci veli{"Veli Demir", 1002, 72.0};
veli.bilgi();
return 0;
}Member Initializer List
: isim(i), numara(n) kısmına member initializer list denir. Constructor gövdesinde atama yapmaktan daha verimli ve bazı durumlarda zorunlu:
#include <iostream>
#include <string>
using namespace std;
class Dikdortgen {
private:
const double pi = 3.14159; // const üye
int& referans; // referans üye
double genislik;
double yukseklik;
public:
// const ve referans üyeler MUTLAKA initializer list'te başlatılmalı
Dikdortgen(double g, double y, int& r)
: genislik(g), yukseklik(y), referans(r) {
// pi zaten varsayılan değerle başlatıldı
}
double alan() { return genislik * yukseklik; }
};
int main() {
int sayi = 42;
Dikdortgen d(10.0, 5.0, sayi);
cout << "Alan: " << d.alan() << endl;
return 0;
}Neden initializer list kullan?
constüyeler ve referanslar sadece initializer list'te başlatılabilirÜye değişkenler önce varsayılan olarak oluşturulup sonra atanmak yerine doğrudan istenen değerle oluşturulur (daha verimli)
Bazı sınıf türleri (default constructor'ı olmayan) sadece initializer list'te başlatılabilir
// KÖTÜ — önce boş string oluşur, sonra atanır (2 işlem)
Ogrenci(string i) {
isim = i;
}
// İYİ — doğrudan istenen değerle oluşur (1 işlem)
Ogrenci(string i) : isim(i) {}💡 İpucu: Her zaman member initializer list kullan. Gövdede atama yapmak yerine
:sonrası başlatma hem daha verimli hem de tutarlı.
Copy Constructor
Bir nesneyi başka bir nesneden kopyalayarak oluşturur:
#include <iostream>
#include <string>
using namespace std;
class Ogrenci {
public:
string isim;
int numara;
Ogrenci(string i, int n) : isim(i), numara(n) {
cout << isim << " oluşturuldu" << endl;
}
// Copy constructor
Ogrenci(const Ogrenci& diger)
: isim(diger.isim), numara(diger.numara) {
cout << isim << " kopyalandı" << endl;
}
void bilgi() {
cout << isim << " (" << numara << ")" << endl;
}
};
int main() {
Ogrenci ali("Ali", 1001);
Ogrenci ali_kopya(ali); // Copy constructor çağrılır
Ogrenci ali_kopya2 = ali; // Bu da copy constructor!
ali.bilgi();
ali_kopya.bilgi();
return 0;
}Copy constructor şu durumlarda otomatik çağrılır:
Ogrenci kopya(orijinal);veyaOgrenci kopya = orijinal;Fonksiyona nesne değer ile geçirildiğinde
Fonksiyondan nesne değer ile döndürüldüğünde
Destructor
Destructor, nesne yok edildiğinde otomatik çağrılan fonksiyondur. Adı sınıf adının başına ~ (tilde) eklenerek oluşturulur:
#include <iostream>
#include <string>
using namespace std;
class Kaynak {
public:
string isim;
Kaynak(string i) : isim(i) {
cout << isim << " oluşturuldu" << endl;
}
~Kaynak() {
cout << isim << " yok edildi" << endl;
}
};
int main() {
cout << "--- main başladı ---" << endl;
Kaynak a("A");
{
Kaynak b("B");
Kaynak c("C");
cout << "--- iç scope ---" << endl;
} // b ve c burada yok edilir (ters sırada: önce C, sonra B)
cout << "--- main bitiyor ---" << endl;
return 0;
} // a burada yok edilirÇıktı:
--- main başladı ---
A oluşturuldu
B oluşturuldu
C oluşturuldu
--- iç scope ---
C yok edildi
B yok edildi
--- main bitiyor ---
A yok edildiDestructor'lar oluşturulma sırasının tersinde çağrılır. Stack'teki nesneler scope bitince otomatik yok edilir (RAII prensibi).
Ne Zaman Destructor Yazmalısın?
Sınıfın new ile ayırdığı bellek, açtığı dosya veya veritabanı bağlantısı gibi kaynakları varsa destructor'da bunları temizlemelisin:
#include <iostream>
using namespace std;
class DinamikDizi {
private:
int* veri;
int boyut;
public:
DinamikDizi(int n) : boyut(n) {
veri = new int[n]; // Bellek ayır
cout << n << " elemanlık dizi oluşturuldu" << endl;
}
~DinamikDizi() {
delete[] veri; // Belleği serbest bırak!
cout << "Dizi yok edildi" << endl;
}
void ata(int indeks, int deger) {
if (indeks >= 0 && indeks < boyut) {
veri[indeks] = deger;
}
}
int al(int indeks) {
return veri[indeks];
}
};
int main() {
DinamikDizi d(5);
d.ata(0, 42);
cout << d.al(0) << endl;
return 0;
} // destructor otomatik çağrılır, bellek serbest kalırRule of Three
Eğer aşağıdakilerden birini yazman gerekiyorsa, muhtemelen üçünü de yazman gerekir:
Destructor (
~Sinif)Copy constructor (
Sinif(const Sinif&))Copy assignment operator (
Sinif& operator=(const Sinif&))
Neden? Çünkü bunlardan birini yazman gerektiren durum genellikle sınıfın kaynak yönetimi yaptığı anlamına gelir. Ve kaynak yönetimi yapan bir sınıfta üçünü de doğru yazmak şart.
Sorunun Gösterimi
#include <iostream>
#include <cstring>
using namespace std;
class YanlisSinif {
public:
char* veri;
YanlisSinif(const char* s) {
veri = new char[strlen(s) + 1];
strcpy(veri, s);
}
~YanlisSinif() {
delete[] veri; // Belleği serbest bırak
}
// Copy constructor ve copy assignment YAZILMADI!
};
int main() {
YanlisSinif a("Merhaba");
YanlisSinif b = a; // Shallow copy! a.veri ve b.veri aynı adresi gösterir
// b yok edilince veri silinir
// a yok edilince AYNI veri tekrar silinir → ÇÖKME! (double free)
return 0;
}Doğru Uygulama
#include <iostream>
#include <cstring>
using namespace std;
class DogruSinif {
private:
char* veri;
public:
// Constructor
DogruSinif(const char* s) {
veri = new char[strlen(s) + 1];
strcpy(veri, s);
}
// Copy constructor — DEEP COPY
DogruSinif(const DogruSinif& diger) {
veri = new char[strlen(diger.veri) + 1];
strcpy(veri, diger.veri);
cout << "Copy constructor: " << veri << endl;
}
// Copy assignment operator — DEEP COPY
DogruSinif& operator=(const DogruSinif& diger) {
if (this != &diger) { // Self-assignment kontrolü
delete[] veri; // Eski veriyi sil
veri = new char[strlen(diger.veri) + 1];
strcpy(veri, diger.veri);
}
cout << "Copy assignment: " << veri << endl;
return *this;
}
// Destructor
~DogruSinif() {
delete[] veri;
}
void yazdir() { cout << veri << endl; }
};
int main() {
DogruSinif a("Merhaba");
DogruSinif b = a; // Copy constructor
DogruSinif c("Geçici");
c = a; // Copy assignment operator
a.yazdir(); // Merhaba
b.yazdir(); // Merhaba
c.yazdir(); // Merhaba
return 0;
}Rule of Five — Move Semantics (C++11)
C++11 ile move semantics geldi. Kopyalama yerine "taşıma" yaparak performansı artırır. Rule of Three'ye iki yeni üye eklenir:
Move constructor (
Sinif(Sinif&&))Move assignment operator (
Sinif& operator=(Sinif&&))
Topluca Rule of Five olur.
Move işlemi bir evin taşınması gibi düşünülebilir. Copy her eşyayı birebir kopyalar (yeni koltuk, yeni masa al). Move ise mevcut eşyaları yeni eve taşır — eski ev boş kalır ama yeni ev dolu.
#include <iostream>
#include <cstring>
#include <utility> // std::move
using namespace std;
class Metin {
private:
char* veri;
size_t uzunluk;
public:
// Constructor
Metin(const char* s) {
uzunluk = strlen(s);
veri = new char[uzunluk + 1];
strcpy(veri, s);
}
// Copy constructor
Metin(const Metin& diger)
: uzunluk(diger.uzunluk) {
veri = new char[uzunluk + 1];
strcpy(veri, diger.veri);
cout << "COPY: " << veri << endl;
}
// Move constructor — kaynağı ÇAL
Metin(Metin&& diger) noexcept
: veri(diger.veri), uzunluk(diger.uzunluk) {
diger.veri = nullptr; // eski nesneyi güvenli bırak
diger.uzunluk = 0;
cout << "MOVE: " << veri << endl;
}
// Copy assignment
Metin& operator=(const Metin& diger) {
if (this != &diger) {
delete[] veri;
uzunluk = diger.uzunluk;
veri = new char[uzunluk + 1];
strcpy(veri, diger.veri);
}
return *this;
}
// Move assignment — kaynağı ÇAL
Metin& operator=(Metin&& diger) noexcept {
if (this != &diger) {
delete[] veri;
veri = diger.veri;
uzunluk = diger.uzunluk;
diger.veri = nullptr;
diger.uzunluk = 0;
}
return *this;
}
// Destructor
~Metin() {
delete[] veri;
}
void yazdir() {
if (veri) cout << veri << endl;
else cout << "(boş)" << endl;
}
};
int main() {
Metin a("Merhaba");
Metin b = a; // Copy constructor
Metin c = move(a); // Move constructor — a artık boş!
a.yazdir(); // (boş)
b.yazdir(); // Merhaba
c.yazdir(); // Merhaba
return 0;
}Move semantics'in asıl gücü geçici nesnelerle (rvalue) çalışırken ortaya çıkar. Fonksiyondan dönen geçici nesne kopyalanmak yerine taşınır — büyük performans kazancı.
= default ve = delete
C++11 ile derleyiciye "sen yaz" veya "bunu yasakla" diyebilirsin:
= default
Derleyiciye varsayılan implementasyonu kullanmasını söyler:
class Ogrenci {
public:
string isim;
int numara;
Ogrenci() = default; // Derleyici default constructor üretsin
Ogrenci(string i, int n) : isim(i), numara(n) {}
// Derleyicinin ürettiği copy/move yeterli
Ogrenci(const Ogrenci&) = default;
Ogrenci& operator=(const Ogrenci&) = default;
Ogrenci(Ogrenci&&) = default;
Ogrenci& operator=(Ogrenci&&) = default;
~Ogrenci() = default;
};= delete
Belirli bir fonksiyonu kullanılamaz yapar:
#include <iostream>
using namespace std;
class Tekil {
public:
Tekil() { cout << "Oluşturuldu" << endl; }
// Kopyalama yasak!
Tekil(const Tekil&) = delete;
Tekil& operator=(const Tekil&) = delete;
};
int main() {
Tekil a;
// Tekil b = a; // DERLEME HATASI! Copy constructor silindi
// Tekil c; c = a; // DERLEME HATASI! Copy assignment silindi
return 0;
}Bu özellikle "bu nesneden sadece bir tane olsun" veya "bu nesne kopyalanamasın" durumlarında kullanılır (singleton, mutex, file handle gibi).
💡 İpucu: Sınıfın üye değişkenlerinin hepsi
string,vectorgibi RAII türlerindeyse destructor/copy/move yazmana gerek yok. Derleyicinin ürettikleri doğru çalışır. Bu duruma Rule of Zero denir — en ideal durum.
Rule of Zero
Modern C++'ın önerisi: Mümkünse hiçbirini yazma!
#include <string>
#include <vector>
using namespace std;
class ModernOgrenci {
public:
string isim; // Kendi bellek yönetimini yapar
int numara;
vector<int> notlar; // Kendi bellek yönetimini yapar
ModernOgrenci(string i, int n) : isim(i), numara(n) {}
// Constructor, copy, move, destructor — hiçbiri gerekmiyor!
// Derleyici doğru olanı üretir.
};
int main() {
ModernOgrenci ali("Ali", 1001);
ali.notlar = {90, 85, 78};
ModernOgrenci kopya = ali; // Düzgün çalışır — deep copy
// ali yok olduğunda sorun yok
// kopya yok olduğunda sorun yok
}Özet tablo:
| Kural | Ne Zaman |
|---|---|
| Rule of Zero | Sınıf ham kaynak yönetmiyor (string, vector kullanıyor) |
| Rule of Three | Sınıf C++11 öncesi ham kaynak yönetiyor (new/delete) |
| Rule of Five | Sınıf C++11+ ham kaynak yönetiyor ve move desteği istiyor |
Özet
Constructor nesne oluşturulurken otomatik çağrılır; default constructor parametresiz, parametreli constructor değer alarak nesneyi başlatır.
Member initializer list (
:) ile üye değişkenler doğrudan başlatılır;constve referans üyeler için zorunludur, genel olarak her zaman tercih edilmelidir.Copy constructor bir nesneyi başka bir nesneden kopyalayarak oluşturur; fonksiyona değer ile geçirmede de çağrılır.
Destructor (
~Sinif) nesne yok edilirken çağrılır; kaynakları (bellek, dosya) temizlemek için kullanılır.Rule of Three: Destructor, copy constructor veya copy assignment'tan birini yazıyorsan üçünü de yaz. Rule of Five buna move constructor ve move assignment ekler.
= default derleyicinin varsayılan implementasyonu kullanmasını, = delete fonksiyonu tamamen yasaklar. Rule of Zero en ideal durumdur — hiçbirini yazmana gerek yoktur.
AI Asistan
Sorularını yanıtlamaya hazır