Topik ini membahas copy constructor dan copy assignment, yaitu dua operasi khusus yang menyalin objek bertipe sama, satu untuk membuat objek baru dan satu untuk mengisi objek yang sudah ada. Materinya mencakup kapan keduanya dipanggil, perilaku penyalinan default yang menyalin member satu per satu, perbedaan shallow copy dan deep copy, serta bug double-free yang lahir dari penyalinan dangkal pada kelas dengan member pointer. Setelah membacanya, kedua operasi penyalinan dapat ditulis dengan benar untuk kelas yang mengelola memori sendiri.
Kapan copy constructor dan copy assignment dipanggil#
Copy constructor membuat objek baru sebagai salinan objek lain, sedangkan copy assignment mengisi objek yang sudah jadi dengan isi objek lain. Keduanya dibuat otomatis oleh compiler, sehingga penyalinan terjadi tanpa satu pun baris kode dari perancang kelas. Perbedaan keduanya mudah tertukar karena bentuk penulisannya mirip. Program berikut mencetak pesan pada setiap operasi penyalinan sehingga waktu pemanggilannya terlihat jelas.
#include <iostream>
class Kotak {
private:
int isi;
public:
Kotak(int isiAwal) : isi(isiAwal) {
std::cout << "Konstruktor Kotak(" << isiAwal << ")\n";
}
Kotak(const Kotak& lain) : isi(lain.isi) {
std::cout << "Copy constructor: salin isi " << lain.isi << "\n";
}
Kotak& operator=(const Kotak& lain) {
std::cout << "Copy assignment: ganti isi " << isi
<< " menjadi " << lain.isi << "\n";
isi = lain.isi;
return *this;
}
};
void terimaNilai(Kotak k) {
std::cout << " terimaNilai selesai\n";
}
int main() {
Kotak a(10);
Kotak b = a;
Kotak c(5);
c = a;
terimaNilai(b);
return 0;
}Konstruktor Kotak(10)
Copy constructor: salin isi 10
Konstruktor Kotak(5)
Copy assignment: ganti isi 5 menjadi 10
Copy constructor: salin isi 10
terimaNilai selesaiBaris Kotak b = a; memanggil copy constructor karena objek b baru dibuat pada baris itu. Baris c = a; memanggil copy assignment karena c sudah dibuat sebelumnya, sehingga yang berlangsung adalah penggantian isi, bukan pembangunan objek. Pengoperan b ke fungsi terimaNilai yang parameternya diterima by value juga memanggil copy constructor, karena parameter merupakan objek baru yang dibangun dari argumennya. Pemisahan ini penting diingat karena keduanya harus ditulis terpisah pada kelas yang mengelola sumber daya.
Penyalinan default menyalin member satu per satu#
Copy constructor dan copy assignment implisit menyalin setiap member menggunakan mekanisme penyalinan milik member itu sendiri. Untuk member bertipe int, double, std::string, atau std::vector, hasilnya benar karena tipe-tipe tersebut menyalin isinya dengan lengkap. Masalah muncul pada member berupa pointer mentah yang menyimpan alamat memori hasil new. Yang disalin hanyalah alamatnya, bukan isi yang ditunjuk, sehingga dua objek berakhir menunjuk buffer yang sama. Keadaan semacam ini disebut shallow copy atau penyalinan dangkal, sedangkan penyalinan yang mengalokasikan buffer baru beserta isinya disebut deep copy atau penyalinan dalam.
| Aspek | Shallow copy | Deep copy |
|---|---|---|
| Yang disalin dari member pointer | nilai alamatnya saja | isi buffer yang ditunjuk |
| Buffer memori | dibagi dua objek | satu buffer per objek |
| Perubahan lewat salah satu objek | terbaca pada objek lainnya | tidak memengaruhi objek lain |
| Kode yang diperlukan | tidak ada, dibuat compiler | ditulis sendiri oleh perancang kelas |
Penyalinan dangkal tidak selalu salah. Kelas yang membernya hanya berupa nilai langsung, seperti Kotak pada program pertama, memang cukup dengan penyalinan default. Perbedaan keduanya baru menuntut keputusan desain ketika ada member yang menunjuk sumber daya yang didefinisikan dengan new, karena kepemilikan buffer menjadi tidak jelas.
Bug double-free dari penyalinan dangkal#
Bahaya utama penyalinan dangkal terjadi saat objek dihancurkan. Destructor kelas yang mengelola buffer biasanya memanggil delete[] atas member pointernya. Apabila dua objek menunjuk buffer yang sama karena penyalinan default, destructor kedua objek melepas buffer yang sama dua kali. Program berikut memuat kelas dengan masalah tersebut. Kode ini lolos kompilasi tanpa peringatan, tetapi berhenti dengan error saat dijalankan, sehingga ditandai tidak-dijalankan.
// tidak-dijalankan (program ini lolos kompilasi, tetapi berhenti dengan error double-free saat dijalankan)
#include <cstring>
class ArsipRusak {
private:
char* isi;
public:
ArsipRusak(const char* teks) {
isi = new char[std::strlen(teks) + 1];
std::strcpy(isi, teks);
}
~ArsipRusak() {
delete[] isi;
}
// tanpa copy constructor dan tanpa copy assignment
};
int main() {
ArsipRusak a("data");
ArsipRusak b = a; // penyalinan dangkal: kedua member menunjuk buffer yang sama
return 0; // dua destructor melepas buffer yang sama
}Saat program itu dijalankan, objek a dan b dibangun dengan member isi yang menunjuk buffer identik. Ketika main berakhir, kedua destructor dijalankan dan delete[] isi dipanggil dua kali atas buffer yang sama. Alokator memori mendeteksi kejadian itu dan menghentikan program dengan pesan free(): double free detected in tcache 2. Kegagalan semacam ini tidak terlihat saat kompilasi karena secara tata bahasa seluruh barisnya sah. Perbaikannya bukan menghapus destructor, melainkan melengkapi kelas dengan operasi penyalinan yang menyalin isi buffer, sebagaimana ditunjukkan bagian berikutnya.
Menulis deep copy pada kedua operasi#
Copy constructor yang benar mengalokasikan buffer baru dengan panjang yang sama, lalu menyalin isinya. Copy assignment yang benar melakukan pekerjaan serupa atas objek yang sudah ada, sehingga buffer lama milik objek tujuan harus dilepas lebih dahulu agar tidak bocor. Keduanya juga perlu mengantisipasi self-assignment, yaitu penugasan objek kepada dirinya sendiri seperti x = x, karena melepas buffer sendiri sebelum menyalin dari sumber yang sama berarti membaca memori yang sudah dilepas. Program berikut memuat kelas ArsipTeks dengan ketiga langkah tersebut.
#include <cstring>
#include <iostream>
class ArsipTeks {
private:
char* isi;
int panjang;
public:
ArsipTeks(const char* teks) : panjang(std::strlen(teks)) {
isi = new char[panjang + 1];
std::strcpy(isi, teks);
}
ArsipTeks(const ArsipTeks& lain) : panjang(lain.panjang) {
isi = new char[panjang + 1];
std::strcpy(isi, lain.isi);
}
ArsipTeks& operator=(const ArsipTeks& lain) {
if (this != &lain) {
delete[] isi;
panjang = lain.panjang;
isi = new char[panjang + 1];
std::strcpy(isi, lain.isi);
}
return *this;
}
~ArsipTeks() {
delete[] isi;
}
void tampilkan() const {
std::cout << isi << "\n";
}
};
int main() {
ArsipTeks asli("catatan harian");
ArsipTeks salinan = asli;
ArsipTeks lain("daftar belanja");
lain = asli;
salinan.tampilkan();
lain.tampilkan();
return 0;
}catatan harian
catatan harianPemeriksaan if (this != &lain) membandingkan alamat kedua objek dan melewati seluruh pekerjaan apabila keduanya sama, sehingga penugasan objek kepada dirinya sendiri tidak merusak apa pun. Nilai kembalian *this mengembalikan objek tujuan dan memungkinkan penugasan berantai seperti c = b = a. Dengan kedua operasi tersebut, setiap objek ArsipTeks memiliki buffer sendiri, sehingga penghancurannya tidak pernah bertabrakan dan perubahan isi salah satu objek tidak terbaca pada objek lain.
Perintah berikut mengompilasi berkas itu dan menjalankan hasilnya. Berkas biner ditempatkan di /tmp agar direktori proyek tetap bersih.
# kompilasi arsip-teks.cpp menjadi program di folder sementara /tmp
g++ arsip-teks.cpp -o /tmp/arsip-teks
# jalankan program hasil kompilasi
/tmp/arsip-tekscatatan harian
catatan harianKesalahan umum#
Tiga kekeliruan berikut paling sering muncul saat menangani penyalinan objek.
- Mengira
Kotak b = a;memanggil copy assignment. Bentuk itu adalah inisialisasi, sehingga copy constructor yang berjalan. Copy assignment hanya berlaku ketika objek di ruas kiri sudah dibuat sebelumnya. - Membiarkan penyalinan default pada kelas yang member pointernya didefinisikan dengan
newdan dilepas oleh destructor. Akibatnya double-free saat kedua objek dihancurkan. Perbaikannya adalah menulis kedua operasi dengan deep copy atau mengganti pointer mentah denganstd::stringdanstd::vector. - Menulis copy assignment tanpa melepas buffer lama dan tanpa menguji self-assignment. Akibatnya kebocoran memori dan pembacaan memori yang sudah dilepas. Perbaikannya adalah menambahkan
delete[]sebelum alokasi baru dan pemeriksaanthis != &laindi awal fungsi.
Ringkasan#
- Copy constructor membuat objek baru sebagai salinan, sedangkan copy assignment mengisi objek yang sudah ada, dan keduanya dipanggil secara diam-diam saat inisialisasi, pengoperan parameter by value, dan penugasan.
- Penyalinan default menyalin member satu per satu, sehingga member pointer mentah menghasilkan shallow copy yang berbagi buffer yang sama.
- Kelas yang melepas sumber daya pada destructornya wajib menyediakan copy constructor dan copy assignment ber-deep copy agar penghancurannya aman.
- Copy assignment yang benar melepas buffer lama, mengalokasikan buffer baru, dan menghindari self-assignment dengan pemeriksaan alamat.
Referensi#
- Copy constructors — cppreference — Aturan copy constructor implisit dan bentuk yang didefinisikan sendiri.
- Copy assignment operator — cppreference — Rincian copy assignment operator beserta bentuk yang dianjurkan.
- Shallow vs. deep copying — LearnCpp — Perbedaan penyalinan dangkal dan dalam pada member pointer.
- Copy initialization — LearnCpp — Konteks inisialisasi yang memanggil copy constructor.