一、派生類指針指向父類時父類的虛構函數必須設置爲虛函數
看下面的代碼,其中基類的析構函數並沒有設置爲虛函數
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data構造" << endl;
}
~Data()
{
cout << "~Data析構" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base構造" << endl;
}
~Base()
{
cout << "~Base析構" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_data(data)
{
cout << "Derived構造" << endl;
}
~Derived()
{
cout << "~Derived析構" << endl;
}
private:
Data m_data;
};
int main()
{
Base *p = new Derived(10);
delete p;
system("pause");
return 0;
}
輸出結果
發現並沒有調用派生類Derived的析構函數,也沒有對派生類的成員變量進行析構。這就是基類析構不設置成虛函數引發的問題。
解釋:雖然基類Base的指針指向的是派生類的對象,但是由於函數不是虛函數(沒有在虛函數表裏面)所以調用的時候會直接找基類的成員函數來執行。
把析構函數變成虛函數後:
class Base
{
public:
Base()
{
cout << "Base構造" << endl;
}
virtual ~Base() // 虛析構防止基類指向派生類的指針釋放時無法進行派生類的析構函數
{
cout << "~Base析構" << endl;
}
};
結果如下
說明成功的調用了派生類的析構函數了,並且也調用了成員變量的析構函數。調試過程中的虛函數表信息如下:
二、成員變量爲指針和普通數據成員在析構時的不同
查看下面的代碼,新代碼塊中多加了一個類型爲指針的成員變量m_pData,指向pData類型。
class pData
{
public:
pData(int data)
{
a = data;
cout << "pData構造" << endl;
}
~pData()
{
cout << "~pData析構" << endl;
}
private:
int a;
};
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data構造" << endl;
}
~Data()
{
cout << "~Data析構" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base構造" << endl;
}
virtual ~Base() // 虛析構防止基類指向派生類的指針釋放時無法進行派生類的析構函數
{
cout << "~Base析構" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_pData(NULL),m_data(data)
{
cout << "Derived構造" << endl;
m_pData = new pData(data);
}
~Derived()
{
cout << "~Derived析構" << endl;
}
private:
pData* m_pData;
Data m_data;
};
int main()
{
Base *p = new Derived(10);
delete p;
system("pause");
return 0;
}
打印輸出爲:
在輸出中發現,對指針pData進行了構造,卻沒有進行析構,這是爲啥呢?
我理解m_data和m_pData在對象中的內存結構是不同的,內存結構好像是下圖的樣子:
所以說,在釋放Derived對象的時候
(1)由於m_data有着完整的對象信息,就會直接調用其析構函數來對自己進行釋放。
(2)其實m_pData也是釋放了的,只是變成了一個不知道指向哪裏的指針,原來所指向的地址並沒有釋放,這種情況下也就造成了內存泄露。
下面我們改寫一下程序代碼驗證我們的想法:
class pData
{
public:
pData(int data)
{
a = data;
cout << "pData構造" << endl;
}
int getA() { return a; }
~pData()
{
cout << "~pData析構" << endl;
}
private:
int a;
};
class Data
{
public:
Data(int data)
{
a = data;
cout << "Data構造" << endl;
}
~Data()
{
cout << "~Data析構" << endl;
}
private:
int a;
};
class Base
{
public:
Base()
{
cout << "Base構造" << endl;
}
virtual pData* getPdata() { return NULL; }
virtual ~Base() // 虛析構防止基類指向派生類的指針釋放時無法進行派生類的析構函數
{
cout << "~Base析構" << endl;
}
};
class Derived : public Base
{
public:
Derived(int data):m_pData(NULL),m_data(data)
{
cout << "Derived構造" << endl;
m_pData = new pData(data);
}
pData* getPdata()
{
return m_pData;
}
~Derived()
{
cout << "~Derived析構" << endl;
}
private:
pData* m_pData;
Data m_data;
};
int main()
{
Base *p = new Derived(8);
pData* pdata = p->getPdata(); // 保存指針指向的實際地址
cout << (*pdata).getA() << endl;
delete p;
cout << (*pdata).getA() << endl;
system("pause");
return 0;
}
在delete前後都打印pdata指向的內存塊裏面的值,我們發現就算是delete掉了也是可以打印出來的
調試的時候,也可以發現derived對象中的m_pdata確實是一個不知道指向哪裏的野指針了
三、總結
1. 基類的析構函數最好聲明爲虛函數,雖然會在虛函數表中多佔一個指針空間,但是會有效的防止不能調用派生類析構的情況。
2. 如果一個類的成員變量中有指針,在析構函數中一定要手動釋放掉。而且要考慮寫好拷貝構造和賦值運算符重載函數(具體原因參考之前寫的博客:有指針時拷貝構造和賦值運算符的不同和帶來的問題https://blog.csdn.net/struggle6688/article/details/105606661)。這個地方是經常出錯的,一定要小心小心再小心。。。
涉及到內存結構的知識掌握的也不深,這些都是在VS2017下實驗得出的結論,如果有問題請告訴我。