首先,虛函數的實現原理是:在定義具有虛函數的類或者繼承類的繼承的時候,會相應建立一個虛函數表vtable,即每個類都對應一個需函數表,而在定義類的對象的時候,每個對象都會有一個指向相應類的虛表指針vptr,vptr指向虛表的入口地址,在調用相應的虛函數的時候,根據該入口地址尋找對應的函數。
對於構造函數,其作用是在對象實例化的時候自動調用,對該對象進行初始化操作。前述中提到,虛函數是通過vptr來調用的,而調用構造函數的時候實例化並未完成,也就是說此時並不存在vptr,因而,無法使用vptr來調用構造函數。
另一方面,虛函數的調用是虛調用,通過在運行時查詢虛函數表得到具體函數入口地址,相當於只需要有部分信息就可以調用該函數。然而定義具體類的對象的時候,需要明確指定對象類型,而且在定義子類對象的時候首先調用的是父類的構造函數然後纔是調用子類構造函數,如果使用了虛函數,那麼僅僅是調用子類構造函數並不能完成對象的初始化。
而對於析構函數,則需要定義爲虛析構函數,防止內存泄露的發生。
如下代碼:
#include <iostream>
class A{
public:
A(){
std::cout<<"construct A!"<<std::endl;
};
~A(){
std::cout<<"A has been destructed!"<<std::endl;
}
};
class B:public A{
public:
B(){
std::cout<<"construct B!"<<std::endl;
};
~B(){
std::cout<<"B has been destructed!"<<std::endl;
}
};
int main()
{
B* ptrB=new B;
delete ptrB;
}
此時的輸出爲:
construct A
construct B
B has been destructed!
A has been destructed!
也就是對於普通的定義子類對象然後析構該對象的行爲,其首先是調用子類的析構函數,然後再調用父類的析構函數,因爲在定義子類對象的時候,首先是調用了父類對象的構造函數,然後纔是調用子類的構造函數,那麼子類對象中包含了父類的信息,析構的時候也必然需要調用父類的析構函數。
而如果使用父類的指針指向子類的對象:
int main()
{
A* ptrAB=new B;
delete ptrAB;
}
此時的輸出是
construct A
construct B
A has been constructe!
此時僅僅是調用了父類的析構函數,因爲指針類型是父類的。此時會造成的後果是,保存子類信息的那部分內存空間沒有被析構,導致內存泄露。
如果將析構函數定義成虛函數,那麼輸出的時候則和普通定義的子類的對象析構一樣,輸出爲:
construct A
construct B
B has been destructed!
A has been destructed!
此時的析構行爲纔是正確的,即先調用子類析構函數,再調用父類析構函數。