İçeriğe geç
academia.sh

Ders 05 / 14

Kopyalama ve Atama

Kopyalayan yapıcı ile kopyalayan atamanın kendi dizisini alan derin bir kopya ürettiğini, ve kopyanın derinliğinin sınıfın tasarımına bağlı olduğunu ölçer.

Yayın Güncelleme

Bu derste

  • Kopyalayan yapıcı ile kopyalayan atamanın ne zaman çağrıldığını ayırt eder
  • Bir sınıfın kendi dizisini alan derin bir kopya ürettiğini ölçer
  • Kendine atama denetiminin neden gerektiğini uygular
İçindekiler

Önceki ders bir koruyucunun kaynağı değerle tuttuğunu, bu yüzden koruyucu kopyalandığında kaynağın da kopyalanacağını öne sürdü. Bu ders o iddiayı ölçer: bir nesne kopyalandığında, kopya orijinaliyle aynı belleği mi paylaşır, yoksa kendi belleğini mi alır? Ölçüm sınıfı kopya: Tampon b(a) ile kurulan b, a’dan yeni kurulmuş, a’dan farklı bir nesnedir — ama “farklı” olmanın derinliği, sınıfın kopyalayan yapıcısının nasıl yazıldığına bağlıdır.

C’de bir dizi kopyalamak istendiğinde bu her zaman açık bir işlemdi: memcpy çağrılır, hedefin yeterince büyük olduğu elle denetlenir, kaynak ve hedefin çakışmadığı elle garanti edilir. C++’ta bir sınıf nesnesi = ile ya da bir argüman olarak değerle aktarıldığında kopyalama örtüktir — derleyici hangi işlevin çağrılacağına kendisi karar verir, elle bir çağrı yazılmaz. Bu, kopyalamayı kolaylaştırır ama aynı zamanda gizler: bir satırda Tampon b(a); görüp bunun ne kadar iş yaptığını (yeni bellek ayırma, eleman eleman kopyalama) anlamak için sınıfın tanımına bakmak gerekir — satırın kendisi bunu söylemez.

Kopyalayan Yapıcı ile Kopyalayan Atama

Bir sınıfın kopyalayan yapıcısı (copy constructor), aynı sınıftan bir const& alan bir yapıcıdır — Tampon(const Tampon& digeri). Bu yapıcı, Tampon b(a); ya da Tampon b = a; gibi bir kurma sırasında çağrılır: b henüz yoktur, a’dan kurulur. Kopyalayan atama (copy assignment) ise operator= bindirimidir — Tampon& operator=(const Tampon& digeri). Bu, b = a; gibi bir atama sırasında çağrılır: b zaten vardır (daha önce kurulmuştur), yalnız değeri a’nınkiyle değiştirilir. İkisi de argüman tipi olarak const Tampon& alır, ama biri yeni bir nesne kurar, diğeri var olan bir nesnenin içeriğini değiştirir — bu fark, hangisinin çağrılacağını belirler: sol taraftaki nesne yeni mi, var olan mı.

Bu ayrım yalnız sözdizimsel değildir. Kopyalayan yapıcı çalıştığında nesnenin veri üyeleri henüz kurulmamıştır — üçüncü dersteki üye ilklendirme listesiyle aynı kural burada da geçerlidir: veri_(new int[digeri.uzunluk_]) üyeyi ilk kez ilklendirir. Kopyalayan atama çalıştığında ise nesne zaten kurulmuş, veri_ zaten geçerli bir diziye işaret ediyordur — bu yüzden operator= önce eski diziyi bırakmak (delete[] veri_) zorundadır; aksi hâlde eski dizi hiç serbest bırakılmadan üzerine yeni bir tane atanır, bu da bir bellek sızıntısıdır (memory leak). Kopyalayan yapıcının böyle bir eski durumu yoktur, çünkü nesne henüz yoktur — bu yüzden kopyalayan yapıcı delete[] çağırmaz, yalnız ayırır.

// tampon.hpp — kendi dizisini tutan, derin kopyalayan bir sinif
#ifndef TAMPON_HPP
#define TAMPON_HPP

#include <cstdio>

class Tampon {
public:
    explicit Tampon(int uzunluk) : uzunluk_(uzunluk), veri_(new int[uzunluk]) {
        for (int i = 0; i < uzunluk_; ++i) {
            veri_[i] = i;
        }
        std::printf("yapici calisti uzunluk=%d\n", uzunluk_);
    }

    Tampon(const Tampon& digeri) : uzunluk_(digeri.uzunluk_), veri_(new int[digeri.uzunluk_]) {
        for (int i = 0; i < uzunluk_; ++i) {
            veri_[i] = digeri.veri_[i];
        }
        std::printf("kopyalayan yapici calisti uzunluk=%d\n", uzunluk_);
    }

    Tampon& operator=(const Tampon& digeri) {
        if (this == &digeri) {
            return *this;
        }
        delete[] veri_;
        uzunluk_ = digeri.uzunluk_;
        veri_ = new int[uzunluk_];
        for (int i = 0; i < uzunluk_; ++i) {
            veri_[i] = digeri.veri_[i];
        }
        std::printf("kopyalayan atama calisti uzunluk=%d\n", uzunluk_);
        return *this;
    }

    ~Tampon() {
        delete[] veri_;
    }

    int ilkEleman() const { return veri_[0]; }
    void ilkElemaniYaz(int deger) { veri_[0] = deger; }
    const int* adres() const { return veri_; }

private:
    int uzunluk_;
    int* veri_;
};

#endif

Derin Kopya: Kendi Dizisini Alır

// kopya-olcumu.cpp — b, a'dan yeni kurulmus, kendi dizisini alan bir kopya mi
#include <cstdio>

#include "tampon.hpp"

int main() {
    Tampon a(3);
    Tampon b(a);

    std::printf("a ile b ayni dizi mi %s\n", a.adres() == b.adres() ? "evet" : "hayir");

    b.ilkElemaniYaz(99);
    std::printf("a ilk eleman %d\n", a.ilkEleman());
    std::printf("b ilk eleman %d\n", b.ilkEleman());
    return 0;
}
yapici calisti uzunluk=3
kopyalayan yapici calisti uzunluk=3
a ile b ayni dizi mi hayir
a ilk eleman 0
b ilk eleman 99

SK10. Tampon b(a); b’yi a’dan kopyalayan yapıcıyla kurar; kopyalayan yapıcı digeri.uzunluk_’i okuyup kendi new int[digeri.uzunluk_] çağrısıyla ayrı bir dizi ayırır, sonra digeri.veri_’nin elemanlarını bu yeni diziye tek tek kopyalar. Bu bir derin kopyadır (deep copy): b.veri_ ile a.veri_ farklı adreslerdir, a.adres() == b.adres() hayir basar. Bu yüzden b.ilkElemaniYaz(99) yalnız b’nin kendi dizisini değiştirir; a.ilkEleman() hâlâ 0’dır. b, a’dan kopya sınıfındadır: yeni kurulmuş, a’dan adres olarak farklı, ama kurulduğu andaki değeriyle eşit bir nesnedir.

Kopyalayan Atama ve Kendine Atama Denetimi

// atama-olcumu.cpp — b = a atamasi kopyalayan atamayi calistirir mi
#include <cstdio>

#include "tampon.hpp"

int main() {
    Tampon a(2);
    Tampon b(5);

    b.ilkElemaniYaz(-1);
    a.ilkElemaniYaz(42);
    b = a;
    std::printf("atamadan sonra b'nin ilk elemani a'ninkiyle ayni mi %s\n",
                b.ilkEleman() == a.ilkEleman() ? "evet" : "hayir");
    std::printf("a ile b ayni dizi mi %s\n", a.adres() == b.adres() ? "evet" : "hayir");

    b = b;
    std::printf("kendine atamadan sonra b gecerli mi %s\n", b.ilkEleman() == a.ilkEleman() ? "evet" : "hayir");
    return 0;
}
yapici calisti uzunluk=2
yapici calisti uzunluk=5
kopyalayan atama calisti uzunluk=2
atamadan sonra b'nin ilk elemani a'ninkiyle ayni mi evet
a ile b ayni dizi mi hayir
kendine atamadan sonra b gecerli mi evet

SK11. b = a; satırında b zaten kurulmuş bir nesnedir, bu yüzden kopyalayan yapıcı değil kopyalayan atama çağrılır. operator= önce b’nin eski dizisini delete[] veri_ ile serbest bırakır, sonra a’nınkiyle aynı uzunlukta yeni bir dizi ayırıp a.veri_’nin içeriğini kopyalar — bu yüzden atamadan sonra b’nin ilk elemanı a’nınkiyle eşittir (içerik kopyalandı), ama b.adres() a.adres()’e eşit değildir (yeni bir dizi ayrıldı, paylaşılmadı). b’nin atamadan önceki adresi burada ayrıca yazdırılıp karşılaştırılmaz: delete[] ile hemen ardından gelen new’in aynı adresi geri verip vermeyeceği ayırıcıya (allocator) bağlıdır, C++ standardı bunu garanti etmez — bu yüzden böyle bir karşılaştırma kanıt sayılmaz, yalnız içerik ve a ile paylaşılmama durumu ölçülür. b = b; satırı operator=’in başındaki if (this == &digeri) return *this; denetimini tetikler: bu denetim olmasaydı, işlev önce b’nin kendi dizisini delete[] ile bırakır, sonra digeri.veri_’yi (ki bu artık bırakılmış, geçersiz aynı adrestir) okumaya çalışırdı — bu tanımsız davranış olurdu. Denetim sayesinde b = b; güvenle erken döner, b değişmeden kalır.

Sığ Kopyanın Riski ve Üç Kuralı

Tampon’un kopyalayan yapıcısı ile kopyalayan ataması elle yazılmasaydı, derleyici kendi ürettiği varsayılan versiyonları kullanırdı — bunlar her veri üyesini üye üye kopyalar. uzunluk_ bir int olduğu için bu sorunsuzdur, ama veri_ bir int*’tir; üye üye kopyalama işaretçinin kendisini kopyalar, gösterdiği diziyi değil. Bu bir sığ kopyadır (shallow copy): iki Tampon aynı veri_ adresini paylaşır. Sığ kopyanın sonucu şudur — biri yıkılırken delete[] veri_ çalıştırır, diğeri hâlâ aynı (artık bırakılmış) diziye işaret ediyor olur; o nesne de yıkılırken aynı diziyi tekrar delete[] ile bırakmaya çalışır. Bu çift serbest bırakma (double free) tanımsız davranıştır ve bu ders bunu koşturmaz — yalnız düzyazıda anılır, Tampon her zaman elle yazılmış derin kopyalayan yapıcı ve atamayla koşturulur.

Bu gözlem üç kuralına (rule of three) götürür: bir sınıf yıkıcısını, kopyalayan yapıcısını ya da kopyalayan atamasını elle yazmak zorundaysa (genellikle bir kaynağı ham bir işaretçiyle tuttuğu için), üçünü de elle yazmalıdır — biri elle yazılıp diğer ikisi derleyiciye bırakılırsa, sığ kopya riski doğar. Tampon bu kuralı tam uygular: yıkıcı, kopyalayan yapıcı ve kopyalayan atama, üçü de veri_’yi doğru yönetir.

Kuralın “üç” ile sınırlı olmasının bir nedeni var: üçü de aynı sorunun (kaynağın sahipliği) farklı yüzleridir. Yıkıcı kaynağı ne zaman bırakacağını, kopyalayan yapıcı kaynağın nasıl çoğaltılacağını, kopyalayan atama ise var olan bir kaynağın nasıl değiştirileceğini tanımlar. Biri unutulursa (örneğin yalnız yıkıcı yazılıp kopyalayan yapıcı derleyiciye bırakılırsa), derleyicinin ürettiği sığ kopyalayan yapıcı veri_’yi paylaştırır — sonra iki nesne aynı diziyi ayrı ayrı delete[] ile bırakmaya çalışır, bu da çift serbest bırakmadır. Yıkıcının varlığı bile, geri kalan ikisinin de elle yazılması gerektiğinin bir işaretidir: bir sınıf yıkıcısında özel bir şey yapıyorsa (yalnız delete[] gibi), bu genellikle o sınıfın bir kaynağın sahibi olduğu anlamına gelir, ve sahiplik kopyalama sırasında da düşünülmelidir.

= delete ile Kopyalamayı Engellemek

Her sınıfın kopyalanabilir olması gerekmez. Bir kaynağın tek bir sahibi olması isteniyorsa — örneğin bir dosya tanıtıcısı (file handle) ya da bir ağ bağlantısı ikinci bir nesneyle paylaşılmamalıysa — kopyalayan yapıcı ve kopyalayan atama = delete ile açıkça silinebilir: Tampon(const Tampon&) = delete;. Bu, derleyiciye o işlevi hiç üretmemesini değil, kullanıldığında bir derleme hatası vermesini söyler — Tampon b(a); yazmaya çalışan kod derlenmez. Tampon bu dersin ölçümü için kopyalanabilir kalır, = delete yalnız burada anılır; hangi sınıfların kopyalanamaz tasarlanması gerektiği ve bu kararın sahiplik modeliyle ilişkisi K05’in konusudur.

Kopyalayan Yapıcının Argümanı Neden const&dir

Kopyalayan yapıcının argümanı const Tampon& olarak alınır, Tampon olarak değil. Argüman değerle (Tampon digeri) alınsaydı, işlevi çağırmak için digeri kendisi bir kopya gerektirirdi — bu kopyayı kuracak işlev de kopyalayan yapıcının kendisi olurdu, sonsuz bir döngü. Referansla almak bu döngüyü kırar: hiçbir ek kopya üretilmeden orijinal nesneye doğrudan erişilir. const ise digeri’nin yalnız okunacağını, kopyalama sırasında değiştirilmeyeceğini garanti eder — kopyalayan yapıcı kaynağını değiştirmemelidir, yalnız ondan okumalıdır.

Kopya, Kaynağın Derinliğini Söylemez

Önceki dersin Koruyucu’su gibi, Tampon da kaynağını değerle tutar — ama Koruyucu’nun kaynak_ üyesi bir Kaynak değeriyken, Tampon’un veri_ üyesi bir int*’tir; kaynak (dizi) öbek üzerindedir, üye yalnız ona bir işaretçidir. Bu ders Tampon’u derin kopyalayacak şekilde elle yazarak bu farkı kapatır, ama kopyanın derinliği hiçbir zaman otomatik değildir — sınıfın kopyalayan yapıcısının ne yazdığına bağlıdır. Bir sınıf ham bir işaretçiyle paylaşılan bir kayda bağlıysa (örneğin int* yalnız başka bir nesnenin sahip olduğu bir diziyi işaret ediyorsa, sahiplik Tampon’da değilse), üye üye kopyalama bu işaretçiyi de kopyalar ve kopya da orijinaliyle aynı kaydı paylaşır — derin kopya yalnız sınıf kendi kaynağının sahibi olduğunda ve kopyalayan yapıcı bunu açıkça yeniden ayırdığında gerçekleşir.

Bu, aynı sınıfın farklı üyelerinin farklı kopyalama davranışları isteyebileceği anlamına gelir. Tampon’un bir de, sahibi olmadığı bir kaynağa işaret eden const int* referans_ adında bir üyesi olsaydı, kopyalayan yapıcının bu üyeyi kasıtlı olarak sığ kopyalaması — işaretçiyi olduğu gibi kopyalayıp yeni bellek ayırmaması — doğru olurdu, çünkü Tampon o kaynağın sahibi değildir, yalnız ona bakar. Kopyalayan yapıcı yazarken sorulması gereken soru şudur: bu üye bu nesnenin sahip olduğu bir kaynak mı, yoksa yalnız bakılan bir kaynak mı? Cevap üyeden üyeye değişebilir, ve kopyalayan yapıcının gövdesi bu kararların toplamıdır — derleyicinin ürettiği varsayılan üye üye kopyalama bu ayrımı hiç yapmaz, her üyeyi aynı biçimde (sığ) kopyalar.

Taşıma (move) — bir nesnenin kaynağını kopyalamadan, kaynağı devralarak aktarması — bu sorunu farklı bir yoldan çözer ve bu kursun kapsamı dışındadır (K05). M11/K01’de bir yapının atamayla bütün alanlarıyla kopyalandığı ve bellek düzeni — üye sırası, hizalama ve dolgu — işlenmişti; bu ders ikisini birleştirir: kopyalama hem değer semantiğine (kopya orijinalden bağımsızdır) hem bellek yönetimine (kopya kendi belleğini alır) aynı anda uyar.

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

Bu dersin ölçtüğü ifade kopya sınıfındadır: b, a’dan yeni kurulmuş, a’dan adres olarak ayrı bir nesnedir. Ama kopya “ne kadar derin” olduğunu kendiliğinden söylemez — bu bilgi kopyanın sonucunda değil, sınıfın kopyalayan yapıcısının kaynağıyla nasıl davrandığında gizlidir. Tampon derin kopyalar çünkü kopyalayan yapıcı new int[...] ile yeni bir dizi ayırır; kaynağa ham bir işaretçiyle bağlı, kendi kopyalayan yapıcısını doğru yazmamış bir sınıf sığ kopyalar ve kopya da orijinaliyle aynı kaynağı paylaşır. Bir sınıfı okuyup “bu kopyalanabilir mi, ve kopyalandığında ne olur” sorusuna cevap vermek için kopyalayan yapıcının gövdesine bakmak gerekir — sınıfın adı ya da kullanım biçimi bunu söylemez.

Özet

  • Bu dersin ölçtüğü ifade kopya sınıfındadır: Tampon b(a) a’dan yeni kurulmuş, adres olarak ayrı bir nesnedir.
  • Kopyalayan yapıcı bir kurmada, kopyalayan atama bir atamada çağrılır; sol taraftaki nesnenin yeni mi var olan mı olduğu hangisinin çalışacağını belirler.
  • Tampon kendi dizisini new’la ayırarak derin kopya üretir; üye üye kopyalama yalnız işaretçiyi kopylardı, bu sığ kopya olurdu.
  • Kopyalayan atama this == &digeri denetimiyle kendine atamayı güvenle karşılar.
  • Yıkıcı, kopyalayan yapıcı ve kopyalayan atamadan biri elle yazılırsa üçü de yazılmalıdır (üç kuralı).

Sonraki Adım

Bu ders bir nesnenin kopyalandığında kendi kaynağını aldığını ölçtü; bir sonraki ders kopyalamadan uzaklaşıp bir referansın çağıranın nesnesini nasıl doğrudan değiştirdiğini — hiçbir kopya üretmeden — ö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