Sejauh ini kita sudah mengatur cara object lahir (constructor) dan cara object berakhir (destructor). Masih ada satu kejadian yang berlangsung diam-diam di hampir setiap program: object disalin. Setiap kali kita menulis Pelacak b = a;, mengirim object ke parameter fungsi by value, atau menyimpan object ke dalam container, compiler menyalin object tersebut — dan bila class kita mengelola resource seperti memori heap, salinan yang keliru bisa membuat program berperilaku tak terdefinisi. Chapter ini membedah dua mekanisme penyalinan tersebut, lalu melengkapi class pengelola resource dengan deep copy yang aman.
Copy Constructor: Menyalin Saat Object Lahir #
Copy constructor adalah constructor yang parameter pertamanya berupa referensi ke object sejenis — lazim ditulis Pelacak(const Pelacak& lain). Bentuk const& bukan pilihan acak. Parameter by value akan menuntut penyalinan lagi dari dalam penyalinan sendiri (rekursi tanpa ujung), sedangkan referensi membuat constructor menerima object sumber apa adanya; const menjamin sumber salinan tidak ikut diubah. Bila kita tidak menulisnya, compiler membangkitkan copy constructor secara otomatis dan versi bawaan itu menyalin setiap data member satu per satu.
Copy constructor berjalan setiap kali object baru diinisialisasi dari object lain yang sejenis:
Pelacak b = a;atauPelacak b(a);— inisialisasi object baru dari object yang sudah ada.- Pengiriman argument ke parameter by value, misalnya
catat(a);pada fungsivoid catat(Pelacak p). - Pengembalian object by value dari fungsi — salinan ini bisa dipangkas oleh compiler, topik yang dibahas pada Chapter Copy Elision di Section Move Semantics dan Value Categories.
Copy Assignment: Mengisi Object yang Sudah Ada #
Copy assignment berjalan pada situasi yang berbeda: object di sisi kiri sudah ada sejak sebelumnya, lalu kita mengganti isinya dengan isi object lain. Bentuk fungsinya adalah Pelacak& operator=(const Pelacak& lain) — member function bernama operator= dengan tipe kembalian referensi agar penugasan bisa dirangkai seperti a = b = c. Garis pemisah keduanya adalah status object di sisi kiri: copy constructor menciptakan object baru, sedangkan copy assignment menimpa object lama. Perhatikan bahwa tanda = saja tidak menentukan mana yang berjalan — Pelacak c = a; adalah inisialisasi, bukan penugasan. Program berikut membuktikan kapan masing-masing mekanisme dijalankan:
#include <iostream>
#include <string>
class Pelacak {
private:
std::string nama;
std::string asal; // jejak: bagaimana object ini diperoleh
public:
Pelacak(const std::string& n) : nama(n), asal("konstruksi") {
std::cout << " konstruksi : " << nama << "\n";
}
// Copy constructor: parameter pertama referensi ke object sejenis
Pelacak(const Pelacak& lain) : nama(lain.nama), asal("salinan") {
std::cout << " copy ctor : salinan bernama " << nama << "\n";
}
// Copy assignment: object di sisi kiri sudah ada lebih dulu
Pelacak& operator=(const Pelacak& lain) {
nama = lain.nama;
asal = "hasil penugasan";
std::cout << " copy assign: isi object kini milik " << nama << "\n";
return *this;
}
~Pelacak() {
std::cout << " destruksi : " << nama << " (" << asal << ")\n";
}
void cetak() {
std::cout << " object " << nama << " dicatat\n";
}
};
// Parameter by value: setiap pemanggilan menyalin argument
void catat(Pelacak p) {
p.cetak();
}
int main() {
std::cout << "awal main\n";
Pelacak a("asli");
Pelacak b("bekas");
std::cout << "penugasan b = a\n";
b = a; // penugasan -> copy assignment
std::cout << "inisialisasi c dari a\n";
Pelacak c = a; // inisialisasi -> copy constructor
std::cout << "memanggil catat(a)\n";
catat(a); // parameter by value -> copy constructor
std::cout << "akhir main\n";
return 0;
}Output yang Dihasilkan:
awal main
konstruksi : asli
konstruksi : bekas
penugasan b = a
copy assign: isi object kini milik asli
inisialisasi c dari a
copy ctor : salinan bernama asli
memanggil catat(a)
copy ctor : salinan bernama asli
object asli dicatat
destruksi : asli (salinan)
akhir main
destruksi : asli (salinan)
destruksi : asli (hasil penugasan)
destruksi : asli (konstruksi)Bedah baris-per-baris:
- Baris 15-17 (
Pelacak(const Pelacak& lain)): copy constructor menerima object sumber lewat referensiconst, menyalinnamalewat member initializer list, dan menandaiasalsebagai"salinan"— jejak yang nanti terbaca pada output destruksi. - Baris 20-25 (
operator=): copy assignment menyalin member ke object yang sudah ada, lalureturn *this;mengembalikan object pemanggil dengan tipe referensi. - Baris 47 (
b = a;):bsudah lahir di baris 44, sehingga pernyataan ini memanggil copy assignment — output mencetakcopy assign. - Baris 50 (
Pelacak c = a;): meski memakai tanda=, objectcbaru sedang diciptakan, sehingga yang berjalan adalah copy constructor. Penentu utamanya adalah status sisi kiri, bukan simbolnya. - Baris 53 (
catat(a);): parameterpby value menyalin argument, dan salinan itu dihancurkan segera setelahcatat()selesai — itulah sebabnya barisdestruksi : asli (salinan)muncul sebelumakhir main. - Tiga baris terakhir output: destruksi berjalan urut terbalik (c, b, a) seperti yang kita buktikan pada Chapter Destructor, dan nilai
asalpada tiap baris merangkum sejarah tiap object.
Shallow Copy: Jebakan Salinan Alamat #
Copy bawaan compiler menyalin member satu per satu. Untuk member int atau std::string, cara ini tepat. Masalah muncul pada class yang mengelola resource lewat member pointer: yang disalin adalah nilai pointernya (alamat), bukan isi memori yang ditunjuk. Dua object kini menunjuk satu resource yang sama — pola ini disebut shallow copy.
// Versi yang dibangkitkan compiler untuk class BufferTeks kira-kira seperti ini:
BufferTeks(const BufferTeks& lain) : data(lain.data), ukuran(lain.ukuran) {}
// Akibatnya: data dan lain.data menunjuk SATU alamat heap yang sama.
// - mengubah isi buffer milik satu object ikut mengubah milik object lain
// - saat kedua object berakhir, delete[] dijalankan dua kali untuk satu memori
Jawabannya adalah deep copy: object salinan mengalokasikan resource sendiri, lalu menyalin isi resource sumber ke dalamnya. Isi dua object menjadi sama, tetapi miliknya terpisah.
Deep Copy pada Class Pengelola Resource #
Program berikut memperbaiki class BufferTeks — class dengan member char* yang dialokasi lewat new[] — dengan copy constructor, copy assignment, dan self-assignment guard yang benar:
#include <iostream>
#include <string>
class BufferTeks {
private:
char* data; // resource di heap, dialokasi dengan new[]
int ukuran;
public:
BufferTeks(const std::string& teks) : ukuran(static_cast<int>(teks.size())) {
data = new char[ukuran + 1];
for (int i = 0; i < ukuran; ++i) {
data[i] = teks[i];
}
data[ukuran] = '\0';
std::cout << " konstruksi : buffer baru dialokasi (" << ukuran << " karakter)\n";
}
// Copy constructor: DEEP COPY — alokasi buffer sendiri, lalu salin isinya
BufferTeks(const BufferTeks& lain) : ukuran(lain.ukuran) {
data = new char[ukuran + 1];
for (int i = 0; i < ukuran; ++i) {
data[i] = lain.data[i];
}
data[ukuran] = '\0';
std::cout << " copy ctor : buffer baru dialokasi, " << ukuran
<< " karakter disalin\n";
}
// Copy assignment dengan self-assignment guard
BufferTeks& operator=(const BufferTeks& lain) {
if (this != &lain) { // penjaga self-assignment
char* baru = new char[lain.ukuran + 1]; // 1) siapkan buffer baru
for (int i = 0; i < lain.ukuran; ++i) {
baru[i] = lain.data[i];
}
baru[lain.ukuran] = '\0';
delete[] data; // 2) bebaskan buffer lama
data = baru; // 3) tunjuk buffer baru
ukuran = lain.ukuran;
std::cout << " copy assign: buffer lama diganti buffer baru ("
<< ukuran << " karakter)\n";
} else {
std::cout << " copy assign: self-assignment, tidak ada yang diubah\n";
}
return *this;
}
void ubahKarakter(int posisi, char c) {
data[posisi] = c;
}
std::string isi() {
return std::string(data);
}
~BufferTeks() {
std::cout << " destruksi : memori buffer dibebaskan\n";
delete[] data;
}
};
int main() {
BufferTeks a("kopi");
BufferTeks b = a; // deep copy: b punya buffer sendiri
b.ubahKarakter(0, 'K'); // mengubah b tidak menyentuh a
std::cout << "a = " << a.isi() << "\n";
std::cout << "b = " << b.isi() << "\n";
BufferTeks c("teh");
c = b; // copy assignment: isi b disalin ke c
std::cout << "c = " << c.isi() << "\n";
c = c; // self-assignment: harus aman
std::cout << "c setelah self-assignment = " << c.isi() << "\n";
return 0;
}Output yang Dihasilkan:
konstruksi : buffer baru dialokasi (4 karakter)
copy ctor : buffer baru dialokasi, 4 karakter disalin
a = kopi
b = Kopi
konstruksi : buffer baru dialokasi (3 karakter)
copy assign: buffer lama diganti buffer baru (4 karakter)
c = Kopi
copy assign: self-assignment, tidak ada yang diubah
c setelah self-assignment = Kopi
destruksi : memori buffer dibebaskan
destruksi : memori buffer dibebaskan
destruksi : memori buffer dibebaskanBedah bagian pentingnya:
- Baris 6-7 (
char* data; int ukuran;): class ini mengelola resource heap sendiri, sehingga copy bawaan compiler tidak lagi memadai dan kita wajib menulis salinan sendiri. - Baris 21 (
data = new char[ukuran + 1];): copy constructor mengalokasi buffer milik object baru — inilah inti deep copy. Slot tambahan+ 1menampung karakter penutup'\0'. - Baris 22-25 (loop penyalinan): setiap karakter disalin dari buffer sumber
lain.datake buffer baru. Setelah ini, dua object punya isi sama tetapi alamat memori berbeda. - Baris 32 (
if (this != &lain)): penjaga self-assignment.thisadalah pointer tersembunyi ke object pemanggil (dibahas tuntas pada Chapter berikutnya), dan&lainadalah alamat object sumber; bila keduanya sama,c = c;sedang terjadi dan tidak ada pekerjaan yang perlu dilakukan. - Baris 33-39 (tiga langkah penugasan): buffer baru disiapkan lebih dulu, buffer lama baru kemudian dihapus, dan pointer diarahkan ke buffer baru. Urutan ini menjaga buffer lama tetap utuh selama penyalinan berlangsung.
- Baris 46 (
return *this;): mengembalikan object pemanggil sehingga penugasan berantai sepertia = b = ctetap bekerja. - Baris 67 dan output
a = kopi/b = Kopi: perubahan padabtidak memengaruhia— bukti bahwa keduanya tidak lagi berbagi memori. Tiga baris destruksi terakhir membuktikan tiap object membebaskan buffer miliknya sendiri, tepat satu kali.
Self-Assignment Guard: Mengapa Wajib #
Pernyataan c = c; sah dalam bahasa C++ dan bisa terjadi tanpa sengaja — misalnya ketika dua referensi berbeda ternyata merujuk object yang sama. Bila kita menulis copy assignment tanpa penjaga seperti ini:
BufferTeks& operator=(const BufferTeks& lain) {
delete[] data; // buffer milik pemanggil dihapus lebih dulu
data = new char[lain.ukuran + 1];
// bila lain adalah object pemanggil itu sendiri, lain.data menunjuk memori
// yang baru saja dibebaskan — menyalin dari situ adalah undefined behavior
for (int i = 0; i < lain.ukuran; ++i) {
data[i] = lain.data[i];
}
return *this;
}Kasus c = c; membuat langkah pertama menghapus buffer yang justru sedang menjadi sumber penyalinan. Penjaga if (this != &lain) mencegah seluruh rangkaian itu berjalan, dan output program utama membuktikannya: baris self-assignment, tidak ada yang diubah dicetak, sementara c tetap utuh.
Copy pada Class dengan Container Member #
Tidak semua class butuh copy constructor tulisan tangan. Class yang member-nya std::string atau std::vector aman dengan salinan bawaan compiler, karena container tersebut menyalin isinya sendiri ketika disalin. Program berikut tidak menulis satu pun fungsi penyalinan:
#include <iostream>
#include <string>
#include <vector>
class Catatan {
private:
std::string judul;
std::vector<std::string> baris;
public:
Catatan(const std::string& judulAwal) : judul(judulAwal) {}
void tambahBaris(const std::string& teks) {
baris.push_back(teks);
}
void ubahJudul(const std::string& judulBaru) {
judul = judulBaru;
}
void cetak(const std::string& label) {
std::cout << label << ": \"" << judul << "\", " << baris.size()
<< " baris:";
for (const std::string& b : baris) {
std::cout << " [" << b << "]";
}
std::cout << "\n";
}
};
int main() {
Catatan asli("Resep Soto");
asli.tambahBaris("Rebus ayam");
asli.tambahBaris("Tambah bumbu");
Catatan salinan = asli; // copy ctor buatan compiler: tiap member disalin
asli.ubahJudul("Resep Soto Istimewa"); // mengubah asli ...
asli.tambahBaris("Sajikan panas"); // ... tidak menyentuh salinan
asli.cetak("asli ");
salinan.cetak("salinan");
return 0;
}Output yang Dihasilkan:
asli : "Resep Soto Istimewa", 3 baris: [Rebus ayam] [Tambah bumbu] [Sajikan panas]
salinan: "Resep Soto", 2 baris: [Rebus ayam] [Tambah bumbu]Baris Catatan salinan = asli; memicu copy constructor bawaan, yang menyalin judul dan baris — dan penyalinan std::vector<std::string> menyalin seluruh elemennya, bukan sekadar alamat internalnya. Hasilnya terlihat pada output: perubahan pada asli tidak memengaruhi salinan. Bila seluruh member mengurus resource-nya masing-masing, salinan bawaan compiler adalah pilihan yang benar — prinsip yang kelak diformalkan sebagai Rule of Zero.
= default dan = delete #
Ada dua situasi yang ingin kita nyatakan secara eksplisit: “perilaku bawaan compiler memang yang saya mau” dan “class ini memang tidak boleh disalin”. Kata kunci = default menyatakan yang pertama, = delete menyatakan yang kedua:
#include <iostream>
#include <string>
// = default: kita menegaskan perilaku bawaan compiler adalah yang kita mau
class Titik {
private:
int x;
int y;
public:
Titik(int xAwal, int yAwal) : x(xAwal), y(yAwal) {}
Titik(const Titik&) = default;
Titik& operator=(const Titik&) = default;
int ambilX() { return x; }
int ambilY() { return y; }
};
// = delete: menonaktifkan copy secara eksplisit
class Koneksi {
private:
std::string target;
public:
Koneksi(const std::string& t) : target(t) {}
Koneksi(const Koneksi&) = delete;
Koneksi& operator=(const Koneksi&) = delete;
void gunakan() {
std::cout << "menggunakan koneksi ke " << target << "\n";
}
};
int main() {
Titik p1(3, 4);
Titik p2 = p1; // copy ctor = default: x dan y disalin satu per satu
Titik p3(10, 20);
p2 = p3; // copy assignment = default
std::cout << "p2 = (" << p2.ambilX() << ", " << p2.ambilY() << ")\n";
Koneksi k("db.example.com");
k.gunakan();
// Koneksi k2 = k; // tidak bisa dikompilasi: copy constructor di-delete
// k = k; // tidak bisa dikompilasi: copy assignment di-delete
return 0;
}Output yang Dihasilkan:
p2 = (10, 20)
menggunakan koneksi ke db.example.comFungsi yang di-delete tetap terdaftar sebagai bagian antarmuka class, sehingga pelanggarannya tertangkap compiler — bukan sebagai bug diam-diam saat runtime. Bila baris Koneksi k2 = k; kita aktifkan, kompilasi berhenti dengan pesan yang eksplisit:
error: use of deleted function 'Koneksi::Koneksi(const Koneksi&)'
note: declared here
Koneksi(const Koneksi&) = delete;Menuju Rule of Three #
Class BufferTeks pada chapter ini menuntut tiga fungsi sekaligus: destructor untuk membebaskan resource, copy constructor untuk menyalinnya, dan copy assignment untuk menggantinya. Ketiganya bekerja atas resource yang sama, dan kehadiran satu hampir selalu menuntut dua lainnya — salinan bawaan tanpa penjaga menjadi celah di tempat lain. Pengamatan ini dikenal sebagai Rule of Three, dan versi lengkapnya (Rule of Zero, Rule of Five, beserta perannya dalam move semantics) dibahas tuntas pada Chapter Rule of Zero, Three, Five di Section Move Semantics dan Value Categories.
Penjaga if (this != &lain) yang kita tulis tadi menyentuh satu token yang belum kita bongkar: pointer this, parameter tersembunyi pada setiap member function. Chapter berikutnya, this Pointer, membuktikan cara compiler melekatkannya dan kegunaan praktisnya — termasuk pola return *this yang baru saja kita pakai.
Rujukan resmi: Copy constructors (cppreference) , Copy assignment operator (cppreference) .