-
1 # 夏夜辰風68
-
2 # KuangXiang
虛擬函式,簡單來說就是實現多型,多型,就是程式執行的時候能夠決定呼叫父類的還是子類的函式,就這麼個意思。看我的專欄以及我的文章,都對多型有非常詳細的描述!如果你想對多型有細緻入骨的瞭解,可以看我的專欄《c++物件模型探索》。
-
3 # IT達人說
虛擬函式的作用就是實現“動態聯編”,也就是在程式的執行階段動態地選擇合適的成員函式。
具體的實現方式是:在定義了虛擬函式後,可以在基類的派生類中對虛擬函式重新定義,在派生類中重新定義的函式應與虛擬函式具有相同的形參個數和形參型別。以實現統一的介面,不同定義過程。如果在派生類中沒有對虛擬函式重新定義,則它繼承其基類的虛擬函式。編譯器在編譯過程中發現類的函式名前的關鍵字virtual後,會自動將其作為動態聯編處理,即在程式執行時動態地選擇合適的成員函式。
-
4 # 程式設計老大叔
我們都知道程式碼執行時各個系統會為各種物件分配記憶體,每個具體的函式其實就是一個具體的物件,那麼系統在程式執行時也會為每個方法分配對應的記憶體。而且之前有講過,為了避免記憶體的浪費,所有同類的物件是共享同一函式記憶體的。但是當有繼承發生時,函式呼叫方式是怎麼樣的呢?
普通函式的呼叫方式:
假設這裡有一個很簡單的類Base
我們例項化出來一個類(Base b)然後在vs裡用除錯模式看一下變數b裡面的內容:
可以看到,物件b中只有成員a並沒有函式。用sizeof(b)看一下物件大小為4,只有一個整型變數的記憶體 。
我們可以再例項化幾個物件看看,結果還是一樣的。這是什麼原因呢?假設如果類成員方法也如同屬性一樣計算在例項大小中的話,那麼我們每例項化一個物件時,系統都需要分配更大的記憶體給每個物件。而函式呼叫與屬性不同,屬性每個物件的自有屬性值不同,需要做區分,但是函式是公用的,不同的知識函式引數和函式體內部類物件的值,這些都是可以透過呼叫物件的屬性找到的,所以就沒必要給每個物件分配函式記憶體。而是採用下圖所示的呼叫方式:
畫起來感覺像神經網路,哈哈!其實很簡單,就是函式存在程式碼區,每個物件要呼叫的時候直接去對應地址呼叫就可以了。
好,接下來問題來了,那麼碰到繼承怎麼處理呢?
虛擬函式的作用
虛擬函式到底有什麼用呢?這邊有兩個類Base和Child,其中Child繼承自Base。
class Base {
public:
int a ;
virtual void aFunc() { cout << "Base::aFunc" << endl; }
virtual void bFunc() { cout << "Base::bFunc" << endl; }
void cFunc() { cout << "Base::cFunc" << endl; }
};
class Child:public Base{
public:
int n ;
void aFunc() { cout << "Child::aFunc" << endl; }
void bFunc() { cout << "Child::bFunc" << endl; }
void cFunc() { cout << "Child::cFunc" << endl; }
};
這裡,我們先只看兩個類中的cFunc()函式,各自例項化Base b ;和Child c ;
分別呼叫cFunc()函式。
Base b;
Child c ;
b.cFunc();
c.cFunc();
c.Base::cFunc();
執行結果:
Base::cFunc
Child::cFunc
Base::cFunc
好像並沒有什麼問題,Child類中會覆蓋父類Base 中的cFunc()函式,然後透過::域限定符由可以呼叫父類cFunc()函式。
但是,當我們想用父類物件呼叫子類函式時,這樣的寫法又不行:
Base* b_p = new Child;
b_p->cFunc();
結果還是:Base::cFunc
但是如果將Base類中cFunc改為虛擬函式virtual cFunc後再呼叫,結果就變成
Child::cFunc,說明父類呼叫子類方法成功;
這就是虛擬函式的作用,實現多型;
虛擬函式和虛擬函式表
C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後透過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。
概念講完,我們來看一下例項,將上述的Base類中的三個函式中間其中兩個函式改成虛擬函式,具體怎麼實現呢?這就需要用到C++的關鍵virtual。
接下來例項化一個物件b,我們除錯並在變量表中檢視變數b,看看會發生什麼?
我們除了看到有成員a之外還有一個叫_vfptr的成員,此時sizeof(b)的結果已經變成了8。_vfptr的型別為void**型別並且它好像陣列一樣還有還可以用下標訪問,說明他是一個集合。那麼它代表什麼意思呢?這其實就是一個虛擬函式表,這個虛擬函式表存什麼呢?對,存地址嘛,地址可以直接找到對應函式,而且地址本身所佔空間很小,這樣就不浪費空間了。
虛擬函式表,如此看來就是一個儲存著類成員函式的地址的一個數組。如此,我們看繼承的時候系統是怎麼處理的。
繼承
首先建立一個類Child繼承於Base,完全不去覆蓋父類的虛擬函式,如圖:
我們例項化一個Child物件,Child c;並且sizeof(c)檢視其大小。除錯檢視變數:
可以看到c中也有一個虛擬函式表,並且地址與父類的地址不同,說明這是兩個記憶體,但是我們看到虛擬函式表中的成員均與父類虛擬函式表中一致,這說明,c中的虛擬函式都是指向父類中的虛擬函式的。
重寫父類函式
class Base {
public:
int a ;
virtual void aFunc() { cout << "Base::aFunc" << endl; }
virtual void bFunc() { cout << "Base::bFunc" << endl; }
void cFunc() { cout << "Base::cFunc" << endl; }
};
class Child:public Base{
public:
int n ;
void aFunc() { cout << "Child::aFunc" << endl; }
void bFunc() { cout << "Child::bFunc" << endl; }
void cFunc() { cout << "Child::cFunc" << endl; }
};
我們看下各自虛擬函式表:
兩個虛擬函式表中的內容已經完全不一樣了,這說明在子類中重寫了父類的方法,執行時間系統重新分配了記憶體,如圖所示:
父調子
在main函式中新增程式碼:
Base* b_p = new Child;
b_p->aFunc();
除錯檢視各變數內容
我們注意看物件b_p中的虛擬函式表第0項,也就是aFunc() 的地址跟c中虛擬函式表第0項,也就是aFunc()一樣,而不是與b一樣,這就說明父類虛擬函式表中的某個函式指標指向了子類的函式。
回覆列表
虛擬函式是學習類和物件這一模組的一個重點,對於部分人來說可能也是個難點。有必要詳細講解。以下是本人親自除錯的結果,除錯基於windows下的VisualStudio2017。虛擬函式的作用:使得透過基類型別的指標,可以使屬於不同派生類的不同物件產生不同的行為。否則基類指標訪問派生類物件時訪問到的只是從基類繼承來的同名成員。廢話少說直接上圖。
執行結果:
這是Base的display()
這是Base2的display()
這是Base3的display()
由以上的部分截圖可以看到,Base是基類,Base2,Base3是派生類。display的函式有virtual關鍵字宣告,為虛擬函式。因為display為虛擬函式,使得fun函式成功的實現了透過基類指標訪問到了正在指向的物件成員,這就是執行中的多型。而如果我們沒有沒有宣告為虛擬函式,則執行結果是這樣的:
這是Base的display()
這是Base的display()
這是Base的display()
原因在於派生類並沒有改寫從基類繼承來的display()函式,只是多了一個同名函式而已。而透過基類指標訪問到的只是從基類繼承來的那個。
虛解構函式在c++中雖然不能宣告虛建構函式,但是可以宣告虛解構函式,當一個類的解構函式是虛擬函式,那麼由它派生來的所有子類的解構函式也是虛擬函式。值得注意的是:如果有可能透過基類指標呼叫物件的解構函式(透過delete),就需要讓基類的解構函式為虛擬函式。再上圖:
執行結果:
Base destructor