İçeriğe geç
academia.sh

Ders 10 / 14

Soyut Sınıflar ve Arayüzler

Salt sanal bir işlevin taban sınıftan hiçbir örnek kurulamamasını nasıl garanti ettiğinin ve bir arayüzün, veri taşımayan salt sözleşmeden ibaret olduğunun ölçümü.

Yayın Güncelleme

Bu derste

  • Salt sanal bir işlevin taban sınıftan doğrudan nesne kurulmasını nasıl engellediğini açıklar
  • Taban sınıfın devraldığı gövdeli bir işlevin bile hep türetilmiş nesneyle çalıştığını ölçer
  • Soyut sınıf ile veri taşımayan bir arayüz sınıfı arasındaki farkı örnekle ayırır
İçindekiler

Önceki ders, Sekil::tanit’in bir gövdesi olduğunu ve türetilmiş sınıflar onu yeniden tanımlamasa da çağrılabildiğini gösterdi. Bu ders, bir taban sınıfın hiç gövdesi olmayan, yalnız bir sözleşme bildiren işlevlerle kurulduğu durumu ele alıyor: salt sanal işlev (= 0 ile işaretlenen). Böyle bir işlevi taşıyan sınıftan doğrudan nesne kurulamıyor — yalnız onu tam olarak dolduran türetilmiş sınıflardan. Ölçü sorusu yine aynı: bir taban işaretçisi ya da referansı üzerinden yapılan bir çağrı hangi nesneyle çalışıyor — ama bu kez taban tipin kendi başına hiçbir örneği yok.

Taban Sınıftan Nesne Kurulamıyor, Yalnız Türetilmişlerden

Araci’nin ayni_mi işlevi salt sanal (= 0); bu, Araci’den doğrudan bir nesne kurmayı derleme hatasına çeviriyor — Araci a; yazılamaz. Yalnız ayni_mi’yi dolduran Otomobil ve Bisiklet gibi sınıflardan nesne kurulabiliyor. Bir Araci* dizisindeki her eleman, yine kendi gösterdiği tikel nesneye mi ulaşıyor?

// soyut_taban.cpp — salt sanal isleve sahip sinifin ornegi kurulamiyor, yalnizca dolduranlarin
#include <iostream>

class Araci {
public:
    virtual ~Araci() = default;
    virtual bool ayni_mi(const void* adres) const = 0;
};

class Otomobil : public Araci {
public:
    bool ayni_mi(const void* adres) const override {
        return static_cast<const void*>(this) == adres;
    }
};

class Bisiklet : public Araci {
public:
    bool ayni_mi(const void* adres) const override {
        return static_cast<const void*>(this) == adres;
    }
};

int main() {
    Otomobil o;
    Bisiklet b;
    Araci* araclar[2] = { &o, &b };

    std::cout << std::boolalpha;
    std::cout << "araclar[0] kendi nesnesi o ile ayni mi "
              << araclar[0]->ayni_mi(&o) << "\n";
    std::cout << "araclar[1] kendi nesnesi b ile ayni mi "
              << araclar[1]->ayni_mi(&b) << "\n";
    std::cout << "araclar[0] b ile ayni mi (capraz denetim) "
              << araclar[0]->ayni_mi(&b) << "\n";

    return 0;
}
araclar[0] kendi nesnesi o ile ayni mi true
araclar[1] kendi nesnesi b ile ayni mi true
araclar[0] b ile ayni mi (capraz denetim) false

CB7. Araci a; yazılsaydı derleyici reddederdi — salt sanal ayni_mi Araci’yi tek başına eksik bırakıyor, yalnız Otomobil ve Bisiklet gibi işlevi tam dolduran sınıflar kurulabilir. Bu yüzden bir Araci*‘nin gösterebileceği tek şey, ayni_mi‘yi dolduran somut bir sınıfın örneğidir — taban sınıfın kendisine ait, “yedek” bir örneğe düşme ihtimali hiç yok. araclar[0]->ayni_mi(&o) true, araclar[1]->ayni_mi(&b) true, çapraz denetim false: her eleman kendi somut nesnesine ulaşıyor. (M08/K02’de sorulan soru çağrıyı hangi tipin çözdüğüydü; burada taban tipin kendi başına hiç örneği olmadığından çözüm hep somut bir türe düşüyor — ve Java’da bir arayüz referansı da hiçbir zaman salt arayüz tipinde bir nesneye işaret edemez, C++’ta salt sanal işlev aynı kısıtlamayı sağlıyor.) Sınıf kendisi: çağrı, taban tipin kendisi hiç var olamadığı için, zorunlu olarak somut bir türetilmiş nesneyle çalışıyor.

Devralınan Gövde de Aynı Nesneyle Çalışıyor

Araci, salt sanal ayni_mi’nin yanında gövdeli bir bakim_bilgisi de taşıyor. Otomobil bunu yeniden tanımlamıyor, Bisiklet tanımlıyor. Devralınan gövde çalıştığında this yine çağrılan nesnenin adresini mi taşıyor?

// soyut_govdeli_isliv.cpp — devralinan govdeli islev de cagrilan nesnenin adresini tasiyor
#include <iostream>

class Araci {
public:
    virtual ~Araci() = default;
    virtual bool ayni_mi(const void* adres) const = 0;
    virtual void bakim_bilgisi(const void* beklenen) const {
        std::cout << "Araci::bakim_bilgisi calisan nesne beklenenle ayni mi "
                  << std::boolalpha << (static_cast<const void*>(this) == beklenen) << "\n";
    }
};

class Otomobil : public Araci {
public:
    bool ayni_mi(const void* adres) const override {
        return static_cast<const void*>(this) == adres;
    }
};

class Bisiklet : public Araci {
public:
    bool ayni_mi(const void* adres) const override {
        return static_cast<const void*>(this) == adres;
    }
    void bakim_bilgisi(const void* beklenen) const override {
        std::cout << "Bisiklet::bakim_bilgisi calisan nesne beklenenle ayni mi "
                  << std::boolalpha << (static_cast<const void*>(this) == beklenen) << "\n";
    }
};

int main() {
    Otomobil o;
    Bisiklet b;
    Araci* a1 = &o;
    Araci* a2 = &b;

    a1->bakim_bilgisi(&o);
    a2->bakim_bilgisi(&b);

    return 0;
}
Araci::bakim_bilgisi calisan nesne beklenenle ayni mi true
Bisiklet::bakim_bilgisi calisan nesne beklenenle ayni mi true

CB8. Otomobil, bakim_bilgisi’yi yeniden tanımlamadığı için a1->bakim_bilgisi(&o) çağrısı Araci::bakim_bilgisi’nin gövdesini çalıştırıyor — ama bu gövde içinde bile this, o’nun adresiyle karşılaştırıldığında true dönüyor. Bisiklet kendi gövdesini sağladığı için a2->bakim_bilgisi(&b) Bisiklet::bakim_bilgisi’ni çalıştırıyor, this yine b’nin adresi. Devralınan bir gövdenin çalışması, Araci’nin kendi başına bir örneğinin var olduğu anlamına gelmiyor — o gövde yalnız somut bir Otomobil nesnesi üzerinde çalıştırılıyor. Sınıf kendisi: taban sınıfın sağladığı ortak gövde bile, çağrıldığı somut nesneden başka hiçbir nesneyle çalışmıyor.

Bir Arayüz: Yalnızca Sözleşme, Ortak Veri Yok

Araci veri taşımıyordu ama ortak bir gövde taşıyordu — tam bir soyut sınıftı. Yazdirilabilir bundan farklı: hiç veri alanı, hiç gövdeli işlev yok, yalnız bir sözleşme. Rapor bu sözleşmeyi dolduruyor ve kendi verisini taşıyor.

// arayuz_sozlesme.cpp — arayuz hic veri tasimiyor, sozlesmeyi dolduran kendi verisini tasiyor
#include <iostream>
#include <string>
#include <utility>

class Yazdirilabilir {
public:
    virtual ~Yazdirilabilir() = default;
    virtual bool ayni_mi(const void* adres) const = 0;
};

class Rapor : public Yazdirilabilir {
public:
    explicit Rapor(std::string baslik) : baslik_(std::move(baslik)) {}
    bool ayni_mi(const void* adres) const override {
        return static_cast<const void*>(this) == adres;
    }
    const std::string& baslik() const { return baslik_; }
private:
    std::string baslik_;
};

void listele(const Yazdirilabilir& y, const void* beklenen) {
    std::cout << std::boolalpha
              << "listele icinde cagrilan nesne beklenenle ayni mi "
              << y.ayni_mi(beklenen) << "\n";
}

int main() {
    Rapor r("Yillik Rapor");
    std::cout << "raporun basligi " << r.baslik() << "\n";
    listele(r, &r);
    return 0;
}
raporun basligi Yillik Rapor
listele icinde cagrilan nesne beklenenle ayni mi true

CB9. Yazdirilabilir hiçbir veri alanı bildirmiyor, baslik_ tümüyle Rapor’a ait — Yazdirilabilir bu veriye hiç karışmıyor, yalnız ayni_mi sözleşmesini dayatıyor. r, Yazdirilabilir&’a bağlanıp listele içine geçirildiğinde y.ayni_mi(&r) yine true dönüyor: referans bağlama burada da bir kopya kurmuyor, y main içindeki r ile aynı nesne. Araci ile Yazdirilabilir arasındaki fark somut: Araci veri taşıyabilir ve ortak gövdeli işlevler sunabilir (soyut sınıf), Yazdirilabilir yalnızca imzalardan ibaret, hiç veri hiç gövde yok (arayüz). Sınıf kendisi: ikisinde de çağrı hep sözleşmeyi dolduran somut nesneyle çalışıyor.

Salt Sanal İşlevin Gövdesi Olabilir, Ama Yalnız Nitelenmiş Çağrıyla

= 0 bir işlevi salt sanal yapıyor ve taşıyan sınıfın örneklenmesini engelliyor — ama bu, işlevin hiç gövdesi olamayacağı anlamına gelmiyor. SesliArac::uyar hem salt sanal hem de bir gövdeye sahip. Bu gövdeye normal bir sanal çağrıyla mı, yoksa başka bir yolla mı ulaşılıyor?

// soyut_salt_sanalin_govdesi.cpp — salt sanal islevin govdesi olabilir, ama yalnizca nitelenmis cagriyla calisir
#include <iostream>

class SesliArac {
public:
    virtual ~SesliArac() = default;
    virtual void uyar() const = 0;
};

void SesliArac::uyar() const {
    std::cout << "SesliArac::uyar (varsayilan govde) calisti\n";
}

class Kamyon : public SesliArac {
public:
    void uyar() const override {
        std::cout << "Kamyon::uyar calisti, sonra taban govdeyi nitelenmis cagiriyor\n";
        SesliArac::uyar();
    }
};

int main() {
    Kamyon k;
    SesliArac& taban_referans = k;
    taban_referans.uyar();
    return 0;
}
Kamyon::uyar calisti, sonra taban govdeyi nitelenmis cagiriyor
SesliArac::uyar (varsayilan govde) calisti

CB28. taban_referans.uyar() sıradan bir sanal çağrı — ve Kamyon uyar’ı yeniden tanımladığı için SesliArac’ın gövdesine kendiliğinden hiç ulaşmıyor, tıpkı önceki derslerdeki her override’da olduğu gibi. SesliArac::uyar’ın gövdesi yalnız Kamyon::uyar’ın içinden nitelenmiş bir çağrıyla (SesliArac::uyar();) elle çağrıldığında çalışıyor — bu satır sanal gönderimi devre dışı bırakıp doğrudan o ismi hedefliyor. “Salt sanal” ile “gövdesiz” burada ayrışıyor: = 0, sınıfın örneklenmesini engelleyen ve türetilmiş sınıfları işlevi doldurmaya zorlayan şey; gövdenin olup olmaması ayrı bir soru, ve varsa bu gövdeye yalnız nitelenmiş çağrıyla erişilebiliyor, olağan sanal gönderimle değil. Sınıf kendisi: nitelenmiş çağrı bile k’nin kendisi üzerinde çalışıyor, yalnız hangi gövdenin çalıştırılacağını isim çözümü belirliyor.

Soyut Tabanın Örneği Kurulamıyor, Somut Taban Değerle Alınıp Dilimlenebiliyor

Araci salt sanal bir işlev taşıdığı için Araci a; hiç derlenmiyor — bu, Araci tipinde bir değerin de hiçbir zaman var olamayacağı, dolayısıyla Araci için nesne dilimleme riskinin bile oluşamayacağı anlamına geliyor. Peki soyut olmayan, gövdesi tam bir taban sınıf söz konusu olduğunda durum nasıl değişiyor?

// soyut_somut_taban_degerle_alma.cpp — soyut tabanin ornegi kurulamaz, somut taban ise degerle alinip dilimlenebilir
#include <iostream>
#include <string>

class TasitKaydi {
public:
    explicit TasitKaydi(std::string plaka) : plaka_(std::move(plaka)) {}
    std::string plaka() const { return plaka_; }
protected:
    std::string plaka_;
};

class AyrintiliKayit : public TasitKaydi {
public:
    AyrintiliKayit(std::string plaka, int km)
        : TasitKaydi(std::move(plaka)), km_(km) {}
    int km() const { return km_; }
private:
    int km_;
};

int main() {
    AyrintiliKayit ayrintili("34ABC123", 15000);
    TasitKaydi dilimlenmis = ayrintili;

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

    return 0;
}
dilimlenmis ile ayrintili'nin ayni adresi tasiyip tasimadigi false
dilimlenmis nesnenin plakasi 34ABC123

CB29. TasitKaydi salt sanal işlev taşımıyor, somut bir sınıf — bu yüzden TasitKaydi dilimlenmis = ayrintili; derleniyor ve Kalıtım dersindeki CB21’de ölçülen dilimlemenin aynısını üretiyor: dilimlenmis, ayrintili’den bağımsız, kendi adresinde yaşayan ayrı bir nesne (false), ve AyrintiliKayit’in eklediği km_ alanı hiç taşınmıyor. Araci için bu satırın bir karşılığı yok, çünkü Araci tipinde bir değişken tanımlamanın kendisi derleme hatası — dilimlemenin hedefi olacak bir Araci değeri hiç var olamıyor. Soyutluk, yalnız “yeniden tanımlama zorunluluğu” değil, aynı zamanda “taban tipte hiçbir zaman bir değer oluşamaması” anlamına geliyor: soyut bir taban, işaretçi ya da referansla erişilebilir ama asla bir değişkene, parametreye ya da dönüş değerine değerle konulamaz — bu da onu dilimlemeye karşı yapısal olarak bağışık kılıyor. Dilimleme riski yalnız somut sınıflar için var. Sınıf kopya: dilimlenmis, ayrintili’nin taban parçasının bağımsız bir kopyası.

Arayüzün Rolü: Akraba Olmayan Tipleri Tek Sözleşme Altında İşlemek

Rapor ve Fatura birbirinden türemiyor, hiçbir veri ya da davranışı paylaşmıyorlar — tek ortak noktaları Yazdirilabilir sözleşmesini doldurmaları. Bu ikisi aynı vektörde, somut tipleri bilinmeden, tekdüze işlenebiliyor mu?

// arayuz_rolu_coklu_tip.cpp — farkli somut tipler ayni arayuz uzerinden tekduze islenebiliyor
#include <iostream>
#include <string>
#include <vector>

class Yazdirilabilir {
public:
    virtual ~Yazdirilabilir() = default;
    virtual std::string metin() const = 0;
};

class Rapor : public Yazdirilabilir {
public:
    explicit Rapor(std::string baslik) : baslik_(std::move(baslik)) {}
    std::string metin() const override { return "Rapor: " + baslik_; }
private:
    std::string baslik_;
};

class Fatura : public Yazdirilabilir {
public:
    explicit Fatura(int tutar) : tutar_(tutar) {}
    std::string metin() const override { return "Fatura: " + std::to_string(tutar_); }
private:
    int tutar_;
};

int main() {
    Rapor r("Yillik Rapor");
    Fatura f(150);
    std::vector<Yazdirilabilir*> belgeler{ &r, &f };

    for (const auto* belge : belgeler) {
        std::cout << belge->metin() << "\n";
    }

    return 0;
}
Rapor: Yillik Rapor
Fatura: 150

CB30. Rapor ve Fatura aynı hiyerarşi ağacında akraba değiller, ortak hiçbir veri alanı ya da gövdeli işlev paylaşmıyorlar — bağları yalnız ikisinin de Yazdirilabilir’i doldurması. belgeler vektörü ikisini de tek bir Yazdirilabilir* tipi altında tutuyor ve döngü, elemanın somut tipini hiç bilmeden metin()’i çağırıyor; her çağrı yine de doğru somut tipin işlevine ulaşıyor: “Rapor: Yillik Rapor” ve “Fatura: 150”. Bu, arayüzün asıl rolünü gösteriyor: soyut sınıfın aksine, arayüz akrabalık kurmuyor, yalnız imzaca bir sözleşme dayatıyor — sözleşmeyi dolduran her tip, aralarında başka hiçbir ortaklık olmasa da aynı koleksiyonda, aynı döngüde işlenebiliyor. Sınıf kendisi: her metin() çağrısı, döngüdeki elemanın işaret ettiği somut nesnenin ta kendisiyle çalışıyor.

Arayüzün de Sanal Bir Yıkıcıya İhtiyacı Var

Yazdirilabilir yalnız sözleşme sunuyor gibi görünse de, Rapor gibi somut bir türetilmişi Yazdirilabilir* üzerinden delete edebilmek için kendi yıkıcısının da sanal olması gerekiyor. Bu ders bunu doğruluyor mu?

// arayuz_sanal_yikici.cpp — arayuzun sanal yikicisi turetilmis nesnenin tam yikim zincirini calistiriyor
#include <iostream>

class Yazdirilabilir {
public:
    virtual ~Yazdirilabilir() { std::cout << "Yazdirilabilir yikici\n"; }
    virtual void yaz() const = 0;
};

class Rapor : public Yazdirilabilir {
public:
    ~Rapor() override { std::cout << "Rapor yikici\n"; }
    void yaz() const override { std::cout << "Rapor::yaz\n"; }
};

int main() {
    Yazdirilabilir* belge = new Rapor();
    belge->yaz();
    std::cout << "-- silme basliyor --\n";
    delete belge;
    return 0;
}
Rapor::yaz
-- silme basliyor --
Rapor yikici
Yazdirilabilir yikici

CB31. belge’nin statik tipi Yazdirilabilir*, ama delete belge önce Rapor yikici’yi, sonra Yazdirilabilir yikici‘yi çalıştırıyor — tıpkı Sanal Fonksiyonlar dersindeki CB24’te ölçülen ilkenin aynısı, burada arayüz bağlamında. Yazdirilabilir salt sanal işlevler sunan bir sözleşme olsa da, yıkıcısı virtual olmasaydı aynı delete belge yalnız Yazdirilabilir’in kendi yıkıcısını çağırırdı, Rapor‘un kaynakları hiç serbest bırakılmadan — ve statik/dinamik tip farklıyken sanal olmayan bir yıkıcı üzerinden silme tanımsız davranış olurdu. Bu dal, CB24’te olduğu gibi, deterministik bir çıktı vermeyeceği için burada çalıştırılmıyor. Sonuç: “yalnız sözleşme sunmak” bir sınıfı sanal yıkıcı gereksiniminden muaf tutmuyor — taban işaretçi üzerinden polymorphic silme yapılacaksa, arayüz de dâhil, yıkıcı sanal olmalı. Sınıf kendisi: yıkım zinciri hep belge’nin işaret ettiği gerçek Rapor nesnesi üzerinde işliyor.

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

Bu derste ölçülen soyutluk tekli kalıtım altında: her somut sınıf tek bir soyut taban sınıfı ya da arayüzü dolduruyordu. Bir sınıfın birden çok taban sınıftan türeyerek birden çok sözleşmeyi aynı anda doldurduğu durum — çoklu kalıtım — bu dersin kapsamı dışında; bir sonraki ders bunu ölçüyor. Salt sanal bir işlevin gövdesi de olabileceği yalnız tek bir örnekle (SesliArac::uyar) gösterildi; o gövdenin hangi tasarımlarda gerçekten gerektiği genellenmedi. Araci a; gibi bir satırın gerçekten derleme hatası verdiği bu derste çalıştırılarak gösterilmedi, çünkü doğrulama yalnız başarıyla derlenen kodu çalıştırıyor — bu, dersin metninde belirtilen ama koşulan kod dışında kalan bir olgu.

Özet

  • Salt sanal bir işlev (= 0) taşıyan bir sınıftan doğrudan nesne kurulamıyor; yalnız o işlevi dolduran türetilmiş sınıflardan nesne kurulabiliyor.
  • Bir Araci* dizisindeki her eleman kendi gösterdiği somut nesneye ulaşıyor, çünkü taban tipin kendi başına hiç örneği yok — sınıf kendisi.
  • Taban sınıfın sağladığı devralınan, gövdeli bir işlev bile çağrıldığı somut nesneden başka hiçbir nesneyle çalışmıyor.
  • Bir soyut sınıf (Araci) veri taşıyabilir ve ortak gövdeli işlevler sunabilir; bir arayüz (Yazdirilabilir) yalnızca imzalardan ibarettir, hiç veri hiç gövde taşımaz.
  • Arayüz referansına geçirilen nesne, işlev içinde de dışarıdaki aynı nesne — referans bağlama burada da bir kopya kurmuyor.

Sonraki Adım

Bu derste her somut sınıf tek bir soyut taban sınıfı ya da arayüzü dolduruyordu; taban alt nesne hep türetilmiş nesnenin başında durdu, tıpkı Kalıtım dersindeki gibi. Bir sonraki ders, Çoklu Kalıtım ve Elmas Sorunu, bir sınıfın birden çok taban sınıftan aynı anda türediği durumu ölçüyor — orada ikinci ve sonraki taban alt nesneler artık türetilmiş nesnenin başında durmayabiliyor, adresleri türetilmiş nesnenin adresiyle çakışmayabiliyor. Ölçülen yine bir ifadenin hangi nesneyle çalıştığı, ama bu kez taban alt nesnenin türetilmiş nesnenin hangi parçasında durduğu da sorunun bir parçası.

İ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