- Bir nesneyi kopyalamak ne demektir?
- Nelerdir kopya yapıcı ve kopya atama operatörü ?
- Onları ne zaman kendim beyan etmeliyim?
- Nesnelerimin kopyalanmasını nasıl önleyebilirim?
Yanıtlar:
C ++, kullanıcı tanımlı türlerin değişkenlerini değer semantiği ile ele alır . Bu, nesnelerin çeşitli bağlamlarda dolaylı olarak kopyalandığı ve "bir nesnenin kopyalanmasının" gerçekte ne anlama geldiğini anlamamız gerektiği anlamına gelir.
Basit bir örneği ele alalım:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age) : name(name), age(age)
{
}
};
int main()
{
person a("Bjarne Stroustrup", 60);
person b(a); // What happens here?
b = a; // And here?
}
( name(name), age(age)
Parça tarafından şaşkınsanız , buna üye başlangıç listesi denir .)
Bir person
nesneyi kopyalamak ne anlama geliyor ? main
Fonksiyonu, iki farklı kopyalama senaryoları göstermektedir. Başlatma person b(a);
, kopya oluşturucu tarafından gerçekleştirilir . Görevi, mevcut bir nesnenin durumuna göre yeni bir nesne inşa etmektir. Atama b = a
, kopya atama operatörü tarafından gerçekleştirilir . İşi genellikle biraz daha karmaşıktır, çünkü hedef nesne zaten ele alınması gereken geçerli bir durumdadır.
Ne kopya kurucu ne de görevlendirme operatörü (ne de yıkıcı) kendimizi açıkladığımızdan, bunlar bizim için örtük olarak tanımlanmıştır. Standarttan alıntı:
[...] kopya oluşturucu ve kopya atama operatörü, [...] ve yıkıcı özel üye işlevleridir. [ Not : Uygulama, program açıkça belirtmediğinde bazı üye türleri için bu üye işlevlerini dolaylı olarak bildirir. Uygulama, kullanıldıklarında bunları dolaylı olarak tanımlayacaktır. [...] son not ] [n3126.pdf bölüm 12 §1]
Varsayılan olarak, bir nesneyi kopyalamak üyelerini kopyalamak anlamına gelir:
Birliğe bağlı olmayan X sınıfı için dolaylı olarak tanımlanan kopya yapıcı, alt nesnelerinin üye olarak bir kopyasını gerçekleştirir. [n3126.pdf bölüm 12.8 §16]
Birleşik olmayan X sınıfı için dolaylı olarak tanımlanmış kopya atama işleci, alt nesnelerinin üye olarak kopya atamasını gerçekleştirir. [n3126.pdf bölüm 12.8 §30]
Örtük olarak tanımlanmış özel üye işlevleri aşağıdaki person
gibi görünür:
// 1. copy constructor
person(const person& that) : name(that.name), age(that.age)
{
}
// 2. copy assignment operator
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
// 3. destructor
~person()
{
}
Üye olarak kopyalama, bu durumda tam olarak istediğimiz şeydir:
name
ve age
kopyalanır, böylece bağımsız, bağımsız bir person
nesne elde ederiz . Örtük olarak tanımlanmış yıkıcı her zaman boştur. Bu durumda da yapıcıda herhangi bir kaynak edinmediğimiz için sorun yok. Üyelerin yıkıcıları, person
yıkıcı bittikten sonra örtük olarak çağrılır :
Yıkıcının gövdesini uyguladıktan ve vücut içinde tahsis edilen herhangi bir otomatik nesneyi yok ettikten sonra, X sınıfı için bir yıkıcı, X'in doğrudan [...] üyeleri için yıkıcıları çağırır [n3126.pdf 12.4 §6]
Peki bu özel üye işlevlerini ne zaman açık bir şekilde ilan etmeliyiz? Sınıfımız bir kaynağı yönettiğinde , yani sınıfın bir nesnesi bu kaynaktan sorumlu olduğunda. Bu genellikle kaynağın kurucuda edinildiği (veya kurucuya geçirildiği) ve yıkıcıda bırakıldığı anlamına gelir .
Önceden standart C ++ 'a geri dönelim. Böyle bir şey yoktu std::string
ve programcılar işaretçilerle aşıktı. person
Sınıf bu benziyordu olabilir:
class person
{
char* name;
int age;
public:
// the constructor acquires a resource:
// in this case, dynamic memory obtained via new[]
person(const char* the_name, int the_age)
{
name = new char[strlen(the_name) + 1];
strcpy(name, the_name);
age = the_age;
}
// the destructor must release this resource via delete[]
~person()
{
delete[] name;
}
};
Bugün bile, insanlar hala bu tarzda sınıflar yazıyor ve sorun yaşıyor: " Bir kişiyi bir vektöre ittim ve şimdi çılgın bellek hataları alıyorum! " Varsayılan olarak, bir nesneyi kopyalamak üyelerini kopyalamak, ancak name
üyeyi sadece kopyalamak demektir. işaret ettiği karakter dizisini değil , bir işaretçiyi kopyalar ! Bunun birkaç hoş olmayan etkisi vardır:
a
gözlemlenebilir b
.b
yok edildiğinde, a.name
sarkan bir işaretçi.a
Yok edilirse , sarkan işaretçiyi silmek tanımlanmamış davranış sağlar .name
Ödev, ödevden önce neye işaret ettiğini dikkate almadığından , er ya da geç her yerde bellek sızıntıları alırsınız.Üye yönde kopyalama istenen etkiye sahip olmadığından, karakter dizisinin derin kopyalarını oluşturmak için kopyalama yapıcısını ve kopyalama atama operatörünü açıkça tanımlamalıyız:
// 1. copy constructor
person(const person& that)
{
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
// 2. copy assignment operator
person& operator=(const person& that)
{
if (this != &that)
{
delete[] name;
// This is a dangerous point in the flow of execution!
// We have temporarily invalidated the class invariants,
// and the next statement might throw an exception,
// leaving the object in an invalid state :(
name = new char[strlen(that.name) + 1];
strcpy(name, that.name);
age = that.age;
}
return *this;
}
Başlatma ve atama arasındaki farka dikkat edin: name
bellek sızıntılarını önlemek için atamadan önce eski durumu yıkmalıyız. Ayrıca, formun kendi kendine atanmasına karşı da korunmalıyız x = x
. O çek olmadan, delete[] name
içeren diziyi silmek istiyorsunuz kaynak yazdığınız zaman, çünkü dize x = x
, hem this->name
ve that.name
aynı işaretçi içerir.
Ne yazık ki, new char[...]
bellek tükenmesi nedeniyle bir istisna atarsa bu çözüm başarısız olacaktır . Olası bir çözüm, yerel bir değişken sunmak ve ifadeleri yeniden sıralamaktır:
// 2. copy assignment operator
person& operator=(const person& that)
{
char* local_name = new char[strlen(that.name) + 1];
// If the above statement throws,
// the object is still in the same state as before.
// None of the following statements will throw an exception :)
strcpy(local_name, that.name);
delete[] name;
name = local_name;
age = that.age;
return *this;
}
Bu aynı zamanda açık bir denetim olmadan kendi kendine görevlendirmeyi de halleder. Bu soruna daha da sağlam bir çözüm, kopyala-takas deyimidir , ancak burada istisna güvenliği ayrıntılarına girmeyeceğim. Sadece aşağıdaki noktaya gelmek için istisnalardan bahsettim: Kaynakları yöneten sınıflar yazmak zor.
Dosya tanıtıcıları veya muteksler gibi bazı kaynaklar kopyalanamaz veya kopyalanmamalıdır. Bu durumda, kopya oluşturucu ve kopya atama işlecini private
bir tanım vermeden açıklayın:
private:
person(const person& that);
person& operator=(const person& that);
Alternatif olarak, bunları devralınabilir boost::noncopyable
veya silindi olarak bildirebilirsiniz (C ++ 11 ve üstü sürümlerde):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Bazen bir kaynağı yöneten bir sınıf uygulamanız gerekir. (Asla tek bir sınıfta birden fazla kaynağı yönetmeyin, bu sadece acıya yol açacaktır.) Bu durumda, üç kuralı unutmayın :
Yıkıcı, kopya oluşturucu veya kopya atama operatörünü kendiniz açıkça bildirmeniz gerekiyorsa, muhtemelen bunların üçünü de açıkça bildirmeniz gerekir.
(Ne yazık ki, bu "kural" C ++ standardı veya bildiğim herhangi bir derleyici tarafından uygulanmıyor.)
C ++ 11'den itibaren, bir nesnenin 2 ekstra özel üye işlevi vardır: taşıma yapıcısı ve taşıma ataması. Beş devlet kuralı da bu işlevleri yerine getirmek için.
İmzalara bir örnek:
class person
{
std::string name;
int age;
public:
person(const std::string& name, int age); // Ctor
person(const person &) = default; // Copy Ctor
person(person &&) noexcept = default; // Move Ctor
person& operator=(const person &) = default; // Copy Assignment
person& operator=(person &&) noexcept = default; // Move Assignment
~person() noexcept = default; // Dtor
};
3/5 kuralı, 0/3/5 kuralı olarak da adlandırılır. Kuralın sıfır kısmı, sınıfınızı oluştururken özel üye işlevlerinden hiçbirini yazmamanıza izin verildiğini belirtir.
Çoğu zaman, bir kaynağı kendiniz yönetmeniz gerekmez, çünkü varolan bir sınıf std::string
zaten sizin için yapar. Bir std::string
üyeyi kullanarak basit kodu, a kullanarak kıvrımlı ve hataya açık alternatifle karşılaştırın char*
ve ikna olmalısınız. Ham işaretçi üyelerinden uzak durduğunuz sürece, üç kuralın kendi kodunuzu ilgilendirmesi olası değildir.
Üç Kuralı C ++ için pratik bir kuraldır, temelde söyleyerek
Sınıfınızın şunlardan birine ihtiyacı varsa
- bir kopya kurucu ,
- bir atama operatörü ,
- ya da bir yıkıcı ,
explictly tanımlanan, o zaman ihtiyaç muhtemeldir üçünü .
Bunun nedeni, üçünün de genellikle bir kaynağı yönetmek için kullanılmasıdır ve sınıfınız bir kaynağı yönetiyorsa, genellikle kopyalamanın yanı sıra serbest bırakmayı da yönetmesi gerekir.
Sınıfınızın yönettiği kaynağı kopyalamak için iyi bir anlam bilgisi yoksa , kopya oluşturucu ve atama işlecini olarak bildirerek ( tanımlamamak ) kopyalamayı yasaklamayı düşünün private
.
(C ++ standardının yakında çıkacak olan yeni sürümünün (C ++ 11 olan) C ++ 'a hareket semantiği eklediğini ve bu da Üçün Kuralını değiştireceğini unutmayın. Bununla birlikte, bir C ++ 11 bölümü yazmak için çok az şey biliyorum Üç Kuralı hakkında.)
boost::noncopyable
) bir sınıftan miras (özel olarak) almaktır . Ayrıca çok daha net olabilir. Ben C ++ 0x ve "silme" fonksiyonları burada yardımcı olabilir, ancak sözdizimini unuttum düşünüyorum: /
noncopyable
std lib'in bir parçası olmadığı sürece , bunu bir gelişme olarak görmüyorum. (Oh, ve silme sözdizimini unuttuysan, daha önce bildiğim mor :)
Büyük üçün yasası yukarıda belirtildiği gibidir.
Basit bir İngilizcede çözdüğü problemin kolay bir örneği:
Varsayılan olmayan yıkıcı
Yapıcıya bellek ayırdınız ve bu yüzden silmek için bir yıkıcı yazmanız gerekir. Aksi takdirde bellek sızıntısına neden olursunuz.
Bunun bir iş olduğunu düşünebilirsiniz.
Sorun, nesnenizin bir kopyası yapılmışsa, kopya orijinal nesneyle aynı belleği gösterecektir.
Bunlardan biri yıkıcıdaki hafızayı siler, diğeri geçersiz hafızaya bir işaretçi (buna sarkan bir işaretçi denir) kullanmaya başladığında işler kıllı olur.
Bu nedenle, yeni nesnelerin yok etmek için kendi bellek parçalarını ayırması için bir kopya oluşturucu yazarsınız.
Atama operatörü ve kopya oluşturucu
Oluşturucunuzdaki belleği sınıfınızın üye işaretçisine ayırdınız. Bu sınıftaki bir nesneyi kopyaladığınızda, varsayılan atama işleci ve kopya oluşturucu bu üye işaretçisinin değerini yeni nesneye kopyalar.
Bu, yeni nesnenin ve eski nesnenin aynı bellek parçasını göstereceği anlamına gelir; böylece bir nesnede değiştirdiğinizde, diğer objektif için de değiştirilir. Bir nesne bu hafızayı silerse, diğeri onu kullanmaya çalışacaktır - eek.
Bu sorunu çözmek için, kopya oluşturucu ve atama işlecinin kendi sürümünü yazarsınız. Sürümleriniz yeni nesnelere ayrı bellek ayırır ve adresinden ziyade ilk işaretçinin işaret ettiği değerlere kopyalar.
Temel olarak, bir yıkıcı (varsayılan yıkıcı değil) varsa, tanımladığınız sınıfın bir miktar bellek ayırması olduğu anlamına gelir. Sınıfın dışarıda bazı istemci kodları veya sizin tarafınızdan kullanıldığını varsayalım.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
MyClass yalnızca bazı ilkel yazılan üyelere sahipse, varsayılan atama işleci çalışır, ancak bazı işaretçi üyeleri ve atama işleçleri olmayan nesneleri varsa, sonuç tahmin edilemez olur. Bu nedenle, bir sınıfın yıkıcısında silinecek bir şey varsa, derin bir kopya operatörüne ihtiyacımız olabileceğini söyleyebiliriz, bu da bir kopya oluşturucu ve atama operatörü sağlamamız gerektiği anlamına gelir.
Bir nesneyi kopyalamak ne demektir? Nesneleri kopyalamanın birkaç yolu vardır - en çok bahsettiğiniz 2 çeşit hakkında konuşalım - derin kopya ve sığ kopya.
Nesneye yönelik bir dilde olduğumuzdan (veya en azından öyle olduğunu varsaydığımızdan), diyelim ki bir bellek ayırdınız. Bir OO dili olduğundan, ayırdığımız bellek parçalarına kolayca başvurabiliriz çünkü bunlar genellikle ilkel değişkenler (ints, chars, bayt) veya kendi tiplerimiz ve ilkellerden yapılmış tanımladığımız sınıflardır. Diyelim ki şöyle bir Araba sınıfımız var:
class Car //A very simple class just to demonstrate what these definitions mean.
//It's pseudocode C++/Javaish, I assume strings do not need to be allocated.
{
private String sPrintColor;
private String sModel;
private String sMake;
public changePaint(String newColor)
{
this.sPrintColor = newColor;
}
public Car(String model, String make, String color) //Constructor
{
this.sPrintColor = color;
this.sModel = model;
this.sMake = make;
}
public ~Car() //Destructor
{
//Because we did not create any custom types, we aren't adding more code.
//Anytime your object goes out of scope / program collects garbage / etc. this guy gets called + all other related destructors.
//Since we did not use anything but strings, we have nothing additional to handle.
//The assumption is being made that the 3 strings will be handled by string's destructor and that it is being called automatically--if this were not the case you would need to do it here.
}
public Car(const Car &other) // Copy Constructor
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
public Car &operator =(const Car &other) // Assignment Operator
{
if(this != &other)
{
this.sPrintColor = other.sPrintColor;
this.sModel = other.sModel;
this.sMake = other.sMake;
}
return *this;
}
}
Derin bir kopya, bir nesneyi bildirip sonra nesnenin tamamen ayrı bir kopyasını oluşturmamızdır ... 2 tamamen bellek kümesinde 2 nesne ile sonuçlanırız.
Car car1 = new Car("mustang", "ford", "red");
Car car2 = car1; //Call the copy constructor
car2.changePaint("green");
//car2 is now green but car1 is still red.
Şimdi garip bir şey yapalım. Diyelim ki car2 yanlış programlanmış ya da bilerek car1'in yapıldığı gerçek belleği paylaşmak içindir. (Bunu yapmak genellikle bir hatadır ve sınıflarda genellikle tartışıldığı battaniyedir.) Car2 hakkında her soru sorduğunuzda, car1'in bellek alanına gerçekten bir işaretçi çözdüğünüzü varsayalım ... dır-dir.
//Shallow copy example
//Assume we're in C++ because it's standard behavior is to shallow copy objects if you do not have a constructor written for an operation.
//Now let's assume I do not have any code for the assignment or copy operations like I do above...with those now gone, C++ will use the default.
Car car1 = new Car("ford", "mustang", "red");
Car car2 = car1;
car2.changePaint("green");//car1 is also now green
delete car2;/*I get rid of my car which is also really your car...I told C++ to resolve
the address of where car2 exists and delete the memory...which is also
the memory associated with your car.*/
car1.changePaint("red");/*program will likely crash because this area is
no longer allocated to the program.*/
Hangi dilde yazdığınızdan bağımsız olarak, nesneleri kopyalama konusunda ne demek istediğiniz konusunda çok dikkatli olun çünkü çoğu zaman derin bir kopya istiyorsunuz.
Kopya oluşturucu ve kopya atama operatörü nedir? Onları yukarıda zaten kullandım. Kopya yapıcı aşağıdaki gibi kod yazdığınızda denir Car car2 = car1;
Eğer bir değişken tanımlayın ve tek satırda atarsanız kopya yapıcısı çağrıldığında olduğunu, Esasen. Atama işleci, eşittir işaretini kullandığınızda olan şeydir car2 = car1;
. Uyarı car2
aynı açıklamada bildirilmedi. Bu işlemler için yazdığınız iki kod parçası büyük olasılıkla çok benzerdir. Aslında, tipik tasarım deseninin, ilk kopya / atamanın meşru olduğunu düşündüğünüzde her şeyi ayarlamak için çağırdığınız başka bir işlevi vardır - yazdığım uzun el koduna bakarsanız, işlevler hemen hemen aynıdır.
Onları ne zaman kendim beyan etmeliyim? Paylaşılacak veya bir şekilde üretim için kod yazmıyorsanız, gerçekten sadece ihtiyacınız olduğunda bildirmeniz gerekir. Eğer 'kazara' kullanmayı tercih ederseniz ve dil yapmazsanız, program dilinizin ne yaptığının farkında olmanız gerekir - yani derleyicinin varsayılanını alırsınız. Örneğin nadiren kopya kurucuları kullanıyorum, ancak atama operatörü geçersiz kılmaları çok yaygın. Toplama, çıkarma vb.'nin de anlamını geçersiz kılabileceğinizi biliyor muydunuz?
Nesnelerimin kopyalanmasını nasıl önleyebilirim? Özel bir işlevle nesneniz için bellek ayırmanıza izin verilen tüm yolları geçersiz kılmak makul bir başlangıçtır. İnsanların onları kopyalamasını gerçekten istemiyorsanız, bir istisna atarak ve aynı zamanda nesneyi kopyalayarak programı herkese açık hale getirebilir ve programcıyı uyarabilirsiniz.
Onları ne zaman kendim beyan etmeliyim?
Üçüncül Kural,
o zaman üçünü de ilan etmelisiniz. Bir kopyalama işleminin anlamını devralma ihtiyacının neredeyse her zaman bir çeşit kaynak yönetimi gerçekleştiren sınıftan kaynaklandığı ve neredeyse her zaman
bir kopyalama işleminde yapılan kaynak yönetimi ne olursa olsun, diğer kopyalama işleminde yapılması gerekiyordu ve
sınıf yıkıcısı da kaynağın yönetimine katılırdı (genellikle onu serbest bırakır). Yönetilecek klasik kaynak bellekti ve bu nedenle belleği yöneten tüm Standart Kütüphane sınıflarının (örn. Dinamik bellek yönetimi yapan STL kapsayıcıları) hepsi “büyük üç” bildiriyor: hem kopyalama işlemleri hem de bir yıkıcı.
Üç Kuralı'nın bir sonucu, kullanıcı tarafından bildirilen bir yıkıcının varlığının, basit üye akıllı kopyasının, sınıftaki kopyalama işlemleri için uygun olmadığını göstermesidir. Bu da, eğer bir sınıf bir yıkıcı ilan ederse, kopyalama işlemlerinin muhtemelen otomatik olarak üretilmemesi gerektiğini, çünkü doğru olanı yapmayacaklarını gösterir. C ++ 98 kabul edildiğinde, bu akıl yürütme çizgisinin önemi tam olarak takdir edilmedi, bu nedenle C ++ 98'de, kullanıcının bildirdiği bir yıkıcı varlığının, derleyicilerin kopya işlemleri oluşturma isteği üzerinde hiçbir etkisi yoktu. Bu, C ++ 11'de geçerli olmaya devam eder, ancak yalnızca kopyalama işlemlerinin oluşturulduğu koşulların kısıtlanması çok eski kodu kıracağı için.
Nesnelerimin kopyalanmasını nasıl önleyebilirim?
Kopya oluşturucu ve kopya atama işlecini özel erişim belirteci olarak bildirin.
class MemoryBlock
{
public:
//code here
private:
MemoryBlock(const MemoryBlock& other)
{
cout<<"copy constructor"<<endl;
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
return *this;
}
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
C ++ 11 ve sonrasında kopya oluşturucu ve atama operatörünün silindiğini bildirebilirsiniz
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Mevcut cevapların çoğu zaten kopya oluşturucuya, atama operatörüne ve yıkıcıya dokunuyor. Bununla birlikte, C ++ 11 sonrası, semantik hareketinin tanıtımı bunu 3'ün ötesine genişletebilir.
Son zamanlarda Michael Claisse bu konuya değinen bir konuşma yaptı: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
C ++ 'da üç kural, aşağıdaki üye işlevlerden birinde açık bir tanım varsa, programcının diğer iki üye işlevlerini birlikte tanımlaması gereken üç gereksinimin tasarım ve temel ilkesidir. Yani şu üç üye işlevi vazgeçilmezdir: yıkıcı, kopya oluşturucu, kopya atama operatörü.
C ++ 'da kopya kurucu özel bir kurucudur. Varolan bir nesnenin kopyasına eşdeğer olan yeni nesne olan yeni bir nesne oluşturmak için kullanılır.
Kopya atama işleci, genellikle varolan bir nesneyi aynı nesne türüne sahip başkalarına belirtmek için kullanılan özel bir atama işlecidir.
Hızlı örnekler var:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
.