Topik ini membahas urutan pasti yang dijalankan compiler saat objek kelas turunan dibuat dan dihancurkan. Materinya mencakup sekuens constructor kelas dasar, constructor member, dan badan constructor kelas turunan, aturan urutan inisialisasi member, serta urutan destructor yang menjadi kebalikannya. Setelah membacanya, pembaca dapat membaca jejak keluaran program berhierarki dan menempatkan logika inisialisasi pada tahap yang benar.
Objek turunan dibangun dari kelas dasarnya#
Pembentukan objek kelas turunan selalu dimulai dari bagian kelas dasarnya. Compiler menyisipkan pemanggilan constructor kelas dasar sebelum badan constructor kelas turunan berjalan, karena bagian turunan belum sah untuk disiapkan sebelum bagian dasarnya ada. Program berikut memakai kelas dasar tunggal dengan pesan pada constructor dan destructor agar sekuensnya terlihat.
#include <iostream>
#include <string>
class AlatTulis {
public:
AlatTulis() {
std::cout << " constructor AlatTulis (kelas dasar)\n";
}
~AlatTulis() {
std::cout << " destructor AlatTulis (kelas dasar)\n";
}
};
int main() {
std::cout << "Sebelum objek dibuat\n";
{
AlatTulis pensil;
std::cout << "Objek sedang hidup\n";
}
std::cout << "Setelah lingkup objek berakhir\n";
return 0;
}Sebelum objek dibuat
constructor AlatTulis (kelas dasar)
Objek sedang hidup
destructor AlatTulis (kelas dasar)
Setelah lingkup objek berakhirConstructor dijalankan tepat saat deklarasi objek tercapai, sedangkan destructor dijalankan tepat saat lingkup kurawal penutup berakhir. Urutan ini berlaku untuk setiap objek berumur otomatis. Topik ini menambahkan satu lapisan lagi pada aturan itu, yaitu hubungan antara bagian kelas dasar dan bagian kelas turunan dari satu objek yang sama.
Cara membacanya adalah memandang setiap objek turunan sebagai gabungan dua bagian, yaitu subobjek milik kelas dasar dan member milik kelas turunannya. Subobjek kelas dasar dibentuk lebih dulu karena member kelas turunan umumnya bertumpu padanya. Fungsi yang diwariskan, misalnya, sudah dapat dipanggil ketika badan constructor kelas turunan mulai berjalan. Aturan yang sama berlaku terbalik saat penghancuran, karena bagian turunan yang bertumpu pada dasarnya harus dilepas sebelum dasarnya.
Sekuens lengkap: dasar, member, dan badan turunan#
Untuk objek kelas turunan yang memiliki member bertipe kelas, urutan pembentukannya terdiri atas tiga tahap. Constructor kelas dasar berjalan terlebih dahulu, kemudian member kelas turunan diinisialisasi sesuai urutan deklarasinya, dan badan constructor kelas turunan terakhir. Program berikut menandai setiap tahap dengan pesan, termasuk urutan daftar inisialisasi yang sengaja dibalik terhadap urutan deklarasi member untuk menguji aturannya.
#include <iostream>
#include <string>
class Komponen {
public:
explicit Komponen(const std::string& namaKomponen) : nama{namaKomponen} {
std::cout << " constructor Komponen: " << nama << "\n";
}
~Komponen() {
std::cout << " destructor Komponen: " << nama << "\n";
}
private:
std::string nama;
};
class AlatTulis {
public:
explicit AlatTulis(const std::string& namaAlat) : nama{namaAlat} {
std::cout << "constructor AlatTulis (kelas dasar): " << nama << "\n";
}
~AlatTulis() {
std::cout << "destructor AlatTulis (kelas dasar)\n";
}
private:
std::string nama;
};
class Spidol : public AlatTulis {
public:
// Urutan daftar inisialisasi sengaja dibalik terhadap urutan deklarasi member
Spidol() : AlatTulis{"Spidol"}, tinta{"tinta"}, badan{"badan"} {
std::cout << "constructor Spidol (kelas turunan)\n";
}
~Spidol() {
std::cout << "destructor Spidol (kelas turunan)\n";
}
private:
Komponen badan;
Komponen tinta;
};
int main() {
Spidol spidol;
return 0;
}constructor AlatTulis (kelas dasar): Spidol
constructor Komponen: badan
constructor Komponen: tinta
constructor Spidol (kelas turunan)
destructor Spidol (kelas turunan)
destructor Komponen: tinta
destructor Komponen: badan
destructor AlatTulis (kelas dasar)Keluaran tersebut memperlihatkan dua aturan sekaligus. Pertama, urutan inisialisasi member mengikuti urutan deklarasi pada definisi kelas, yaitu badan lebih dulu baru tinta, meskipun daftar inisialisasi menuliskan tinta sebelum badan. Kedua, destruksi menelusuri jalur konstruksi secara terbalik, yaitu badan destructor Spidol, member tinta, member badan, lalu destructor AlatTulis. Aturan yang sama berlaku bertingkat pada hierarki berlapis, karena setiap kelas dasar diperlakukan sebagai bagian yang dibangun lebih dulu dari bagian yang mewarisinya.
Karena kelas AlatTulis tidak memiliki constructor tanpa parameter, constructor Spidol wajib memanggilnya secara eksplisit melalui AlatTulis{"Spidol"} di daftar inisialisasi. Apabila pemanggilan itu tidak ditulis, compiler menolak programnya, karena tidak ada cara lain memberi argumen kepada constructor kelas dasar. Inilah alasan kelas dasar yang berparameter selalu menyertai daftar inisialisasi setiap kelas turunannya.
Urutan tahap tersebut juga menegaskan letak yang tepat bagi setiap jenis logika inisialisasi. Ketika badan constructor kelas turunan mulai berjalan, subobjek kelas dasar dan seluruh membernya sudah siap, sehingga badan tersebut aman menggunakan member apa pun milik objeknya. Penugasan di dalam badan yang melibatkan member lain pun sah. Meskipun demikian, inisialisasi melalui daftar inisialisasi tetap lebih diutamakan, sesuai kebiasaan yang ditegaskan pada modul kelas dan objek.
Hierarki berlapis tiga tingkat#
Aturan tersebut berlaku bertingkat pada hierarki yang lebih panjang, karena setiap tingkat pewarisan menambah satu subobjek pada rantai pembentukannya. Objek dari kelas turunan tingkat ketiga memuat tiga bagian, yaitu subobjek tingkat pertama, subobjek tingkat kedua, dan member milik tingkat ketiganya. Konstruktor dari puncak hierarki ke bawah dijalankan satu per satu, sedangkan destruktor menelusurinya dari bawah ke atas. Program berikut memakai rantai pewarisan tiga tingkat dari ranah alat tulis untuk membuktikannya.
#include <iostream>
class AlatTulis {
public:
AlatTulis() {
std::cout << "constructor AlatTulis (tingkat 1)\n";
}
~AlatTulis() {
std::cout << "destructor AlatTulis (tingkat 1)\n";
}
};
class Pensil : public AlatTulis {
public:
Pensil() {
std::cout << "constructor Pensil (tingkat 2)\n";
}
~Pensil() {
std::cout << "destructor Pensil (tingkat 2)\n";
}
};
class PensilMekanik : public Pensil {
public:
PensilMekanik() {
std::cout << "constructor PensilMekanik (tingkat 3)\n";
}
~PensilMekanik() {
std::cout << "destructor PensilMekanik (tingkat 3)\n";
}
};
int main() {
PensilMekanik pm;
return 0;
}constructor AlatTulis (tingkat 1)
constructor Pensil (tingkat 2)
constructor PensilMekanik (tingkat 3)
destructor PensilMekanik (tingkat 3)
destructor Pensil (tingkat 2)
destructor AlatTulis (tingkat 1)Tidak ada satu baris kode pun di program itu yang memanggil constructor atau destructor secara eksplisit, karena seluruh sekuens disusun oleh compiler. Setiap constructor di rantai memanggil constructor tingkat atasnya secara implisit sebelum badannya sendiri berjalan, dan setiap destructor membiarkan destructor tingkat bawahnya dijalankan setelah badannya selesai. Pola ini menjelaskan mengapa logika yang bergantung pada keadaan lengkap objek tidak boleh ditaruh di constructor tingkat atas, sebab tingkat di bawahnya belum siap saat constructor itu berjalan. Aturan yang sama menjelaskan mengapa destructor tingkat atas masih aman memakai seluruh member, karena bagian turunannya baru dilepas setelah badannya selesai.
Kesalahan umum#
Empat kesalahan berikut paling sering muncul pada urutan konstruksi dan destruksi. Kesalahan semacam ini jarang tampak sebagai error sintaks, melainkan sebagai keluaran program yang urutannya tidak sesuai harapan.
- Menganggap urutan daftar inisialisasi menentukan urutan inisialisasi member. Yang menentukan adalah urutan deklarasi member, dan daftar inisialisasi hanya memasok argumennya. Perbaikannya adalah menyusun daftar inisialisasi searah dengan deklarasi agar kodenya tidak menyesatkan pembaca.
- Melupakan pemanggilan constructor kelas dasar yang berparameter. Daftar inisialisasi kelas turunan harus menyebut
AlatTulis{"..."}bila kelas dasarnya tidak memiliki constructor tanpa parameter, sehingga pesan error dari compiler justru menjadi pengingat yang tepat. - Menaruh pembersihan yang bergantung pada objek lain tanpa mempertimbangkan urutan destruksi. Karena destruksi berjalan terbalik dari konstruksi, objek yang dibangun belakangan dihancurkan lebih dulu, dan logika pembersihannya perlu disusun mengikuti kenyataan tersebut.
- Mengira member dihancurkan sesuai urutan deklarasinya. Urutan penghancuran member justru kebalikannya, sehingga member yang bergantung pada member lain saat membersihkan diri perlu dinyatakan dengan kesadaran penuh atas urutannya.
Ringkasan#
- Pembentukan objek turunan berjalan dalam urutan constructor kelas dasar, inisialisasi member sesuai deklarasinya, lalu badan constructor kelas turunan.
- Destruksi menelusuri urutan yang sama secara terbalik, yaitu badan destructor kelas turunan, member dari belakang, lalu destructor kelas dasar.
- Urutan member mengikuti deklarasi di dalam kelas, bukan urutan penulisan daftar inisialisasi.
- Kelas dasar tanpa constructor tanpa parameter wajib dipanggil secara eksplisit dari daftar inisialisasi setiap kelas turunannya.
- Pada hierarki berlapis, constructor berjalan dari puncak hierarki ke bawah dan destructor dari bawah ke puncak.
Referensi#
- Order of construction of derived classes — LearnCpp — Urutan konstruksi bagian kelas dasar, member, dan badan constructor kelas turunan.
- Constructors and initialization of derived classes — LearnCpp — Pemanggilan constructor kelas dasar melalui daftar inisialisasi kelas turunan.
- Derived classes — cppreference — Aturan standar mengenai inisialisasi kelas dasar dan member pada kelas turunan.