Saya tahu bahwa compiler C ++ membuat salinan konstruktor untuk kelas. Dalam hal apa kita harus menulis konstruktor salinan yang ditentukan pengguna? Bisakah Anda memberikan beberapa contoh?
c++
copy-constructor
penguru
sumber
sumber
Jawaban:
Konstruktor salinan yang dihasilkan oleh kompilator melakukan penyalinan berdasarkan anggota. Terkadang itu tidak cukup. Sebagai contoh:
class Class { public: Class( const char* str ); ~Class(); private: char* stored; }; Class::Class( const char* str ) { stored = new char[srtlen( str ) + 1 ]; strcpy( stored, str ); } Class::~Class() { delete[] stored; }
dalam hal ini penyalinan anggota yang bijaksana
stored
tidak akan menduplikasi buffer (hanya penunjuk yang akan disalin), jadi yang pertama akan dihancurkan, salinan yang berbagi buffer akandelete[]
berhasil dipanggil dan yang kedua akan mengalami perilaku tidak terdefinisi. Anda perlu menyalin konstruktor salinan dalam (dan operator tugas juga).Class::Class( const Class& another ) { stored = new char[strlen(another.stored) + 1]; strcpy( stored, another.stored ); } void Class::operator = ( const Class& another ) { char* temp = new char[strlen(another.stored) + 1]; strcpy( temp, another.stored); delete[] stored; stored = temp; }
sumber
delete stored[];
dan saya percaya seharusnya begitudelete [] stored;
std::string
. Ide umumnya adalah bahwa hanya kelas utilitas yang mengelola sumber daya yang perlu membebani Tiga Besar, dan semua kelas lain sebaiknya menggunakan kelas utilitas tersebut saja, sehingga tidak perlu mendefinisikan salah satu dari Tiga Besar.Saya sedikit kesal karena aturan
Rule of Five
itu tidak dikutip.Aturan ini sangat sederhana:
Tetapi ada pedoman yang lebih umum yang harus Anda ikuti, yang berasal dari kebutuhan untuk menulis kode pengecualian-aman:
Berikut
@sharptooth
's kode masih (kebanyakan) baik-baik saja, namun jika ia menambahkan atribut kedua untuk kelasnya itu tidak akan. Pertimbangkan kelas berikut:class Erroneous { public: Erroneous(); // ... others private: Foo* mFoo; Bar* mBar; }; Erroneous::Erroneous(): mFoo(new Foo()), mBar(new Bar()) {}
Apa yang terjadi jika
new Bar
melempar? Bagaimana Anda menghapus objek yang ditunjukmFoo
? Ada solusi (coba tingkat fungsi / tangkap ...), mereka tidak skala.Cara yang tepat untuk menghadapi situasi ini adalah dengan menggunakan kelas yang tepat daripada petunjuk mentah.
class Righteous { public: private: std::unique_ptr<Foo> mFoo; std::unique_ptr<Bar> mBar; };
Dengan implementasi konstruktor yang sama (atau sebenarnya, menggunakan
make_unique
), sekarang saya memiliki keamanan pengecualian gratis !!! Bukankah itu menarik? Dan yang terbaik dari semuanya, saya tidak perlu lagi khawatir tentang destruktor yang tepat! Saya memang perlu menulis sendiriCopy Constructor
danAssignment Operator
meskipun, karenaunique_ptr
tidak mendefinisikan operasi ini ... tetapi tidak masalah di sini;)Dan karena itu,
sharptooth
kelas meninjau kembali:class Class { public: Class(char const* str): mData(str) {} private: std::string mData; };
Saya tidak tahu tentang Anda, tetapi saya menemukan milik saya lebih mudah;)
sumber
Saya dapat mengingat dari praktik saya dan memikirkan kasus-kasus berikut ketika seseorang harus berurusan dengan secara eksplisit menyatakan / mendefinisikan konstruktor salinan. Saya telah mengelompokkan kasus menjadi dua kategori
Ketepatan / Semantik
Saya menempatkan di bagian ini kasus-kasus di mana mendeklarasikan / mendefinisikan konstruktor salinan diperlukan untuk pengoperasian program yang benar menggunakan jenis itu.
Setelah membaca seluruh bagian ini, Anda akan mempelajari tentang beberapa kendala dalam mengizinkan kompilator untuk membuat konstruktor salinannya sendiri. Oleh karena itu, seperti yang dicatat oleh seand dalam jawabannya , selalu aman untuk menonaktifkan kemampuan copyability untuk kelas baru dan sengaja mengaktifkannya nanti saat benar-benar dibutuhkan.
Cara membuat kelas tidak dapat disalin di C ++ 03
Deklarasikan copy-konstruktor pribadi dan jangan sediakan implementasinya (sehingga build gagal pada tahap penautan meskipun objek jenis itu disalin dalam cakupan kelas sendiri atau oleh temannya).
Cara membuat kelas tidak dapat disalin di C ++ 11 atau yang lebih baru
Deklarasikan konstruktor salinan dengan
=delete
di akhir.Shallow vs Deep Copy
Ini adalah kasus yang paling dipahami dan sebenarnya satu-satunya yang disebutkan dalam jawaban lain. shaprtooth telah menutupinya dengan cukup baik. Saya hanya ingin menambahkan bahwa menyalin sumber daya yang harus dimiliki secara eksklusif oleh objek dapat berlaku untuk semua jenis sumber daya, di mana memori yang dialokasikan secara dinamis hanyalah satu jenis. Jika perlu, menyalin objek secara mendalam juga mungkin diperlukan
Objek yang mendaftar sendiri
Pertimbangkan kelas di mana semua objek - tidak peduli bagaimana mereka telah dibangun - HARUS terdaftar. Beberapa contoh:
Contoh paling sederhana: mempertahankan jumlah total objek yang ada saat ini. Pendaftaran objek hanyalah tentang menaikkan penghitung statis.
Contoh yang lebih kompleks adalah memiliki registri tunggal, di mana referensi ke semua objek yang ada dari jenis tersebut disimpan (sehingga pemberitahuan dapat dikirimkan ke semuanya).
Pointer cerdas yang dihitung referensi dapat dianggap sebagai kasus khusus dalam kategori ini: penunjuk baru "mendaftar" sendiri dengan sumber daya bersama daripada di registri global.
Operasi pendaftaran sendiri seperti itu harus dilakukan oleh SETIAP konstruktor jenis dan konstruktor salinan tidak terkecuali.
Objek dengan referensi silang internal
Beberapa objek mungkin memiliki struktur internal non-trivial dengan referensi silang langsung antara sub-objek yang berbeda (pada kenyataannya, hanya satu referensi silang internal yang cukup untuk memicu kasus ini). Konstruktor salinan yang disediakan kompiler akan memutus asosiasi intra-objek internal , mengubahnya menjadi asosiasi antar-objek .
Sebuah contoh:
struct MarriedMan; struct MarriedWoman; struct MarriedMan { // ... MarriedWoman* wife; // association }; struct MarriedWoman { // ... MarriedMan* husband; // association }; struct MarriedCouple { MarriedWoman wife; // aggregation MarriedMan husband; // aggregation MarriedCouple() { wife.husband = &husband; husband.wife = &wife; } }; MarriedCouple couple1; // couple1.wife and couple1.husband are spouses MarriedCouple couple2(couple1); // Are couple2.wife and couple2.husband indeed spouses? // Why does couple2.wife say that she is married to couple1.husband? // Why does couple2.husband say that he is married to couple1.wife?
Hanya objek yang memenuhi kriteria tertentu yang diperbolehkan untuk disalin
Mungkin ada kelas di mana objek aman untuk disalin sementara dalam beberapa kondisi (misalnya default-built-state) dan tidak aman untuk menyalin sebaliknya. Jika kita ingin mengizinkan penyalinan objek safe-to-copy, maka - jika memprogram secara defensif - kita memerlukan pemeriksaan run-time di konstruktor salinan yang ditentukan pengguna.
Sub-objek yang tidak dapat disalin
Terkadang, kelas yang harus dapat disalin menggabungkan sub-objek yang tidak dapat disalin. Biasanya, hal ini terjadi untuk objek dengan status non-observasi (kasus tersebut dibahas lebih detail di bagian "Pengoptimalan" di bawah). Kompilator hanya membantu mengenali kasus itu.
Sub-objek yang bisa disalin semu
Sebuah kelas, yang harus dapat disalin, dapat menggabungkan sub-objek dari tipe yang dapat disalin. Tipe yang dapat disalin semu tidak menyediakan konstruktor salinan dalam arti yang sebenarnya, tetapi memiliki konstruktor lain yang memungkinkan untuk membuat salinan konseptual objek. Alasan untuk membuat sebuah tipe semantik dapat disalin adalah ketika tidak ada kesepakatan penuh tentang semantik salinan dari tipe tersebut.
Kemudian keputusan akhir diserahkan kepada pengguna jenis itu - saat menyalin objek, mereka harus secara eksplisit menentukan (melalui argumen tambahan) metode penyalinan yang dimaksudkan.
Dalam kasus pendekatan non-defensif untuk pemrograman, mungkin juga ada konstruktor salinan biasa dan konstruktor kuasi-salinan. Hal ini dapat dibenarkan jika dalam sebagian besar kasus metode penyalinan tunggal harus diterapkan, sedangkan dalam situasi yang jarang tetapi dipahami dengan baik, metode penyalinan alternatif harus digunakan. Kemudian kompilator tidak akan mengeluh bahwa ia tidak dapat secara implisit mendefinisikan konstruktor salinan; itu akan menjadi tanggung jawab pengguna untuk mengingat dan memeriksa apakah sub-objek dari tipe itu harus disalin melalui konstruktor kuasi-copy.
Jangan salin status yang sangat terkait dengan identitas objek
Dalam kasus yang jarang terjadi, bagian dari keadaan objek yang dapat diamati dapat merupakan (atau dianggap) bagian yang tidak dapat dipisahkan dari identitas objek dan tidak boleh dipindahtangankan ke objek lain (meskipun ini bisa agak kontroversial).
Contoh:
UID objek (tetapi yang ini juga termasuk dalam kasus "pendaftaran sendiri" dari atas, karena id harus diperoleh dalam tindakan pendaftaran sendiri).
Sejarah objek (misalnya, Urungkan / Ulangi tumpukan) dalam kasus ketika objek baru tidak boleh mewarisi sejarah objek sumber, tetapi dimulai dengan satu item riwayat " Disalin pada <TIME> dari <OTHER_OBJECT_ID> ".
Dalam kasus seperti itu, konstruktor salinan harus melewati penyalinan sub-objek yang sesuai.
Menerapkan tanda tangan yang benar dari pembuat salinan
Tanda tangan dari konstruktor salinan yang disediakan kompiler bergantung pada konstruktor salinan apa yang tersedia untuk sub-objek. Jika setidaknya satu sub-objek tidak memiliki konstruktor salinan nyata (mengambil objek sumber dengan referensi konstan) tetapi memiliki konstruktor salinan yang bermutasi (mengambil objek sumber dengan referensi non-konstan) maka compiler tidak akan punya pilihan tetapi untuk secara implisit mendeklarasikan dan kemudian mendefinisikan konstruktor salinan yang bermutasi.
Sekarang, bagaimana jika copy-constructor yang "bermutasi" dari tipe sub-objek tidak benar-benar mengubah objek sumber (dan hanya ditulis oleh programmer yang tidak tahu tentang
const
kata kunci)? Jika kita tidak dapat memperbaiki kode itu dengan menambahkan yang hilangconst
, maka opsi lainnya adalah mendeklarasikan konstruktor salinan yang ditentukan pengguna kita sendiri dengan tanda tangan yang benar dan melakukan dosa beralih ke aconst_cast
.Salin-saat-tulis (KK)
Kontainer COW yang telah memberikan referensi langsung ke data internalnya HARUS disalin secara mendalam pada saat pembuatan, jika tidak, kontainer tersebut dapat berperilaku sebagai pegangan penghitungan referensi.
Optimasi
Dalam kasus berikut, Anda mungkin ingin / perlu menentukan konstruktor salinan Anda sendiri karena masalah pengoptimalan:
Pengoptimalan struktur selama penyalinan
Pertimbangkan wadah yang mendukung operasi penghapusan elemen, tetapi dapat melakukannya hanya dengan menandai elemen yang dihapus sebagai dihapus, dan mendaur ulang slotnya nanti. Saat salinan penampung seperti itu dibuat, mungkin masuk akal untuk memadatkan data yang masih ada daripada menyimpan slot yang "dihapus" sebagaimana adanya.
Lewati penyalinan status non-observasi
Sebuah objek mungkin berisi data yang bukan merupakan bagian dari statusnya yang dapat diamati. Biasanya, ini adalah data cache / memoized yang terakumulasi selama masa pakai objek untuk mempercepat operasi kueri lambat tertentu yang dilakukan oleh objek. Aman untuk melewati penyalinan data itu karena akan dihitung ulang ketika (dan jika!) Operasi yang relevan dilakukan. Menyalin data ini mungkin tidak dapat dibenarkan, karena dapat dengan cepat menjadi tidak valid jika status objek yang dapat diamati (dari mana data cache diturunkan) diubah dengan operasi mutasi (dan jika kita tidak akan memodifikasi objek, mengapa kita membuat salin kemudian?)
Pengoptimalan ini dibenarkan hanya jika data tambahannya besar dibandingkan dengan data yang mewakili status yang dapat diamati.
Nonaktifkan penyalinan implisit
C ++ memungkinkan untuk menonaktifkan penyalinan implisit dengan mendeklarasikan konstruktor salinan
explicit
. Kemudian objek dari kelas itu tidak bisa diteruskan ke fungsi dan / atau dikembalikan dari fungsi dengan nilai. Trik ini dapat digunakan untuk jenis yang tampak ringan tetapi memang sangat mahal untuk disalin (meskipun, membuatnya dapat disalin secara semu mungkin merupakan pilihan yang lebih baik).TODO
sumber
Jika Anda memiliki kelas yang memiliki konten yang dialokasikan secara dinamis. Misalnya Anda menyimpan judul buku sebagai karakter * dan menyetel judul dengan baru, penyalinan tidak akan berfungsi.
Anda harus menulis konstruktor salinan yang melakukannya
title = new char[length+1]
dan kemudianstrcpy(title, titleIn)
. Pembuat salinan hanya akan melakukan penyalinan "dangkal".sumber
Copy Constructor dipanggil ketika sebuah objek diteruskan oleh nilai, dikembalikan oleh nilai, atau disalin secara eksplisit. Jika tidak ada konstruktor salinan, c ++ membuat konstruktor salinan default yang membuat salinan dangkal. Jika objek tidak memiliki pointer ke memori yang dialokasikan secara dinamis maka salinan dangkal akan dilakukan.
sumber
Seringkali merupakan ide yang baik untuk menonaktifkan copy ctor, dan operator = kecuali kelas secara khusus membutuhkannya. Ini dapat mencegah inefisiensi seperti meneruskan argumen dengan nilai saat referensi dimaksudkan. Juga metode yang dihasilkan kompilator mungkin tidak valid.
sumber
Mari pertimbangkan cuplikan kode di bawah ini:
class base{ int a, *p; public: base(){ p = new int; } void SetData(int, int); void ShowData(); base(const base& old_ref){ //No coding present. } }; void base :: ShowData(){ cout<<this->a<<" "<<*(this->p)<<endl; } void base :: SetData(int a, int b){ this->a = a; *(this->p) = b; } int main(void) { base b1; b1.SetData(2, 3); b1.ShowData(); base b2 = b1; //!! Copy constructor called. b2.ShowData(); return 0; }
Output: 2 3 //b1.ShowData(); 1996774332 1205913761 //b2.ShowData();
b2.ShowData();
memberikan keluaran sampah karena ada konstruktor salinan yang ditentukan pengguna yang dibuat tanpa kode yang ditulis untuk menyalin data secara eksplisit. Jadi compiler tidak membuat yang sama.Berpikirlah untuk berbagi pengetahuan ini dengan Anda semua, meskipun sebagian besar dari Anda sudah mengetahuinya.
Cheers ... Selamat membuat kode !!!
sumber