您好,登錄后才能下訂單哦!
virtual在C++中有兩個重要的用途:一是解決由多繼承中父類有相同基類引起的子類中成員的二義性問題,二是實現多態。
一、解決二義性
1、引起二義性的原因
二義性是在多繼承中出現的,如果派生類的父類繼承了同一個基類,那么派生類對象訪問繼承自基類中成員時便會出現二義性。如下代碼:
#include <iostream> #include <cstdlib> class Base { public: int _b; }; class Base1: public Base { public: int _b1; }; class Base2: public Base { public: int _b1; }; class Deriver : public Base1, public Base2 { public: int _d; }; int main() { Deriver d; //d._b= system("pause"); return 0; }
派生類Derive的父類Base1和Base2都繼承了同一個基類Base,在派生類Derive中,Base的成員_b被繼承了兩次,在訪問成員_b時會出現二義性,因為無法確定訪問繼承自Base1中的_b還是訪問繼承自Base2中的_b。
2、引入虛擬繼承,解決二義性問題
讓Base1和Base2虛擬繼承Base,其他代碼不變,就可以解決Deriver中_b的二義性問題。代碼如下:
class Base1: virtual public Base { public: int _b1; }; class Base2: virtual public Base { public: int _b1; };
Base1和Base2虛擬繼承Base后的模型如下:
由上圖可以看出,當Base1虛擬繼承Base后,Base1前四字節存放了一個地址,通過這個地址可以找到Base中成員相對于存放這個地址空間的偏移,進而找到Base中的成員。
當Deriver繼承了Base1和Base2后,Deriver的模型如下:
當Base1和Base2虛擬繼承了Base后,Deriver繼承Base1和Base2就不會出現二義性的問題。因為Base的成員_d在Deriver中只保留了一份,而Base1和Base2中本來屬于_b的空間被放了兩個地址,通過這個地址可以找到_b相對于當前位置的偏移,進而找到_b。
▲當一個基類由多個派生類時,這些派生類在繼承基類時最好使用虛擬繼承,以防某個類繼承多個這些派生類時,產生二義性。
3、深度探索virtual繼承模型
在學習這部分知識時,一起學學習的同學問了我一個問題,在菱形繼承中(也就是上舉的的例子),在哪要虛擬繼承?在Deriver繼承Base1和Base2時要不要虛擬繼承?只在Deriver繼承Base1和Base2時虛擬繼承行不行? 當時學藝不精,把我也難住了,我只知道Base1和Base2虛擬繼承Base就可以解決二義性問題,個中細節并不是很清楚,于是就在VS2013和vc 6.0環境下研究了一下虛擬繼承的模型。
我們知道在直接繼承中,先繼承的放在前面,后繼承的放在后面,最后才是派生類中新增的成員。在本文開始的部分,存在二義性的那個例子就是這樣的。
那么在虛擬繼承中有什么規律呢?下面的規律是我測試了許多例子后得出的,如有錯誤還望指正。
規律一:如果一個派生類虛擬繼承一個基類,那么派生類的模型如下:
也就是說,只要有虛擬繼承,派生類模型最開始的部分必然是一個地址,這個地址間接指向虛擬繼承自基類的部分。然后是派生類定義的成員,最后是虛擬繼承自基類的部分。
規律二:如果一個派生類虛擬繼承了兩個基類,那么派生類的模型如下:
如上圖,無論派生類虛擬繼承多少個基類,在開始的部分只有一個地址,指向所有虛擬繼承自基類的開始,虛擬繼承自基類的部分放在一起,先繼承的放在前面,后繼承的放在后面。
規律三:在多繼承中,只要有虛擬繼承不論有無直接繼承,派生類模型開始的四個字節必然是一個地址,這個地址間接指向虛擬繼承自基類的開始部分。
規律四:如果在多繼承中既有直接繼承又有虛擬繼承,無論先是直接繼承還是先是虛擬繼承,在滿足規律三的前提下,存放的順序依次是直接繼承的部分、派生類固有成員、虛擬繼承基類的部分。如下圖所示:
在上面的繼承中直接繼承部分,先繼承的放在前面,后繼承的放在后面。虛擬繼承部分同理。
規律五:虛擬繼承部分永遠放在直接繼承部分和派生類定義的成員部分后面,其中虛擬繼承部分中,直接繼承類中的虛擬繼承部分放在虛擬繼承部分的最前面。如下圖所示:
二、virtual實現多態
1、什么是多態
多態即多種狀態,我的理解是,具有不同功能的函數可以用同一個函數名,這樣就可以用同一個函數名調用不同功能的函數,比如函數重載。
多態分為靜態多態如函數重載,和動態多態。動態多態是通過虛函數來實現的。
2、virtual函數實現多態
如果在基類定義一個虛函數,這個虛函數允許在派生類中重寫[virtual 函數名相同,參數類型相同,返回值類型相同(協變除外)注:函數重載、重寫、覆蓋的區別見附錄]與基類同名的函數,并且可以通過基類的指針或引用訪問基類和派生類中的同名函數。這就是多態的實現。
代碼如下:
#include <iostream>
#include <cstdlib>
using namespace std;
class Base
{
public:
virtual void Display()
{
cout << "Base::Display()" << endl;
}
};
class Deriver :public Base
{
public:
virtual void Display()
{
cout << "Deriver::Display()" << endl;
}
};
void FunTest()
{
Base b;
Base* pb = &b;
pb->Display();
Deriver d;
pb = &d;
pb->Display();
}
int main()
{
FunTest();
system("pause");
return 0;
}
上面的代碼輸出結果如下:
多態能夠實現除了虛函數外,還有一個重要的原因是基類對象的指針可以指向或者引用派生類的對象。
派生類繼承了基類,用基類指針指向或者引用派生類時,就會找到派生類繼承基類的部分。反則,派生類的指針不能指向或者引用基類的對象,這是因為如果這樣可以指向的話,就會發生越界訪問的情況。
3、深度探索多態的實現
上面說只要在基類中定義虛函數,在派生類重寫虛函數,然后就可以用基類的指針或者引用調用基類或者派生類的虛函數,那么為什么這樣能夠實現呢?
在基類定義虛函數后,基類就會產生一個地址_vfptr,這個地址指向的地方存放著所有虛函數的入口地址,以00 00 00 00即NULL結束。 派生類直接繼承這個基類時,會把這個地址也繼承過去。如果派生類重寫繼承來的虛函數,那么這個虛函數在虛表中的入口地址就會更新為重寫的虛函數的地址,那么用基類的引用或者指針指向派生類時,調用這個虛函數時就會調用派生類重寫的虛函數。
#include <iostream> #include <cstdlib> using namespace std; class Base { public: void f() { cout << "Base:: f()" << endl; } virtual void g() { cout << "Base:: g()" << endl; } virtual void h() { cout << "Base:: h()" << endl; } int _b; }; class Deriver :public Base { public: virtual void g() { cout << "Deriver:: g()" << endl; } int _d; }; int main() { Base b; Deriver d; Base* pb = &b; pb->f(); pb->g(); pb->h(); pb = &d; pb->f(); pb->g(); pb->h(); system("pause"); return 0; }
上面的代碼中,在基類定義了一個普通函數 f() 和兩個虛函數 g() 、h() ,在派生類只對g() 進行了重寫。然后用基類的指針指向基類和派生類來調用這些函數,結果如上圖所示。
Base和Derive的模型如下:
派生類在繼承基類時也把基類的虛表也繼承了,當用基類的指針指向派生類后,調用虛函數時,會找到虛表進而找到虛函數,所以在派生類對g() 重寫后,調用的就是重寫的函數,因為重寫更新了虛表。至于f()不是虛函數,但是派生類繼承了它當然可以調用它,只不過調用的是基類的函數。
4、深度探索虛表的模型
在VS2013 和vc6.0中 :
4.1 虛表地址(_vfptr)的位置
(1)如果一個基類定義了虛函數,那么_vfptr 的位置在這個基類的最前面4個字節。如果基類虛擬繼承了其他的基類,那么_vfptr的位置在指向偏移量的那個地址之后,即第二個四字節的位置
(2)在直接繼承中,有多少個含有虛函數的基類就有多少個_vfptr, _vfptr的位置滿足(1)中所述
(3)在只有虛擬繼承中,有多少個含有虛函數的基類就有多少個_vfptr,如果派生類定義新的虛函數,那么就會再產生一個派生類的_vfptr。_vfptr的位置在 指向虛基類的偏移量的地址 之后。
(4)在既有直接繼承又有虛擬繼承,如果直接繼承至少有一個有虛函數,那么,有多少個含有虛函數的基類就有多少個_vfptr, _vfptr的位置滿足(1)中所述。,如果直接繼承的基類沒有虛函數,那么滿足(3)中所述。
4.2 虛表模型
派生類在繼承基類的同時,也繼承了基類的虛表。
注意:繼承的時候并不是把基類的虛表的地址直接拿來,而是拷貝了一份虛表,虛表的地址不同,但是內容一樣的。
通過_vfptr就可以找到虛表,虛表在填寫的時候遵循一定的規律,如下:
(1)在基類中定義虛函數,先定義的虛函數在前,后定義的虛函數在后,以NULL結束
(2)在派生類中如果重寫了虛函數,那么就會更新對應的函數的入口地址。
(3)派生類新定義的虛函數數會存到第一個直接繼承的含有虛函數的基類的虛表中,存放規則如(1)所述,如果沒有直接繼承的基類,派生類會重新生成一個虛表,填寫規則如(1)所述。
注:如果繼承的多個基類含有相同的虛函數,必須在派生類重寫,否則在用派生類 類型的指針調用的函數時會出現二義性問題。同樣在多個基類定義普通的函數也會出現二義性。解決方法很簡單,就是換個函數名……
附錄:函數重載、重寫、隱藏的區別
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。