C++虚基类详解
我們知道,如果一個派生類有多個直接基類,而這些直接基類又有一個共同的基類,則在最終的派生類中會保留該間接共同基類數據成員的多份同名成員。在引用這些同名的成員時,必須在派生類對象名后增加直接基類名,以避免產生二義性,使其惟一地標識一個成員,如:
? ? c1.A::display( )
在一個類中保留間接共同基類的多份同名成員,雖然有時是有必要的,可以在不同的數據成員中分別存放不同的數據,也可以通過構造函數分別對它們進行初始化。但在大多數情況下,這種現象是人們不希望出現的。因為保留多份數據成員的拷貝,不僅占用較多的存儲空間,還增加了訪問這些成員時的困難,容易出錯。而且在實際上,并不需要有多份拷貝。
C++提供虛基類(virtual base class)的方法,使得在繼承間接共同基類時只保留一份成員。假設類D是類B和類C公用派生類,而類B和類C又是類A的派生類,如圖11.21所示。 設類A有數據成員data和成員函數fun;派生類B和C分別從類A繼承了data和fun,此外類B還增加了自己的數據成員data_b,類C增加了數據成員data_c。如果不用虛基類,就會在類D中保留了類A成員data的兩份拷貝,分別表示為int B::data和int C::data。同樣有兩個同名的成員函數,表示為void B::fun()和void C::fun()。類B中增加的成員data_b和類C中增加的成員dat_c不同名,不必用類名限定。此外,類D還增加了自己的數據成員data_d和成員函數fun_d。
圖 11.21
現在,將類A聲明為虛基類,方法如下: 復制純文本新窗口class A //聲明基類A { // 代碼 }; class B: virtual public A //聲明類B是類A的公用派生類,A是B的虛基類 { // 代碼 }; class C: virtual public A //聲明類C是類A的公用派生類,A是C的虛基類 { // 代碼 }; class A //聲明基類A
{// 代碼
};
class B: virtual public A //聲明類B是類A的公用派生類,A是B的虛基類
{// 代碼
};
class C: virtual public A //聲明類C是類A的公用派生類,A是C的虛基類
{// 代碼
}; 注意: 虛基類并不是在聲明基類時聲明的,而是在聲明派生類時,指定繼承方式時聲明的。因為一個基類可以在生成一個派生類時作為虛基類,而在生成另一個派生類時不作為虛基類。
聲明虛基類的一般形式為:
? ?class 派生類名: virtual 繼承方式 ?基類名
即在聲明派生類時,將關鍵字 virtual 加到相應的繼承方式前面,經過這樣的聲明后,當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次,也就是說,基類成員只保留一次。
需要注意:為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類。否則仍然會出現對基類的多次繼承。
如果在派生類B和C中將類A聲明為虛基類,而在派生類D中沒有將類A聲明為虛基類,則在派生類E中,雖然從類B和C路徑派生的部分只保留一份基類成員,但從類D路徑派生的部分還保留一份基類成員。class A //定義基類A { A(int i){ } //基類構造函數,有一個參數}; class B :virtual public A //A作為B的虛基類 { B(int n):A(n){ } //B類構造函數,在初始化表中對虛基類初始化 }; class C :virtual public A //A作為C的虛基類 { C(int n):A(n){ } //C類構造函數,在初始化表中對虛基類初始化 }; class D :public B,public C //類D的構造函數,在初始化表中對所有基類初始化 { D(int n):A(n),B(n),C(n){ } }; class A //定義基類A
{A(int i){ } //基類構造函數,有一個參數};
class B :virtual public A //A作為B的虛基類
{B(int n):A(n){ } //B類構造函數,在初始化表中對虛基類初始化
};
class C :virtual public A //A作為C的虛基類
{C(int n):A(n){ } //C類構造函數,在初始化表中對虛基類初始化
};
class D :public B,public C //類D的構造函數,在初始化表中對所有基類初始化
{D(int n):A(n),B(n),C(n){ }
}; 注意: 在定義類D的構造函數時,與以往使用的方法有所不同。以往,在派生類的構造函數中只需負責對其直接基類初始化,再由其直接基類負責對間接基類初始化。現在,由于虛基類在派生類中只有一份數據成員,所以這份數據成員的初始化必須由派生類直接給出。如果不由最后的派生類直接對虛基類初始化,而由虛基類的直接派生類(如類B和類C)對虛基類初始化,就有可能由于在類B和類C的構造函數中對虛基類給出不同的初始化參數而產生矛盾。所以規定:在最后的派生類中不僅要負責對其直接基類進行初始化,還要負責對虛基類初始化。
有的讀者會提出:類D的構造函數通過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也通過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?大家不必過慮,C++編譯系統只執行最后的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生類(如類B和類C) 對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被多次初始化。#include <iostream> #include <string> using namespace std; //聲明公共基類Person class Person { public: Person(string nam,char s,int a) //構造函數 { name=nam; sex=s; age=a; } protected: //保護成員 string name; char sex; int age; }; ? //聲明Person的直接派生類Teacher class Teacher:virtual public Person //聲明Person為公用繼承的虛基類 { public: Teacher(string nam,char s,int a, string t):Person(nam,s,a)//構造函數 { title=t; } protected: //保護成員 string title; //職稱 }; ? //聲明Person的直接派生類Student class Student:virtual public Person //聲明Person為公用繼承的虛基類 { public: Student(string nam,char s,int a,float sco) //構造函數 :Person(nam,s,a),score(sco){ } //初始化表 protected: //保護成員 float score; //成績 }; ? //聲明多重繼承的派生類Graduate class Graduate:public Teacher,public Student //Teacher和Student為直接基類 { public: Graduate(string nam,char s,int a, string t,float sco,float w)//構造函數 :Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){} //初始化表 void show( ) //輸出研究生的有關數據 { cout<<"name:"<<name<<endl; cout<<"age:"<<age<<endl; cout<<"sex:"<<sex<<endl; cout<<"score:"<<score<<endl; cout<<"title:"<<title<<endl; cout<<"wages:"<<wage<<endl; } private: float wage; //工資 }; ? //主函數 int main( ) { Graduate grad1("Wang-li",'f',24,"assistant",89.5,1234.5); grad1.show( ); return 0; } #include <iostream>
#include <string>
using namespace std;
//聲明公共基類Person
class Person
{
public:Person(string nam,char s,int a) //構造函數{name=nam;sex=s;age=a;}
protected: //保護成員string name;char sex;int age;
};//聲明Person的直接派生類Teacher
class Teacher:virtual public Person //聲明Person為公用繼承的虛基類
{
public: Teacher(string nam,char s,int a, string t):Person(nam,s,a)//構造函數{title=t;}
protected: //保護成員string title; //職稱
};//聲明Person的直接派生類Student
class Student:virtual public Person //聲明Person為公用繼承的虛基類
{
public:Student(string nam,char s,int a,float sco) //構造函數:Person(nam,s,a),score(sco){ } //初始化表
protected: //保護成員float score; //成績
};//聲明多重繼承的派生類Graduate
class Graduate:public Teacher,public Student //Teacher和Student為直接基類
{
public:Graduate(string nam,char s,int a, string t,float sco,float w)//構造函數:Person(nam,s,a),Teacher(nam,s,a,t),Student(nam,s,a,sco),wage(w){}//初始化表void show( ) //輸出研究生的有關數據{cout<<"name:"<<name<<endl;cout<<"age:"<<age<<endl;cout<<"sex:"<<sex<<endl;cout<<"score:"<<score<<endl;cout<<"title:"<<title<<endl;cout<<"wages:"<<wage<<endl;}
private:float wage; //工資
};//主函數
int main( )
{Graduate grad1("Wang-li",'f',24,"assistant",89.5,1234.5);grad1.show( );return 0;
}
對程序的兩點說明:
1) 請注意各類的構造函數的寫法。在Person類中定義了包含3個形參的構造函數,用它對數據成員name、sex和age進行初始化。在Teacher和Student類的構造函數中,按規定要在初始化表中包含對基類的初始化,盡管對虛基類來說,編譯系統不會由此調用基類的構造函數,但仍然應當按照派生類構造函數的統一格式書寫。在最后派生類Graduate的構造函數中,既包括對虛基類構造函數的調用,也包括對其直接基類的初 始化。
2) 在Graduate類中,只保留一份基類的成員,因此可以用Graduate類中的show函數引用Graduate類對象中的公共基類Person的數據成員name、sex、age的值,不需要加基類名和域運算符(::),不會產生二義性。
可以看到:使用多重繼承時要十分小心,經常會出現二義性問題。前面介紹的例子是簡單的,如果派生的層次再多一些,多重繼承更復雜一些,程序設計人員很容易陷人迷 魂陣,程序的編寫、調試和維護工作都會變得更加困難。因此,許多專業人員認為:不要提倡在程序中使用多重繼承,只有在比較簡單和不易出現二義性的情況或實在必要時才使用多重繼承,能用單一繼承解決的問題就不要使用多重繼承。也是由于這個原因,有些面向對象的程序設計語言(如Java,Smalltalk)并不支持多重繼承。
? ? c1.A::display( )
在一個類中保留間接共同基類的多份同名成員,雖然有時是有必要的,可以在不同的數據成員中分別存放不同的數據,也可以通過構造函數分別對它們進行初始化。但在大多數情況下,這種現象是人們不希望出現的。因為保留多份數據成員的拷貝,不僅占用較多的存儲空間,還增加了訪問這些成員時的困難,容易出錯。而且在實際上,并不需要有多份拷貝。
C++提供虛基類(virtual base class)的方法,使得在繼承間接共同基類時只保留一份成員。假設類D是類B和類C公用派生類,而類B和類C又是類A的派生類,如圖11.21所示。 設類A有數據成員data和成員函數fun;派生類B和C分別從類A繼承了data和fun,此外類B還增加了自己的數據成員data_b,類C增加了數據成員data_c。如果不用虛基類,就會在類D中保留了類A成員data的兩份拷貝,分別表示為int B::data和int C::data。同樣有兩個同名的成員函數,表示為void B::fun()和void C::fun()。類B中增加的成員data_b和類C中增加的成員dat_c不同名,不必用類名限定。此外,類D還增加了自己的數據成員data_d和成員函數fun_d。
圖 11.21
現在,將類A聲明為虛基類,方法如下: 復制純文本新窗口
聲明虛基類的一般形式為:
? ?class 派生類名: virtual 繼承方式 ?基類名
即在聲明派生類時,將關鍵字 virtual 加到相應的繼承方式前面,經過這樣的聲明后,當基類通過多條派生路徑被一個派生類繼承時,該派生類只繼承該基類一次,也就是說,基類成員只保留一次。
需要注意:為了保證虛基類在派生類中只繼承一次,應當在該基類的所有直接派生類中聲明為虛基類。否則仍然會出現對基類的多次繼承。
如果在派生類B和C中將類A聲明為虛基類,而在派生類D中沒有將類A聲明為虛基類,則在派生類E中,雖然從類B和C路徑派生的部分只保留一份基類成員,但從類D路徑派生的部分還保留一份基類成員。
虛基類的初始化
如果在虛基類中定義了帶參數的構造函數,而且沒有定義默認構造函數,則在其所有派生類(包括直接派生或間接派生的派生類)中,通過構造函數的初始化表對虛基類進行初始化。例如 復制純文本新窗口有的讀者會提出:類D的構造函數通過初始化表調了虛基類的構造函數A,而類B和類C的構造函數也通過初始化表調用了虛基類的構造函數A,這樣虛基類的構造函數豈非被調用了3次?大家不必過慮,C++編譯系統只執行最后的派生類對虛基類的構造函數的調用,而忽略虛基類的其他派生類(如類B和類C) 對虛基類的構造函數的調用,這就保證了虛基類的數據成員不會被多次初始化。
虛基類的簡單應用舉例
[例11.9] 在例11. 8(具體代碼請查看:C++類的多重繼承)的基礎上,在Teacher類和Student類之上增加一個共同的基類Person。作為人員的一些基本數據都放在Person中,在 Teacher類和Student類中再增加一些必要的數據。可寫出以下程序: 復制純文本新窗口對程序的兩點說明:
1) 請注意各類的構造函數的寫法。在Person類中定義了包含3個形參的構造函數,用它對數據成員name、sex和age進行初始化。在Teacher和Student類的構造函數中,按規定要在初始化表中包含對基類的初始化,盡管對虛基類來說,編譯系統不會由此調用基類的構造函數,但仍然應當按照派生類構造函數的統一格式書寫。在最后派生類Graduate的構造函數中,既包括對虛基類構造函數的調用,也包括對其直接基類的初 始化。
2) 在Graduate類中,只保留一份基類的成員,因此可以用Graduate類中的show函數引用Graduate類對象中的公共基類Person的數據成員name、sex、age的值,不需要加基類名和域運算符(::),不會產生二義性。
可以看到:使用多重繼承時要十分小心,經常會出現二義性問題。前面介紹的例子是簡單的,如果派生的層次再多一些,多重繼承更復雜一些,程序設計人員很容易陷人迷 魂陣,程序的編寫、調試和維護工作都會變得更加困難。因此,許多專業人員認為:不要提倡在程序中使用多重繼承,只有在比較簡單和不易出現二義性的情況或實在必要時才使用多重繼承,能用單一繼承解決的問題就不要使用多重繼承。也是由于這個原因,有些面向對象的程序設計語言(如Java,Smalltalk)并不支持多重繼承。
總結
- 上一篇: 虚函数的实现机制
- 下一篇: Java泛型的实现原理