以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16631774.html 先說結論: 構造函數不能聲明為虛函數,析構函數可以聲明為虛函數。 構造函數可以聲明為虛函數嗎? 虛函數表裡都存了些什麼東西?不是金 ...
以下內容為本人的著作,如需要轉載,請聲明原文鏈接 微信公眾號「englyf」https://www.cnblogs.com/englyf/p/16631774.html
先說結論:
構造函數不能聲明為虛函數,析構函數可以聲明為虛函數。
構造函數可以聲明為虛函數嗎?
虛函數表裡都存了些什麼東西?不是金,不是銀,是對應類里聲明為虛函數的成員地址。在編譯期,每個類的虛函數表即被分配和生成。同一個類的所有實例對象都是共用這個虛函數表的,那麼每個實例對象也就會隱含有一個成員指針變數專門用來存儲虛函數表的地址。這個隱含的成員指針變數需要在實例對象初始化後才會指向虛函數表。
很顯然,對象不能在沒有初始化之前就知道自己對應的虛函數表在哪裡,因此也不能在對象初始化之前調用訪問虛函數表的內容(虛函數)。是不是在對象初始化之前就不能調用訪問這些虛函數成員了?不是這個意思,這裡說的不能僅限於通過虛函數表來訪問(比如派生後的類實例對象通過父類的指針變數訪問調用虛函數成員),也就是動態訪問時才需要虛函數表的信息。不過,就算某個成員函數被聲明為虛函數時,也可以通過類的靜態特性合法訪問的,這是無關痛癢的題外話,評價____。
對象的初始化就是依賴於構造函數的執行,首先是找到最上一層父類的構造函數並執行,然後逐層往下執行構造函數,直到執行完當前類(被實例化的類)的構造函數,這個過程不需要虛函數表的任何信息,在編譯期就確定了所有需要的信息了。在構造函數執行之前,對象還無法確定自身的虛函數表在哪裡,又怎麼從虛函數表裡查找對應的虛構造函數呢?如果把構造函數聲明為虛函數,那麼意義在哪兒呢?想來想去,可能費勁打造出一把刀刃能削鐵如泥的好刀,卻只是在需要敲釘子時,想起用這把刀的刀背。
所以,很明顯構造函數不能聲明為虛函數。
析構函數可以聲明為虛函數嗎?
先看下麵的代碼,析構函數不聲明為虛函數時,
#include <iostream>
using namespace std;
class Base
{
public:
Base() {
cout << "Base constructor" << endl;
}
~Base() {
cout << "Base destructor" << endl;
}
};
class Derived : public Base
{
public:
Derived() {
cout << "Derived constructor" << endl;
}
~Derived() {
cout << "Derived destructor" << endl;
}
};
class Derived_again : public Derived
{
public:
Derived_again() {
cout << "Derived_again constructor" << endl;
}
~Derived_again() {
cout << "Derived_again destructor" << endl;
}
};
int main()
{
Base *obj = new Derived();
cout << "----" << endl;
delete obj;
return 0;
}
看看編譯後執行的結果(這裡用的編譯器是g++)
Base constructor
Derived constructor
----
Base destructor
從上面的輸出來看,類Derived的析構函數沒有被調用到,這會導致典型的問題--記憶體泄漏。
然後把所有析構函數聲明為虛函數,重新編譯再看看執行結果
Base constructor
Derived constructor
----
Derived destructor
Base destructor
可以看到,需要調用的析構函數都調用了。
那麼怎麼去理解上面這段代碼的執行邏輯呢?
delete obj;
銷毀對象時,系統執行的邏輯有兩種情況。
一種是,如果析構函數沒有被聲明為虛函數時,那麼在指針obj指向的對象的虛函數表裡,是找不到虛析構函數的。由於系統無法往下查找派生類的內容,而且變數obj被聲明為某個類(上面代碼對應的是Base)的指針類型,那麼系統就轉為調用這個類(Base)的成員析構函數,這裡利用的是靜態特性。
另一種是,如果析構函數被聲明為虛函數時,先在指針obj指向的對象的虛函數表裡,嘗試尋找當前對象obj的虛析構函數,找到後執行它。對應代碼,被實例化的類是Derived,對象obj的虛析構函數地址應該指向類Derived的成員析構函數。
上面兩種情況中,找到第一個析構函數並執行後,會按照靜態特性(也就是按照編譯期生成的信息),逐層往上一層父類查找成員析構函數並執行,直到最上一層的父類的析構函數被執行完畢為止。
於是,第一種情況下,類Derived的成員析構函數被漏掉執行了,這會導致類Derived所申請的資源沒有對應的析構函數來執行釋放,記憶體泄漏發生地那麼順理成章。
可以看到,動態特性可以把事情玩得妥妥噹噹,面向對象的高級感可能就來自這裡。
總結一下,很明顯,析構函數可以聲明為虛函數,但不是必須。某些情況下,也是必須的,比如,當類指針指向的是該類的子類實例時,析構函數必須聲明為虛函數,以防止記憶體泄漏。