İçeriğe geç
academia.sh

Ders 08 / 14

Kalıtım

Bir türetilmiş nesnenin içinde taşıdığı taban alt nesnesinin, aynı adreste durduğunun ve tek bir kurma/yıkma sırasının parçası olduğunun ölçümü.

Yayın Güncelleme

Bu derste

  • Bir türetilmiş sınıfın taban alt nesnesini nesnenin bir parçası olarak tanımlar
  • Taban ve türetilmiş nesnenin aynı adresi paylaştığını adres karşılaştırmasıyla ölçer
  • Kurucu ve yıkıcı çağrı sırasının tek bir nesnenin iki aşaması olduğunu gözlemler
İçindekiler

Önceki konu bir nesnenin kendi kaynağını nasıl kurduğunu, kopyaladığını ve serbest bıraktığını ölçtü; sınıf tek başınaydı, başka bir sınıfa bağlı değildi. Bu konu bir sınıfın başka bir sınıfın üzerine nasıl kurulduğunu ele alıyor: kalıtım, bir sınıfı bir başkasının içine yerleştirir. Dersin ölçü sorusu aynı — bu ifade hangi nesneyle çalışıyor — ama artık nesnenin kendisi iki katmanlı: bir taban alt nesne ve onu saran bir türetilmiş nesne. Bu ders şunu soruyor: taban alt nesne, türetilmiş nesneden ayrı bir nesne mi, yoksa onun bir parçası mı?

Taban Alt Nesnesi ve Ortak Adres

Yonetici, Calisan’dan türüyor. Yonetici kurulduğunda önce Calisan yapıcısı çalışıyor, sonra Yonetici’nin kendi gövdesi. Taban tipinden bir işaretçiyle (taban) y’yi adlandırmak bir kopya çıkarmıyor mu, yoksa aynı adresi mi taşıyor — bunu adres karşılaştırmasıyla ölçmek gerekiyor.

// kalitim_taban.cpp — turetilmis nesnenin taban alt nesnesi ve ortak adres
#include <iostream>
#include <string>

class Calisan {
public:
    explicit Calisan(std::string ad) : ad_(ad) {
        std::cout << "Calisan yapici: " << ad_ << "\n";
    }
    ~Calisan() {
        std::cout << "Calisan yikici: " << ad_ << "\n";
    }
protected:
    std::string ad_;
};

class Yonetici : public Calisan {
public:
    Yonetici(std::string ad, int takim_no) : Calisan(ad), takim_no_(takim_no) {
        std::cout << "Yonetici yapici: takim " << takim_no_ << "\n";
    }
    ~Yonetici() {
        std::cout << "Yonetici yikici\n";
    }
private:
    int takim_no_;
};

int main() {
    Yonetici y("Aylin", 4);
    Calisan* taban = &y;

    std::cout << std::boolalpha;
    std::cout << "y ile taban adresinin ayni olup olmadigi "
              << (static_cast<void*>(&y) == static_cast<void*>(taban)) << "\n";

    return 0;
}
Calisan yapici: Aylin
Yonetici yapici: takim 4
y ile taban adresinin ayni olup olmadigi true
Yonetici yikici
Calisan yikici: Aylin

CB1. Yonetici yapıcısı önce Calisan yapıcısını çağırıyor (“Calisan yapici: Aylin”), sonra kendi gövdesini çalıştırıyor (“Yonetici yapici: takim 4”) — bu, iki ayrı nesnenin art arda kurulması değil, tek bir nesnenin iki aşamalı kurulması. taban = &y ataması bir kopya çıkarmıyor, yalnız aynı adresi taban tipinden bir işaretçiyle adlandırıyor: static_cast<void*>(&y) == static_cast<void*>(taban) karşılaştırması bunu doğruluyor, true. Çıkışta y kapsam dışına çıkarken yıkım sırası kurmanın tam tersini izliyor — önce Yonetici‘nin kendi yıkıcısı, sonra Calisan‘ınki — yine tek nesnenin iki aşamalı çözülüşü. (M08/K02’de sorulan soru çağrıyı hangi tipin çözdüğüydü; burada soru değişti — çağrı değil, ifadenin işaret ettiği nesne ölçülüyor, ve Java’da nesne dilimleme yokken C++’ta taban tipe değerle atama yeni ve eksik bir nesne kurar; burada yalnız işaretçi erişimi var, dilimleme yok.) Sınıf parça: taban’ın gösterdiği şey y’den bağımsız bir nesne değil, y’nin içindeki taban alt nesnesi.

Taban Referansıyla Erişim: Parçaya Erişim, Ayrı Nesne Değil

Bir işleve Calisan& parametresiyle bir Yonetici geçirmek de aynı soruyu soruyor: referans parametre y’den ayrı bir nesneye mi bağlanıyor, yoksa y’nin içindeki taban parçasına mı?

// kalitim_referans.cpp — taban referansiyla erisim ayni parcaya ulasiyor
#include <iostream>
#include <string>

class Calisan {
public:
    explicit Calisan(std::string ad) : ad_(ad) {}
    std::string ad() const { return ad_; }
protected:
    std::string ad_;
};

class Yonetici : public Calisan {
public:
    Yonetici(std::string ad, int takim_no) : Calisan(ad), takim_no_(takim_no) {}
    int takim_no() const { return takim_no_; }
private:
    int takim_no_;
};

bool ayni_nesne_mi(const Calisan& c, const Yonetici& y) {
    return static_cast<const void*>(&c) == static_cast<const void*>(&y);
}

void etiketle(const Calisan& c) {
    std::cout << "referansin gordugu ad " << c.ad() << "\n";
}

int main() {
    Yonetici y("Baris", 7);

    etiketle(y);

    std::cout << std::boolalpha;
    std::cout << "referans parametrenin y ile ayni nesneyi gosterip gostermedigi "
              << ayni_nesne_mi(y, y) << "\n";

    return 0;
}
referansin gordugu ad Baris
referans parametrenin y ile ayni nesneyi gosterip gostermedigi true

CB2. etiketle(y) çağrısında y bir Calisan&’a bağlanıyor; ad() çağrısı y’nin içindeki Calisan parçasının ad_ alanını okuyor ve “Baris” basıyor — Yonetici’nin eklediği takim_no_’ya hiç erişmeden. ayni_nesne_mi(y, y) çağrısında ilk parametre y’yi Calisan&’a, ikinci parametre aynı y’yi Yonetici&’ya bağlıyor; iki referansın adresi karşılaştırıldığında sonuç true. Referans bağlama burada da bir kopya kurmuyor — parametre, çağrıda adı geçen nesnenin kendisine (daha doğrusu, o nesnenin taban parçasına) bağlanıyor. Sınıf yine parça: Calisan& parametresinin gösterdiği şey, y’nin dışında yaşayan bir nesne değil, y’nin içindeki taban alt nesnesi.

Taşan Bir Nesnenin Boyutu: Parça Taban Nesneyi Büyütür

Adres eşitliği taban alt nesnenin y’nin başında durduğunu gösteriyor; boyut karşılaştırması ise taban alt nesnenin y’nin içinde, ayrı bir kutuda değil, aynı bellek bölgesinde yer kapladığını gösteriyor.

// kalitim_boyut.cpp — turetilmis nesnenin boyutu taban parcayi icine aliyor
#include <iostream>
#include <string>

class Calisan {
public:
    explicit Calisan(std::string ad) : ad_(ad) {}
protected:
    std::string ad_;
};

class Yonetici : public Calisan {
public:
    Yonetici(std::string ad, int takim_no) : Calisan(ad), takim_no_(takim_no) {}
private:
    int takim_no_;
};

int main() {
    std::cout << std::boolalpha;
    std::cout << "yonetici boyutunun calisan arti en az bir int kadar olup olmadigi "
              << (sizeof(Yonetici) >= sizeof(Calisan) + sizeof(int)) << "\n";
    std::cout << "yonetici boyutunun calisandan buyuk olup olmadigi "
              << (sizeof(Yonetici) > sizeof(Calisan)) << "\n";
    return 0;
}
yonetici boyutunun calisan arti en az bir int kadar olup olmadigi true
yonetici boyutunun calisandan buyuk olup olmadigi true

CB3. sizeof(Yonetici), Calisan alt nesnesinin tam boyutunu ve Yonetici’nin kendi eklediği takim_no_ alanını birlikte taşıyor — iki karşılaştırma da true. Bu, ilk iki ölçümün adres eşitliğini bellek düzeni tarafından doğruluyor: taban alt nesne y’nin içinde ayrı, kendi başına ayrılmış bir kutuda durmuyor; y’nin fiziksel bir bölümü olarak, Yonetici’nin kendi alanlarıyla aynı bellek bloğunun içinde yer kaplıyor. Sınıf parça: Yonetici nesnesinin boyutu, taşıdığı Calisan parçasının boyutunu içeriyor, ona ek bir referans ya da işaretçi olarak değil.

Genel, Korumalı ve Özel Kalıtım: Adres Aynı, Dışarı Açıklık Farklı

Üç sınıf aynı Taban’dan farklı erişim belirleyicileriyle türüyor: Genel public, Korumali protected, Ozel private kalıtımla. Üçü de kendi içinde Taban’ın korumalı üyesine erişebiliyor mu? Ve dışarıdan Taban*’a yalnız hangisi dönüştürülebiliyor?

// kalitim_erisim_belirleyicileri.cpp — genel korumali ozel kalitim ayni adresi tasiyor, farkli disari aciklik saglar
#include <iostream>

class Taban {
protected:
    int deger_ = 42;
};

class Genel : public Taban {
public:
    int oku() const { return deger_; }
};

class Korumali : protected Taban {
public:
    int oku() const { return deger_; }
};

class Ozel : private Taban {
public:
    int oku() const { return deger_; }
};

int main() {
    Genel g;
    Korumali k;
    Ozel o;

    std::cout << "genel kalitimda oku() " << g.oku() << "\n";
    std::cout << "korumali kalitimda oku() " << k.oku() << "\n";
    std::cout << "ozel kalitimda oku() " << o.oku() << "\n";

    Taban* taban_g = &g;
    std::cout << std::boolalpha
              << "genel kalitimda disaridan alinan taban* adresinin g ile ayni olup olmadigi "
              << (static_cast<void*>(taban_g) == static_cast<void*>(&g)) << "\n";

    return 0;
}
genel kalitimda oku() 42
korumali kalitimda oku() 42
ozel kalitimda oku() 42
genel kalitimda disaridan alinan taban* adresinin g ile ayni olup olmadigi true

CB19. Üç sınıfın da oku() işlevi deger_’e sınıfın kendi gövdesi içinden erişiyor ve üçü de 42 basıyor — erişim belirleyicisi (public, protected, private) sınıfın kendi içindeki erişimi değiştirmiyor, çünkü protected bir üye, türeyen sınıfın kendi gövdesinden zaten erişilebilir. Değişen şey dışarıdan görünürlük: Taban* taban_g = &g; yalnız Genel public kalıtımla türediği için doğrudan derleniyor — Korumali ya da Ozel’den aynı dönüştürme yapılsaydı, taban alt nesne dışarıya Taban* olarak sunulamazdı. Adres karşılaştırması yine true: erişim belirleyicisi, Genel’in bellek düzenini ya da taban alt nesnenin adresini değiştirmiyor, yalnız o adrese dışarıdan hangi işaretçi tipiyle ulaşılabileceğini sınırlıyor. Sınıf parça: oku()’nun çalıştığı deger_, hangi erişim belirleyicisiyle türetilmiş olursa olsun, hep sınıfın kendi taban parçası — erişim belirleyicisi bunu görünür kılıp kılmamayı seçiyor, nesneyi değiştirmiyor.

Üç Katmanlı Yapıcı Zinciri

İlk ölçüm iki katmanlı bir zinciri (Calisan → Yonetici) gösterdi. Zincir üçüncü bir katmana (A → B → C) uzatıldığında kurma ve yıkma sırası nasıl genişliyor?

// kalitim_uc_katmanli_zincir.cpp — uc katmanli yapici zinciri en tabandan en turemise
#include <iostream>

class A {
public:
    A() { std::cout << "A yapici\n"; }
    ~A() { std::cout << "A yikici\n"; }
};

class B : public A {
public:
    B() { std::cout << "B yapici\n"; }
    ~B() { std::cout << "B yikici\n"; }
};

class C : public B {
public:
    C() { std::cout << "C yapici\n"; }
    ~C() { std::cout << "C yikici\n"; }
};

int main() {
    std::cout << "-- kurma --\n";
    {
        C c;
        std::cout << "-- kapsam sonu, yikim basliyor --\n";
    }
    return 0;
}
-- kurma --
A yapici
B yapici
C yapici
-- kapsam sonu, yikim basliyor --
C yikici
B yikici
A yikici

CB20. C c; tek bir nesne kuruyor, ama bu kurma üç aşamaya ayrılıyor: önce en tabandaki A, sonra B, sonra C’nin kendi gövdesi — zincir kaç katmanlı olursa olsun sıra hep en tabandan en türemişe. Yıkım, kapsam sonunda tam tersi sırayla işliyor: önce C, sonra B, sonra A. Bu, ilk ölçümdeki iki aşamalı kurma/yıkmanın genellemesi — C nesnesi aslında üç iç içe parçadan oluşuyor (A alt nesnesi, onun içinde büyüyen B alt nesnesi, onun içinde büyüyen C’nin kendi alanları), ve her parça yalnız kendinden önceki parça tamamlandıktan sonra kuruluyor. Sınıf parça: C tek bir nesne, ama o nesnenin üç katmanı var ve her katman bir öncekinin üzerine inşa ediliyor.

Tabana Değerle Atama Nesneyi Diliyor: Ayrı Bir Kopya

Önceki ölçümler hep işaretçi ya da referans üzerinden taban parçasına eriştiler — adres hep aynıydı. Yonetici nesnesi Calisan tipinde değerle bir değişkene atandığında adres de aynı mı kalıyor, yoksa bağımsız bir nesne mi ortaya çıkıyor?

// kalitim_dilimleme.cpp — tabana degerle atama nesneyi diliyor, ayri ve bagimsiz bir kopya olusuyor
#include <iostream>
#include <string>

class Calisan {
public:
    explicit Calisan(std::string ad) : ad_(std::move(ad)) {}
    std::string ad() const { return ad_; }
    void ad_ata(std::string ad) { ad_ = std::move(ad); }
protected:
    std::string ad_;
};

class Yonetici : public Calisan {
public:
    Yonetici(std::string ad, int takim_no) : Calisan(std::move(ad)), takim_no_(takim_no) {}
    int takim_no() const { return takim_no_; }
private:
    int takim_no_;
};

int main() {
    Yonetici y("Deniz", 9);
    Calisan dilimlenmis = y;

    std::cout << std::boolalpha;
    std::cout << "dilimlenmis ile y'nin ayni adresi tasiyip tasimadigi "
              << (static_cast<void*>(&dilimlenmis) == static_cast<const void*>(&y)) << "\n";

    dilimlenmis.ad_ata("Baska");
    std::cout << "dilimlenmis adi degistirildikten sonra y'nin adi " << y.ad() << "\n";
    std::cout << "dilimlenmis nesnenin adi " << dilimlenmis.ad() << "\n";

    return 0;
}
dilimlenmis ile y'nin ayni adresi tasiyip tasimadigi false
dilimlenmis adi degistirildikten sonra y'nin adi Deniz
dilimlenmis nesnenin adi Baska

CB21. Calisan dilimlenmis = y; bir işaretçi ya da referans değil, yeni bir Calisan nesnesi kuruyor — Calisan’ın kopya kurucusu y’nin taban parçasından ad_’i kopyalıyor, Yonetici’nin eklediği takim_no_ ise hiç taşınmıyor, çünkü Calisan’ın kendi tanımında öyle bir alan yok. Adres karşılaştırması bunu doğruluyor: false — dilimlenmis, y’den bağımsız, kendi adresinde yaşayan ayrı bir nesne. dilimlenmis’in adı değiştirildiğinde y’nin adı Deniz olarak kalıyor: iki nesne birbirinden bağımsız. Bu, dersin geri kalanından farklı bir sınıf: işaretçi ve referans erişimi hep aynı taban parçasına ulaşırken, değerle atama (dilimleme) Calisan tipinde yeni, bağımsız bir kopya çıkarıyor — ve bu kopya Yonetici’nin eklediği alanları taşımıyor, yalnız Calisan’ın tanımladığı kısmı.

Aşırı Yükleme Seçimi Statik Tipe Göre, Nesne Kimliği Değişmiyor

bilgi işlevi hem Calisan& hem Yonetici& için aşırı yüklenmiş. Aynı y nesnesi bir kez Yonetici& olarak, bir kez Calisan& bir referans üzerinden bilgi’ye geçirildiğinde, hangi aşırı yükleme seçiliyor — ve ikisi de aynı nesneyle mi çalışıyor?

// kalitim_asiri_yukleme_karsilastirmasi.cpp — asiri yukleme secimi statik tipe gore, nesne kimligi calisma zamaninda sabit
#include <iostream>
#include <string>

class Calisan {
public:
    explicit Calisan(std::string ad) : ad_(std::move(ad)) {}
    std::string ad_;
};

class Yonetici : public Calisan {
public:
    Yonetici(std::string ad, int takim_no) : Calisan(std::move(ad)), takim_no_(takim_no) {}
    int takim_no_;
};

void bilgi(const Calisan& c) {
    std::cout << "bilgi(Calisan&) cagrildi, ad " << c.ad_ << "\n";
}

void bilgi(const Yonetici& y) {
    std::cout << "bilgi(Yonetici&) cagrildi, ad " << y.ad_ << ", takim " << y.takim_no_ << "\n";
}

int main() {
    Yonetici y("Kaan", 3);
    Calisan& taban_referans = y;

    bilgi(y);
    bilgi(taban_referans);

    std::cout << std::boolalpha
              << "iki cagrinin da ayni y nesnesiyle calisip calismadigi "
              << (static_cast<const void*>(&y) == static_cast<const void*>(&taban_referans)) << "\n";

    return 0;
}
bilgi(Yonetici&) cagrildi, ad Kaan, takim 3
bilgi(Calisan&) cagrildi, ad Kaan
iki cagrinin da ayni y nesnesiyle calisip calismadigi true

CB22. bilgi(y) derleme zamanında y’nin statik tipine (Yonetici) bakarak bilgi(Yonetici&)’yi seçiyor; bilgi(taban_referans) ise taban_referans’ın statik tipine (Calisan&) bakarak bilgi(Calisan&)‘ı seçiyor — ikisi de virtual değil, seçim tamamen derleme zamanında, ifadenin statik tipine göre yapılıyor. Bu, M08/K02’de ölçülen soruyla aynı eksende: hangi aşırı yüklemenin seçileceği bir derleme zamanı kararı. Ama bu dersin ölçtüğü soru başka: seçilen işlev hangi nesneyle çalışıyor? Adres karşılaştırması true — bilgi(Yonetici&)’nin çalıştığı y ile bilgi(Calisan&)’ın çalıştığı taban_referans’ın gösterdiği nesne tamamen aynı. Aşırı yükleme seçimi hangi işlevin çağrılacağını statik tipe göre belirliyor; nesne kimliği ise bu seçimden bağımsız olarak hep aynı kalıyor. Sınıf parça: iki farklı aşırı yükleme, aynı nesnenin taban parçasında çalışıyor.

Sınırlayıcı Ölçüm

Bu derste ölçülen kalıtım tekli ve sanal olmayan: Yonetici tek bir taban sınıftan türedi ve hiçbir işlev virtual değildi, dolayısıyla taban alt nesnesinin y’nin başında durduğu ve adresinin y ile çakıştığı gözlem bu koşullara bağlı — çoklu kalıtımda (Çoklu Kalıtım ve Elmas Sorunu dersi) ikinci ve sonraki taban alt nesneleri farklı bir ofsette durabilir, adresleri y ile çakışmayabilir. Ölçüm ayrıca Calisan’ın Yonetici olmadan, kendi başına var olduğu durumları kapsamıyor — yalnız bir Yonetici nesnesinin içindeki Calisan parçasını ölçtü. Bir taban işaretçisi ya da referansı üzerinden yapılan bir çağrının hangi işlevi çalıştıracağı — çağrının statik tipe mi, gerçek tipe mi baktığı — bu dersin konusu değil; bir sonraki ders bunu ölçüyor.

Özet

  • Bir Yonetici nesnesi kurulduğunda önce Calisan yapıcısı, sonra Yonetici’nin kendi gövdesi çalışıyor — iki ayrı nesnenin kurulması değil, tek nesnenin iki aşaması.
  • Calisan* taban = &y bir kopya çıkarmıyor; taban ile y aynı adresi taşıyor, çünkü tekli, sanal olmayan kalıtımda taban alt nesne türetilmiş nesnenin başında duruyor.
  • Bir Calisan& parametresine y geçirmek de aynı adresi bağlıyor — referans bağlama bir kopya kurmuyor, sınıf parça.
  • sizeof(Yonetici), Calisan alt nesnesinin tam boyutunu ve Yonetici’nin kendi eklediği alanları birlikte taşıyor; taban alt nesne ayrı bir kutuda değil, aynı nesnenin fiziksel bir bölümünde duruyor.
  • Yıkım sırası kurmanın tersini izliyor: önce Yonetici’nin kendi yıkıcısı, sonra Calisan’ınki — yine tek nesnenin iki aşamalı çözülüşü.

Sonraki Adım

Bu derste taban alt nesnenin adresi hep y ile çakıştı, çünkü hiçbir işlev virtual değildi ve çağrılacak işlev derleme zamanında, ifadenin statik tipine bakılarak belirlendi. Bir sonraki ders, Sanal Fonksiyonlar ve Sanal Tablolar, bir taban işaretçisi ya da referansı üzerinden yapılan bir çağrının hangi işlevi çalıştıracağını nesnenin gerçek tipine göre nasıl belirlediğini ölçüyor — ve bu kez ölçülen adres eşitliği değil, aynı ifadenin çalışma zamanında hep aynı nesneyle çalıştığı.

İ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