Urutan konstruksi dan destruksi

Sekuens base, member, dan derived saat objek dibuat dan dihancurkan.

Daftar isi

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.

cpp alat-tulis-dasar.cpp
#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;
}
text Keluaran
Sebelum objek dibuat
  constructor AlatTulis (kelas dasar)
Objek sedang hidup
  destructor  AlatTulis (kelas dasar)
Setelah lingkup objek berakhir

Constructor 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.

cpp spidol.cpp
#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;
}
text Keluaran
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.

cpp rantai-tiga-tingkat.cpp
#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;
}
text Keluaran
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#