Ders 03 / 14
Yapıcılar ve Yıkıcılar
Bir int'ten örtük kurulan argümanın adsız, geçici bir nesne olduğunu ve yaşam süresini yapıcı/yıkıcı çiftiyle ölçer.
Yayın Güncelleme
Bu derste
- Örtük tip dönüşümüyle kurulan bir argümanın geçici nesne olduğunu ölçer
- explicit anahtar sözcüğünün örtük kurmayı nasıl engellediğini uygular
- Üye ilk değer listesi ve yapıcı zincirinin sırasını gösterir
İçindekiler
Önceki ders bir üye işlevin hangi nesneyle çalıştığını this üzerinden ölçtü — ama o nesne
her zaman zaten vardı. Bu ders aynı soruyu bir nesnenin var olma anına sorar: bir
yapıcı (constructor — sınıfın adıyla aynı adı taşıyan, nesne kurulurken bir kez çalışan
üye işlev) ne zaman çağrılır, ve bir yıkıcı (destructor — ~ önekli, nesne yok olurken
bir kez çalışan üye işlev) ne zaman çalışır? Ölçüm sınıfı bu kez geçici: bir işlev
çağrısının bir argümanından örtük olarak kurulan, adı olmayan bir nesnenin ömrünü
izleyecek.
Yapıcı ve Yıkıcı
Bir yapıcı, sınıfla aynı adı taşır, dönüş tipi yoktur (void bile yazılmaz) ve bir nesne
kurulurken derleyici tarafından çağrılır. Bir yıkıcı ~SinifAdi() biçimindedir, bağımsız
değişken almaz, ve bir nesne yaşam süresinin sonuna geldiğinde derleyici tarafından çağrılır
— programcı onu elle çağırmaz. Bir sınıf hiç yapıcı yazmazsa derleyici varsayılan yapıcı
üretir (argüman almayan, veri üyelerini kendi varsayılan değerleriyle kuran); bir
yapıcı yazılırsa bu üretim durur.
Bir sınıfın birden çok yapıcısı olabilir — parametre sayısı ya da tipi farklı olduğu sürece aynı işlev bindirimi (overload) kurallarıyla ayrılırlar. Buna karşılık bir sınıfın yalnız bir yıkıcısı olabilir; yıkıcı argüman almadığı için bindirilemez.
Yapıcı ile sıradan bir üye işlev arasındaki en büyük fark, yapıcının çalıştığı anda nesnenin
henüz tamamlanmamış olmasıdır. Tutar(int kurus) çalışırken kurus_ üye ilk değer
listesindeki kurus_(kurus) ile kurulur; gövde çalışmaya başladığında kurus_ artık
geçerli bir değerdedir ve gövde içinde okunabilir, ama gövde çalışana kadar nesnenin diğer
üyeleri (bu sınıfta yalnız kurus_ var, ama çok üyeli bir sınıfta sıradaki üyeler) henüz
kurulmamış olabilir. Bu yüzden bir yapıcının gövdesinde this üzerinden başka bir üye
işlevi çağırmak — özellikle sanal bir üye işlevi, bu kursun kapsamı dışında kalan bir konu
— nesnenin henüz tam kurulmamış bir durumuna erişme riski taşır; bu ders böyle bir çağrı
yapmaz.
Yıkıcının konumu simetriktir: yıkıcı çalıştığında nesne hâlâ tam ve geçerlidir, ama yıkıcı
döndükten sonra nesne artık yok sayılır — üye değişkenlerin belleği hâlâ durabilir ama
programın onlara erişmesi tanımsız davranıştır (bu kursun M11/K02’den devraldığı kural:
tanımsız davranış hiçbir zaman koşturulmaz, yalnız düzyazıda anılır). Tutar::~Tutar()
gövdesi kurus_’ü son kez okuyup basar; bu satırdan sonra hiçbir kod aynı nesnenin
kurus_’üne erişmemelidir.
// tutar.hpp — bir yapici, bir yikici, bir de yapici zinciri
#ifndef TUTAR_HPP
#define TUTAR_HPP
#include <cstdio>
class Tutar {
public:
Tutar(int kurus) : kurus_(kurus) {
std::printf("yapici calisti kurus=%d\n", kurus_);
}
~Tutar() {
std::printf("yikici calisti kurus=%d\n", kurus_);
}
int kurus() const { return kurus_; }
private:
int kurus_;
};
#endif
SK5. Tutar(int kurus) bir argüman alır ve gövdesinden önce çalışan : kurus_(kurus) bir üye ilk değer listesidir (member initializer list) — kurus_ veri
üyesini gövde çalışmadan önce kurus değeriyle kurar. Gövde içinde kurus_ = kurus yazmak
da aynı sonucu verirdi, ama ilk değer listesi kurmayla atamayı ayırır: gövde çalıştığında
kurus_ zaten vardır, yalnız değiştirilir.
Örtük Kurma ve explicit
Tutar(int kurus) tek bir int argüman alan bir yapıcıdır; C++ böyle bir
yapıcıyı örtük tip dönüşümü için de kullanır. goster(Tutar t) diye bir işlev
goster(5) ile çağrılırsa, derleyici 5’i Tutar’a dönüştürmek için bu yapıcıyı örtük
olarak çağırır — kurulan nesnenin adı yoktur, main’de hiçbir değişken görünmez, ama nesne
gerçekten vardır ve yapıcısı gerçekten çalışır.
// gecici-olcumu.cpp — int'ten ortuk kurulan bir tutar gecici midir
#include <cstdio>
#include "tutar.hpp"
static void goster(Tutar t) {
std::printf("goster icinde kurus=%d\n", t.kurus());
}
int main() {
std::printf("cagridan once\n");
goster(5);
std::printf("cagridan sonra\n");
return 0;
}
cagridan once yapici calisti kurus=5 goster icinde kurus=5 yikici calisti kurus=5 cagridan sonra
SK6. goster(5) çağrısında 5 önce Tutar(5) ile bir Tutar nesnesine dönüştürülür —
bu nesnenin adı yoktur, bu yüzden geçicidir. Bu geçici, goster’in parametresi t’ye
kopyalanır (ya da doğrudan onun yerine kurulur — bu kursun kapsamı dışında kalan bir
derleyici optimizasyonu); goster döndüğünde t’nin yıkıcısı çağrılır ve çıktı sırası
bunu gösterir: yapıcı çağrıdan hemen sonra, yıkıcı goster döndükten hemen sonra, ama
cagridan sonra satırından önce çalışır — geçicinin ömrü goster(5); ifadesiyle
sınırlıdır, main’in geri kalanına taşmaz.
Bu örtük dönüşüm her zaman istenmez: goster(5) yazıldığında 5’in bir Tutar olarak mı
yoksa yanlışlıkla bir tamsayı olarak mı geçirildiği kod okuyucusuna açık değildir. explicit
anahtar sözcüğü bunu engeller: explicit Tutar(int kurus) yazılsaydı, goster(5) derlenmez,
yalnız goster(Tutar(5)) ya da goster(Tutar{5}) gibi açık bir kurma derlenirdi. Bu ders
Tutar’ı explicit yazmadan bırakır çünkü ölçülmek istenen tam olarak örtük kurmanın
ürettiği geçicidir; ama bir sınıf yazarı tek-bağımsız-değişkenli her yapıcıyı explicit
yapmayı düşünmelidir — bu kursun ilerideki derslerinde yazılan yapıcılar da aksi
belirtilmedikçe explicit olarak yazılır.
Örtük dönüşümün riski yalnız okunabilirlik değildir. İki argüman zinciri örtük
kurmaya izin verdiğinde — örneğin Tutar bir yapıcıdan başka bir sınıfa örtük dönüşüyorsa —
derleyici bir çağrıyı beklenmedik bir zincirle derleyebilir; C++ standardı bir çağrıda
en fazla bir örtük kullanıcı tanımlı dönüşüme izin vererek bunu sınırlar, ama tek
adımlık örtük dönüşüm bile kod okuyanın beklemediği bir yapıcı çağrısı gizleyebilir.
goster(5) örneğinde bu risk küçüktür çünkü Tutar’ın tek yapıcısı vardır ve niyet
açıktır; büyük bir sınıf hiyerarşisinde aynı örtük kurma, çağıranın fark etmediği bir
nesne kurma maliyetini (yapıcı çalışması, yıkıcı çalışması) sessizce ekleyebilir. explicit
bu sessizliği kaldırır: her kurma kaynak kodda görünür bir Tür(...) çağrısı olarak
görünmek zorunda kalır.
Yapıcı Zinciri ve Ters Sıra Yıkım
Birden çok veri üyesi olan bir sınıfta üye ilk değer listesi, üyelerin bildirim sırasıyla kurulmasını sağlar — listede yazılma sırası değil, sınıf gövdesindeki bildirim sırası geçerlidir. Yıkım bunun tam tersi sırada olur: en son kurulan üye en önce yıkılır.
// ciftlik.cpp — birden fazla uyenin kurulma ve yikilma sirasi
#include <cstdio>
#include "tutar.hpp"
class Ciftlik {
public:
Ciftlik() : birinci_(1), ikinci_(2) {
std::printf("ciftlik yapicisi calisti\n");
}
private:
Tutar birinci_;
Tutar ikinci_;
};
int main() {
std::printf("blok basladi\n");
{
Ciftlik c;
std::printf("ciftlik kuruldu\n");
}
std::printf("blok bitti\n");
return 0;
}
blok basladi yapici calisti kurus=1 yapici calisti kurus=2 ciftlik yapicisi calisti ciftlik kuruldu yikici calisti kurus=2 yikici calisti kurus=1 blok bitti
SK7. Ciftlik’in kendi yapıcı gövdesi çalışmadan önce birinci_ ve ikinci_, sınıf
gövdesindeki bildirim sırasıyla (üye ilk değer listesindeki sıra değil — burada ikisi aynı
olduğu için fark görünmüyor, ama sıra her zaman bildirime göre belirlenir) kurulur; her
kurma kendi Tutar yapıcısını tetikler, bu yüzden ciftlik yapicisi calisti satırından
önce iki ayrı yapici calisti satırı görülür. c iç blok kapanırken yok olduğunda önce
Ciftlik’in kendi yıkıcısı (burada yazılmadığı için derleyici üretir, hiçbir çıktı basmaz),
sonra üyeleri ters sırada yıkılır: ikinci_ önce, birinci_ sonra. Kurma ile yıkım
birbirinin aynasıdır.
M10/K02’de (Rust) bir değerin kapsam dışına çıkışı Drop çalıştırıyordu; C++’ın yıkıcısı
aynı fikri karşılar, ama tetiklenme kuralları farklıdır ve bu ders o karşılaştırmayı
derinleştirmez.
Bildirim sırasının bağlayıcı olması, üye ilk değer listesinin yazılma sırasını görmezden
gelmesi anlamına gelir — bazı derleyiciler bir uyarı olarak listedeki sıranın bildirim
sırasıyla eşleşmediğini bildirir, çünkü bu iki sıranın farklı olması okuyucuyu yanıltabilir:
liste ikinci_(2), birinci_(1) sırasıyla yazılsa bile kurulma sırası yine birinci_ önce
olurdu, çünkü sınıf gövdesinde birinci_ önce bildirilmiştir. Ciftlik bu ders için listeyi
bildirim sırasıyla aynı yazar, bu yüzden iki sıra çakışır ve karışıklık doğmaz — ama kural
her zaman bildirim sırasıdır, listenin yazılış sırası değil.
Ters sıra yıkımın bir başka sonucu, bir üyenin yapıcısı bir öncekinin kurduğu bir kaynağa
güvenle kullanabilmesidir: ikinci_ kurulurken birinci_ zaten tam kurulmuştur, çünkü
bildirim sırasında ondan öncedir. Aynı güvence yıkımda tersine işler: ikinci_ yıkılırken
birinci_ henüz yıkılmamıştır, bu yüzden ikinci_’nin yıkıcısı gerekirse birinci_‘ye
hâlâ güvenle erişebilir. Bu simetri, bir sonraki dersin konusu olan RAII’nin (Resource
Acquisition Is Initialization) üzerine kurulduğu temeldir: bir kaynağı tutan üye, onu
tutan nesnenin dışına çıkmadan önce yıkılmaz.
Yapıcıların Birbirini Çağırması
Bir sınıfın birden çok yapıcısı olduğunda, bu yapıcılar arasında kod tekrarını önlemek için
bir yapıcı diğerini devrederek (delegating constructor) çağırabilir: Tutar() : Tutar(0) {} yazmak, argümansız çağrıldığında işi tek-bağımsız-değişkenli yapıcıya
devreden bir yapıcı tanımlar. Devir, üye ilk değer listesinde sınıfın kendi adını çağırarak
yazılır — Tutar(0) burada başka bir üyenin değil, sınıfın kendisinin bir yapıcısını
çağırır. Devredilen yapıcı tam olarak çalışır (kendi gövdesi dahil) ve devreden yapıcının
gövdesi ondan sonra çalışır. Tutar bu dersin kapsamında devir kullanmaz — tek yapıcısı
vardır — ama bu teknik, birden çok yapıcı arasında ortak kurma mantığını tek bir yerde
tutmanın standart yoludur; ilk değer listesinde üye kurmakla karıştırılmamalıdır, çünkü
biri sınıfın kendi adını, diğeri bir üyenin adını çağırır.
Varsayılan Yapıcı ve Varsayılan Değerler
Tutar argüman almayan bir yapıcı yazmaz, çünkü bir tutarın her zaman bir başlangıç
değeriyle kurulmasını ister — Tutar t; yazmak bu sınıf için derlenmez, çünkü tek yapıcı
bir int ister. Bir sınıf hiçbir yapıcı yazmazsa derleyici argüman almayan bir
varsayılan yapıcı üretir; bu üretilen yapıcı her veri üyesini kendi varsayılan değeriyle
kurar — sınıf tipindeki üyeler kendi varsayılan yapıcılarını çalıştırır, int gibi temel
tipler ise (üye bildiriminde bir ilk değer verilmemişse) kurulmamış kalır, yani rastgele
bir bellek değeri taşır. Bu, C’den gelen bir davranıştır ve C++’ta değişmemiştir: Ciftlik
gibi yalnız sınıf tipinde üyesi olan bir sınıfın varsayılan yapıcısı güvenlidir, çünkü her
üye kendi yapıcısını çalıştırır; ama int miktar_; gibi bir üye ilk değersiz bırakılırsa
varsayılan yapıcı onu kurulmamış bırakır. Bu kursta yazılan sınıflar, temel tipteki her
üyeye ya bildirimde (int miktar_ = 0;) ya da üye ilk değer listesinde açık bir başlangıç
değeri verir; kurulmamış bir üyeyi okumak da tanımsız davranıştır ve koşturulmaz.
Sınırlayıcı Ölçüm
Bu dersin ölçtüğü ifade — goster(5) içindeki örtük kurulan Tutar — geçici
sınıfındadır: adı yoktur, goster(5); ifadesinin sonunda yok olur. Ama geçicinin ömrü her
zaman bu kadar kısa kalmaz: bir geçici, const bir referansa bağlanırsa referansın ömrüne
uzar. const Tutar& r = Tutar(7); yazılsaydı, sağ taraftaki geçici normalde ifade
bitiminde yok olurdu, ama r’ye bağlandığı için ömrü r’nin kapsamının sonuna kadar
uzatılır — derleyici bunu otomatik yapar, programcının bir şey yazması gerekmez. Bu
uzatma yalnız const referanslar (ve bu kursun kapsamı dışındaki rvalue referanslar) için
geçerlidir; Tutar& r = Tutar(7); yazmak derlenmez, çünkü const olmayan bir referans bir
geçiciye bağlanamaz. Referansın kendisi bir başka geçiciye yeniden bağlanamaz olması —
bir referansın kurulduktan sonra hep aynı nesneyi göstermesi — bir sonraki konudaki
referanslar dersinin ölçtüğü şeydir.
Bu sınır, geçicilerin neden işlev çağrı zincirlerinde dikkatli kullanılması gerektiğini
açıklar: goster(Tutar(5)) gibi bir çağrıda geçici yalnız goster’in çalıştığı ifade
boyunca yaşar; geçiciye ait bir işaretçi ya da const olmayan bir referans bu ifadeden
dışarı taşınırsa, ifade bittikten sonra o işaretçi ya da referans sarkan (dangling) hâle
gelir — gösterdiği bellek artık geçerli değildir. Tutar’ın kurus() işlevinin bir int
döndürmesi, const Tutar& ya da Tutar* değil, tam olarak bu riski taşımamak içindir:
int bir değer olarak kopyalanır, geçicinin kendisi yok olsa da kopyalanan değer geçerli
kalır.
Bu üç kural — örtük kurmanın geçici ürettiği, bildirim sırasının kurmayı ve yıkımı
belirlediği, const referansın geçicinin ömrünü uzatabildiği — bir arada bir nesnenin
“var olmak” ile “adlandırılmış olmak”ın aynı şey olmadığını gösterir. goster(5) içindeki
Tutar hiçbir değişkene atanmadan kurulur, yapıcısı çalışır, kullanılır, yıkıcısı çalışır;
bu tüm süreç boyunca hiçbir isim ona işaret etmez. Bir sonraki dersin RAII’si tam olarak bu
gözlemi kullanır: bir kaynağı serbest bırakmak için bir nesnenin adlandırılmış olması
gerekmez, yalnız yaşam süresinin sonuna gelmesi yeterlidir — derleyici o anı, adı olsun ya
da olmasın, her zaman bilir.
Özet
- Bu dersin ölçtüğü ifade geçici sınıfındadır:
goster(5)içinde örtük kurulanTutaradsızdır vegoster(5);ifadesinin sonunda yok olur. - Bir yapıcı sınıfla aynı adı taşır, bir yıkıcı
~önekiyle yazılır; yıkıcı bağımsız değişken almaz ve bindirilemez. - Tek argümanlı bir yapıcı örtük tip dönüşümüne izin verir;
explicitbunu engelleyip yalnız açık kurmaya izin verir. - Üye ilk değer listesi üyeleri bildirim sırasıyla kurar; yıkım bunun tersi sırada olur.
constbir referansa bağlanan bir geçicinin ömrü, referansın kapsamına kadar uzatılır.
Sonraki Adım
Bu ders bir nesnenin ne zaman kurulduğunu ve yok olduğunu ölçtü; bir sonraki ders bu bilgiyi bir kaynağı (bellek, dosya tanıtıcısı gibi) güvenle serbest bırakmak için kullanır — yıkıcının bir üyeyi nasıl bıraktığını, koruyucunun kendisiyle içindeki üyeyi ayırarak ölçer.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.