← Kursa Dön
📄 Text · 20 min

Object Slicing, Virtual Table ve Early/Late Binding

Polimorfizm (polymorphism) C++'ın en güçlü özelliklerinden biri. Ama bu gücü kullanırken dikkat etmezsen, kodun sessizce yanlış çalışabilir — ve en tehlikeli bug'lar da bu "sessiz" olanlardır. Bu derste üç kritik konuyu ele alacağız: object slicing denen sinsi hatayı, virtual table'ın perde arkasında nasıl çalıştığını ve derleyicinin fonksiyon çağrılarını nasıl çözümlediğini (early vs late binding).


Object Slicing Nedir?

Elinde güzel bir manzara fotoğrafı var. Geniş açıyla çekilmiş — dağlar, göl, ağaçlar, her şey görünüyor. Şimdi bu fotoğrafı küçük bir çerçeveye koymak istiyorsun ama çerçeve küçük. Fotoğrafı kırparsın. Kırptığında kenarlarındaki detaylar — dağın zirvesi, göldeki yansıma — hepsi gider. Elinde sadece ortadaki kısım kalır.

Object slicing tam olarak bu. Bir derived (türetilmiş) sınıf nesnesini, base (temel) sınıf tipinde bir değişkene value olarak atadığında, derived sınıfa ait ekstra veri ve davranış "kesilip atılır." Geriye sadece base sınıfın bildiği kısım kalır.

Temel Slicing Örneği

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

class Animal {
public:
    string name;
    Animal(const string& n) : name(n) {}
    virtual void speak() const {
        cout << name << " ses cikarmiyor." << endl;
    }
};

class Dog : public Animal {
public:
    string breed;  // Derived'a ozel veri
    Dog(const string& n, const string& b) : Animal(n), breed(b) {}
    void speak() const override {
        cout << name << " (" << breed << ") havliyor!" << endl;
    }
};

int main() {
    Dog rex("Rex", "Golden Retriever");
    rex.speak();  // Rex (Golden Retriever) havliyor!

    // SLICING BURADA OLUYOR!
    Animal a = rex;  // Dog -> Animal value kopyalama
    a.speak();       // Rex ses cikarmiyor.  (Dog davranisi kayboldu!)
    return 0;
}

rex bir Dog — hem name hem breed bilgisi var, speak() "havlıyor" diyor. Ama Animal a = rex; dediğin anda a sıradan bir Animal oldu. breed yok oldu, speak() da Animal'ın versiyonunu çağırıyor. Derleyici sana hiçbir uyarı vermez. Kod derlenir, çalışır — ama yanlış çalışır.

Fonksiyona Value ile Geçirmede Slicing

Slicing sadece atamada değil, fonksiyona value ile parametre geçirdiğinde de olur:

class Shape {
public:
    string color;
    Shape(const string& c) : color(c) {}
    virtual double area() const { return 0.0; }
    virtual void describe() const {
        cout << color << " sekil, alan: " << area() << endl;
    }
};

class Circle : public Shape {
public:
    double radius;
    Circle(const string& c, double r) : Shape(c), radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
};

// DIKKAT: value ile aliyor — slicing!
void printShape(Shape s) {
    s.describe();
}

int main() {
    Circle c("Kirmizi", 5.0);
    c.describe();      // Kirmizi sekil, alan: 78.5398
    printShape(c);     // Kirmizi sekil, alan: 0  (YANLIS!)
    return 0;
}

printShape fonksiyonu Shape tipinde parametre alıyor — value olarak. Circle'ı geçirdiğinde, fonksiyon içinde artık bir Shape kopyası var. radius kesildi, area() sıfır döndürüyor.

⚠️ Dikkat: Object slicing sadece value semantics ile olur — nesneyi kopyaladığında. Pointer veya reference kullandığında slicing olmaz çünkü orijinal nesneye erişiyorsundur.


Slicing'den Kaçınma Yolları

Temel kural: polimorfik nesneleri asla value olarak kopyalama. Pointer veya reference kullan.

Pointer ve Reference ile

class Animal {
public:
    virtual void speak() const { cout << "..." << endl; }
    virtual ~Animal() = default;
};

class Cat : public Animal {
public:
    void speak() const override { cout << "Miyav!" << endl; }
};

// Pointer ile — slicing YOK
void makeSound(const Animal* a) { a->speak(); }

// Reference ile — slicing YOK
void makeSound2(const Animal& a) { a.speak(); }

int main() {
    Cat cat;
    makeSound(&cat);   // Miyav!
    makeSound2(cat);   // Miyav!

    Animal* ptr = &cat;
    ptr->speak();       // Miyav!

    const Animal& ref = cat;
    ref.speak();        // Miyav!
    return 0;
}

💡 İpucu: Polimorfik parametreler için const Base& kullanmak en yaygın ve güvenli yöntemdir. Hem slicing'i önler, hem gereksiz kopyalamayı engeller.

Smart Pointer ile Koleksiyonlar

Farklı türdeki nesneleri bir koleksiyonda tutmak istersen vector<Base> kullanırsan slicing olur. Çözüm: vector<unique_ptr<Base>>.

#include <vector>
#include <memory>

class Enemy {
public:
    virtual void attack() const { cout << "Saldiri." << endl; }
    virtual ~Enemy() = default;
};

class Goblin : public Enemy {
public:
    void attack() const override { cout << "Goblin bicakla!" << endl; }
};

class Dragon : public Enemy {
public:
    void attack() const override { cout << "Ejderha ates!" << endl; }
};

int main() {
    // YANLIS: value vector — slicing
    vector<Enemy> bad;
    bad.push_back(Goblin{});   // Slicing!
    for (const auto& e : bad) e.attack();  // Hepsi "Saldiri."

    // DOGRU: smart pointer vector
    vector<unique_ptr<Enemy>> good;
    good.push_back(make_unique<Goblin>());
    good.push_back(make_unique<Dragon>());
    for (const auto& e : good) e->attack();  // Goblin bicakla! Ejderha ates!
    return 0;
}

Abstract Base Class ile Koruma

En radikal çözüm: base sınıfı abstract yap. Base b = derived; yazmak bile derleme hatası verir:

class Shape {
public:
    virtual double area() const = 0;   // Pure virtual
    virtual void draw() const = 0;
    virtual ~Shape() = default;
};

class Rectangle : public Shape {
    double w, h;
public:
    Rectangle(double w, double h) : w(w), h(h) {}
    double area() const override { return w * h; }
    void draw() const override {
        cout << "Dikdortgen (" << w << "x" << h << ")" << endl;
    }
};

int main() {
    Rectangle rect(4, 5);
    // Shape s = rect;  // DERLEME HATASI! Shape abstract.
    Shape& ref = rect;  // Dogru
    ref.draw();
    return 0;
}

Abstract base class kullandığında derleyici seni korur. Slicing'e karşı en güçlü kalkan budur.


Virtual Table (vtable) Nasıl Çalışır?

Polimorfizm güzel de, perde arkasında nasıl çalışıyor? Cevap: virtual table mekanizması.

Analoji: Restoran Menüsü

Bir restoran zincirindesin. Her şubenin kendi menüsü var. Temel yemekler aynı ama bazı şubeler yerel tatlar eklemiş. "3 numaralı yemeği istiyorum" dediğinde, hangi şubedeysen o şubenin menüsünden gelir. vtable bu menü gibi — her sınıfın kendi tablosu var, her nesne hangi tabloya bakacağını biliyor.

vtable Yapısı

Derleyici, her polimorfik sınıf için derleme zamanında bir vtable oluşturur. Bu tablo, o sınıfın virtual fonksiyonlarının adreslerini tutar. Her polimorfik nesnenin bellek düzenine bir vptr (virtual table pointer) eklenir.

Animal vtable:              Dog vtable:
+------------------+        +------------------+
| speak() -> 0x100 |        | speak() -> 0x200 |  (Dog::speak)
| eat()   -> 0x110 |        | eat()   -> 0x110 |  (Animal::eat — override yok)
+------------------+        +------------------+

Bellekteki nesneler:

Animal nesnesi:              Dog nesnesi:
+------------------+        +---------------------------+
| vptr -> Animal   |        | vptr -> Dog vtable        |
|   vtable         |        | name (Animal'dan miras)   |
| name             |        | breed (Dog'a ozel)        |
+------------------+        +---------------------------+

Animal* ptr = &dog; yapıp ptr->speak() dediğinde: nesne → vptr → vtable → Dog::speak() adresi → çağır. Pointer tipi Animal* olsa bile, nesne Dog ise Dog::speak() çağrılır.

vtable'ı sizeof ile Gözlemleme

class NoVirtual {
    int x;
public:
    void foo() {}
};

class WithVirtual {
    int x;
public:
    virtual void foo() {}
};

class FiveVirtuals {
    int x;
public:
    virtual void a() {}
    virtual void b() {}
    virtual void c() {}
    virtual void d() {}
    virtual void e() {}
};

int main() {
    cout << "NoVirtual:    " << sizeof(NoVirtual) << " byte" << endl;
    cout << "WithVirtual:  " << sizeof(WithVirtual) << " byte" << endl;
    cout << "FiveVirtuals: " << sizeof(FiveVirtuals) << " byte" << endl;

    // 64-bit sistemde tipik cikti:
    // NoVirtual:    4  (sadece int)
    // WithVirtual:  16 (vptr 8 + int 4 + padding 4)
    // FiveVirtuals: 16 (AYNI! Nesne basina 1 vptr, vtable buyur ama nesne buyumez)
    return 0;
}

Önemli detay: ilk virtual fonksiyon vptr ekler (nesne büyür), sonrakiler sadece vtable'a satır ekler ama nesne boyutu değişmez.

vtable ve Inheritance Zinciri

Her seviye kendi override ettiği fonksiyonları günceller, override etmediklerini miras alır:

class Base {
public:
    virtual void f1() { cout << "Base::f1" << endl; }
    virtual void f2() { cout << "Base::f2" << endl; }
    virtual void f3() { cout << "Base::f3" << endl; }
    virtual ~Base() = default;
};

class Middle : public Base {
public:
    void f2() override { cout << "Middle::f2" << endl; }
};

class Child : public Middle {
public:
    void f3() override { cout << "Child::f3" << endl; }
};

int main() {
    Child c;
    Base* ptr = &c;
    ptr->f1();  // Base::f1    (hic override edilmedi)
    ptr->f2();  // Middle::f2  (Middle override etti)
    ptr->f3();  // Child::f3   (Child override etti)
    return 0;
}
Base vtable:     Middle vtable:    Child vtable:
+-----------+    +-----------+    +-----------+
| Base::f1  |    | Base::f1  |    | Base::f1  |
| Base::f2  |    | Middle::f2|    | Middle::f2|
| Base::f3  |    | Base::f3  |    | Child::f3 |
+-----------+    +-----------+    +-----------+

⚠️ Dikkat: vtable bir implementasyon detayıdır — C++ standardı "vtable kullanılmalı" demez. Ama pratikte tüm büyük derleyiciler (GCC, Clang, MSVC) bu mekanizmayı kullanır.


Early Binding vs Late Binding

Derleyici fonksiyon çağrısını gördüğünde "hangi fonksiyonu çağıracağım?" sorusuna cevap vermeli. Bu cevabı ne zaman verdiği, binding türünü belirler.

Analoji: Randevu vs Acil Servis

Early binding: Doktora randevu alıyorsun. Hangi doktor, hangi oda — önceden belli. Late binding: Acil servise gidiyorsun. Hangi doktora denk geleceğin o anda belli oluyor.

Early Binding (Statik Bağlama)

Derleme zamanında hangi fonksiyonun çağrılacağı kesin olarak bilinir. Non-virtual fonksiyonlar, overload'lar, template'ler:

class Calculator {
public:
    int add(int a, int b) { return a + b; }
    int add(int a, int b, int c) { return a + b + c; }
    double add(double a, double b) { return a + b; }
};

int main() {
    Calculator calc;
    // Derleyici parametre tiplerine bakarak DERLEME ZAMANINDA karar verir
    cout << calc.add(3, 5) << endl;       // int add(int, int)
    cout << calc.add(3, 5, 7) << endl;    // int add(int, int, int)
    cout << calc.add(3.0, 5.0) << endl;   // double add(double, double)
    return 0;
}

Late Binding (Dinamik Bağlama)

Çağrılacak fonksiyon çalışma zamanında vtable üzerinden belirlenir:

class Logger {
public:
    virtual void log(const string& msg) {
        cout << "[LOG] " << msg << endl;
    }
    virtual ~Logger() = default;
};

class FileLogger : public Logger {
public:
    void log(const string& msg) override {
        cout << "[FILE] " << msg << endl;
    }
};

class NetworkLogger : public Logger {
public:
    void log(const string& msg) override {
        cout << "[NET] " << msg << endl;
    }
};

void writeLog(Logger* logger, const string& msg) {
    logger->log(msg);  // LATE BINDING — calisma zamaninda karar
}

int main() {
    FileLogger file;
    NetworkLogger net;
    writeLog(&file, "Dosyaya");   // [FILE] Dosyaya
    writeLog(&net, "Aga");        // [NET] Aga
    return 0;
}

Karşılaştırma

+-------------------+---------------------------+---------------------------+
|                   | Early Binding             | Late Binding              |
+-------------------+---------------------------+---------------------------+
| Karar zamani      | Derleme zamani            | Calisma zamani            |
| Keyword           | (default — virtual yok)   | virtual                   |
| Mekanizma         | Dogrudan fonksiyon adresi | vptr -> vtable -> adres   |
| Performans        | Hizli (tek jump)          | ~1-2 pointer dereference  |
| Esneklik          | Dusuk                     | Yuksek (polimorfizm)      |
| Inline olabilir?  | Evet                      | Genellikle hayir          |
+-------------------+---------------------------+---------------------------+

Non-Virtual Tuzağı

Non-virtual fonksiyonlar her zaman early binding kullanır. Bu kafa karıştırıcı sonuçlar doğurur:

class Base {
public:
    void greet() { cout << "Ben Base!" << endl; }  // NON-VIRTUAL
};

class Derived : public Base {
public:
    void greet() { cout << "Ben Derived!" << endl; }  // Hiding, override degil!
};

int main() {
    Derived d;
    d.greet();         // "Ben Derived!"

    Base* ptr = &d;
    ptr->greet();      // "Ben Base!" — TUZAK!
    // virtual olmadigindan pointer'in statik tipine bakilir
    return 0;
}

⚠️ Dikkat: Base'de fonksiyonu virtual yapmayı unutup derived'da aynı imzayla tanımlamak yaygın bir hatadır. Override sanılır ama aslında hiding (gizleme) olmuştur. -Woverloaded-virtual derleyici flag'ini kullan.


final ile Devirtualization

C++11'in final keyword'ü, late binding'i tekrar early binding'e çevirme fırsatı sunar.

final Fonksiyonda

class Animal {
public:
    virtual void speak() const { cout << "..." << endl; }
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() const override final {  // Daha fazla override edilemez
        cout << "Hav hav!" << endl;
    }
};

class Puppy : public Dog {
public:
    // void speak() const override { }  // DERLEME HATASI! final.
};

int main() {
    Dog d;
    Dog* ptr = &d;
    // Derleyici bilir: Dog::speak() final
    // vtable'a bakmasina gerek yok — devirtualization
    ptr->speak();
    return 0;
}

final Sınıfta

class Base {
public:
    virtual void process() const { cout << "Base" << endl; }
    virtual ~Base() = default;
};

class Processor final : public Base {
public:
    void process() const override { cout << "Processor" << endl; }
};

// class Sub : public Processor { };  // DERLEME HATASI! final sinif.

int main() {
    Processor p;
    Processor* ptr = &p;
    // Tum virtual cagrilar devirtualize edilebilir
    ptr->process();
    return 0;
}

Devirtualization'ın asıl faydası: devirtualize edilmiş çağrı inline edilebilir. Inline sonrası derleyici constant folding, dead code elimination gibi ileri optimizasyonlar yapabilir.

💡 İpucu: Bir sınıf veya virtual fonksiyon daha fazla türetilmeyecek/override edilmeyecekse final ekle. Maliyeti sıfır, faydası potansiyel olarak büyük.


Pratik Örnekler

Hayvan Koleksiyonu — Slicing Bug'ı ve Düzeltmesi

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

class Animal {
protected:
    string name;
public:
    Animal(const string& n) : name(n) {}
    virtual void makeSound() const { cout << name << ": ..." << endl; }
    virtual string getInfo() const { return name + " (Animal)"; }
    virtual ~Animal() = default;
};

class Cat : public Animal {
    int lives;
public:
    Cat(const string& n, int l = 9) : Animal(n), lives(l) {}
    void makeSound() const override {
        cout << name << ": Miyav! (" << lives << " can)" << endl;
    }
    string getInfo() const override {
        return name + " (Cat, " + to_string(lives) + " can)";
    }
};

class Parrot : public Animal {
    string phrase;
public:
    Parrot(const string& n, const string& p) : Animal(n), phrase(p) {}
    void makeSound() const override {
        cout << name << ": " << phrase << endl;
    }
    string getInfo() const override {
        return name + " (Parrot, soz: " + phrase + ")";
    }
};

int main() {
    // BUG'LI: value vector — slicing
    cout << "=== Value vector (BUGGY) ===" << endl;
    vector<Animal> bad;
    bad.push_back(Cat("Tekir", 7));
    bad.push_back(Parrot("Polly", "Cracker!"));
    for (const auto& a : bad) {
        cout << a.getInfo() << " -> ";
        a.makeSound();  // Hepsi "Animal" gibi davranir
    }

    // DOGRU: smart pointer vector
    cout << "\n=== Smart pointer vector (DOGRU) ===" << endl;
    vector<unique_ptr<Animal>> good;
    good.push_back(make_unique<Cat>("Tekir", 7));
    good.push_back(make_unique<Parrot>("Polly", "Cracker!"));
    for (const auto& a : good) {
        cout << a->getInfo() << " -> ";
        a->makeSound();  // Her biri kendi tipinde davranir
    }
    return 0;
}

override + final Best Practices

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

// 1. Abstract base — slicing imkansiz
class Renderable {
public:
    virtual void render() const = 0;
    virtual void update(double dt) = 0;
    virtual string name() const = 0;
    virtual ~Renderable() = default;  // 2. Virtual destructor SART
};

// 3. override her zaman yaz
class Button : public Renderable {
    string label;
public:
    Button(const string& l) : label(l) {}
    void render() const override {
        cout << "[" << label << "]" << endl;
    }
    void update(double dt) override { /* animasyon */ }
    string name() const override { return "Button(" + label + ")"; }
};

// 4. final ile hiyerarshiyi kapat
class IconButton final : public Button {
    string icon;
public:
    IconButton(const string& l, const string& i) : Button(l), icon(i) {}
    void render() const override {
        cout << "[" << icon << " " << "]" << endl;
    }
};

// 5. Smart pointer koleksiyon
class UIManager {
    vector<unique_ptr<Renderable>> elements;
public:
    void add(unique_ptr<Renderable> elem) {
        elements.push_back(move(elem));
    }
    void renderAll() const {
        for (const auto& e : elements) {
            cout << e->name() << ": ";
            e->render();
        }
    }
};

int main() {
    UIManager ui;
    ui.add(make_unique<Button>("Kaydet"));
    ui.add(make_unique<IconButton>("Sil", "X"));
    ui.renderAll();
    return 0;
}

Beş kural özetlenecek olursa: (1) virtual destructor tanımla, (2) base'i abstract yap, (3) override her zaman kullan, (4) gerektiğinde final ekle, (5) koleksiyonda smart pointer kullan.


Üç Kavram Arasındaki Bağlantı

Bu üç kavram birbirinden bağımsız değil:

Slicing neden olur? Value kopyalama sırasında sadece base'in copy constructor'ı çalışır. Kopyalanan nesnenin vptr'ı base'in vtable'ını gösterir.

vtable neden önemli? Polimorfizmin tüm mekanizması vtable üzerinden çalışır. Slicing olduğunda vptr base'in vtable'ını gösterir, derived fonksiyonlar çağrılmaz.

Binding neden önemli? Early binding'de pointer tipi üzerinden karar verilir — polimorfizm çalışmaz. Late binding'de vtable'a bakılır — ama nesnenin doğru vptr'a sahip olması (slicing olmamış olması) gerekir.

Dogru polimorfizm icin uc sart:
1. Virtual fonksiyon olmali (late binding aktif)
2. Pointer veya reference kullanilmali (slicing yok)
3. Nesne dogru turde olmali (vptr dogru vtable'i gostermeli)

Özet

  • Object slicing, derived nesneyi base tipinde bir değişkene value olarak kopyaladığında oluşur. Derived'a özel veri ve davranış kaybolur — fotoğrafı kırpmak gibi, kenarları geri getiremezsin.

  • Slicing'den kaçınmak için polimorfik nesneleri pointer veya reference ile kullan. Koleksiyonlarda vector<unique_ptr<Base>> pattern'ini tercih et. Base sınıfı abstract yaparak slicing'i derleme zamanında engelle.

  • vtable, derleyicinin her polimorfik sınıf için oluşturduğu fonksiyon adresi tablosudur. Her nesne bir vptr taşır. Virtual çağrıda nesne → vptr → vtable → fonksiyon adresi zinciri izlenir.

  • Early binding derleme zamanında, late binding çalışma zamanında karar verir. Non-virtual = early, virtual = late. Performans farkı genellikle ihmal edilebilir.

  • `final` keyword'ü ile derleyici devirtualization yaparak late binding'i early binding'e çevirebilir — bu da inline ve ileri optimizasyonlara kapı açar.

  • Best practices: virtual destructor mutlaka tanımla, override her zaman kullan, final ile hiyerarşiyi kapat, koleksiyonlarda smart pointer kullan, base'i mümkünse abstract yap.