Kode berikut menyebabkan kebocoran memori:
#include <iostream>
#include <memory>
#include <vector>
using namespace std;
class base
{
void virtual initialize_vector() = 0;
};
class derived : public base
{
private:
vector<int> vec;
public:
derived()
{
initialize_vector();
}
void initialize_vector()
{
for (int i = 0; i < 1000000; i++)
{
vec.push_back(i);
}
}
};
int main()
{
for (int i = 0; i < 100000; i++)
{
unique_ptr<base> pt = make_unique<derived>();
}
}
Itu tidak masuk akal bagi saya, karena kelas yang diturunkan tidak mengalokasikan memori dinamis mentah, dan unique_ptr mendeallocate sendiri. Saya mendapatkan destruktor implisit basis kelas itu disebut bukan diturunkan, tapi saya tidak mengerti mengapa itu menjadi masalah di sini. Jika saya menulis destruktor eksplisit untuk diturunkan, saya tidak akan menulis apa pun untuk vec.
c++
inheritance
memory
allocation
Ketidaktahuan inersia
sumber
sumber
~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.Jawaban:
Ketika kompiler pergi untuk mengeksekusi implisit
delete _ptr;
di dalamunique_ptr
destruktor (di mana_ptr
pointer disimpan diunique_ptr
), ia tahu persis dua hal:_ptr
. Karena pointer ada diunique_ptr<base>
, itu artinya_ptr
adalah tipebase*
.Ini yang diketahui kompiler. Jadi, mengingat bahwa itu menghapus objek tipe
base
, itu akan memanggil~base()
.Jadi ... di mana bagian di mana ia menghancurkan
dervied
objek yang sebenarnya ditunjukkannya? Karena jika kompiler tidak tahu bahwa itu menghancurkanderived
, maka ia tidak tahuderived::vec
sama sekali , apalagi bahwa itu harus dihancurkan. Jadi, Anda telah merusak objek dengan membiarkan setengahnya tidak rusak.Kompilator tidak dapat berasumsi bahwa
base*
yang dihancurkan sebenarnya adalahderived*
; Bagaimanapun, mungkin ada sejumlah kelas yang berasalbase
. Bagaimana ia tahu jenis yangbase*
ditunjuk untuk jenis ini ?Apa yang harus dilakukan oleh kompiler adalah mencari tahu destructor yang benar untuk dipanggil (ya,
derived
memiliki destructor. Kecuali Anda= delete
seorang destructor, setiap kelas memiliki destructor, baik Anda menulis satu atau tidak). Untuk melakukan ini, itu harus menggunakan beberapa informasi yang disimpan dalambase
untuk mendapatkan alamat yang tepat dari kode destruktor untuk memohon, informasi yang ditetapkan oleh konstruktor dari kelas aktual. Maka harus menggunakan informasi ini untuk mengkonversibase*
ke pointer ke alamatderived
kelas yang sesuai (yang mungkin atau mungkin tidak pada alamat yang berbeda. Ya, sungguh). Dan kemudian dapat memanggil destruktor itu.Mekanisme yang baru saja saya jelaskan? Ini biasa disebut "pengiriman virtual": alias, hal yang terjadi setiap kali Anda memanggil fungsi yang ditandai
virtual
ketika Anda memiliki pointer / referensi ke kelas dasar.Jika Anda ingin memanggil fungsi kelas turunan ketika semua yang Anda miliki adalah pointer / referensi kelas dasar, fungsi itu harus dideklarasikan
virtual
. Destructors secara fundamental tidak berbeda dalam hal ini.sumber
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 ditemukanBase
, atau implementasi yang ditemukan diDerived
. Ada dua cara untuk memutuskan ini:Base
jenis, maksud Anda implementasi diBase
.Base
diketik bisa sajaBase
, atauDerived
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
danDerived2
? 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
danDerived
? Mereka hanya panggilan fungsi, sehingga perilaku panggilan fungsi terjadi. Tanpa virtual destructor yang dideklarasikan, kompiler akan memutuskan untuk mengikat langsung keBase
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.
sumber