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?
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?
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 akan delete[]
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;
}
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:
Aturan Lima :
Setiap kali Anda menulis salah satu Destructor, Copy Constructor, Copy Assignment Operator, Move Constructor atau Move Assignment Operator, Anda mungkin perlu menulis empat lainnya.
Tetapi ada pedoman yang lebih umum yang harus Anda ikuti, yang berasal dari kebutuhan untuk menulis kode pengecualian-aman:
Setiap sumber daya harus dikelola oleh objek khusus
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 ditunjuk mFoo
? 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 sendiri Copy Constructor
dan Assignment Operator
meskipun, karena unique_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;)
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
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.
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).
Deklarasikan konstruktor salinan dengan =delete
di akhir.
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
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.
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?
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.
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.
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.
Misalnya, meninjau kembali kasus pendaftaran diri objek, kita dapat berargumen bahwa mungkin ada situasi di mana sebuah objek harus didaftarkan dengan manajer objek global hanya jika itu adalah objek mandiri yang lengkap. Jika ia merupakan sub obyek dari obyek lain, maka tanggung jawab mengelolanya ada pada obyek yang memuatnya.
Atau, penyalinan dangkal dan dalam harus didukung (tidak ada yang menjadi default).
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.
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.
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 hilang const
, maka opsi lainnya adalah mendeklarasikan konstruktor salinan yang ditentukan pengguna kita sendiri dengan tanda tangan yang benar dan melakukan dosa beralih ke a const_cast
.
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.
Meskipun COW adalah teknik pengoptimalan, logika dalam konstruktor salinan ini sangat penting untuk implementasi yang benar. Itulah mengapa saya menempatkan kasus ini di sini daripada di bagian "Pengoptimalan", yang akan kita lanjutkan selanjutnya.
Dalam kasus berikut, Anda mungkin ingin / perlu menentukan konstruktor salinan Anda sendiri karena masalah pengoptimalan:
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.
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.
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).
Dalam C ++ 03 mendeklarasikan konstruktor salinan diperlukan juga untuk mendefinisikannya (tentu saja, jika Anda bermaksud menggunakannya). Oleh karena itu, pergi ke konstruktor salinan seperti itu hanya karena perhatian yang sedang dibahas berarti Anda harus menulis kode yang sama yang akan dihasilkan kompilator secara otomatis untuk Anda.
C ++ 11 dan standar yang lebih baru mengizinkan deklarasi fungsi anggota khusus (konstruktor default dan copy, operator penugasan salinan, dan destruktor) dengan permintaan eksplisit untuk menggunakan implementasi default (cukup akhiri deklarasi dengan
=default
).
Jawaban ini dapat diperbaiki sebagai berikut:
- Tambahkan lebih banyak kode contoh
- Gambarkan kasus "Objek dengan referensi silang internal"
- Tambahkan beberapa tautan
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 kemudian strcpy(title, titleIn)
. Pembuat salinan hanya akan melakukan penyalinan "dangkal".
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.
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 !!!