İçeriğe geç
academia.sh

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 kurulan Tutar adsızdır ve goster(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; explicit bunu 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.
  • const bir 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.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat