Warisan
Inti dari warisan adalah untuk berbagi antarmuka dan protokol yang sama di antara banyak implementasi yang berbeda sehingga instance turunan kelas dapat diperlakukan secara identik dengan instance lain dari tipe turunan lainnya.
Dalam C + + warisan juga membawa serta rincian implementasi, menandai (atau tidak menandai) destructor sebagai virtual adalah salah satu detail implementasi tersebut.
Mengikat fungsi
Sekarang ketika fungsi, atau kasus khusus seperti konstruktor atau destruktor dipanggil, kompiler harus memilih implementasi fungsi yang dimaksud. Maka itu harus menghasilkan kode mesin yang mengikuti niat ini.
Cara paling sederhana untuk bekerja adalah dengan memilih fungsi pada waktu kompilasi dan mengeluarkan kode mesin yang cukup sehingga terlepas dari nilai apa pun, ketika potongan kode itu dijalankan, selalu menjalankan kode untuk fungsi tersebut. Ini berfungsi baik kecuali untuk warisan.
Jika kita memiliki kelas dasar dengan fungsi (bisa berupa fungsi apa pun, termasuk konstruktor atau destruktor) dan kode Anda memanggil fungsi di atasnya, apa artinya ini?
Mengambil dari contoh Anda, jika Anda menelepon initialize_vector()
kompiler harus memutuskan apakah Anda benar-benar bermaksud memanggil implementasi yang ditemukan Base
, atau implementasi yang ditemukan di Derived
. Ada dua cara untuk memutuskan ini:
- Yang pertama adalah memutuskan itu karena Anda memanggil dari suatu
Base
jenis, maksud Anda implementasi di Base
.
- Yang kedua adalah memutuskan bahwa karena tipe runtime dari nilai yang disimpan dalam nilai yang
Base
diketik bisa saja Base
, atau Derived
bahwa keputusan untuk membuat panggilan, harus dibuat pada saat runtime ketika dipanggil (setiap kali dipanggil).
Kompiler pada titik ini bingung, kedua opsi sama-sama valid. Inilah saatnya virtual
masuk ke dalam campuran. Ketika kata kunci ini hadir, kompiler mengambil opsi 2 menunda keputusan antara semua implementasi yang mungkin sampai kode berjalan dengan nilai nyata. Ketika kata kunci ini tidak ada, kompiler memilih opsi 1 karena itu adalah perilaku normal.
Kompiler mungkin masih memilih opsi 1 jika ada panggilan fungsi virtual. Tetapi hanya jika itu dapat membuktikan bahwa ini selalu terjadi.
Konstruktor dan Destruktor
Jadi mengapa kita tidak menentukan Konstruktor virtual?
Lebih intuitif bagaimana kompiler akan memilih antara implementasi identik dari konstruktor untuk Derived
dan Derived2
? Ini sangat sederhana, tidak bisa. Tidak ada nilai yang sudah ada sebelumnya dari mana kompiler dapat mempelajari apa yang sebenarnya dimaksudkan. Tidak ada nilai yang sudah ada sebelumnya karena itu adalah pekerjaan konstruktor.
Jadi mengapa kita perlu menentukan destruktor virtual?
Lebih intuitif bagaimana kompiler akan memilih antara implementasi untuk Base
dan Derived
? Mereka hanya panggilan fungsi, sehingga perilaku panggilan fungsi terjadi. Tanpa virtual destructor yang dideklarasikan, kompiler akan memutuskan untuk mengikat langsung ke Base
destructor terlepas dari jenis runtime nilai.
Dalam banyak kompiler, jika turunan tidak mendeklarasikan anggota data, atau mewarisi dari tipe lain, perilaku dalam ~Base()
akan cocok, tetapi tidak dijamin. Itu akan bekerja murni karena kebetulan, sama seperti berdiri di depan penyembur api yang belum dinyalakan. Anda baik-baik saja untuk sementara waktu.
Satu-satunya cara yang benar untuk mendeklarasikan basis atau tipe antarmuka apa pun di C ++ adalah mendeklarasikan destruktor virtual, sehingga destruktor yang benar dipanggil untuk setiap instance dari hierarki tipe tipe itu. Ini memungkinkan fungsi dengan pengetahuan paling banyak dari instance untuk membersihkan instance itu dengan benar.
~derived()
yang mendelegasikan ke destructor vec. Atau, Anda mengasumsikan bahwaunique_ptr<base> pt
akan mengetahui destruktor yang diturunkan. Tanpa metode virtual, ini tidak dapat terjadi. Sementara unique_ptr dapat diberikan fungsi penghapusan yang merupakan parameter template tanpa representasi runtime, dan fitur itu tidak berguna untuk kode ini.