Ders 09 / 14
Sanal Fonksiyonlar ve Sanal Tablolar
Bir taban işaretçisi ya da referansı üzerinden yapılan sanal çağrının, ifadenin statik tipinden bağımsız olarak hep aynı nesneyle çalıştığının ölçümü.
Yayın Güncelleme
Bu derste
- Sanal çağrının hangi işlevi çalıştıracağını nesnenin gerçek tipine göre seçtiğini gösterir
- Bir taban işaretçisi ya da referansı üzerinden yapılan çağrının hep aynı nesneyle çalıştığını ölçer
- Bir dizideki her taban işaretçisinin kendi gösterdiği nesneye ulaştığını, başka bir nesneye değil, doğrular
İçindekiler
Önceki ders, taban alt nesnenin türetilmiş nesnenin bir parçası olduğunu, adresinin
onunla çakıştığını ölçtü — ama hiçbir işlev virtual değildi, çağrılacak işlev
derleme zamanında ifadenin statik tipine bakılarak belirlendi. Bu ders virtual
anahtar sözcüğünü ekliyor: artık bir Sekil* ya da Sekil& üzerinden yapılan bir
çağrı, işaretçinin statik tipine değil, gösterdiği nesnenin gerçek tipine bakarak
hangi işlevi çalıştıracağına karar veriyor. Soru değişti: taban alt nesnenin adresi
değil, sanal bir çağrının gerçekte hangi nesneyle çalıştığı ölçülüyor.
Sanal Çağrı Aynı Nesneye Ulaşıyor
Dikdortgen, Sekil’in tanit işlevini yeniden tanımlıyor. d nesnesine hem bir
Sekil* işaretçiyle hem bir Sekil& referansla erişilip tanit çağrıldığında, ikisi de
d’nin kendi işlevine mi ulaşıyor?
// sanal_ayni_nesne.cpp — sanal cagri statik tipten bagimsiz olarak ayni nesneye ulasiyor
#include <iostream>
class Sekil {
public:
virtual ~Sekil() = default;
virtual void tanit(const void* beklenen) const {
std::cout << "tanit(Sekil): calisan nesne beklenenle ayni mi "
<< std::boolalpha << (static_cast<const void*>(this) == beklenen) << "\n";
}
};
class Dikdortgen : public Sekil {
public:
void tanit(const void* beklenen) const override {
std::cout << "tanit(Dikdortgen): calisan nesne beklenenle ayni mi "
<< std::boolalpha << (static_cast<const void*>(this) == beklenen) << "\n";
}
};
int main() {
Dikdortgen d;
Sekil* taban_isaretci = &d;
Sekil& taban_referans = d;
taban_isaretci->tanit(&d);
taban_referans.tanit(&d);
return 0;
}
tanit(Dikdortgen): calisan nesne beklenenle ayni mi true tanit(Dikdortgen): calisan nesne beklenenle ayni mi true
CB4. İki çağrı da statik tipi Sekil olan bir ifade üzerinden yapıldı
(taban_isaretci, taban_referans), ama ikisi de “tanit(Dikdortgen)” bastı — sanal
çağrı, işaretçinin/referansın statik tipine değil, gösterdiği nesnenin gerçek tipine
bakarak Dikdortgen::tanit’i seçti. İşlev içinde this, d‘nin adresiyle
karşılaştırıldığında true dönüyor: çağrı yalnız doğru işlevi seçmekle kalmıyor,
seçtiği işlevi de tam olarak d‘nin kendisi üzerinde çalıştırıyor, bir kopya ya da
başka bir nesne üzerinde değil. (M08/K02’de sorulan soru çağrıyı hangi tipin
çözdüğüydü; burada da benzer bir mekanizma var, ama ölçülen şey çağrının hangi
nesneyle çalıştığı — ve Java’da nesne dilimleme yokken C++‘ta taban tipe değerle
atama yeni ve eksik bir nesne kurardı; burada yalnız işaretçi/referans erişimi var,
dilimleme devreye girmiyor.) M11/K03’ün Nesne Benzeri Tasarım dersinde elle kurulan
yöntem tablosu (işlev işaretçileri taşıyan bir struct) burada derleyicinin ürettiği
sanal işlev tablosuna karşılık geliyor; mekanizmanın ayrıntısı orada işlendi, burada yeniden
anlatılmıyor. Sınıf kendisi: this, çağrının yapıldığı ifadenin statik tipinden
bağımsız olarak hep d’nin adresini taşıyor.
Taban İşaretçi Dizisi: Her Çağrı Kendi Nesnesine Ulaşıyor
Bir Sekil* dizisi farklı türetilmiş nesnelere işaret ettiğinde, her eleman kendi
gösterdiği nesneye mi ulaşıyor, yoksa aralarında bir karışma mı oluyor?
// sanal_dizi.cpp — dizideki her taban isaretcisi kendi gosterdigi nesneye ulasiyor
#include <iostream>
class Sekil {
public:
virtual ~Sekil() = default;
virtual bool ayni_mi(const void* adres) const {
return static_cast<const void*>(this) == adres;
}
};
class Dikdortgen : public Sekil {};
class Ucgen : public Sekil {};
int main() {
Dikdortgen d;
Ucgen u;
Sekil* sekiller[2] = { &d, &u };
std::cout << std::boolalpha;
std::cout << "sekiller[0] kendi nesnesi d ile ayni mi "
<< sekiller[0]->ayni_mi(&d) << "\n";
std::cout << "sekiller[1] kendi nesnesi u ile ayni mi "
<< sekiller[1]->ayni_mi(&u) << "\n";
std::cout << "sekiller[0] u ile ayni mi (capraz denetim) "
<< sekiller[0]->ayni_mi(&u) << "\n";
return 0;
}
sekiller[0] kendi nesnesi d ile ayni mi true sekiller[1] kendi nesnesi u ile ayni mi true sekiller[0] u ile ayni mi (capraz denetim) false
CB5. Dikdortgen ve Ucgen tanit’i yeniden tanımlamıyor, Sekil::ayni_mi’yi
olduğu gibi devralıyor — ama işlev yine de her çağrıldığında this’i çağrıldığı
elemanın gösterdiği gerçek nesneye bağlıyor. sekiller[0]->ayni_mi(&d) true,
sekiller[1]->ayni_mi(&u) true: her eleman kendi nesnesine ulaşıyor. Çapraz
denetim bunu daha da netleştiriyor — sekiller[0] (d’yi gösteren eleman) &u ile
karşılaştırıldığında false dönüyor, çünkü sekiller[0]’ın çalıştığı nesne d,
u değil. Sınıf kendisi: dizideki her taban işaretçisi, aynı taban tipini taşısa
da, kendi gösterdiği tikel nesneden başka hiçbir nesneyle çalışmıyor.
İşleve Referansla Geçirmek Nesneyi Değiştirmiyor
Bir işleve const Sekil& parametresiyle geçirilen bir nesne, işlev içinde hâlâ
dışarıdaki aynı nesne mi, yoksa işlevin kendi kopyası mı?
// sanal_referans_parametre.cpp — isleve referansla gecirilen nesne disaridakiyle ayni
#include <iostream>
class Sekil {
public:
virtual ~Sekil() = default;
virtual bool ayni_mi(const void* adres) const {
return static_cast<const void*>(this) == adres;
}
};
class Dikdortgen : public Sekil {};
void dogrula(const Sekil& s, const void* beklenen) {
std::cout << std::boolalpha
<< "islev icinde cagrilan nesne beklenenle ayni mi "
<< s.ayni_mi(beklenen) << "\n";
}
int main() {
Dikdortgen d;
dogrula(d, &d);
return 0;
}
islev icinde cagrilan nesne beklenenle ayni mi true
CB6. dogrula(d, &d) çağrısında s, d’nin bir kopyası değil, d’nin kendisine
bağlanan bir referans — const Sekil& parametre türü bunu bir kopya kurmadan
sağlıyor. İşlev içinde s.ayni_mi(&d) çağrıldığında sanal çağrı yine Sekil::ayni_mi’yi
seçiyor (çünkü Dikdortgen onu yeniden tanımlamadı) ve this, dışarıdaki d’nin
adresiyle karşılaştırıldığında true dönüyor. İşlev sınırını geçmek — parametrenin
referans olması koşuluyla — nesnenin kimliğini değiştirmiyor: dogrula içindeki s,
main içindeki d ile aynı nesne. Sınıf kendisi: işlev çağrısı bir kopya
kurmadığı sürece, sanal çağrı hep çağrının başladığı yerdeki gerçek nesneyle çalışıyor.
override ve final: Aynı Nesneye Yine de Gerçek Tipe Göre Ulaşmak
KenarliUcgen, Ucgen’den türeyen ve ayni_mi’yi hem override hem final olarak
işaretleyen, kendisi de final olan bir sınıf. Bu iki anahtar sözcük çalışma zamanı davranışını değil, derleme
zamanı denetimini etkiliyor — ama sonuçta çalışan sanal çağrı yine aynı nesneye mi
ulaşıyor?
// sanal_override_final.cpp — override ve final ile isaretlenmis sanal cagri yine nesnenin gercek tipine gore calisiyor
#include <iostream>
class Sekil {
public:
virtual ~Sekil() = default;
virtual bool ayni_mi(const void* adres) const {
return static_cast<const void*>(this) == adres;
}
};
class Ucgen : public Sekil {
public:
bool ayni_mi(const void* adres) const override {
return static_cast<const void*>(this) == adres;
}
};
class KenarliUcgen final : public Ucgen {
public:
bool ayni_mi(const void* adres) const override final {
return static_cast<const void*>(this) == adres;
}
};
int main() {
KenarliUcgen k;
Sekil& taban_referans = k;
std::cout << std::boolalpha
<< "taban referans uzerinden cagrilan ayni_mi beklenenle ayni mi "
<< taban_referans.ayni_mi(&k) << "\n";
return 0;
}
taban referans uzerinden cagrilan ayni_mi beklenenle ayni mi true
CB23. override, derleyiciye “bu işlev bir taban sınıf işlevini yeniden
tanımlıyor” der ve imza eşleşmezse (örneğin const unutulursa, ya da parametre tipi
yanlış yazılırsa) derleme hatası çıkarır — imza kazara uyuşmayıp sessizce yeni, ayrı bir
işlev tanımlanmasını (ve taban işlevin hiç yeniden tanımlanmamasını) önler. Bu koruma
derleme zamanında işliyor; sandbox yalnız başarıyla derlenen kodu çalıştırdığından,
imza uyuşmazlığının nasıl bir hataya yol açtığı burada çalıştırılarak gösterilemiyor,
yalnız anlatılabiliyor. final, hem KenarliUcgen sınıfının hem ayni_mi işlevinin
daha fazla türetilmesini/yeniden tanımlanmasını derleme zamanında engelliyor — bu da
çalıştırılarak değil, yalnızca yasaklanan bir ihlalin derlenmemesiyle gözlenebilecek bir
şey. Çalışan kısım değişmedi: taban_referans.ayni_mi(&k) true basıyor, çünkü
override/final çalışma zamanı davranışını değiştirmiyor, yalnız hangi imzaların
geçerli sayılacağını daraltıyor. Sınıf kendisi: final bir sınıfın nesnesi de,
diğerleri gibi, sanal çağrının ulaştığı nesnenin ta kendisi.
Sanal Yıkıcı: Taban İşaretçiyle Silme Doğru Zinciri Çalıştırıyor
Sekil’in yıkıcısı virtual. new ile kurulan bir Dikdortgen bir Sekil*
üzerinden delete edildiğinde, yıkım Dikdortgen’in kendi yıkıcısından mı başlıyor,
yoksa doğrudan Sekil’inkine mi atlıyor?
// sanal_yikici_taban_isaretciyle_silme.cpp — sanal yikici taban isaretciyle silindiginde dogru yikim zincirini calistiriyor
#include <iostream>
class Sekil {
public:
virtual ~Sekil() { std::cout << "Sekil yikici\n"; }
};
class Dikdortgen : public Sekil {
public:
~Dikdortgen() override { std::cout << "Dikdortgen yikici\n"; }
};
int main() {
Sekil* taban_isaretci = new Dikdortgen();
std::cout << "-- silme basliyor --\n";
delete taban_isaretci;
return 0;
}
-- silme basliyor -- Dikdortgen yikici Sekil yikici
CB24. taban_isaretci’nin statik tipi Sekil*, ama delete taban_isaretci
çağrıldığında önce Dikdortgen yikici basılıyor, sonra Sekil yikici — yıkım da,
kurma gibi, en türemişten en tabana doğru işliyor, ve bunu doğru sırayla başlatabilen
şey ~Sekil()’in virtual olması. Bu, standartın tanımladığı davranış: taban sınıfın
yıkıcısı sanal olduğunda, taban işaretçi üzerinden yapılan delete gerçek (dinamik)
tipin yıkıcısını çağırarak başlıyor. Karşılığının ise tanımlı bir sonucu yok: ~Sekil()
virtual olmasaydı, statik ile dinamik tipin farklı olduğu bu delete taban_isaretci
standarda göre tanımsız davranış olurdu. Yaygın derleyicilerde bu çoğu zaman yalnız
Sekil’in yıkıcısının çalışması ve Dikdortgen‘in eklediği kaynakların serbest
bırakılmaması biçiminde görülür, ama bu bir derleyici davranışıdır, dilin güvencesi
değildir. Bu dal burada çalıştırılmıyor: tanımsız davranış deterministik
bir çıktı vermeyeceğinden sandbox’ta doğrulanabilir bir sonucu yok, yalnız anlatılabilir.
Sınıf kendisi: delete çağrısı taban_isaretci’nin statik tipinden bağımsız olarak
hep d’nin (gerçek nesnenin) tam yıkım zincirini işletiyor.
Elle Kurulan Yöntem Tablosu ile Derleyicinin Sanal İşlev Tablosu Aynı Sonucu Veriyor
M11/K03’ün Nesne Benzeri Tasarım dersi, işlev işaretçileri taşıyan bir struct
ile elle bir “yöntem tablosu” kurmuştu. Böyle elle kurulan bir tablo üzerinden yapılan
çağrı ile derleyicinin ürettiği sanal işlev tablosu üzerinden yapılan virtual çağrı, aynı
nesne için aynı sonucu mu veriyor?
// sanal_el_ile_yontem_tablosu.cpp — elle kurulan yontem tablosu derleyicinin urettigi sanal islev tablosuyla ayni sonucu veriyor
#include <iostream>
struct Sekil {
virtual ~Sekil() = default;
virtual int alan() const { return 0; }
};
struct Kare : public Sekil {
explicit Kare(int kenar) : kenar_(kenar) {}
int alan() const override { return kenar_ * kenar_; }
int kenar_;
};
struct ElleYontemTablosu {
int (*alan)(const void*);
};
int kare_alan_el_ile(const void* nesne) {
return static_cast<const Kare*>(nesne)->alan();
}
int main() {
Kare kare(5);
Sekil& sanal_erisim = kare;
ElleYontemTablosu tablo{ kare_alan_el_ile };
std::cout << "derleyicinin sanal cagrisi: " << sanal_erisim.alan() << "\n";
std::cout << "elle kurulan tablo cagrisi: " << tablo.alan(&kare) << "\n";
std::cout << std::boolalpha << "iki sonuc ayni mi "
<< (sanal_erisim.alan() == tablo.alan(&kare)) << "\n";
return 0;
}
derleyicinin sanal cagrisi: 25 elle kurulan tablo cagrisi: 25 iki sonuc ayni mi true
CB25. sanal_erisim.alan() derleyicinin ürettiği sanal işlev tablosu üzerinden
Kare::alan‘a ulaşıyor; tablo.alan(&kare) ise elle kurulan ElleYontemTablosu
üzerinden aynı işleve (bu kez doğrudan bir işlev işaretçisiyle) ulaşıyor. İkisi de
25 basıyor ve karşılaştırma true: iki farklı mekanizma, aynı nesne için aynı
işlevi seçiyor. Fark, seçimin nasıl yapıldığında — sanal çağrıda derleyici her
nesneye gizli bir sanal işlev tablosu işaretçisi ekliyor ve alan() her çağrıldığında o
işaretçi üzerinden doğru girdiyi buluyor; elle kurulan tabloda bu adımı burada
ElleYontemTablosu tablo{...} satırı ve tabloyu doğru nesneyle eşleştirme
sorumluluğu programcıya ait. M11/K03’te bu mekanizma ayrıntılı işlendi; burada yalnız
iki yaklaşımın aynı sonuca vardığı doğrulanıyor. Sınıf kendisi: her iki çağrı da
kare’nin kendisi üzerinde çalışıyor.
Sanal Dinamik Gönderimin Yapısal Maliyeti: Daha Büyük Nesne
SanalNokta, SanalsizNokta ile aynı iki int alanını taşıyor, tek farkla: bir sanal
yıkıcısı var. Aynı alanları taşıyan bu iki sınıf aynı boyutta mı, yoksa sanal işlev
taşımanın nesne başına bir yapısal bedeli mi var?
// sanal_dinamik_gonderim_maliyeti.cpp — sanal islev tasiyan sinif ayni alanlari tasiyan sanal olmayan siniftan daha buyuk
#include <iostream>
class SanalsizNokta {
public:
int x_ = 0;
int y_ = 0;
};
class SanalNokta {
public:
virtual ~SanalNokta() = default;
int x_ = 0;
int y_ = 0;
};
int main() {
std::cout << std::boolalpha
<< "sanal islev tasiyan sinif, tasimayandan daha buyuk mu "
<< (sizeof(SanalNokta) > sizeof(SanalsizNokta)) << "\n";
return 0;
}
sanal islev tasiyan sinif, tasimayandan daha buyuk mu true
CB26. İki sınıf birebir aynı iki int alanını taşıyor, ama karşılaştırma true:
SanalNokta daha büyük. Fark, virtual bir işlev tanımlanır tanımlanmaz derleyicinin
her nesneye eklediği gizli bir sanal işlev tablosu işaretçisinden geliyor — bu, dinamik
gönderimin nesne başına, bir kerelik yapısal bedeli. Bunun yanında bir de çağrı
başına bir bedel var: her sanal çağrı, doğrudan bir atlama yerine önce nesnenin sanal
işlev tablosu işaretçisi üzerinden doğru girdiyi bulan bir dolaylama yapıyor. Bu ikinci bedel
burada ölçülmüyor, çünkü süre ölçümü donanıma, derleyici optimizasyon düzeyine ve
sandbox’ın kendi yüküne bağlı olur — bu ders bu tür ortama bağlı verileri raporlamıyor.
Ölçülebilir ve deterministik olan yalnız boyut farkı. Sınıf parça: sanal işlev tablosu
işaretçisi nesnenin bir parçası, ayrı bir nesne değil.
Yapıcı İçinde Sanal Çağrı: Türemişe Değil, Tabana Gidiyor
Taban’ın yapıcısı kendi gövdesi içinde tanimla()’yı çağırıyor; Turemis bu işlevi
yeniden tanımlıyor. Bir Turemis nesnesi kurulurken Taban’ın yapıcısı içindeki bu
çağrı Turemis::tanimla’ya mı ulaşıyor, yoksa Taban::tanimla’ya mı?
// sanal_yapici_icinde_cagri.cpp — yapici icinde yapilan sanal cagri turemisin degil tabanin kendi islevine gidiyor
#include <iostream>
class Taban {
public:
Taban() {
std::cout << "Taban yapicisi icinde tanimla() cagriliyor\n";
tanimla();
}
virtual void tanimla() const {
std::cout << "Taban::tanimla calisti\n";
}
};
class Turemis : public Taban {
public:
Turemis() {
std::cout << "Turemis yapicisi icinde tanimla() cagriliyor\n";
tanimla();
}
void tanimla() const override {
std::cout << "Turemis::tanimla calisti\n";
}
};
int main() {
Turemis t;
return 0;
}
Taban yapicisi icinde tanimla() cagriliyor Taban::tanimla calisti Turemis yapicisi icinde tanimla() cagriliyor Turemis::tanimla calisti
CB27. t’nin son hâli bir Turemis, ama Taban’ın yapıcısı çalışırken içindeki
tanimla() çağrısı Taban::tanimla’yı çalıştırıyor, Turemis::tanimla’yı değil.
Neden: Taban’ın yapıcısı çalıştığı sırada Turemis’in kendi parçası henüz
kurulmamış — nesnenin o andaki etkin tipi, sanal gönderim açısından, hâlâ Taban. Bu
zincir yukarı doğru ilerledikçe değişiyor: Turemis’in kendi yapıcısı çalışırken
yapılan tanimla() çağrısı artık Turemis::tanimla’yı çalıştırıyor, çünkü o noktada
nesnenin Turemis parçası da kurulmuş durumda. Sınıf kendisi: her iki çağrı da
t’nin ta kendisi üzerinde çalışıyor, ama hangi işlevin seçileceği, çağrının yapıldığı
anda inşa zincirinin nerede olduğuna bağlı.
Sınırlayıcı Ölçüm
Bu derste ölçülen sanal çağrılar hep bir işaretçi ya da referans üzerinden yapıldı;
Dikdortgen’i değerle bir Sekil değişkenine atamak (Sekil s = d;) bu ölçümün
kapsamı dışında — böyle bir atama sanal çağrıyı değil, bir önceki dersin sonunda
değinilen dilimlemeyi devreye sokar ve sonuç yeni, eksik bir Sekil nesnesidir. Ölçüm
ayrıca tanit’in ve ayni_mi’nin kendi içinde başka bir sanal çağrı yapmadığı basit
durumları kapsıyor; bir taban yıkıcının sanal olmaması durumunda ortaya çıkabilecek
eksik yıkım bu derste hiç gözlenmedi, çünkü her iki sınıf da (Sekil, türetilenleri)
sanal bir yıkıcı taşıyor. Bir taban sınıfın hiç gövdesi olmayan, yalnız arayüz sunan
işlevlerle kurulması — soyut sınıflar — bu dersin konusu değil; bir sonraki ders bunu
ölçüyor.
Özet
- Bir
Sekil*ya daSekil&üzerinden yapılan sanal çağrı, ifadenin statik tipine değil, gösterdiği nesnenin gerçek tipine bakarak hangi işlevi çalıştıracağına karar veriyor. - Seçilen işlev içinde
this, çağrının yapıldığı taban tipinden ifadeden bağımsız olarak hep aynı gerçek nesnenin adresini taşıyor — sınıf kendisi. - Bir
Sekil*dizisindeki her eleman kendi gösterdiği tikel nesneye ulaşıyor; çapraz denetim başka bir elemanın nesnesiyle eşleşmediğini doğruluyor. - Bir işleve
const Sekil¶metresiyle geçirilen nesne, işlev içinde de dışarıdaki aynı nesne — referans bağlama bir kopya kurmuyor. - Taban tipe değerle atama (dilimleme) bu dersin kapsamı dışında; sanal çağrı yalnız işaretçi/referans erişiminde bu şekilde çalışıyor.
Sonraki Adım
Bu derste Sekil::tanit ve Sekil::ayni_mi’nin bir gövdesi vardı — türetilmiş sınıflar
onu yeniden tanımlamasa da çağrılabiliyordu. Bir sonraki ders, Soyut Sınıflar ve
Arayüzler, bir taban sınıfın hiç gövdesi olmayan, yalnız bir sözleşme bildiren
işlevlerle kurulduğu durumu ölçüyor — orada taban sınıftan doğrudan bir nesne
kurulamıyor, yalnız türetilmiş sınıflardan; ölçülen yine bir sanal çağrının hangi
nesneyle çalıştığı, ama bu kez taban tipin kendi başına hiçbir örneği yok.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.