- Apa artinya menyalin objek ?
- Apa konstruktor salin dan operator penugasan salinan ?
- Kapan saya harus menyatakannya sendiri?
- Bagaimana saya bisa mencegah objek saya disalin?
Jawaban:
C ++ memperlakukan variabel tipe yang ditentukan pengguna dengan semantik nilai . Ini berarti bahwa objek secara tersirat disalin dalam berbagai konteks, dan kita harus memahami apa sebenarnya arti "menyalin objek".
Mari kita perhatikan contoh sederhana:
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?
}
(Jika Anda bingung dengan name(name), age(age)
bagian ini, ini disebut daftar penginisialisasi anggota .)
Apa artinya menyalin person
objek? The main
Fungsi menunjukkan dua skenario menyalin berbeda. Inisialisasi person b(a);
dilakukan oleh copy constructor . Tugasnya adalah membangun objek baru berdasarkan keadaan objek yang ada. Penugasan b = a
dilakukan oleh operator penugasan salinan . Pekerjaannya umumnya sedikit lebih rumit, karena objek target sudah dalam keadaan valid yang perlu ditangani.
Karena kami mendeklarasikan bukan pembuat salinan atau operator penugasan (atau destruktor) sendiri, ini secara implisit didefinisikan untuk kami. Kutipan dari standar:
Operator penyalin dan penyalin salin [...], [...] dan destruktor adalah fungsi anggota khusus. [ Catatan : Implementasi secara implisit akan mendeklarasikan fungsi anggota ini untuk beberapa tipe kelas ketika program tidak secara eksplisit mendeklarasikannya. Implementasi akan secara implisit menentukan mereka jika digunakan. [...] catatan akhir ] [n3126.pdf bagian 12 §1]
Secara default, menyalin objek berarti menyalin anggotanya:
Konstruktor salinan yang terdefinisi secara implisit untuk kelas X non-serikat melakukan salinan anggota sub-proyeknya. [n3126.pdf bagian 12.8 §16]
Operator penugasan salinan yang ditentukan secara implisit untuk kelas X non-serikat pekerja melakukan penugasan salin anggota dari sub-proyeknya. [n3126.pdf bagian 12.8 §30]
Fungsi anggota khusus yang didefinisikan secara implisit untuk person
terlihat seperti ini:
// 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()
{
}
Penyalinan dengan anggota adalah yang kita inginkan dalam kasus ini:
name
dan age
disalin, jadi kita mendapatkan person
objek mandiri yang mandiri . Destructor yang didefinisikan secara implisit selalu kosong. Ini juga baik dalam hal ini karena kami tidak memperoleh sumber daya apa pun di konstruktor. Destructor anggota secara implisit dipanggil setelah person
destruktor selesai:
Setelah mengeksekusi tubuh destruktor dan menghancurkan objek otomatis yang dialokasikan dalam tubuh, destruktor untuk kelas X memanggil destruktor untuk anggota langsung [...] [n3126.pdf 12.4 §6]
Jadi kapan kita harus mendeklarasikan fungsi anggota khusus itu secara eksplisit? Ketika kelas kita mengelola sumber daya , yaitu, ketika sebuah objek kelas bertanggung jawab atas sumber daya itu. Itu biasanya berarti sumber daya diperoleh dalam konstruktor (atau diteruskan ke konstruktor) dan dirilis pada destruktor.
Mari kita kembali ke masa pra-standar C ++. Tidak ada yang namanya std::string
, dan programmer suka dengan pointer. The person
kelas mungkin tampak seperti ini:
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;
}
};
Bahkan hari ini, orang masih menulis kelas dengan gaya ini dan mendapat masalah: " Saya mendorong seseorang ke vektor dan sekarang saya mendapatkan kesalahan memori gila! " Ingat bahwa secara default, menyalin objek berarti menyalin anggota, tetapi menyalin name
anggota hanya menyalin sebuah pointer, bukan array karakter yang ditunjuknya! Ini memiliki beberapa efek yang tidak menyenangkan:
a
dapat diamati melalui b
.b
dihancurkan, a.name
adalah pointer menjuntai.a
dihancurkan, menghapus pointer menggantung menghasilkan perilaku yang tidak terdefinisi .name
menunjuk sebelum tugas, cepat atau lambat Anda akan mendapatkan kebocoran memori di semua tempat.Karena penyalinan dengan anggota tidak memiliki efek yang diinginkan, kita harus mendefinisikan konstruktor salin dan operator penugasan secara eksplisit untuk membuat salinan mendalam dari susunan karakter:
// 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;
}
Perhatikan perbedaan antara inisialisasi dan penugasan: kita harus merobohkan keadaan lama sebelum menugaskan name
untuk mencegah kebocoran memori. Juga, kita harus melindungi diri dari penugasan formulir x = x
. Tanpa centang itu, delete[] name
akan menghapus array yang berisi string sumber , karena ketika Anda menulis x = x
, keduanya this->name
dan that.name
berisi pointer yang sama.
Sayangnya, solusi ini akan gagal jika new char[...]
melempar pengecualian karena kehabisan memori. Salah satu solusi yang mungkin adalah dengan memperkenalkan variabel lokal dan menyusun ulang pernyataan:
// 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;
}
Ini juga menangani penugasan diri tanpa pemeriksaan eksplisit. Solusi yang lebih kuat untuk masalah ini adalah idiom copy-and-swap , tapi saya tidak akan membahas detail keamanan pengecualian di sini. Saya hanya menyebutkan pengecualian untuk membuat poin berikut: Menulis kelas yang mengelola sumber daya sulit.
Beberapa sumber daya tidak dapat atau tidak boleh disalin, seperti pegangan file atau mutex. Dalam hal itu, cukup deklarasikan copy constructor dan copy assignment operator private
tanpa memberikan definisi:
private:
person(const person& that);
person& operator=(const person& that);
Atau, Anda dapat mewarisi boost::noncopyable
atau mendeklarasikannya sebagai dihapus (dalam C ++ 11 dan di atas):
person(const person& that) = delete;
person& operator=(const person& that) = delete;
Terkadang Anda perlu mengimplementasikan kelas yang mengelola sumber daya. (Jangan pernah mengelola banyak sumber daya dalam satu kelas, ini hanya akan menimbulkan rasa sakit.) Dalam hal ini, ingat aturan tiga :
Jika Anda perlu secara eksplisit mendeklarasikan baik destruktor, copy constructor atau operator penugasan sendiri, Anda mungkin perlu secara eksplisit mendeklarasikan ketiganya.
(Sayangnya, "aturan" ini tidak ditegakkan oleh standar C ++ atau kompiler apa pun yang saya ketahui.)
Dari C ++ 11 dan seterusnya, sebuah objek memiliki 2 fungsi anggota khusus ekstra: konstruktor pemindahan dan pemindahan tugas. Aturan lima negara untuk mengimplementasikan fungsi-fungsi ini juga.
Contoh dengan tanda tangan:
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
};
Aturan 3/5 juga disebut sebagai aturan 0/3/5. Bagian nol dari aturan menyatakan bahwa Anda diizinkan untuk tidak menulis fungsi anggota khusus saat membuat kelas Anda.
Sebagian besar waktu, Anda tidak perlu mengelola sumber daya sendiri, karena kelas yang ada seperti std::string
sudah melakukannya untuk Anda. Bandingkan saja kode sederhana menggunakan std::string
anggota dengan alternatif berbelit-belit dan rawan menggunakan char*
dan Anda harus diyakinkan. Selama Anda tinggal jauh dari anggota pointer mentah, aturan tiga kemungkinan tidak akan menyangkut kode Anda sendiri.
The Rule of Three adalah aturan praktis untuk C ++, pada dasarnya mengatakan
Jika kelas Anda membutuhkan
- a copy constructor ,
- sebuah operator penugasan ,
- atau destruktor ,
didefinisikan secara eksplisit, maka kemungkinan membutuhkan mereka bertiga .
Alasan untuk ini adalah bahwa ketiganya biasanya digunakan untuk mengelola sumber daya, dan jika kelas Anda mengelola sumber daya, biasanya perlu mengelola penyalinan serta membebaskan.
Jika tidak ada semantik yang baik untuk menyalin sumber daya yang dikelola kelas Anda, maka pertimbangkan untuk melarang menyalin dengan mendeklarasikan (tidak mendefinisikan ) operator penyalin dan penugasan sebagai private
.
(Perhatikan bahwa versi baru standar C ++ yang akan datang (yang merupakan C ++ 11) menambahkan pindahan semantik ke C ++, yang kemungkinan akan mengubah Aturan Tiga. Namun, saya tahu terlalu sedikit tentang ini untuk menulis bagian C ++ 11 tentang Aturan Tiga.)
boost::noncopyable
). Itu juga bisa menjadi lebih jelas. Saya berpikir bahwa C ++ 0x dan kemungkinan untuk "menghapus" fungsi dapat membantu di sini, tetapi lupa sintaksnya: /
noncopyable
adalah bagian dari std lib, saya tidak menganggapnya sebagai perbaikan. (Oh, dan jika Anda lupa sintaks penghapusan, Anda lupa mor ethan yang pernah saya kenal. :)
)
Hukum tiga besar adalah sebagaimana ditentukan di atas.
Contoh mudah, dalam bahasa Inggris sederhana, dari jenis masalah yang dipecahkannya:
Destruktor non default
Anda mengalokasikan memori di konstruktor Anda dan karenanya Anda perlu menulis destruktor untuk menghapusnya. Kalau tidak, Anda akan menyebabkan kebocoran memori.
Anda mungkin berpikir bahwa ini adalah pekerjaan yang dilakukan.
Masalahnya adalah, jika salinan dibuat dari objek Anda, maka salinan itu akan menunjuk ke memori yang sama dengan objek aslinya.
Sekali, salah satu dari ini menghapus memori dalam destruktornya, yang lain akan memiliki pointer ke memori yang tidak valid (ini disebut pointer menggantung) ketika mencoba menggunakannya hal-hal yang akan menjadi berbulu.
Oleh karena itu, Anda menulis copy constructor sehingga mengalokasikan objek baru yang akan dihancurkan oleh memori mereka sendiri.
Operator penugasan dan copy constructor
Anda mengalokasikan memori di konstruktor Anda ke pointer anggota kelas Anda. Saat Anda menyalin objek kelas ini, operator penugasan default dan copy constructor akan menyalin nilai dari pointer anggota ini ke objek baru.
Ini berarti bahwa objek baru dan objek lama akan menunjuk pada bagian memori yang sama sehingga ketika Anda mengubahnya di satu objek itu akan berubah untuk objek lain juga. Jika satu objek menghapus memori ini, yang lain akan melanjutkan mencoba menggunakannya - eek.
Untuk mengatasinya, Anda menulis versi konstruktor dan tugas penugasan versi Anda sendiri. Versi Anda mengalokasikan memori terpisah ke objek-objek baru dan menyalin nilai-nilai yang ditunjuk oleh pointer pertama daripada alamatnya.
Pada dasarnya jika Anda memiliki destruktor (bukan destruktor default) itu berarti kelas yang Anda tentukan memiliki alokasi memori. Misalkan kelas digunakan di luar oleh beberapa kode klien atau oleh Anda.
MyClass x(a, b);
MyClass y(c, d);
x = y; // This is a shallow copy if assignment operator is not provided
Jika MyClass hanya memiliki beberapa anggota yang diketik primitif operator penugasan default akan bekerja tetapi jika memiliki beberapa anggota penunjuk dan objek yang tidak memiliki operator penugasan, hasilnya akan tidak dapat diprediksi. Oleh karena itu kita dapat mengatakan bahwa jika ada sesuatu untuk dihapus pada destruktor suatu kelas, kita mungkin memerlukan operator penyalinan yang dalam yang berarti kita harus menyediakan operator penyalin dan penugasan salinan.
Apa artinya menyalin objek? Ada beberapa cara Anda dapat menyalin objek - mari kita bicara tentang 2 jenis yang paling Anda rujuk - salinan dalam dan salinan dangkal.
Karena kita menggunakan bahasa berorientasi objek (atau setidaknya mengasumsikan demikian), katakanlah Anda memiliki memori yang dialokasikan. Karena ini adalah bahasa OO, kita dapat dengan mudah merujuk pada potongan memori yang kita alokasikan karena mereka biasanya variabel primitif (ints, karakter, byte) atau kelas yang kita definisikan terbuat dari tipe dan primitif kita sendiri. Jadi katakanlah kita memiliki kelas Mobil sebagai berikut:
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;
}
}
Salinan yang dalam adalah jika kita mendeklarasikan objek dan kemudian membuat salinan objek yang sepenuhnya terpisah ... kita berakhir dengan 2 objek dalam 2 set memori yang sepenuhnya.
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.
Sekarang mari kita lakukan sesuatu yang aneh. Katakanlah car2 diprogram dengan salah atau dengan sengaja dimaksudkan untuk membagikan memori aktual yang dibuat dari car1. (Biasanya kesalahan untuk melakukan ini dan di kelas biasanya selimut itu dibahas di bawah.) Berpura-pura bahwa setiap kali Anda bertanya tentang car2, Anda benar-benar menyelesaikan pointer ke ruang memori car1 ... itu kurang lebih seperti salinan yang dangkal adalah.
//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.*/
Jadi, terlepas dari bahasa apa yang Anda tulis, berhati-hatilah dengan apa yang Anda maksud ketika menyalin objek karena sebagian besar waktu Anda menginginkan salinan yang dalam.
Apa konstruktor salin dan operator penugasan salinan? Saya sudah menggunakannya di atas. Copy constructor dipanggil ketika Anda mengetik kode seperti pada Car car2 = car1;
dasarnya jika Anda mendeklarasikan variabel dan menetapkannya dalam satu baris, saat itulah copy constructor dipanggil. Operator penugasan adalah apa yang terjadi ketika Anda menggunakan tanda sama dengan-- car2 = car1;
. Pemberitahuan car2
tidak dinyatakan dalam pernyataan yang sama. Dua potongan kode yang Anda tulis untuk operasi ini kemungkinan sangat mirip. Sebenarnya pola desain yang khas memiliki fungsi lain yang Anda panggil untuk mengatur semuanya setelah Anda puas dengan salinan / penugasan awal yang sah - jika Anda melihat kode lama yang saya tulis, fungsinya hampir identik.
Kapan saya harus menyatakannya sendiri? Jika Anda tidak menulis kode yang akan dibagikan atau untuk produksi dengan cara tertentu, Anda benar-benar hanya perlu menyatakannya saat Anda membutuhkannya. Anda harus mengetahui apa yang dilakukan bahasa program Anda jika Anda memilih untuk menggunakannya 'secara tidak sengaja' dan tidak membuatnya - yaitu Anda mendapatkan default kompiler. Saya jarang menggunakan copy constructor misalnya, tetapi menimpa operator penugasan sangat umum. Tahukah Anda bahwa Anda dapat mengesampingkan apa arti penambahan, pengurangan, dll. Juga?
Bagaimana saya bisa mencegah objek saya disalin? Mengganti semua cara Anda diizinkan mengalokasikan memori untuk objek Anda dengan fungsi pribadi adalah awal yang masuk akal. Jika Anda benar-benar tidak ingin orang menyalinnya, Anda dapat membuatnya publik dan mengingatkan programmer dengan melemparkan pengecualian dan juga tidak menyalin objek.
Kapan saya harus menyatakannya sendiri?
Aturan Tiga menyatakan bahwa jika Anda mendeklarasikan salah satu dari a
maka Anda harus mendeklarasikan ketiganya. Tumbuhnya dari pengamatan bahwa kebutuhan untuk mengambil alih arti dari operasi penyalinan hampir selalu berasal dari kelas yang melakukan semacam manajemen sumber daya, dan yang hampir selalu menyiratkan bahwa
manajemen sumber daya apa pun yang sedang dilakukan dalam satu operasi penyalinan mungkin perlu dilakukan dalam operasi penyalinan lainnya dan
destruktor kelas juga akan berpartisipasi dalam pengelolaan sumber daya (biasanya melepaskannya). Sumber daya klasik yang akan dikelola adalah memori, dan inilah sebabnya semua kelas Perpustakaan Standar yang mengelola memori (misalnya, wadah STL yang melakukan manajemen memori dinamis) semuanya menyatakan "tiga besar": operasi penyalinan dan destruktor.
Konsekuensi dari Aturan Tiga adalah bahwa keberadaan destruktor yang dideklarasikan oleh pengguna menunjukkan bahwa salinan bijaksana anggota sederhana tidak mungkin sesuai untuk operasi penyalinan di kelas. Itu, pada gilirannya, menunjukkan bahwa jika suatu kelas menyatakan destruktor, operasi penyalinan mungkin tidak boleh dibuat secara otomatis, karena mereka tidak akan melakukan hal yang benar. Pada saat C ++ 98 diadopsi, signifikansi dari garis penalaran ini tidak sepenuhnya dihargai, sehingga dalam C ++ 98, keberadaan pengguna yang dinyatakan destruktor tidak berdampak pada kesediaan kompiler untuk menghasilkan operasi penyalinan. Itu terus menjadi kasus di C ++ 11, tetapi hanya karena membatasi kondisi di mana operasi penyalinan dihasilkan akan memecah terlalu banyak kode warisan.
Bagaimana saya bisa mencegah objek saya disalin?
Menyatakan operator konstruktor & salin penyalinan sebagai penentu akses pribadi.
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);
}
Di C ++ 11 dan seterusnya, Anda juga dapat mendeklarasikan copy constructor & operator penugasan dihapus
class MemoryBlock
{
public:
MemoryBlock(const MemoryBlock& other) = delete
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other) =delete
};
int main()
{
MemoryBlock a;
MemoryBlock b(a);
}
Banyak jawaban yang ada sudah menyentuh copy constructor, operator penugasan, dan destructor. Namun, dalam posting C ++ 11, pengenalan langkah semantik dapat memperluas ini di luar 3.
Baru-baru ini Michael Claisse memberikan ceramah yang menyentuh topik ini: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class
Aturan tiga dalam C ++ adalah prinsip dasar desain dan pengembangan tiga persyaratan bahwa jika ada definisi yang jelas dalam salah satu fungsi anggota berikut, maka programmer harus mendefinisikan dua fungsi anggota lainnya secara bersamaan. Yaitu, tiga fungsi anggota berikut sangat diperlukan: destruktor, copy constructor, operator penugasan salinan.
Salin konstruktor di C ++ adalah konstruktor khusus. Ini digunakan untuk membangun objek baru, yang merupakan objek baru yang setara dengan salinan objek yang sudah ada.
Operator penugasan salin adalah operator penugasan khusus yang biasanya digunakan untuk menentukan objek yang ada untuk orang lain dari jenis objek yang sama.
Ada beberapa contoh cepat:
// default constructor
My_Class a;
// copy constructor
My_Class b(a);
// copy constructor
My_Class c = a;
// copy assignment operator
b = a;
c++-faq
tag wiki sebelum Anda memilih untuk menutup .