SANAL FONKSİYONLAR VE ÇOK BİÇİMLİLİK Yılmaz Kılıçaslan
GİRİŞ C++, hem derleyici-zamanlı hem de çalışma-zamanlı çok biçimliliği destekler. Derleyici-zamanlı çok biçimlilik fonksiyonların veya operatörlerin aşırı yüklenmesi yoluyla gerçekleştirilir. Çalışma-zamanlı çok biçimlilik ise kalıtım ve sanal fonksiyon mekanizmaları aracılığıyla gerçekleştirilir.
Sınıf Hiyerarşisi Dört sınıf arasında tanımlamış olduğumuz kalıtım ilişkisi şematik olarak aşağıdaki gibi gösterilebilir: Calisan UcretliCalisan Yonetici SatisElemanı
TEMEL VE TÜRETİLMİŞ SINIFLAR ARASINDA DÖNÜŞÜM - 1 Bir satış elemanının aynı zamanda bir ücretli çalışan olması nedeniyle, bir UcretliCalisan nesnesine ihtiyaç duyduğumuz zaman bir SatisElemani nesnesi kullanabiliyor olmamız anlamlı olacaktır. Bu tarz bir ilişkiyi desteklemek için, C++ örtük olarak bir türetilmiş sınıf nesnesini bir temel sınıf nesnesine dönüştürür: UcretliCalisan isci; SatisElemani satici( “Ali Uzun” ); isci = satici; //SatisElemani => UcretliCalisan //turetilmis => temel Ters yönlü bir atama kabul edilmeyecektir: satici = isci; // Error; cannot convert
TEMEL VE TÜRETİLMİŞ SINIFLAR ARASINDA DÖNÜŞÜM - 2 Benzer şekilde, türetilmiş bir sınıf nesnesine işaret eden bir işaretçi örtük olarak bir temel sınıf nesnesine işaret eden bir işaretçiye dönüştürülebilir: Calisan *cPtr; UcretliCalisan isci( “Oya Kiper” ); SatisElemani satici( “Ali Uzun” ); Yonetici mudur( “Mehmet Demir “ ); cPtr = &isci; //UcretliCalisan * => Calisan * cPtr = &satici; //SatisElemani * => Calisan * cPtr = &mudur; //Yonetici * => Calisan *
TEMEL VE TÜRETİLMİŞ SINIFLAR ARASINDA DÖNÜŞÜM - 3 Bir nesneye işaretçi ile referansta bulunulduğu zaman hangi eleman fonksiyonun çağrılacağını işaretçinin tipi belirler. Eğer bir türetilmiş sınıf nesnesine bir temel sınıf işaretçisi ile işaret ederseniz yalnızca temel sınıfa ait fonksiyonları çağırabilirsiniz: SatisElemani satici( “Ali Uzun” ); SatisElemani *sPtr; UcretliCalisan *uPtr; sPtr = &satici; uPtr = &satici; uPtr->sureBelirle(40.0); //UcretliCalisan sPtr->ucretBelirle(6.0); //UcretliCalisan uPtr->satislariBelirle(1000.0); //Error sPtr->satislariBelirle(1000.0); //SatisElemani sPtr->komisyonBelirle(0.05); //SatisElemani float taban, toplam; taban = uPtr->odemeHesapla(); //UcretliCalisan toplam = sPtr->odemeHesapla(); //SatisElemani
TEMEL VE TÜRETİLMİŞ SINIFLAR ARASINDA DÖNÜŞÜM - 4 Türetilmiş bir sınıf işaretçisinin bir temel sınıf işaretçisine dönüştürülebilmesi, örneğin, bir çalışanlar topluluğu oluşturmamıza imkan verebilir: Class PersonelListesi {public: PersonelListesi(); ekle( Calisan *yeniPer ); // … };
TEMEL VE TÜRETİLMİŞ SINIFLAR ARASINDA DÖNÜŞÜM - 5 PersonelListesi bolum1; UcretliCalisan *ucretliPtr; SatisElemani *saticiPtr; Yonetici *yoneticiPtr; // Yeni nesneler için bellek talebi ucretliPtr = new UcretliCalisan(“Ayse Suer” ); saticiPtr = new SatisElemani( “Ali Demir” ); yoneticiPtr = new Yonetici( “Mehmet Yilmaz” ); // Listeye ekleme bolum1.ekle( ucretliPtr ); bolum1.ekle( saticiPtr ); bolum1.ekle( yoneticiPtr ); Buradaki sorun her nesne yalnızca genel bir Calisan düzeyinde işleme tabi olabilir.
SANAL FONKSİYONLAR (sentaks) Sanal fonksiyon temel bir sınıfta bildirimi yapılan fakat türetilmiş bir sınıfta yeniden tanımlanmasını beklediğimiz bir eleman fonksiyondur. Bir sanal fonksiyon bildirimi temel sınıftaki eleman fonksiyon bildiriminin öncesine virtual anahtar sözcüğünü yerleştirerek gerçekleştirilir. Global fonksiyonlar, arkadaş fonksiyonlar ve statik eleman fonksiyonlar sanal fonksiyon olamazlar. virtual anahtar sözcüğü, türetilmiş sınıflardaki bildirimler ya da sınıf dışında tanımlanmış sanal fonksiyonlar için gerekli değildir; bir sanal fonksiyonun türetilmiş sınıflardaki versiyonları örtük olarak sanal bildirimlidirler. Türetilmiş sınıftaki sanal fonksiyon bildiriminde ya da sınıf dışındaki sanal fonksiyon tanımında virtual anahtar sözcüğünü kullanmak derleyici tarafından bir hata olarak yorumlanmaz.
SANAL FONKSİYONLAR (semantik) “Normal” bir erişimde, sanal fonksiyonlar diğer bütün sınıfa ait eleman fonksiyonlar gibi davranırlar. Fakat, sanal fonksiyonları önemli kılan ve çalışma zamanlı çok biçimliliği desteklemesini sağlayan bir işaretçi ya da referans ile erişilmeleri halindeki davranışlarıdır. Eğer bir temel sınıf işaretçisi ya da referansı bu temel sınıftan türetilmiş bir sınıfın nesnesine işaret ediyorsa / referansta bulunuyorsa, sıradan fonksiyonlarda olmasını beklediğimizin tersine, C++ sanal bir fonksiyonun hangi versiyonunu çağıracağına nesneye bakarak karar verir. Bu karar çalışma zamanında gerçekleştirilir. Dolayısıyla, farklı nesnelere işaret edildiğinde / referansta bulunulduğunda fonksiyonun farklı versiyonları çalıştırılacaktır.
SANAL FONKSİYONLAR (örnek) //Sanal odemeHesapla fonksiyonlu calisan sinif hiyerarsisi class Calisan { public: Calisan( const char *nm ); char *isimSoyle() const; virtual float odemeHesapla() const; virtual ~Calisan() {} private: char isim[30]; }; class UcretliCalisan : public Calisan UcretliCalisan( const char *nm ); void ucretBelirle( float ucrt ); void sureBelirle( float st ); float odemeHesapla() const; // sanal (ortuk) float ucret; float saat; };
SANAL FONKSİYONLAR (örnek) class SatisElemani : public UcretliCalisan { public: SatisElemani( const char *nm ); void komisyonBelirle( float kom ); void satislariBelirle( float satislar ); float odemeHesapla() const; // sanal (ortuk) private: float komisyon; float satisMiktari; }; class Yonetici : Calisan Yonetici( const char *nm ); void maasBelirle( float maas ); float haftalikMaas; };
SANAL FONKSİYONLAR (örnek) Her sınıfın kendisine ait odemeHesapla fonksiyonunun tanımı değişmeden kalabilir. Fakat, temel sınıfa eklenen odemeHesapla fonksiyonu için bir tanım gerekmektedir: float Calisan::odemeHesapla() const { cout << “Hesaplama islemi tanimlanmamis\n”; return 0.0; } Bu fonksiyon eğer salt bir Calisan nesnesi kullanıldığında ya da türetilmiş sınıflardan bir tanesi kendi odemeHesapla fonksiyonunu tanımlamamış ise çağrılır. // Bir Calisan isaretcisi ile odemeHesapla cagrimi Calisan *cPtr; float ucret; cPtr = &isci; ucret = cPtr->odemeHesapla(); //UcretliCalisan cPtr = &satici; ucret = cPtr->odemeHesapla(); //SatisElemani cPtr = &mudur; ucret = cPtr->odemeHesapla(); //Yonetici Eğer odemeHesapla sanal bildirimli olmasaydı, her fonksiyon çağrımı, 0.0 değerini döndüren Calisan::odemeHesapla fonksiyonunu çalıştıracaktı. Fakat, verilen çağrımların aynı olmasına rağmen çalıştırılacak fonksiyonlar farklı olacaktır.
SANAL FONKSİYONLAR (örnek) // Bir baska ornek #include <iostream> using namespace std; class temel { public: virtual void sanalFonk() {cout << “Temel sinif – sanalFonk\n”;} }; class turetilmis1 : public temel void sanalFonk() {cout << “Turetilmis sinif1 – sanalFonk\n”;} class turetilmis2 : public temel {cout << “Turetilmis sinif2 – sanalFonk\n”;}
SANAL FONKSİYONLAR (örnek) int main() { temel *p, b; turetilmis1 d1; turetilmis2 d2; p = &b; p->sanalFonk(); p = &d1; p = &d2; return 0; } Program çıktısını belirleyiniz.
SANAL FONKSİYONLAR (örnek) Program çıktısı: Temel sinif – sanalFonk Turetilmis sinif1 – sanalFonk Turetilmis sinif2 – sanalFonk “Normal” yolla sanal fonksiyon çağrımı: d2.sanalFonk; // turetilmis2::sanalFonk
SANAL FONKSİYONLAR ve REFERANSLAR - 1 // Temel sinif referansi ile sanal fonksiyon erisimi #include <iostream> using namespace std; class temel { public: virtual void sanalFonk() {cout << “Temel sinif – sanalFonk\n”;} }; class turetilmis1 : public temel void sanalFonk() {cout << “Turetilmis sinif1 – sanalFonk\n”;} class turetilmis2 : public temel {cout << “Turetilmis sinif2 – sanalFonk\n”;}
SANAL FONKSİYONLAR ve REFERANSLAR - 2 // Temel sinif referansini parametre alan fonksiyon void f(temel &r) { r.sanalFonk() ; } int main() { temel b; turetilmis1 d1; turetilmis2 d2; f(b); f(d1); f(d2); return 0; } Program çıktısını belirleyiniz.
Sanallık Kalıtımla Aktarılır - 1 Sanal bir fonksiyon kalıtıldığında, sanal doğası da kalıtılır: #include <iostream> using namespace std; class temel { public: virtual void sanalFonk() {cout << “Temel sinif – sanalFonk\n”;} }; class turetilmis1 : public temel void sanalFonk() {cout << “Turetilmis sinif1 – sanalFonk\n”;} class turetilmis2 : public turetilmis1 {cout << “Turetilmis sinif2 – sanalFonk\n”;}
Sanallık Kalıtımla Aktarılır - 2 int main() { temel *p, b; turetilmis1 d1; turetilmis2 d2; p = &b; p->sanalFonk(); p = &d1; p = &d2; return 0; } Bu kez, turetilmis2 adlı sınıf temel adlı sınıftan değil turetilmis1 adlı sınıftan türetilmiştir ama sanalFonk yine sanal kalmıştır. Program çıktısını belirleyiniz.
Sanal Fonksiyonlar Hiyerarşiktir - 1 Eğer türetilmiş bir sınıf sanal bir fonksiyonu yeniden tanımlamaz ise bu sınıfın bir nesnesi sanal fonksiyona erişmek istediğinde temel sınıf tarafından tanımlanan versiyon kullanılır: #include <iostream> using namespace std; class temel { public: virtual void sanalFonk() {cout << “Temel sinif – sanalFonk\n”;} }; class turetilmis1 : public temel void sanalFonk() {cout << “Turetilmis sinif1 – sanalFonk\n”;} class turetilmis2 : public temel // sanalFonk() yeniden tanimlanmamis.
Sanal Fonksiyonlar Hiyerarşiktir - 2 int main() { temel *p, b; turetilmis1 d1; turetilmis2 d2; p = &b; p->sanalFonk(); p = &d1; p = &d2; return 0; } Program çıktısını belirleyiniz.
Sanal Fonksiyonlar Hiyerarşiktir - 3 Program çıktısı: Temel sinif – sanalFonk Turetilmis sinif1 – sanalFonk Yukarıdaki program genel bir kuralın özel bir durumunu örneklemektedir. Kalıtım, C++’da hiyerarşik olarak tanımlandığı için, sanal fonksiyonlar da hiyerarşik olmalıdır. Bu ise, eğer türetilmiş sınıf sanal fonksiyonu yeniden tanımlamış ise tersinden hiyerarşik sıralamada ilk yeniden tanımlanmış fonksiyonun kullanılacağı anlamına gelir (bkz. bir sonraki örnek).
Sanal Fonksiyonlar Hiyerarşiktir - 4 #include <iostream> using namespace std; class temel { public: virtual void sanalFonk() {cout << “Temel sinif – sanalFonk\n”;} }; class turetilmis1 : public temel void sanalFonk() {cout << “Turetilmis sinif1 – sanalFonk\n”;} class turetilmis2 : public turetilmis1 // sanalFonk() yeniden tanimlanmamis.
Sanal Fonksiyonlar Hiyerarşiktir - 5 int main() { temel *p, b; turetilmis1 d1; turetilmis2 d2; p = &b; p->sanalFonk(); p = &d1; p = &d2; return 0; } Program çıktısını belirleyiniz.
Sanal Fonksiyonlar Hiyerarşiktir - 6 Program çıktısı: Temel sinif – sanalFonk Turetilmis sinif1 – sanalFonk
SAF SANAL FONKSİYONLAR - 1 Birçok durumda temel sınıftaki sanal fonksiyon için anlamlı bir tanım bulunmayabilir. Bu durumlarda saf sanal fonksiyonlar (“pure virtual functions”) kullanılır. Saf sanal fonksiyon temel sınıfta tanımı olmayan sanal fonksiyondur ve bildiriminin genel formu aşağıdaki gibidir: virtual tip fonksiyon-ismi(parametreler) = 0; Saf sanal fonksiyon türetilmiş bütün sınıflarda yeniden tanımlanmalıdır. Aksi, derleyici-zamanlı hataya yol açar. En az bir saf sanal fonksiyon içeren sınıfa soyut (“abstract”) sınıf denir. Soyut sınıflara ait nesne yaratılamaz ama soyut sınıflara işaret eden işaretçiler ya da referansta bulunan referanslar yaratılabilir.
SAF SANAL FONKSİYONLAR - 2 #include <iostream> using namespace std; class sayi { protected: int deger; public: void degerBelirle(int i) { deger = i;} virtual void goruntule() = 0; }; class hexTip : public sayi { public: void goruntule() {cout << hex << deger << “\n”;} class desTip : public sayi void goruntule() {cout << deger << “\n”;} class oktTip : public sayi void goruntule() {cout << oct << deger << “\n”;}
SAF SANAL FONKSİYONLAR - 3 int main() { desTip d; hexTip h; oktTip o; d.degerBelirle(20); d.goruntule(); // 20 h.degerBelirle(20); h.goruntule(); // 14 o.degerBelirle(20); o.goruntule(); // 24 return 0; }
SANAL YIKICI FONKSİYONLAR Problem: Dinamik olarak yaratılmış nesneleri delete operatörü ile yok etmek potansiyel bir problem kaynağıdır. Eğer delete bir temel sınıf işaretçisine uygulanıyorsa, işaretçi türetilmiş bir sınıfın nesne işaret ediyor bile olsa, derleyici temel sınıfın yıkıcı fonksiyonunu çağıracaktır. Çözüm: Bu sorun temel sınıfın yıkıcı fonksiyonunu sanal yaparak çözülür. Bu şeklide, türetilmiş sınıfların yıkıcı fonksiyonları da sanal yapılmış olacaktır ve böylelikle delete operatörü temel sınıf işaretçisine uygulandığında ilgili yıkıcı fonksiyon çağrılacaktır. İlke olarak sanal fonksiyon içeren bir sınıf yazıldığında, sanal bir yıkıcı fonksiyon da tanımlanmalıdır. Yıkıcı fonksiyonların aksine, yapıcı fonksiyonlar sanal olarak tanımlanamaz.
Kaynaklar Programmer’s Guides, Microsoft Visual C++.