Nesne Yönelimli Programlamanın (OOP) en kritik ve heyecan verici özelliklerinden biri olan Polimorfizm (Çok biçimlilik), Yunanca kökenli bir kelimedir ve “birden fazla form alma yeteneği” anlamına gelir. Yazılım dünyasında polimorfizm basitçe “tek isim, birden fazla form” ilkesini ifade eder.
Bir işlemin, üzerinde çalıştığı verinin tipine veya nesnenin sınıfına göre farklı davranışlar sergilemesi polimorfizmin temelidir. C++ dilinde bu kavram, karmaşık yazılım sistemlerinin esnekliğini ve yeniden kullanılabilirliğini (reusability) inanılmaz derecede artırır.
C++’ta polimorfizm temel olarak ikiye ayrılır: Derleme Zamanı (Compile-time) Polimorfizmi ve Çalışma Zamanı (Run-time) Polimorfizmi.
1. Derleme Zamanı Polimorfizmi (Erken Bağlama)
Derleme zamanı polimorfizmi, fonksiyon aşırı yükleme (function overloading) ve operatör aşırı yükleme (operator overloading) kullanılarak elde edilir.
Bu yöntemde, aynı isme sahip aşırı yüklenmiş fonksiyonlardan hangisinin çalıştırılacağı, fonksiyona gönderilen argümanların veri tipine ve sayısına bakılarak derleyici (compiler) tarafından derleme aşamasında belirlenir. Bütün eşleştirmeler ve bağlamalar program henüz çalışmadan önce yapıldığı için bu işleme erken bağlama (early binding), statik bağlama (static binding) veya statik bağlama (static linking) adı verilir.
- Örnek: Bir toplama operatörü (
+), iki sayı için kullanıldığında matematiksel toplamı verirken; aynı operatör iki farklı metin (string) için kullanıldığında bu iki metni uç uca ekleme işlemi yapabilir.
2. Çalışma Zamanı Polimorfizmi (Geç Bağlama)
Şimdi farklı bir senaryo düşünelim. Hem temel sınıf (base class) hem de bu sınıftan türetilmiş alt sınıf (derived class), aynı isme ve aynı argüman yapısına sahip bir fonksiyon barındırıyorsa ne olur?. Derleyici sadece prototiplere bakarak hangi fonksiyonun çağrılacağını bilemez çünkü aşırı yükleme şartları (farklı argüman yapıları) sağlanmamıştır.
Hangi fonksiyonun çalıştırılacağına program derlenirken değil, program çalışırken (run-time) dinamik olarak karar verilmesi gerekir. Seçim işleminin dinamik olarak çalışma zamanında yapıldığı bu mekanizmaya geç bağlama (late binding) veya dinamik bağlama (dynamic binding) denir.
Çalışma zamanı polimorfizmi C++’ta nesnelere işaret eden işaretçiler (object pointers) ve sanal fonksiyonlar (virtual functions) aracılığıyla uygulanır.
Nesne İşaretçileri ve Temel Problem
C++’ta bir temel sınıf (base class) işaretçisi, o sınıftan türetilmiş (derived class) herhangi bir alt nesneyi gösterecek şekilde tanımlanabilir. Ancak normal şartlarda, bir temel sınıf işaretçisi türetilmiş bir nesnenin adresini tutsa bile, bu işaretçi üzerinden bir fonksiyon çağrıldığında derleyici daima temel sınıfın fonksiyonunu çalıştırır. İşaretçinin içeriği (hangi nesneyi gösterdiği) göz ardı edilir; derleyici sadece işaretçinin tipini dikkate alır.
İşte bu engeli aşarak “gerçek” polimorfizme ulaşmak için Sanal Fonksiyonlara ihtiyacımız vardır.
3. Sanal Fonksiyonlar (Virtual Functions)
Temel sınıftaki bir fonksiyonun normal bildiriminin başına virtual anahtar kelimesi eklendiğinde, o fonksiyon bir sanal fonksiyon (virtual function) haline gelir.
Bir fonksiyon sanal yapıldığında, C++ hangi fonksiyonun kullanılacağına işaretçinin tipine göre değil, işaretçinin o an gösterdiği nesnenin tipine göre çalışma zamanında karar verir. Böylece, temel sınıf işaretçisini farklı nesnelere yönlendirerek sanal fonksiyonun farklı versiyonlarını çalıştırabiliriz.
Aşağıdaki pedagojik örneği inceleyerek bu kavramı pekiştirelim:
#include <iostream>
using namespace std;
class Base {
public:
void display() { cout << "\n Temel sınıf display() fonksiyonu"; }
virtual void show() { cout << "\n Temel sınıf sanal show() fonksiyonu"; }
};
class Derived : public Base {
public:
void display() { cout << "\n Türetilmiş sınıf display() fonksiyonu"; }
void show() { cout << "\n Türetilmiş sınıf sanal show() fonksiyonu"; }
};
int main() {
Base B;
Derived D;
Base *bptr; // Temel sınıf işaretçisi
cout << "\n bptr Temel nesneyi gösteriyor \n";
bptr = &B;
bptr->display(); // Temel sınıf versiyonunu çağırır
bptr->show(); // Temel sınıf versiyonunu çağırır
cout << "\n\n bptr Türetilmiş nesneyi gösteriyor \n";
bptr = &D;
bptr->display(); // Temel sınıf versiyonunu çağırır (virtual değil!)
bptr->show(); // Türetilmiş sınıf versiyonunu çağırır (VIRTUAL!)
return 0;
}
Yukarıdaki kodda, bptr işaretçisi türetilmiş sınıf olan D nesnesini gösterdiği halde, bptr->display() çağrısı temel sınıfı çalıştırır çünkü display() sanal (virtual) değildir. Ancak show() fonksiyonu virtual olarak tanımlandığı için, bptr->show() komutu doğrudan işaret edilen nesnenin (Türetilmiş sınıfın) show() fonksiyonunu tetikler.
4. Polimorfizm İçin Gerçek Hayattan Örnek: Medya Dükkanı
Polimorfizmin gücünü görmek için hem kitap hem de video kaset satan bir medya dükkanı (book shop) düşünelim. Yazılımımızda bir media temel sınıfı oluşturarak ürünlerin başlık (title) ve fiyat (price) bilgilerini saklayabiliriz. Daha sonra bu sınıftan, kitapların sayfa sayısını tutacak bir book alt sınıfı ve kasetlerin oynatma süresini tutacak bir tape alt sınıfı türetebiliriz.
media temel sınıfında virtual void display() isimli bir sanal fonksiyon tanımlar ve bunu türetilmiş sınıflarda yeniden yazarız (redefine).
Ana programımızda, farklı ürünleri tek bir çatı altında işleyebilmek için media* list; şeklinde bir temel sınıf işaretçi dizisi (heterojen liste) oluşturabiliriz. Bir döngü kurup list[i]->display(); metodunu çağırdığımızda; işaretçinin o an işaret ettiği nesne bir kitap ise kitabın kendi display() fonksiyonu, kaset ise kasetin kendi display() fonksiyonu çağrılır. Bu mekanizma, karmaşık yazılımlarda yüzlerce farklı sınıfı tek bir arayüzden yönetmemizi sağlar!
5. Sanal Fonksiyon Kuralları
Başarılı bir C++ polimorfizm tasarımı için sanal fonksiyonlara dair şu katı kurallara dikkat edilmelidir:
- Sanal fonksiyonlar, bir sınıfın üyesi olmalıdır.
- Statik (static) üye olarak tanımlanamazlar.
- Çalışma zamanı polimorfizmi sadece nesne işaretçileri (object pointers) kullanılarak erişildiğinde devreye girer. Nokta operatörüyle doğrudan erişim bu davranışı tetiklemez.
- Temel sınıftaki sanal fonksiyon ile türetilmiş sınıftaki fonksiyonun prototipi (imzası) tamamen aynı olmalıdır. Farklı olursa C++ bunu aşırı yükleme (overloading) olarak algılar ve sanal fonksiyon mekanizması iptal olur.
- Sanal yıkıcılar (virtual destructors) olabilir ancak sanal kurucular (virtual constructors) olamaz.
- Türetilmiş sınıfta sanal fonksiyonu yeniden tanımlamazsanız, fonksiyon çağrıldığında otomatik olarak temel sınıftaki versiyonu çalıştırılır.
6. Saf Sanal Fonksiyonlar ve Soyut Sınıflar (Pure Virtual Functions & Abstract Classes)
Sıklıkla karşılaşılan bir durum, temel sınıfta (base class) yer alan sanal fonksiyonun kendi başına herhangi bir görev yapmamasıdır. Bu fonksiyon sadece türetilmiş sınıfların kullanımı için bir yer tutucu (placeholder) olarak tanımlanır.
Hiçbir kod bloğu ve işlevi olmayan bu tür fonksiyonlara “hiçbir şey yapmayan” (do-nothing) fonksiyonlar veya Saf Sanal Fonksiyonlar (Pure Virtual Functions) adı verilir. Bir saf sanal fonksiyon şu şekilde tanımlanır:
virtual void display() = 0;
Eğer bir sınıfın içerisinde en az bir tane saf sanal fonksiyon bulunuyorsa, bu sınıftan hiçbir şekilde doğrudan nesne üretilemez. Nesne oluşturulamayan bu özel temel sınıflara programlama dünyasında Soyut Sınıflar (Abstract Classes) veya Soyut Temel Sınıflar (Abstract Base Classes) denir. Bir soyut sınıfın tek amacı, kendisinden türetilecek diğer sınıflar için bir şablon sunmak ve çalışma zamanı polimorfizmi elde etmek için gereken temel işaretçiyi (base pointer) sağlamaktır. Eğer türetilmiş sınıf, temel sınıftaki saf sanal fonksiyonu yeniden tanımlamazsa, o alt sınıf da otomatik olarak soyut sınıf haline gelir.





