Pertanyaan Apa itu Aturan Tiga?


  • Apa yang terjadi menyalin objek berarti?
  • Apa itu salin konstruktor dan salin operator penugasan?
  • Kapan saya harus menyatakannya sendiri?
  • Bagaimana saya bisa mencegah objek saya disalin?

1841
2017-11-13 13:27


asal


Jawaban:


pengantar

C + + memperlakukan variabel tipe yang ditentukan pengguna dengan semantik nilai. Ini berarti bahwa objek secara implisit disalin dalam berbagai konteks, dan kita harus memahami apa yang dimaksud "menyalin objek" sebenarnya.

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 disebut a daftar penginisialisasi anggota.)

Fungsi anggota khusus

Apa artinya menyalin person obyek? Itu main fungsi menunjukkan dua skenario menyalin berbeda. Inisialisasi person b(a); dilakukan oleh salin konstruktor. Tugasnya adalah membangun objek segar berdasarkan keadaan objek yang ada. Tugas b = a dilakukan oleh salin operator penugasan. Pekerjaannya umumnya sedikit lebih rumit, karena objek target sudah dalam beberapa keadaan valid yang perlu ditangani.

Karena kami tidak menyatakan baik konstruktor menyalin maupun operator penugasan (atau perusak), ini secara implisit didefinisikan untuk kita. Kutipan dari standar:

[...] copy constructor dan copy assignment operator, [...] dan destructor adalah fungsi anggota khusus.   [ Catatan: Implementasi secara implisit akan mendeklarasikan fungsi-fungsi anggota ini   untuk beberapa jenis kelas ketika program tidak secara eksplisit menyatakannya.   Implementasi secara implisit akan menentukan mereka jika mereka digunakan. [...] catatan akhir ]   [n3126.pdf bagian 12 §1]

Secara default, menyalin objek berarti menyalin anggotanya:

Copy constructor implisit yang didefinisikan untuk kelas non-union X melakukan copy dari subobjectnya.   [n3126.pdf bagian 12.8 §16]

Operator penugasan duplikat yang didefinisikan secara implisit untuk kelas non-serikat pekerja X melakukan penugasan copy yang berorientasi anggota   dari subobjectnya.   [n3126.pdf bagian 12.8 §30]

Definisi implisit

Fungsi anggota khusus yang implisit didefinisikan 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()
{
}

Fotokopi dari keanggotaan adalah persis seperti yang kita inginkan dalam kasus ini: name dan age disalin, jadi kita mendapatkan mandiri, mandiri person obyek. Perusak yang implisit didefinisikan selalu kosong. Ini juga baik dalam hal ini karena kami tidak memperoleh sumber daya apa pun dalam konstruktor. Perusak para anggota secara implisit dipanggil setelah person destructor telah selesai:

Setelah mengeksekusi tubuh destruktor dan menghancurkan benda otomatis yang dialokasikan di dalam tubuh,   destruktor untuk kelas X memanggil destruktor untuk anggota [...] X langsung   [n3126.pdf 12.4 §6]

Mengelola sumber daya

Jadi kapan kita harus mendeklarasikan fungsi anggota khusus itu secara eksplisit? Ketika kelas kita mengelola sumber daya, itu adalah, ketika sebuah objek dari kelas itu bertanggung jawab untuk sumber daya itu. Itu biasanya berarti sumber daya diperoleh di konstruktor (atau diteruskan ke konstruktor) dan dilepaskan di destructor.

Mari kita kembali pada waktunya untuk C ++ pra-standar. Tidak ada hal seperti itu std::string, dan programmer jatuh cinta dengan pointer. Itu 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 saat ini, orang masih menulis kelas dengan gaya ini dan mendapat masalah: "Saya mendorong seseorang ke dalam vektor dan sekarang saya mendapatkan kesalahan memori gila!" Ingat bahwa secara default, menyalin objek berarti menyalin anggotanya, tetapi menyalin name member hanya menyalin sebuah pointer, tidak Array karakter itu menunjuk ke! Ini memiliki beberapa efek yang tidak menyenangkan:

  1. Berubah melalui a dapat diamati melalui b.
  2. Sekali b dihancurkan, a.name adalah penunjuk yang menjuntai.
  3. Jika a dihancurkan, menghapus hasil penunjuk yang menjuntai perilaku tidak terdefinisi.
  4. Karena penugasan tidak memperhitungkan apa name menunjuk sebelum penugasan, cepat atau lambat Anda akan mendapatkan kebocoran memori di semua tempat.

Definisi eksplisit

Karena kopian memberwise tidak memiliki efek yang diinginkan, kita harus mendefinisikan konstruktor copy dan operator penugasan salinan secara eksplisit untuk membuat salinan mendalam dari array 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 meruntuhkan keadaan lama sebelum menugaskan name untuk mencegah kebocoran memori. Juga, kita harus melindungi terhadap penetapan diri dari formulir x = x. Tanpa cek itu, delete[] name akan menghapus larik yang berisi sumber tali, karena ketika kamu menulis x = x, keduanya this->name dan that.name mengandung penunjuk 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 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 tugas sendiri 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 itu sulit.

Sumber daya yang tidak dapat dibeli

Beberapa sumber daya tidak dapat atau tidak boleh disalin, seperti menangani file atau mutex. Dalam hal ini, cukup nyatakan copy constructor dan copy assignment operator sebagai private tanpa memberikan definisi:

private:

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

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

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

Aturan tiga

Terkadang Anda perlu menerapkan kelas yang mengelola sumber daya. (Jangan pernah mengelola beberapa sumber daya dalam satu kelas, ini hanya akan menyebabkan rasa sakit.) Dalam hal ini, ingatlah aturan tiga:

Jika Anda perlu mendeklarasikan secara eksplisit destruktor,   salin konstruktor atau salin operator penugasan sendiri,   Anda mungkin perlu secara eksplisit menyatakan ketiganya.

(Sayangnya, "aturan" ini tidak diberlakukan oleh standar C ++ atau kompilator yang saya sadari.)

Nasihat

Seringkali, Anda tidak perlu mengelola sumber daya sendiri, karena kelas yang ada seperti std::string sudah melakukannya untuk Anda. Cukup bandingkan kode sederhana menggunakan std::string anggota ke alternatif yang rumit dan rawan kesalahan menggunakan char* dan Anda harus diyakinkan. Selama Anda tinggal jauh dari anggota penunjuk mentah, aturan ketiganya tidak mungkin menyangkut kode Anda sendiri.


1512
2017-11-13 13:27



Itu Aturan Tiga adalah aturan praktis untuk C ++, pada dasarnya mengatakan

Jika kelas Anda membutuhkan salah satunya

  • Sebuah salin konstruktor,
  • sebuah operator penugasan,
  • atau a perusak,

didefinisikan secara jelas, maka kemungkinan perlu 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 dan juga membebaskan.

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

(Perhatikan bahwa versi baru standar C ++ yang akan datang (yang merupakan C ++ 11) menambahkan semantik gerakan ke C ++, yang kemungkinan akan mengubah Rule of Three. Namun, saya tahu terlalu sedikit tentang ini untuk menulis bagian C ++ 11 tentang Rule of Three.)


450
2017-11-13 14:22



Hukum dari tiga besar adalah seperti yang ditentukan di atas.

Contoh yang mudah, dalam bahasa Inggris sederhana, dari jenis masalah yang dipecahkannya:

Perusak non-standar

Anda mengalokasikan memori di konstruktor Anda dan jadi Anda perlu menulis destructor untuk menghapusnya. Kalau tidak, Anda akan menyebabkan kebocoran memori.

Anda mungkin berpikir bahwa ini adalah pekerjaan yang dilakukan.

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

Sekali, salah satu dari ini menghapus memori di destructor, yang lain akan memiliki pointer ke memori yang tidak valid (ini disebut pointer yang menggantung) ketika mencoba untuk menggunakannya hal-hal akan menjadi berbulu.

Oleh karena itu, Anda menulis konstruktor salin sehingga mengalokasikan objek-objek baru potongan-potongan memori mereka sendiri untuk dihancurkan.

Operator penugasan dan salin konstruktor

Anda mengalokasikan memori di konstruktor Anda ke pointer anggota kelas Anda. Ketika Anda menyalin objek dari kelas ini operator penugasan default dan salin konstruktor akan menyalin nilai penunjuk anggota ini ke objek baru.

Ini berarti objek baru dan objek lama akan menunjuk pada bagian memori yang sama sehingga ketika Anda mengubahnya dalam satu objek, objek itu juga akan diubah untuk objek lainnya. Jika satu objek menghapus memori ini, yang lain akan terus berusaha menggunakannya - eek.

Untuk mengatasi ini Anda menulis versi Anda sendiri dari konstruktor penyalinan dan operator penugasan. Versi Anda mengalokasikan memori terpisah ke objek baru dan menyalin seluruh nilai yang penunjuk pertama menunjuk ke daripada alamatnya.


134
2018-05-14 14:22



Pada dasarnya jika Anda memiliki destruktor (bukan destruktor default) itu berarti bahwa kelas yang Anda tentukan memiliki beberapa alokasi memori. Misalkan kelas tersebut 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 berfungsi tetapi jika memiliki beberapa anggota penunjuk dan objek yang tidak memiliki operator penugasan, hasilnya tidak dapat diprediksi. Oleh karena itu kita dapat mengatakan bahwa jika ada sesuatu yang dihapus dalam destruktor kelas, kita mungkin memerlukan operator copy yang dalam yang berarti kita harus menyediakan konstruktor copy dan operator penugasan.


37
2017-12-31 19:29



Apa artinya menyalin objek? Ada beberapa cara Anda dapat menyalin objek - mari kita bicara tentang 2 jenis yang kemungkinan besar Anda maksud - salinan dalam dan salinan dangkal.

Karena kita berada dalam bahasa berorientasi objek (atau setidaknya berasumsi demikian), katakanlah Anda memiliki memori yang dialokasikan. Karena ini adalah bahasa OO, kita dapat dengan mudah merujuk ke potongan memori yang kita alokasikan karena mereka biasanya variabel primitif (int, chars, byte) atau kelas yang kita definisikan 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'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 suatu objek dan kemudian membuat salinan objek yang benar-benar terpisah ... kita berakhir dengan 2 objek dalam 2 set lengkap memori.

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 lakukan sesuatu yang aneh. Katakanlah car2 salah diprogram atau sengaja dimaksudkan untuk berbagi memori yang sebenarnya dari car1 terbuat. (Ini biasanya kesalahan untuk melakukan ini dan di kelas biasanya selimut yang dibahas di bawah.) Berpura-pura bahwa kapan saja Anda bertanya tentang car2, Anda benar-benar menyelesaikan penunjuk ke ruang memori car1 ... itu lebih atau kurang apa salinan dangkal aku s.

//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 saat menyalin objek karena sebagian besar waktu Anda menginginkan salinan yang mendalam.

Apa saja konstruktor salin dan operator penugasan salinan? Saya sudah menggunakan mereka di atas. Copy constructor dipanggil ketika Anda mengetikkan kode seperti Car car2 = car1;  Pada dasarnya jika Anda mendeklarasikan variabel dan menetapkannya dalam satu baris, saat itulah konstruktor salin disebut. Operator penugasan adalah apa yang terjadi ketika Anda menggunakan tanda yang sama--car2 = car1;. Melihat car2 tidak dideklarasikan 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 begitu Anda puas bahwa salinan / tugas awal adalah sah - jika Anda melihat kode tulisan tangan yang saya tulis, fungsinya hampir sama.

Kapan saya harus menyatakannya sendiri? Jika Anda tidak menulis kode yang akan dibagikan atau untuk produksi dalam beberapa cara, Anda benar-benar hanya perlu mendeklarasikannya ketika Anda membutuhkannya. Anda perlu menyadari apa yang dilakukan bahasa program Anda jika Anda memilih untuk menggunakannya 'secara tidak sengaja' dan tidak membuat satu - i.e. Anda mendapatkan default compiler. Saya jarang menggunakan konstruktor menyalin misalnya, tetapi penugasan operator penugasan sangat umum. Tahukah Anda bahwa Anda dapat mengesampingkan apa yang ditambahkan, dikurangi, dll. Juga?

Bagaimana saya bisa mencegah objek saya disalin? Menimpa semua cara Anda diizinkan mengalokasikan memori untuk objek Anda dengan fungsi pribadi adalah permulaan yang wajar. Jika Anda benar-benar tidak ingin orang-orang menyalinnya, Anda dapat membuatnya menjadi publik dan memperingatkan programmer dengan melemparkan pengecualian dan juga tidak menyalin objek.


27
2017-10-17 16:37



Kapan saya harus menyatakannya sendiri?

Aturan Tiga menyatakan bahwa jika Anda menyatakan salah satu dari

  1. salin konstruktor
  2. salin operator penugasan
  3. perusak

maka Anda harus menyatakan ketiganya. Itu tumbuh dari pengamatan bahwa kebutuhan untuk mengambil alih makna operasi copy hampir selalu berasal dari kelas yang melakukan beberapa jenis manajemen sumber daya, dan itu hampir selalu tersirat bahwa

  • manajemen sumber daya apa pun yang sedang dilakukan dalam satu operasi salinan mungkin perlu dilakukan dalam operasi copy lain dan

  • destructor kelas juga akan berpartisipasi dalam manajemen sumber daya (biasanya melepaskannya). Sumber daya klasik yang dikelola adalah memori, dan inilah mengapa semua kelas Standard Library itu mengelola memori (misalnya, kontainer STL yang menjalankan manajemen memori dinamis) semua menyatakan "tiga besar": baik operasi salin dan destruktor.

Konsekuensi dari Aturan Tiga adalah bahwa kehadiran destruktor yang dinyatakan pengguna menunjukkan bahwa salinan bijaksana anggota sederhana tidak mungkin sesuai untuk operasi penyalinan di kelas. Itu, pada gilirannya, menunjukkan bahwa jika sebuah kelas menyatakan destruktor, operasi salinan mungkin tidak seharusnya dibuat secara otomatis, karena mereka tidak akan melakukan hal yang benar. Pada saat C ++ 98 diadopsi, signifikansi garis penalaran ini tidak sepenuhnya dihargai, sehingga dalam C + + 98, keberadaan pengguna yang dinyatakan merusak tidak berdampak pada keinginan para penyusun untuk menghasilkan operasi salinan. Itu terus menjadi kasus di C ++ 11, tetapi hanya karena membatasi kondisi di mana operasi salinan dihasilkan akan melanggar terlalu banyak kode warisan.

Bagaimana saya bisa mencegah objek saya disalin?

Nyatakan copy constructor & copy assignment operator sebagai specifier 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);
}

Dalam C ++ 11 dan seterusnya Anda juga dapat menyatakan copy constructor & assignment operator 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);
}

19
2018-01-12 09:54



Banyak dari jawaban yang ada sudah menyentuh konstruktor menyalin, operator penugasan dan destruktor. Namun, dalam posting C ++ 11, pengenalan semantik gerakan dapat memperluas ini melampaui 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


9
2018-01-07 05:38



Rule of three in C ++ adalah prinsip dasar dari desain dan pengembangan tiga persyaratan bahwa jika ada definisi yang jelas dalam salah satu fungsi anggota berikut, maka programmer harus mendefinisikan dua fungsi anggota lain bersama-sama. Yaitu tiga fungsi anggota berikut ini sangat diperlukan: destructor, copy constructor, copy assignment operator.

Salin konstruktor di C ++ adalah konstruktor khusus. Ini digunakan untuk membangun objek baru, yang merupakan objek baru yang setara dengan salinan objek yang ada.

Copy assignment operator adalah operator penugasan khusus yang biasanya digunakan untuk menentukan objek yang ada untuk orang lain dengan jenis objek yang sama.

Ada contoh cepat:

// default constructor
My_Class a;

// copy constructor
My_Class b(a);

// copy constructor
My_Class c = a;

// copy assignment operator
b = a;

5
2017-08-12 04:27