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.