Constructor dan destructor

Constructor default dan berparameter serta kontak pertama dengan RAII.

Daftar isi

Topik ini membahas constructor dan destructor, yaitu member function khusus yang dijalankan secara otomatis pada saat objek dibuat dan dihancurkan. Materinya mencakup constructor default dan berparameter, urutan pemanggilan destructor di akhir masa hidup objek, serta kontak pertama dengan RAII sebagai pola yang menjadikan masa hidup objek sebagai penjamin pelepasan sumber daya. Setelah membacanya, pembaca dapat memastikan setiap objek dimulai dalam keadaan yang sah dan dibersihkan secara otomatis.

Constructor: nilai awal yang terjamin#

Constructor adalah member function yang namanya sama dengan nama kelas, tidak memiliki tipe kembalian, dan dijalankan otomatis setiap kali objek dibuat. Tugas utamanya menjamin objek lahir dengan nilai awal yang sah, sehingga tidak ada member yang tertinggal tanpa nilai. Kelas berikut menyediakan dua constructor, yaitu satu tanpa parameter dan satu berparameter.

cpp kotak.cpp
#include <iostream>

class Kotak {
private:
    double panjang = 1.0;
    double lebar = 1.0;

public:
    Kotak() {
        std::cout << "Konstruktor default      : kotak 1 x 1\n";
    }

    Kotak(double p, double l) {
        panjang = p;
        lebar = l;
        std::cout << "Konstruktor berparameter : kotak " << p << " x " << l << "\n";
    }

    double luas() const {
        return panjang * lebar;
    }
};

int main() {
    Kotak kosong;
    std::cout << "Luas kosong = " << kosong.luas() << "\n";

    Kotak kertas(21.0, 14.0);
    std::cout << "Luas kertas = " << kertas.luas() << "\n";
    return 0;
}
text Keluaran
Konstruktor default      : kotak 1 x 1
Luas kosong = 1
Konstruktor berparameter : kotak 21 x 14
Luas kertas = 294

Deklarasi Kotak kosong memanggil constructor tanpa parameter, yang disebut default constructor. Bentuk Kotak kertas(21.0, 14.0) memanggil constructor dengan dua parameter, dan pemilihannya mengikuti aturan resolusi overloading yang sama dengan pemilihan fungsi biasa. Apabila kelas tidak memiliki constructor sama sekali, compiler menghasilkan default constructor secara diam-diam, sebagaimana terjadi pada kelas Titik dan PengaturSuhu di topik sebelumnya. Sebaliknya, begitu satu constructor berparameter dideklarasikan, compiler berhenti menghasilkan default constructor. Deklarasi Kotak kosong karenanya ditolak sampai constructor tanpa parameter ditulis secara eksplisit.

Perhatikan pula baris double panjang = 1.0 di dalam definisi kelas. Nilai awal yang ditulis di tempat itu disebut default member initializer dan berlaku untuk setiap objek yang tidak menetapkan nilai lain. Dengan jaminan ini, constructor yang badannya nyaris kosong tetap menghasilkan objek yang terisi penuh.

Destructor dan urutan penghancuran#

Destructor adalah member function yang namanya diawali tanda gelombang ~ diikuti nama kelas, tanpa parameter dan tanpa tipe kembalian. Destructor dijalankan otomatis satu kali pada akhir masa hidup objek, yaitu saat objek lokal keluar dari lingkupnya atau saat objek dinamis dihapus. Setiap kelas hanya boleh memiliki satu destructor, karena destructor tidak dapat di-overload dan tidak menerima argumen apa pun.

cpp masa-hidup.cpp
#include <iostream>
#include <string>

class Pencatat {
private:
    std::string nama;

public:
    Pencatat(const std::string& namaBaru) {
        nama = namaBaru;
        std::cout << "Objek dibuat     : " << nama << "\n";
    }

    ~Pencatat() {
        std::cout << "Objek dihancurkan: " << nama << "\n";
    }
};

int main() {
    Pencatat luar("luar");
    {
        Pencatat dalamA("blok dalam A");
        Pencatat dalamB("blok dalam B");
    }
    std::cout << "Badan main berakhir\n";
    return 0;
}
text Keluaran
Objek dibuat     : luar
Objek dibuat     : blok dalam A
Objek dibuat     : blok dalam B
Objek dihancurkan: blok dalam B
Objek dihancurkan: blok dalam A
Badan main berakhir
Objek dihancurkan: luar

Keluaran tersebut memperlihatkan dua aturan sekaligus. Objek dihancurkan tepat saat lingkupnya berakhir, dan urutan penghancurannya adalah kebalikan dari urutan pembuatannya. Objek blok dalam B dibuat terakhir namun dihancurkan lebih dulu, karena setiap objek yang dibuat belakangan mungkin bergantung pada objek yang dibuat lebih awal. Pemahaman urutan ini penting pada kelas yang saling membaca data saat membersihkan diri.

Kontak pertama dengan RAII#

Resource Acquisition Is Initialization (RAII) adalah pola yang mengaitkan sumber daya dengan masa hidup objek, yaitu sumber daya diambil di dalam constructor dan dilepaskan di dalam destructor. Sumber daya di sini berarti apa pun yang harus dikembalikan setelah dipakai, misalnya memori dinamis, berkas yang terbuka, atau kunci yang dikunci. Compiler yang menjalankan destructor secara otomatis kemudian menjadi penjamin pelepasannya.

cpp raii-buffer.cpp
#include <iostream>

class ArsipAngka {
private:
    int* data = nullptr;
    int jumlah = 0;

public:
    ArsipAngka(int n) {
        jumlah = n;
        data = new int[jumlah];
        for (int i = 0; i < jumlah; ++i) {
            data[i] = i + 1;
        }
        std::cout << "Buffer untuk " << jumlah << " angka dialokasikan\n";
    }

    ~ArsipAngka() {
        delete[] data;
        std::cout << "Buffer dibebaskan\n";
    }

    int ambil(int indeks) const {
        return data[indeks];
    }
};

int main() {
    {
        ArsipAngka angka(5);
        std::cout << "Angka ketiga = " << angka.ambil(2) << "\n";
    }
    std::cout << "Memori sudah dibebaskan secara otomatis\n";
    return 0;
}
text Keluaran
Buffer untuk 5 angka dialokasikan
Angka ketiga = 3
Buffer dibebaskan
Memori sudah dibebaskan secara otomatis

Program di atas tidak memanggil delete[] secara manual, tetapi memori tetap terbebas karena destructor kelas ArsipAngka yang melakukannya. Pola ini menutup kesalahan yang paling sering terjadi pada pengelolaan memori manual, yaitu jalur kode yang lupa membebaskan memori atau melompat melewatinya karena return di tengah fungsi. Apa pun cara fungsi itu berakhir, destructor tetap dijalankan. Pola yang sama dikembangkan penuh pada modul RAII, sementara modul ini cukup memperlihatkan bahwa pasangan constructor dan destructor sudah menyediakan fasilitasnya.

Constructor yang mendelegasikan#

C++ mengizinkan satu constructor memanggil constructor lain pada kelas yang sama, sehingga logika inisialisasi cukup ditulis satu kali. Bentuknya disebut delegating constructor, yaitu constructor yang daftar inisialisasinya menyebut nama kelas itu sendiri beserta argumen constructor lain. Pola ini menghindari duplikasi penugasan member ketika beberapa bentuk konstruk harus menghasilkan keadaan yang sama.

cpp konstruktor-delegasi.cpp
#include <iostream>

class PengaturWaktu {
private:
    int detik;

public:
    PengaturWaktu(int h, int m, int s) : detik{h * 3600 + m * 60 + s} {
        std::cout << "Constructor lengkap: " << detik << " detik\n";
    }

    PengaturWaktu(int m, int s) : PengaturWaktu{0, m, s} {
        std::cout << "Constructor delegasi (menit)\n";
    }

    PengaturWaktu() : PengaturWaktu{0, 0} {
        std::cout << "Constructor delegasi (nol)\n";
    }

    int total() const { return detik; }
};

int main() {
    PengaturWaktu penuh(1, 2, 3);
    PengaturWaktu viaMenit(5, 30);
    PengaturWaktu nol;
    std::cout << "Total terakhir = " << nol.total() << "\n";
    return 0;
}
text Keluaran
Constructor lengkap: 3723 detik
Constructor lengkap: 330 detik
Constructor delegasi (menit)
Constructor lengkap: 0 detik
Constructor delegasi (menit)
Constructor delegasi (nol)
Total terakhir = 0

Keluaran tersebut memperlihatkan urutan yang sering salah ditebak. Badan constructor target, yaitu constructor yang menjadi tujuan delegasi, dijalankan lebih dulu. Badan constructor yang mendelegasikan baru dieksekusi setelahnya, sehingga pesan Constructor delegasi selalu muncul setelah pesan Constructor lengkap. Pada PengaturWaktu nol, delegasinya membentuk rantai dua tingkat, yaitu constructor tanpa parameter mendelegasikan ke constructor dua parameter, yang kemudian mendelegasikan ke constructor tiga parameter. Rantai panjang seperti ini sah, tetapi loop delegasi yang mengarah ke dirinya sendiri ditolak compiler karena tidak pernah selesai.

Kesalahan umum#

Kesalahan berikut paling sering muncul pada pemakaian constructor dan destructor.

  • Mengharapkan default constructor yang sudah tidak ada. Begitu satu constructor berparameter ditulis, deklarasi objek tanpa argumen ditolak compiler, dan perbaikannya adalah menulis ulang default constructor secara eksplisit.
  • Membuat destructor dengan parameter atau tipe kembalian. Bentuk itu tidak sah, sehingga compiler menolaknya. Destructor yang benar selalu ditulis ~NamaKelas() tanpa keduanya.
  • Menebak urutan penghancuran objek searah dengan pembuatannya. Urutan yang benar adalah kebalikannya, sehingga logika pembersihan yang bergantung urutan perlu ditulis dengan kesadaran penuh.

Ringkasan#

  • Constructor dijalankan otomatis saat objek dibuat dan menjamin seluruh member berisi nilai yang sah.
  • Default constructor dihasilkan compiler hanya apabila tidak ada constructor lain yang ditulis.
  • Destructor dijalankan satu kali di akhir masa hidup objek, dengan urutan kebalikan dari urutan pembuatannya.
  • RAII menempatkan pengambilan sumber daya di constructor dan pelepasannya di destructor, sehingga pembersihan menjadi tanggung jawab compiler.

Referensi#