Apa aturan tiga?

  • Apa artinya menyalin objek?
  • Apa itu copy constructor dan pernyataan penugasan copy?
  • Kapan saya harus menyatakannya sendiri?
  • Bagaimana saya bisa mencegah penyalinan objek saya?
1911
13 нояб. diatur oleh fredoverflow pada 13 November 2010-11-13 16:27 '10 pada 16:27 2010-11-13 16:27
@ 8 jawaban

Pendahuluan

C ++ memproses variabel tipe khusus dengan nilai semantik. Ini berarti bahwa objek secara implisit disalin dalam konteks yang berbeda, dan kita perlu memahami apa yang sebenarnya "menyalin objek".

Pertimbangkan 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) , ini disebut daftar inisiator anggota .)

Fungsi anggota khusus

Apa artinya menyalin person ? Fungsi main menunjukkan dua skrip salinan yang berbeda. Inisialisasi person b(a); dieksekusi oleh copy constructor. Tugasnya adalah membangun objek baru berdasarkan keadaan objek yang ada. Penugasan b = a dilakukan oleh operator penugasan salinan. Karyanya biasanya sedikit lebih sulit, karena objek target sudah dalam keadaan yang dapat diterima yang perlu ditangani.

Karena kami belum menyatakan konstruktor salinan, atau operator penugasan (atau destruktor), mereka secara implisit didefinisikan untuk kami. Kutipan dari standar:

Copy constructor [...] dan operator penugasan copy, [...] dan destructor adalah fungsi anggota khusus. [Catatan: Suatu implementasi secara implisit akan mendeklarasikan fungsi anggota ini untuk jenis kelas tertentu 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 elemen-elemennya:

Salin konstruktor yang terdefinisi secara implisit untuk kelas X non-unit melakukan salinan bertahap dari sub-objeknya. [n3126.pdf bagian 12.8 §16]

Operator penugasan salinan yang ditugaskan secara implisit untuk kelas X non-unit melakukan penugasan bertahap dari salinan sub-objeknya. [n3126.pdf bagian 12.8 §30]

Definisi tersirat

Fungsi anggota khusus yang didefinisikan secara implisit untuk person adalah sebagai berikut:

 // 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() { } 

Dalam hal ini, kami ingin menyalin apa yang kami inginkan: name dan age disalin, jadi kami mendapatkan objek person mandiri dan mandiri. Destroyer yang didefinisikan secara implisit selalu kosong. Ini juga bagus dalam hal ini, karena kami tidak mendapatkan sumber daya apa pun di konstruktor. Penghancur anggota secara implisit dipanggil setelah person menyelesaikan penghancur:

Setelah mengeksekusi tubuh destruktor dan menghancurkan objek otomatis yang dialokasikan dalam tubuh, destruktor kelas X menyebabkan destruktor untuk anggota >

Manajemen sumber daya

Jadi, kapan kita harus menyatakan fungsi anggota khusus ini secara eksplisit? Ketika kelas kita mengelola sumber daya, yaitu, ketika objek kelas bertanggung jawab atas sumber daya ini. Ini biasanya berarti bahwa sumber daya diperoleh dalam konstruktor (atau diteruskan ke konstruktor) dan dirilis pada destruktor.

Mari kita kembali ke standar awal C ++. Tidak ada yang namanya std::string , dan programmer menyukai pointer. Kelas person mungkin terlihat 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 dengan menyalin objek secara default berarti menyalin elemen-elemennya, tetapi menyalin name anggota hanya menyalin pointer, bukan array karakter yang ditunjuknya! Ini memiliki beberapa efek yang tidak menyenangkan:

  • Perubahan melalui a dapat diamati melalui b .
  • Segera setelah b dihancurkan, a.name adalah pointer yang a.name .
  • Jika a hancur, menghapus pointer yang rusak memberikan perilaku yang tidak terdefinisi .
  • Karena tugas tidak memperhitungkan name ditentukan sebelum penugasan, cepat atau lambat Anda akan mendapatkan kebocoran memori di mana-mana.

Definisi Eksplisit

Karena penyalinan agar tidak memiliki efek yang diinginkan, kita harus secara eksplisit mendefinisikan konstruktor salin dan operator penugasan salinan 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 !=  { 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 menurunkan status lama sebelum menetapkan name untuk mencegah kebocoran memori. Selain itu, kita harus melindungi formulir x = x dari kesadaran diri. Tanpa pemeriksaan ini, delete[] name akan menghapus array yang berisi string sumber, karena ketika Anda menulis x = x , baik this->name dan that.name berisi pointer yang sama.

Keamanan Pengecualian

Sayangnya, solusi ini akan gagal jika new char[...] melempar pengecualian karena kehabisan memori. Salah satu solusi yang mungkin adalah dengan memperkenalkan variabel lokal dan mengubah urutan operator:

 // 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 memberikan penugasan diri tanpa verifikasi eksplisit. Solusi yang bahkan lebih dapat diandalkan untuk masalah ini adalah ungkapan menyalin dan bertukar , tetapi saya tidak akan masuk ke rincian pengecualian keamanan. Saya hanya menyebutkan pengecualian untuk melakukan hal berikut: Menulis kelas yang mengelola sumber daya sulit.

Sumber daya yang tidak dapat disalin

Beberapa sumber daya tidak dapat atau tidak boleh disalin, misalnya, deskriptor file atau mutex. Dalam hal ini, cukup nyatakan konstruktor salin dan operator tujuan salin sebagai private tanpa menentukan definisi:

 private: person(const person that); person operator=(const person that); 

Atau, Anda dapat mewarisi dari boost::noncopyable atau mendeklarasikannya sebagai dihapus (C ++ 0x):

 person(const person that) = delete; person operator=(const person that) = delete; 

Aturan tiga

Terkadang Anda perlu mengimplementasikan kelas yang mengelola sumber daya. (Jangan pernah mengelola banyak sumber daya di kelas yang sama, itu hanya akan menimbulkan rasa sakit.) Dalam hal ini, ingat aturan tiga :

Jika Anda perlu mendeklarasikan destruktor secara eksplisit, menyalin sendiri konstruktor atau menyalin pernyataan tugas, Anda mungkin perlu secara eksplisit mendeklarasikan ketiganya.

(Sayangnya, "aturan" ini tidak diterapkan oleh standar C ++ atau kompiler, yang saya ketahui.)

Papan tulis

Dalam kebanyakan kasus, Anda tidak perlu mengelola sumber daya sendiri, karena kelas yang ada, seperti std::string , sudah melakukan ini untuk Anda. Bandingkan saja kode sederhana dengan std::string member dengan alternatif yang membingungkan dan rawan menggunakan char* , dan Anda harus yakin. Selama Anda tinggal jauh dari pointer mentah, aturan tiga hampir tidak berlaku untuk kode Anda sendiri.

1561
13 нояб. jawabannya diberikan oleh fredoverflow 13 Nov 2010-11-13 16:27 '10 pada 16:27 2010-11-13 16:27

Aturan tiga adalah aturan praktis untuk C ++, secara umum

Jika kelas Anda membutuhkan

  • copy constructor ,
  • operator penugasan ,
  • atau destruktor ,

ditentukan secara eksplisit, maka mungkin ketiganya akan diperlukan.

Alasan untuk ini adalah bahwa ketiganya biasanya digunakan untuk mengelola sumber daya, dan jika kelas Anda mengontrol sumber daya, biasanya perlu mengelola penyalinan serta membebaskannya.

Jika tidak ada semantik yang baik untuk menyalin sumber daya yang dikelola kelas Anda, maka pertimbangkan untuk melarang penyalinan dengan menyatakan (tidak mendefinisikan ) operator penyalin dan penugasan sebagai private .

(Perhatikan bahwa versi baru standar C ++ yang akan datang (yaitu C ++ 11) menambahkan >

467
13 нояб. Jawabannya diberikan oleh sbi pada 13 November. 2010-11-13 17:22 '10 pada 17:22 2010-11-13 17:22

Hukum tiga besar adalah seperti yang disebutkan di atas.

Contoh mudah, dalam bahasa Inggris, dari masalah yang ia selesaikan:

Destructor Khusus

Anda mengalokasikan memori di konstruktor Anda, dan Anda perlu menulis destruktor untuk menghapusnya. Kalau tidak, akan terjadi kebocoran memori.

Anda mungkin berpikir ini adalah pekerjaan.

Masalahnya adalah jika salinan dibuat dari objek Anda, maka salinan itu akan menunjuk ke memori yang sama dengan objek aslinya.

Begitu salah satu dari mereka menghapus memori di destruktornya, yang lain akan memiliki pointer ke memori yang tidak valid (ini disebut pointer menggantung), ketika ia mencoba menggunakannya, semuanya akan terlihat berbulu.

Akibatnya, Anda menulis copy constructor sehingga mengalokasikan objek baru untuk menghancurkan fragmen memori mereka sendiri.

Operator penugasan dan copy constructor

Anda mengalokasikan memori di konstruktor Anda ke pointer ke anggota kelas Anda. Saat Anda menyalin objek kelas ini, pernyataan penugasan default dan konstruktor salin akan menyalin nilai elemen penunjuk ini ke objek baru.

Ini berarti bahwa objek baru dan objek lama akan menunjuk ke bagian yang sama dari memori, jadi ketika Anda mengubahnya di satu objek, itu akan diubah untuk objek objek lain. Jika satu objek menghapus memori ini, yang lain akan terus mencoba menggunakannya - eek.

Untuk mengatasi masalah ini, Anda menulis versi Anda sendiri dari copy constructor dan operator penugasan. Versi Anda mengalokasikan memori terpisah ke objek baru dan menyalin nilai yang ditunjuk oleh pointer pertama, bukan alamatnya.

143
14 мая '12 в 17:22 2012-05-14 17:22 membalas Stefan pada 14 Mei '12 pukul 17:22 2012-05-14 17:22

Pada dasarnya, jika Anda memiliki destruktor (dan bukan destruktor default), ini berarti bahwa kelas yang Anda tentukan memiliki alokasi memori. Misalkan suatu 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, pernyataan penetapan standar akan dieksekusi, tetapi jika memiliki beberapa elemen penunjuk dan objek yang tidak memiliki pernyataan penetapan, hasilnya akan tidak dapat diprediksi. Oleh karena itu, kita dapat mengatakan bahwa jika ada sesuatu untuk dihapus di destruktor kelas, kita mungkin memerlukan operator penyalinan yang dalam, yang berarti bahwa kita harus menyediakan penyalin salinan dan operator penugasan.

39
31 дек. jawaban diberikan fatma.ekici 31 Des. 2012-12-31 22:29 '13 pada 22:29 2012-12-31 22:29

Apa artinya menyalin objek? Ada beberapa cara untuk menyalin objek - beri tahu kami tentang dua jenis yang kemungkinan besar Anda rujuk: salinan dalam dan salinan dangkal.

Karena kita menggunakan bahasa berorientasi objek (atau setidaknya mengandaikannya), katakanlah Anda memiliki memori yang berdedikasi. 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 telah kita tentukan yang 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 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 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  // Copy Constructor { this.sPrintColor = other.sPrintColor; this.sModel = other.sModel; this.sMake = other.sMake; } public Car  =(const Car  // Assignment Operator { if(this !=  { 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 dua objek dalam 2 set memori penuh.

 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. Biarkan dikatakan bahwa car2 diprogram secara salah atau dengan sengaja dimaksudkan untuk bertukar memori aktual dari mana car1 dibuat. (Ini biasanya kesalahan, dan di kelas biasanya selimut yang disebutkan.) Bayangkan setiap saat ketika Anda bertanya tentang car2, Anda benar-benar memutuskan penunjuk ke ruang memori car1 ... yang kurang lebih sekecil itu ada salinannya.

 //Shallow copy example //Assume we're in C++ because it standard behavior is to shallow copy objects if you do not have a constructor written for an operation. //Now let 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; car1.changePaint("red"); 

Karena itu, apa pun bahasa yang Anda tulis, berhati-hatilah dengan apa yang Anda maksud saat menyalin objek, karena sebagian besar waktu Anda ingin mendapatkan salinan yang dalam.

Apa itu copy constructor dan pernyataan penugasan copy? Saya sudah menggunakannya di atas. Pembuat salinan dipanggil saat memasukkan kode seperti Car car2 = car1; Intinya, jika Anda mendeklarasikan variabel dan menetapkannya dalam satu baris, maka ketika Anda memanggil copy constructor. Operator penugasan adalah apa yang terjadi ketika Anda menggunakan tanda sama dengan - car2 = car1; . Notifikasi car2 tidak diumumkan dalam pernyataan yang sama. Dua potong kode yang Anda tulis untuk operasi ini kemungkinan besar sangat mirip. Bahkan, dalam pola desain yang khas, ada fungsi lain yang Anda panggil untuk mengatur semuanya, setelah Anda puas dengan salinan / penugasan awal, sah - jika Anda melihat kode yang saya tulis, fungsinya hampir identik.

Kapan saya harus menyatakannya sendiri? Jika Anda tidak menulis kode yang perlu dibagikan atau diproduksi dengan cara apa pun, Anda hanya perlu mendeklarasikannya saat Anda membutuhkannya. Anda perlu tahu apa yang dilakukan bahasa pemrograman Anda, jika Anda memutuskan untuk menggunakannya "secara kebetulan" dan belum melakukannya - yaitu, Anda mendapatkan kompiler default. Sebagai contoh, saya jarang menggunakan copy constructor, tetapi operator penugasan utama sangat umum. Apakah Anda tahu bahwa Anda dapat mendefinisikan u>

Bagaimana saya bisa mencegah penyalinan objek saya? Awal yang masuk akal adalah menimpa semua cara di mana Anda dapat mengalokasikan memori untuk objek Anda menggunakan fungsi pribadi. Jika Anda benar-benar tidak ingin orang menyalinnya, Anda bisa membuatnya tersedia untuk umum dan memperingatkan programmer dengan melemparkan pengecualian dan juga tidak menyalin objek.

31
17 окт. jawabannya diberikan user1701047 17 Oktober. 2012-10-17 19:37 '12 pada 19:37 2012-10-17 19:37

Kapan saya harus menyatakannya sendiri?

Aturan tiga menyatakan bahwa jika Anda menyatakan salah

  • salin pernyataan penugasan konstruktor
  • destruktor

maka Anda harus mendeklarasikan ketiganya. Dari kenyataan bahwa kebutuhan untuk menggunakan makna operasi copy hampir selalu mengikuti dari kelas yang melakukan semacam manajemen sumber daya, dan bahwa hampir selalu tersirat bahwa

  • manajemen sumber daya apa pun dilakukan dalam satu operasi penyalinan, mungkin diperlukan untuk melakukan operasi penyalinan di yang lain, dan

  • destruktor kelas juga akan berpartisipasi dalam manajemen sumber daya (biasanya membebaskannya). Sumber daya manajemen klasik adalah memori, dan itulah sebabnya semua kelas pustaka standar yang (misalnya, wadah STL yang melakukan manajemen memori dinamis) semuanya menyatakan "tiga besar": operasi penyalinan dan penghancur.

Konsekuensi dari aturan tiga adalah bahwa keberadaan destruktor yang dideklarasikan pengguna menunjukkan bahwa salinan sederhana dari anggota tidak cocok untuk operasi penyalinan di kelas. Ini, pada gilirannya, menunjukkan bahwa jika sebuah kelas mendeklarasikan destruktor, operasi penyalinan mungkin tidak boleh dibuat secara otomatis, karena mereka tidak akan melakukan hal yang benar. Pada saat C ++ 98 diadopsi, nilai dari garis penalaran ini tidak sepenuhnya dihargai, oleh karena itu dalam C ++ 98 keberadaan destruktor yang dinyatakan pengguna tidak mempengaruhi kesiapan kompiler untuk menghasilkan operasi penyalinan. Ini masih terjadi di C ++ 11, tetapi hanya karena membatasi kondisi di mana operasi penyalinan dilakukan akan memecah terlalu banyak kode usang.

Bagaimana saya bisa mencegah penyalinan objek saya?

Menyatakan konstruktor salinan dan pernyataan penugasan salinan 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, Anda juga dapat menyatakan bahwa konstruktor salin dan 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); } 
21
12 янв. Jawabannya diberikan oleh Ajay yadav 12 Jan 2016-01-12 12:54 '16 pada 12:54 2016-01-12 12:54

Banyak jawaban yang ada sudah menyangkut copy constructor, operator penugasan, dan destructor. Namun, dalam posting C ++ 11, pengenalan semantik >

Baru-baru ini Michael Clyce berbicara tentang topik ini: http://channel9.msdn.com/events/CPP/C-PP-Con-2014/The-Canonical-Class

12
07 янв. jawabannya diberikan wei 07 januari. 2015-01-07 08:38 '15 pada 8:38 AM 2015-01-07 08:38