Polimorfizm ve Virtual Fonksiyonlar
Kalıtımı öğrendin. Şimdi kalıtımın gerçek gücünü açığa çıkaracak konsepte geçiyoruz: polimorfizm. Bu, nesne yönelimli programlamanın en önemli özelliklerinden biridir ve C++'ın en zarif mekanizmalarından birini kullanır.
Polimorfizm Nedir?
Polimorfizm kelimesi Yunanca'dan gelir: poly (çok) + morph (biçim) = çok biçimlilik.
En iyi analoji: TV kumandası. Elindeki kumandanın "aç" tuşuna basıyorsun. Ama kumanda Samsung TV'ye mi, LG TV'ye mi, yoksa projektöre mi bağlı? Her cihaz "aç" komutunu farklı şekilde yorumlar. Sen sadece "aç" diyorsun — cihaz kendi bildiği şekilde açılıyor.
Yazılımda polimorfizm tam olarak bu: aynı arayüz (interface), farklı davranış.
// Kumanda (base pointer) aynı, cihaz (derived object) farklı
Shape* shape = getRandomShape();
shape->draw(); // Circle mı? Rectangle mı? Triangle mı?
// Hangisiyse onun draw()'u çalışırSorun: Virtual Olmadan Ne Olur?
Önce sorunu görelim:
class Animal {
public:
void speak() {
std::cout << "...\n";
}
};
class Dog : public Animal {
public:
void speak() {
std::cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() {
std::cout << "Meow!\n";
}
};
int main() {
Dog dog;
Cat cat;
Animal* ptr1 = &dog;
Animal* ptr2 = &cat;
ptr1->speak(); // "..." — Dog::speak() değil!
ptr2->speak(); // "..." — Cat::speak() değil!
}İkisi de "..." yazdırır! Çünkü derleyici, pointer'ın tipine bakıyor (Animal*), gösterdiği nesnenin tipine değil. Bu davranışa statik bağlama (static binding / early binding) denir — derleme zamanında hangi fonksiyonun çağrılacağı belirlenir.
Biz ise dinamik bağlama (dynamic binding / late binding) istiyoruz: çalışma zamanında, gerçek nesne tipine göre doğru fonksiyon çağrılsın.
Virtual Fonksiyonlar
Çözüm: base class'taki fonksiyonu virtual anahtar kelimesiyle işaretle.
class Animal {
public:
virtual void speak() {
std::cout << "...\n";
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!\n";
}
};
int main() {
Dog dog;
Cat cat;
Animal* ptr1 = &dog;
Animal* ptr2 = &cat;
ptr1->speak(); // "Woof!" ✅
ptr2->speak(); // "Meow!" ✅
}virtual kelimesi, derleyiciye şunu söyler: "Bu fonksiyon çağrısını derleme zamanında bağlama. Çalışma zamanında, gerçek nesne tipine bak ve doğru versiyonu çağır."
Virtual Fonksiyonun Kuralları
virtualsadece base class'ta yazılır. Derived class'ta tekrar yazman gerekmez (amaoverrideeklemen iyi pratiktir).Virtual fonksiyonlar sadece pointer veya referans üzerinden çağrıldığında polimorfik çalışır. Değer kopyalama ile çalışmaz (object slicing!).
Constructor'lar virtual olamaz. Destructor'lar virtual olmalı (birazdan göreceğiz).
Override Keyword
C++11 ile gelen override anahtar kelimesi, derived class'taki fonksiyonun gerçekten base class'taki bir virtual fonksiyonu override ettiğini garanti eder.
class Animal {
public:
virtual void speak() const {
std::cout << "...\n";
}
};
class Dog : public Animal {
public:
// void speak() override { } // HATA! const eksik
void speak() const override { // OK ✅
std::cout << "Woof!\n";
}
};override yazmasaydın, const eksikliği fark edilmezdi. Derleyici yeni bir fonksiyon tanımladığını düşünür ve sessizce kabul ederdi. Bu da çalışma zamanında beklenmedik davranışlara yol açardı.
💡 Her zaman `override` kullan. Bu, C++ topluluğunun en güçlü tavsiyelerinden biridir. Hata yakalamayı dramatik şekilde artırır.
Final Keyword
Bazen bir fonksiyonun daha fazla override edilmesini engellemek istersin. final bunu sağlar:
class Animal {
public:
virtual void speak() {
std::cout << "...\n";
}
};
class Dog : public Animal {
public:
void speak() override final { // Bu fonksiyon artık override edilemez
std::cout << "Woof!\n";
}
};
class Puppy : public Dog {
public:
// void speak() override { } // HATA! Dog::speak() final
};final ayrıca sınıf seviyesinde de kullanılabilir. Bir sınıftan kalıtım yapılmasını tamamen engellersin:
class Singleton final {
// Bu sınıftan kimse türetemez
};
// class MySingleton : public Singleton { }; // HATA!Final Ne Zaman Kullanılır?
Bir sınıfın davranışının değiştirilmemesi gerektiğinde
Performans optimizasyonu için (derleyici devirtualization yapabilir)
Tasarım gereği kalıtımı engellemek istediğinde
Base Pointer ile Derived Nesne Kullanımı
Polimorfizmin gerçek gücü, farklı tipleri aynı koleksiyonda tutabilmekten gelir:
#include <iostream>
#include <vector>
#include <memory>
class Shape {
public:
virtual void draw() const {
std::cout << "Drawing shape\n";
}
virtual double area() const = 0;
virtual ~Shape() = default;
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
void draw() const override {
std::cout << "Drawing circle (r=" << radius << ")\n";
}
double area() const override {
return 3.14159 * radius * radius;
}
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(double w, double h) : w(w), h(h) {}
void draw() const override {
std::cout << "Drawing rectangle (" << w << "x" << h << ")\n";
}
double area() const override {
return w * h;
}
};Şimdi bunları bir arada kullan:
int main() {
std::vector<std::unique_ptr<Shape>> shapes;
shapes.push_back(std::make_unique<Circle>(5.0));
shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));
shapes.push_back(std::make_unique<Circle>(2.5));
for (const auto& shape : shapes) {
shape->draw();
std::cout << " Area: " << shape->area() << "\n";
}
}Çıktı:
Drawing circle (r=5)
Area: 78.5398
Drawing rectangle (3x4)
Area: 12
Drawing circle (r=2.5)
Area: 19.635Tek bir vector<Shape*> içinde farklı tipleri tutuyorsun. Her biri kendi draw() ve area() fonksiyonunu çağırıyor. Bu polimorfizmin özü. Yeni bir şekil eklemek istediğinde sadece yeni bir sınıf yaz — mevcut kodu değiştirmene gerek yok.
Virtual Destructor
Polimorfizm kullanıyorsan, base class'ın destructor'ı mutlaka virtual olmalı:
class Base {
public:
~Base() { // virtual DEĞİL — tehlikeli!
std::cout << "Base destroyed\n";
}
};
class Derived : public Base {
int* data;
public:
Derived() : data(new int[100]) {}
~Derived() {
delete[] data;
std::cout << "Derived destroyed\n";
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // Sadece Base::~Base() çalışır!
// Derived::~Derived() çalışmaz → BELLEK SIZINTISI
}Çıktı (hatalı):
Base destroyedDerived'ın destructor'ı çalışmadı. data serbest bırakılmadı. Memory leak!
Düzeltme: base destructor'ı virtual yap:
class Base {
public:
virtual ~Base() {
std::cout << "Base destroyed\n";
}
};Artık çıktı:
Derived destroyed
Base destroyed⚠️ Altın kural: Sınıfında en az bir virtual fonksiyon varsa, destructor'ı da virtual yap. Modern C++ idiom'u:
virtual ~Base() = default;
vtable Mekanizması (Nasıl Çalışır?)
Peki C++ bu sihri nasıl yapıyor? Çalışma zamanında doğru fonksiyonu nasıl buluyor?
Cevap: vtable (virtual table / sanal fonksiyon tablosu).
Bir sınıfta en az bir virtual fonksiyon varsa, derleyici o sınıf için gizli bir tablo oluşturur. Bu tabloda her virtual fonksiyonun adresi bulunur.
Animal vtable:
┌──────────────┐
│ speak → Animal::speak │
│ ~Animal → Animal::~Animal │
└──────────────┘
Dog vtable:
┌──────────────┐
│ speak → Dog::speak │ ← override edildi
│ ~Dog → Dog::~Dog │
└──────────────┘
Cat vtable:
┌──────────────┐
│ speak → Cat::speak │ ← override edildi
│ ~Cat → Cat::~Cat │
└──────────────┘Her nesnenin içinde gizli bir vptr (virtual pointer) bulunur. Bu pointer, o nesnenin ait olduğu sınıfın vtable'ını gösterir.
Dog nesnesi (bellekte):
┌──────────┐
│ vptr ──────────→ Dog vtable
│ name │
│ age │
│ breed │
└──────────┘ptr->speak() çağırdığında:
Nesnenin
vptr'ına bakvptrüzerinden vtable'ı bulvtable'dan
speakfonksiyonunun adresini alO adresi çağır
Bu bir indirection (dolaylı erişim). Normal fonksiyon çağrısına göre küçük bir ek maliyet var ama modern işlemcilerde bu maliyet neredeyse sıfırdır.
vtable'ın Maliyeti
Her sınıf başına bir vtable (statik, paylaşılan)
Her nesne başına bir vptr (genellikle 8 byte, 64-bit sistemde)
Her virtual fonksiyon çağrısı bir pointer dereference ekstra
💡 Pratik: vtable maliyeti, %99.9 oranında ihmal edilebilir. Performans kaygısıyla virtual fonksiyonlardan kaçınma — profiler'ın sana gerçekten sorun olduğunu göstermedikçe.
Polimorfizm ve Referanslar
Polimorfizm sadece pointer'larla değil, referanslarla da çalışır:
void makeSound(const Animal& animal) {
animal.speak(); // Polimorfik çağrı
}
int main() {
Dog dog;
Cat cat;
makeSound(dog); // "Woof!"
makeSound(cat); // "Meow!"
}Fonksiyon const Animal& alıyor ama gerçek tipe göre doğru speak() çağrılıyor. Bu pattern fonksiyon parametrelerinde çok yaygın.
Polimorfik Fonksiyonlar ve Referans Parametreler
Polimorfizmin en yaygın kullanım yeri fonksiyon parametreleridir. Fonksiyona base class referansı alırsın, herhangi bir derived class nesnesini geçebilirsin:
class Logger {
public:
virtual void log(const std::string& msg) const = 0;
virtual ~Logger() = default;
};
class ConsoleLogger : public Logger {
public:
void log(const std::string& msg) const override {
std::cout << "[CONSOLE] " << msg << "\n";
}
};
class FileLogger : public Logger {
std::string filename;
public:
FileLogger(const std::string& f) : filename(f) {}
void log(const std::string& msg) const override {
std::cout << "[FILE:" << filename << "] " << msg << "\n";
}
};
// Logger tipini bilmeden çalışır
void processOrder(const Logger& logger, int orderId) {
logger.log("Processing order #" + std::to_string(orderId));
// ... iş mantığı ...
logger.log("Order #" + std::to_string(orderId) + " completed");
}
int main() {
ConsoleLogger console;
FileLogger file("orders.log");
processOrder(console, 1001); // Konsola yazar
processOrder(file, 1002); // Dosyaya yazar
}processOrder fonksiyonu, hangi logger tipinin geldiğini bilmiyor. Sadece Logger& arayüzüyle çalışıyor. Bu, dependency injection'ın en basit halidir.
Covariant Return Types
Override edilen bir virtual fonksiyonun dönüş tipi, base class'ınkinden farklı olabilir — ama sadece covariant ise. Yani dönüş tipi, base dönüş tipinin bir derived'ı olan pointer veya referans olmalı:
class Animal {
public:
virtual Animal* clone() const {
return new Animal(*this);
}
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
Dog* clone() const override { // Dog* döndürüyor, Animal* değil — OK!
return new Dog(*this);
}
};
int main() {
Dog rex;
Dog* copy = rex.clone(); // Dog* döner, cast gerekmez
delete copy;
}Bu küçük bir kolaylık ama clone pattern'ında çok kullanışlı.
Virtual Fonksiyon Çağrısı Constructor İçinde
Constructor ve destructor içinde virtual fonksiyon çağrısı polimorfik çalışmaz:
class Base {
public:
Base() {
speak(); // Base::speak() çağrılır, Derived::speak() DEĞİL
}
virtual void speak() {
std::cout << "Base speaking\n";
}
virtual ~Base() = default;
};
class Derived : public Base {
public:
void speak() override {
std::cout << "Derived speaking\n";
}
};
int main() {
Derived d; // "Base speaking" yazdırır!
}Neden? Çünkü Base'in constructor'ı çalışırken Derived kısmı henüz oluşturulmamış. Var olmayan bir nesnenin fonksiyonunu çağırmak tehlikeli olurdu.
⚠️ Kural: Constructor ve destructor içinde virtual fonksiyon çağırma. Eğer çağırırsan, her zaman o sınıfın kendi versiyonu çalışır, derived versiyonu değil.
Pratik Örnek: Ödeme Sistemi
Gerçek dünya senaryosuyla polimorfizmi pekiştirelim:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
class PaymentMethod {
public:
virtual bool processPayment(double amount) = 0;
virtual std::string getName() const = 0;
virtual ~PaymentMethod() = default;
};
class CreditCard : public PaymentMethod {
std::string cardNumber;
public:
CreditCard(const std::string& num) : cardNumber(num) {}
bool processPayment(double amount) override {
std::cout << "Credit card " << cardNumber
<< ": charged $" << amount << "\n";
return true;
}
std::string getName() const override { return "Credit Card"; }
};
class PayPal : public PaymentMethod {
std::string email;
public:
PayPal(const std::string& e) : email(e) {}
bool processPayment(double amount) override {
std::cout << "PayPal " << email
<< ": transferred $" << amount << "\n";
return true;
}
std::string getName() const override { return "PayPal"; }
};
class Crypto : public PaymentMethod {
std::string walletAddress;
public:
Crypto(const std::string& addr) : walletAddress(addr) {}
bool processPayment(double amount) override {
std::cout << "Crypto wallet " << walletAddress
<< ": sent $" << amount << "\n";
return true;
}
std::string getName() const override { return "Cryptocurrency"; }
};Kullanım:
int main() {
std::vector<std::unique_ptr<PaymentMethod>> methods;
methods.push_back(std::make_unique<CreditCard>("4532-XXXX"));
methods.push_back(std::make_unique<PayPal>("user@mail.com"));
methods.push_back(std::make_unique<Crypto>("0xABC123"));
double amount = 99.99;
for (const auto& method : methods) {
std::cout << "Using " << method->getName() << ": ";
method->processPayment(amount);
}
}Yeni ödeme yöntemi eklemek istediğinde (ApplePay, BankTransfer vs.) sadece yeni bir sınıf yaz. main() fonksiyonu veya mevcut sınıflar hiç değişmez. Bu, Open/Closed Principle'ın (SOLID) ta kendisi: genişlemeye açık, değişikliğe kapalı.
Compile-Time vs Run-Time Polimorfizm
C++'ta aslında iki tür polimorfizm var:
Compile-Time (Statik) Polimorfizm
Derleme zamanında çözülür. Araçları:
Fonksiyon overloading: Aynı isim, farklı parametreler
Template'ler: Aynı kod, farklı tipler
Operatör overloading: Aynı operatör, farklı davranış
// Overloading — derleme zamanında hangisi çağrılacağı belirlenir
void process(int x) { std::cout << "int: " << x << "\n"; }
void process(double x) { std::cout << "double: " << x << "\n"; }
void process(const std::string& x) { std::cout << "string: " << x << "\n"; }
int main() {
process(42); // int versiyonu
process(3.14); // double versiyonu
process("hello"s); // string versiyonu
}Run-Time (Dinamik) Polimorfizm
Çalışma zamanında çözülür. Aracı: virtual fonksiyonlar. Bu derste öğrendiğimiz her şey buraya dahil.
İkisi arasındaki temel fark:
| Özellik | Compile-Time | Run-Time |
|---|---|---|
| Çözüm zamanı | Derleme | Çalışma zamanı |
| Mekanizma | Overloading, template | virtual fonksiyon, vtable |
| Performans | Sıfır ek maliyet | Küçük vtable maliyeti |
| Esneklik | Tipler derleme zamanında bilinmeli | Tipler çalışma zamanında belirlenebilir |
Çoğu zaman "polimorfizm" denince run-time polimorfizm kastedilir ama her iki türü de bilmek önemli.
Polimorfizm ile Tasarım: Open/Closed Principle
Polimorfizmin en büyük faydası Open/Closed Principle (Açık/Kapalı İlkesi) uygulamasını mümkün kılmasıdır: "Yazılım birimleri genişlemeye açık, değişikliğe kapalı olmalıdır."
Polimorfizm olmadan yeni bir şekil eklemek istesen:
// KÖTÜ — polimorfizmsiz
enum ShapeType { CIRCLE, RECTANGLE, TRIANGLE };
struct Shape {
ShapeType type;
double radius;
double width, height;
double base, sideHeight;
};
void draw(const Shape& s) {
switch (s.type) { // Yeni şekil ekleyince burayı değiştirmelisin
case CIRCLE: std::cout << "Drawing circle\n"; break;
case RECTANGLE: std::cout << "Drawing rectangle\n"; break;
case TRIANGLE: std::cout << "Drawing triangle\n"; break;
}
}
double area(const Shape& s) {
switch (s.type) { // Burayı da değiştirmelisin
case CIRCLE: return 3.14 * s.radius * s.radius;
case RECTANGLE: return s.width * s.height;
case TRIANGLE: return 0.5 * s.base * s.sideHeight;
}
return 0;
}
// Her yeni şekilde draw(), area(), ve diğer TÜM switch'leri güncellemen gerekir!Polimorfizm ile:
// İYİ — polimorfik
class Shape {
public:
virtual void draw() const = 0;
virtual double area() const = 0;
virtual ~Shape() = default;
};
// Yeni şekil eklemek → sadece yeni sınıf yaz, mevcut kodu DEĞİŞTİRME
class Pentagon : public Shape {
double side;
public:
Pentagon(double s) : side(s) {}
void draw() const override { std::cout << "Drawing pentagon\n"; }
double area() const override { return 1.72 * side * side; }
};Mevcut kodda hiçbir değişiklik yok. Yeni sınıf ekle, mevcut vector<Shape*> otomatik olarak onu da destekler. Bu, polimorfizmin gerçek gücü.
Smart Pointer'lar ile Polimorfizm
Modern C++'ta raw pointer yerine smart pointer kullanmalısın. Polimorfik nesnelerle en çok std::unique_ptr kullanılır:
#include <iostream>
#include <vector>
#include <memory>
class Animal {
public:
virtual void speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override { std::cout << "Woof!\n"; }
};
class Cat : public Animal {
public:
void speak() const override { std::cout << "Meow!\n"; }
};
int main() {
// unique_ptr ile polimorfik koleksiyon
std::vector<std::unique_ptr<Animal>> zoo;
zoo.push_back(std::make_unique<Dog>());
zoo.push_back(std::make_unique<Cat>());
zoo.push_back(std::make_unique<Dog>());
for (const auto& animal : zoo) {
animal->speak();
}
// Bellek yönetimi otomatik — delete çağırmana gerek yok
}unique_ptr<Animal> ile:
Polimorfik davranış çalışır (virtual fonksiyonlar)
Bellek otomatik yönetilir (scope dışına çıkınca delete)
Exception safety garantisi var
💡 Modern C++ kuralı: Polimorfik nesneleri
new/deleteile değil,std::unique_ptrveyastd::shared_ptrile yönet. Bu hem güvenli hem de temiz koddur.
Özet
Polimorfizm, aynı arayüz üzerinden farklı tiplerin farklı davranışlar sergilemesidir — "aynı kumanda, farklı cihaz" mantığı.
virtual anahtar kelimesi, fonksiyonun çalışma zamanında gerçek nesne tipine göre çağrılmasını sağlar (dynamic binding).
override her zaman kullan — derleme zamanında hataları yakalar. final ise override'ı veya kalıtımı engellemek için kullanılır.
vtable mekanizması, her sınıf için bir fonksiyon adresi tablosu tutar; nesneler vptr ile bu tabloya erişir.
Virtual destructor polimorfik sınıflarda zorunludur — aksi halde memory leak olur.
Constructor/destructor içinde virtual fonksiyon çağrısı polimorfik çalışmaz — her zaman o sınıfın kendi versiyonu çağrılır.
Modern C++'ta polimorfik nesneleri smart pointer'lar ile yönet ve Open/Closed Principle uygula.
AI Asistan
Sorularını yanıtlamaya hazır