【C++學習筆記】虛基類(一)
1.為什麼要引入虛基類?
在類的繼承中,如果我們遇到這種情況:
“B和C同時繼承A,而B和C都被D繼承”
在此時,假如A中有一個函式fun()當然同時被B和C繼承,而D按理說繼承了B和C,同時也應該能呼叫fun()函式。這一呼叫就有問題了,到底是要呼叫B中的fun()函式還是呼叫C中的fun()函式呢?在C++中,有兩種方法實現呼叫:
(注意:這兩種方法效果是不同的)
- 使用作用域識別符號來唯一表示它們比如:
B::fun()
- 另一種方法是定義虛基類,使派生類中只保留一份拷貝。
作用域識別符號表示
例子:
#include<iostream> using namespace std; class base{ public: base(){a=5;cout<<"base="<<a<<endl;} procted: int a; }**;** class base1:public base{ public: base1() {a=a+10;cout<<"base1="<<a<<endl;} }; class base2:public base{ public: base2(){a=a+20;cout<<"base2="<<a<<endl;} }; class derived:public base1, public base2{ public: derived(){ cout<<"base1::a="<<base1::a<<endl; cout<<"base2::a="base2::a<<endl; } }; int main() {derived obj; return 0; }
這是第一種方法的典型例子。寫的時候新手要注意幾個易敲錯的點:
1.多繼承定義的時候是一個許可權名對應一個基類,class derived:public base1, public base2
不能是class derived:public base1,base2
2.注意相鄰兩個基類的說明是用逗號分隔,不要再忘了。
3.老生常談的問題吧,不要忘記類定義最後的那個分號!!!!!
(我自己真的老是忘記)
這段程式的呼叫順序一定要學會熟練分析:
1.開始定義base1,而base1繼承了base類,所以base1的定義又要回到base的定義,所以先執行base的建構函式base(){a=5;cout<<"base="<<a<<endl;}
base a=5
. 2.隨後,呼叫base1的建構函式,顯示
base1 a=15
這時base1定義完畢。3.開始呼叫base2,而base2同樣繼承了base類,所以base2的定義又要再次回到base的建構函式所以這時輸出的是
base a=5
。4.隨後再呼叫base2的建構函式,輸出
base2 a=25
。5.最後在derived中分作用域呼叫a,雖然是同樣名稱的變數a,但在base1的作用域中表現為a=15,在base2作用域中表現為a=25。
所以這裡最後的答案為:
base a=5 base1 a=15 base a=5 base2 a=25 base1::a=15 base2::a=25
實際上建構函式呼叫可以通過樹狀圖來寫,特別是對於多級繼承關係,可以寫出每一級裡面繼承的基類,而每一層最後一個樹枝是該類的建構函式,而每一個基類又可以用同樣的方法展開,直到分離到最後完全沒有繼承關係的基類為止。
虛基類的呼叫:
#include<iostream>
using namespace std;
class base{
public:
base(){
a=5;cout<<"base="<<a<<endl;}
protected:
int a;
};
class base1:virtual public base{
public:
base1(){a+=10;cout<<"base1="<<a<<endl;}
};
class base2:virtual public base{
public:
base2(){a+=20;cout<<"base2="<<a<<endl;}
};
class derived:public base1,public base2{
public:
derived(){cout<<"derived a ="<<a<<endl; }
};
int main(){
derived obj;
return 0;
}
在定義了虛基類後,就等於告訴了系統,這裡的a是base1和base2所共有的,對於呼叫base1和base2建構函式的修改都是針對同一個a而言(也就是基類和兩個派生類所共有的)。而對於第一個例子中針對作用域的,相當於在繼承時把a拷貝給了base1和base2,而彼此之間的a是無關聯的。
這個過程最後為:
1.設定為虛基類後,系統知道base1和base2都是由base派生出的,所以它就統一先構造base,呼叫base的建構函式。
2.再按照順序呼叫base1和base2的建構函式,只不過在此時,大家在構造時操作的都是同一個a。
所以在虛基類中,其構造順序的思路是反著來的:
虛基類的另一種理解:虛基類的核心在於這個“虛”字,base1和base2本身作為虛基類相當於算是基類base的兩個延伸(就相當於是base的一個外掛),而對於derived類來說,最本質的基類還是base,而基類base與虛基類base1和base2組成一個基類體系,或者一個基類生態,通過對這個生態中不同虛基類的繼承,就可以形成不同的介面,生成不同的派生類。
虛基類的初始化:
我們対上面的分析再做一個總結:
(1)如果在虛基類中定義有帶形參的建構函式**,並且沒有定義預設形參的建構函式,則整個繼承結構中,所有直接或者間接的派生類都必須在建構函式的成員初始化表中列出對虛基類建構函式的呼叫。**
這句話是什麼意思呢?我們改造上面的程式碼:
#include<iostream>
using namespace std;
class base{
public:
base(int s){
a=s;cout<<"base="<<a<<endl;}
protected:
int a;
};//注意點1:base()建構函式裡面有定義形參,所以此時下面的base1,base2
//虛基類的建構函式在定義時要列出對該基類建構函式的呼叫。
class base1:virtual public base{
public:
base1(int s,int h):base(s){a+=h;cout<<"base1="<<a<<endl;}
};//注意點2:虛基類base1的第一個括號內是**“總表**”也就是裡面既要有輸入上基
//類的建構函式的引數,又要包括自己獨有的引數
class base2:virtual public base{
public:
base2(int s):base(s){a+=20;cout<<"base2="<<a<<endl;}
};
class derived:public base1,public base2{
public:
derived(int s,int h,int d):base(s),base1(s,h),base2(s){cout<<"derived a ="<<a+d<<endl; }//注意點3:此處也一樣,前面的括號裡是總表,不要忘記基類的形參int s。注
//意,此時base基類一定是先放第一個的,之後才是虛基類,而虛基類間順序沒有要求。
};
int main(){
derived obj(5,8,9);//注意點4:此處的填數順序和derived的建構函式的引數順序一樣,相當於在derived的建構函式中,冒號前的括號在接收資料,冒號後是在將接收到的資料分配到各個建構函式。
return 0;
}
我把上述注意點彙總一下:
注意點1:基類建構函式裡面有定義形參,所以此時下面的base1,base2虛基類的建構函式在定義時要列出對該基類建構函式的呼叫。
注意點2:虛基類base1的第一個括號內是**“總表**”也就是裡面既要有輸入上基類的建構函式的引數,又要包括自己獨有的引數。
注意點3:此處也一樣,前面的括號裡是總表,不要忘記基類的形參int s。注意,此時基類建構函式一定是先放第一個的,之後才是虛基類,而虛基類間順序沒有要求。
注意點4:在主函式定義變數時的填數順序和derived的建構函式的引數順序一樣,相當於在derived的建構函式中,冒號前的括號在接收資料,冒號後是在將接收到的資料分配到各個建構函式。
(2)如果一個虛基類派生出了多個派生類,那麼決定虛基類成員的,是那個最遠的派生類所呼叫的建構函式,而其他派生類呼叫的建構函式會被自動忽略。如果是同級的話(一樣遠),那就按照最後一個派生類呼叫的建構函式為準(比如圖中以子類1.1.1.1.1的呼叫為準,因為最遠)
整理自浙大課程PPT,部分理解為原創,圖為原創