← Kursa Dön
📄 Text · 15 min

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, double gibi basit türler çöp değer olarak kalır. string gibi 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?

  1. const üyeler ve referanslar sadece initializer list'te başlatılabilir

  2. Üye değişkenler önce varsayılan olarak oluşturulup sonra atanmak yerine doğrudan istenen değerle oluşturulur (daha verimli)

  3. 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:

  1. Ogrenci kopya(orijinal); veya Ogrenci kopya = orijinal;

  2. Fonksiyona nesne değer ile geçirildiğinde

  3. 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 edildi

Destructor'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ır

Rule of Three

Eğer aşağıdakilerden birini yazman gerekiyorsa, muhtemelen üçünü de yazman gerekir:

  1. Destructor (~Sinif)

  2. Copy constructor (Sinif(const Sinif&))

  3. 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:

  1. Move constructor (Sinif(Sinif&&))

  2. 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, vector gibi 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:

KuralNe Zaman
Rule of ZeroSınıf ham kaynak yönetmiyor (string, vector kullanıyor)
Rule of ThreeSınıf C++11 öncesi ham kaynak yönetiyor (new/delete)
Rule of FiveSı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; const ve 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.