C++ Polimorfizm: Sanal Fonksiyonlar ve Vtable
C++’ta polimorfizmi öğrenin. Sanal fonksiyonlar, vtable yapısı ve dinamik bağlama ile esnek nesne yönelimli tasarımlar geliştirin.
Polimorfizm (çok biçimlilik), aynı arayüzü (taban tür) paylaşan nesnelerin çalışma zamanında farklı davranışlar sergilemesini sağlar. C++’ta dinamik polimorfizm sanal (virtual) fonksiyonlar aracılığıyla gerçekleştirilir ve derleyici bu mekanizmayı tipik olarak vtable (virtual table) ve her nesnede bulunan vptr (virtual pointer) ile uygular. Bu makalede sanal fonksiyonların nasıl çalıştığını, vtable/vptr düzenini, performans etkilerini ve en iyi uygulamaları gerçekçi örneklerle inceleyeceğiz.
1) Dinamik Polimorfizm Nedir?
Dinamik polimorfizm, hangi fonksiyonun çağrılacağının çalışma zamanında belirlenmesidir. Bu sayede taban sınıf referansı veya işaretçisi üzerinden türetilmiş sınıfın davranışı çağrılabilir.
#include <iostream>
#include <memory>
using namespace std;
struct Book {
virtual ~Book() = default; // polimorfik taban için şart
virtual void PrintInfo() const { // sanal fonksiyon
cout << "[Book] Generic info" << endl;
}
};
struct EBook : Book {
void PrintInfo() const override {
cout << "[EBook] Digital edition" << endl;
}
};
struct PrintedBook : Book {
void PrintInfo() const override {
cout << "[PrintedBook] Physical edition" << endl;
}
};
int main() {
unique_ptr<Book> a = make_unique<EBook>();
unique_ptr<Book> b = make_unique<PrintedBook>();
a->PrintInfo(); // [EBook] ...
b->PrintInfo(); // [PrintedBook] ...
}
a ve b taban tür (Book) olarak görünse de çağrılar çalışma zamanında
gerçek türe (EBook/PrintedBook) yönlendirilir.
2) vtable ve vptr Nasıl Çalışır?
Bir sınıf en az bir virtual fonksiyona sahip olduğunda derleyici o sınıf için bir
vtable üretir. Nesnelerin içinde genellikle vptr adlı gizli bir gösterici bulunur ve
bu gösterici ilgili sınıfın vtable’ını işaret eder.
- vtable: Sınıfın sanal fonksiyonlarının adreslerini tutan tablo.
- vptr: Her nesnenin işaret ettiği vtable adresini saklayan gizli alan.
Çağrı sırasında: nesne.vptr → vtable → gerçek fonksiyon adresi.
Book (vptr) ──► vtable(Book) : &Book::PrintInfo
EBook (vptr) ─► vtable(EBook) : &EBook::PrintInfo
Printed (vptr)► vtable(PrintedBook) : &PrintedBook::PrintInfo
Bu dolaylı yönlendirme küçük bir çağrı maliyeti ekler; çoğu uygulamada önemsenmeyecek düzeydedir.
3) override, final ve virtual Destructor
override anahtar sözcüğü, tabandaki sanal bir fonksiyonun gerçekten ezildiğini derleme
zamanında doğrular. final ise metot veya sınıfın daha fazla ezilemeyeceğini belirtir.
Polimorfik tabanlarda virtual destructor şarttır; aksi halde taban işaretçi üzerinden silme
tanımsız davranışa yol açabilir.
struct Book {
virtual ~Book() = default;
virtual double Price() const { return 0.0; }
};
struct EBook final : Book {
double Price() const override { return 99.0; }
};
// struct SpecialEBook : EBook {}; // HATA: EBook final
4) Statik Polimorfizm ile Farkı (Overload/Template)
Overload (aynı isim, farklı imza) ve şablonlar (templates) derleme zamanında çözümlenir, vtable kullanılmaz. Dinamik polimorfizm ise çalışma zamanında sanal çağrı ile çözümlenir. İhtiyaca göre doğru yaklaşımı seçin:
- Çalışma zamanı davranış değişimi gerekliyse → virtual.
- Derleme zamanı optimizasyonu/stratejisi uygun ise → template/CRTP veya overload.
5) Polimorfik Arayüz Tasarımı (Book Örneği)
Ortak davranışı taban sınıfta soyutlayıp alt sınıflarda özelleştirelim. Fiyat ve baskı/dijital detaylar örneği:
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Book {
protected:
string title, author;
public:
Book(string t, string a) : title(t), author(a) {}
virtual ~Book() = default;
virtual double FinalPrice() const = 0; // saf sanal: soyut sözleşme
virtual void PrintInfo() const {
cout << title << " - " << author
<< " | " << FinalPrice() << " TL" << endl;
}
};
class EBook : public Book {
double base, vat; // dijital fiyat ve KDV
public:
EBook(string t, string a, double b, double v)
: Book(t,a), base(b), vat(v) {}
double FinalPrice() const override {
return base*(1.0+vat);
}
};
class PrintedBook : public Book {
double base, ship; int pages; bool hard;
public:
PrintedBook(string t, string a, double b, double s, int p, bool h)
: Book(t,a), base(b), ship(s), pages(p), hard(h) {}
double FinalPrice() const override {
double cost = base + ship + 0.04*pages;
if (hard) cost *= 1.1;
return cost;
}
};
int main() {
unique_ptr<Book> b1 = make_unique<EBook>("Modern C++", "A. Dev", 100, 0.10);
unique_ptr<Book> b2 = make_unique<PrintedBook>("Clean Code", "R. Martin", 120, 20, 464, true);
b1->PrintInfo(); // EBook davranışı
b2->PrintInfo(); // PrintedBook davranışı
}
6) Nesne Dilimleme (Object Slicing) ve Referans/Pointer Kullanımı
Türetilmiş nesneyi değer olarak taban tipe kopyalarsanız, türe özgü kısım dilimlenir. Bu nedenle polimorfizm için her zaman taban referansı veya taban pointer kullanın.
PrintedBook pb("X","Y",100,20,200,false);
Book base = pb; // SLICING: Book kısmı kopyalanır, PrintedBook kısmı kaybolur
base.PrintInfo(); // Book davranışı (türe özel yok)
7) dynamic_cast ve Tür Güvenliği
Taban türden türe güvenli dönüşüm için dynamic_cast kullanılır (RTTI gerekir).
Başarısız olursa nullptr döner (pointer’larda).
void PrintIfPrinted(const Book* b) {
if (auto pb = dynamic_cast<const PrintedBook*>(b)) {
cout << "Printed fiyat: " << pb->FinalPrice() << endl;
} else {
cout << "PrintedBook degil." << endl;
}
}
8) Performans Notları
- Her sanal çağrı, vtable üzerinden dolaylı çağrıdır (branch misprediction olasılığı). Çoğu uygulama için ihmal edilebilir.
- Sanal fonksiyonu inline etme olasılığı düşer (çağrı hedefi çalışma zamanında belirlenir).
- Sıcak döngülerde aşırı sanal çağrı varsa: strateji deseni + şablon (CRTP) veya function objects ile statik bağlamayı düşünün.
9) final Sınıf/Metot, override Hataları ve En İyi Uygulamalar
- Taban sınıflarda virtual destructor kullanın.
- Ezdiğiniz tüm sanal fonksiyonlarda override yazın; imza hatalarını erkenden yakalar.
- Daha fazla ezilmesini istemediğiniz metot/sınıflar için final kullanın.
- Polimorfik hiyerarşide nesneleri heap + akıllı pointer ile yönetin (
unique_ptr,shared_ptr). - Polimorfizm gerekmiyorsa sanal kullanımını gereksizce eklemeyin (aşırı genelleme, gereksiz dolaylılık).
10) vtable’ın Görselleştirilmiş Etkisi (Adım Adım)
- Sınıf en az bir sanal fonksiyon içerir → derleyici sınıf için vtable üretir.
- Nesne oluşturulduğunda vptr, sınıfın vtable’ını işaret eder.
- Çağrı yapılınca derleyici statik adres yerine vptr→vtable üzerinden gerçek adresi okur.
- Türe ezmesi (override) vtable’daki ilgili girdi adresini değiştirir.
11) Sık Yapılan Hatalar
- Virtual destructor unutmak → taban işaretçiyle
deleteederken tanımsız davranış. - Değerle kopyalayıp object slicing’e yol açmak.
overrideyazmamak → imza uyuşmazlığında sessizce yeni fonksiyon oluşturulur (ezme başarısız).- Gereksiz sanallık → basit yapılarda gereksiz dolaylılık ve maliyet.
12) Tam Örnek: Polimorfik Koleksiyon
#include <iostream>
#include <vector>
#include <memory>
using namespace std;
class Book {
public:
virtual ~Book() = default;
virtual double FinalPrice() const = 0;
virtual void Print() const = 0;
};
class EBook : public Book {
double base, vat;
public:
EBook(double b, double v) : base(b), vat(v) {}
double FinalPrice() const override { return base*(1+vat); }
void Print() const override {
cout << "[EBook] " << FinalPrice() << " TL" << endl;
}
};
class PrintedBook : public Book {
double base, ship; int pages;
public:
PrintedBook(double b, double s, int p) : base(b), ship(s), pages(p) {}
double FinalPrice() const override { return base + ship + 0.05*pages; }
void Print() const override {
cout << "[Printed] " << FinalPrice() << " TL" << endl;
}
};
int main() {
vector<unique_ptr<Book>> items;
items.push_back(make_unique<EBook>(100, 0.1));
items.push_back(make_unique<PrintedBook>(120, 20, 400));
for (const auto& it : items) it->Print(); // dinamik polimorfizm
}
13) TL;DR
- Dinamik polimorfizm C++’ta virtual fonksiyonlarla sağlanır.
- Derleyici, sanal çağrıları vtable/vptr ile uygular.
- Polimorfik tabanlarda virtual destructor zorunludur.
- override imza doğruluğu sağlar; final daha fazla ezilmeyi engeller.
- Polimorfizm için değerle kopyalamak yerine referans/pointer kullanın (slicing’e dikkat).
- Performans maliyeti genellikle küçüktür; sıcak yollar için alternatifler (CRTP/overload) düşünülebilir.
- Örnekler Visual Studio 2022 ve GCC 11+ ile doğrudan çalıştırılabilir.