İçeriğe geç
academia.sh

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 da Sekil& ü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& parametresiyle 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat