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.
Tamponkendi dizisininew’la ayırarak derin kopya üretir; üye üye kopyalama yalnız işaretçiyi kopylardı, bu sığ kopya olurdu.- Kopyalayan atama
this == &digeridenetimiyle 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.