- Visual C++實用教程
- 鄭阿奇編著
- 2068字
- 2018-12-30 12:04:34
2.3 繼承和派生
繼承是面向對象語言的一個重要機制,通過繼承可以在一個一般類的基礎上建立新類。被繼承的類稱為基類(base class),在基類上建立的新類稱為派生類(derived class)。如果一個類只有一個基類則稱為單繼承,否則稱為多繼承。通過類繼承,可以使派生類有條件地具有基類的屬性,這個條件就是繼承方式。
2.3.1 單繼承
從一個基類定義一個派生類可按下列格式:
class <派生類名> : [<繼承方式>] <基類名> { [<派生類的成員>] };
其中,繼承方式有3種:public(公有)、private(私有)及protected(保護),若繼承方式沒有指定,則被指定為默認的public方式。繼承方式決定了派生類的繼承基類屬性的使用權限,下面分別說明。
1.公有繼承(public)
公有繼承的特點是基類的公有成員和保護成員作為派生類的成員時,它們都保持原有的狀態,而基類的私有成員仍然是私有的。例如:
class CStick : public CMeter { int m_nStickNum; // 聲明一個私有數據成員 public: void DispStick(); // 聲明一個公有成員函數 }; // 注意分號不能省略 void CStick:: DispStick() { m_nStickNum=GetPos(); // 調用基類CMeter的成員函數 cout<<m_nStickNum<<’ ’; }
這時,從基類CMeter派生的CStick類除具有CMeter所有公有成員和保護成員外,還有自身的私有數據成員m_nStickNum和公有成員函數DispStick。
【例Ex_PublicDerived】 派生類的公有繼承示例
#include <iostream.h> class CMeter { public: CMeter(int nPos=10) { m_nPos=nPos; } ~CMeter() { } void StepIt() {m_nPos++;} int GetPos() {return m_nPos;} protected: void SetPos(int nPos) { m_nPos = nPos; } private: int m_nPos; }; class CStick:public CMeter // 從CMeter派生,公有繼承 { int m_nStickNum; // 聲明一個私有數據成員 public: void DispStick(); // 聲明一個公有成員函數 void SetStick(int nPos) { SetPos(nPos); // 類中調用基類的保護成員 } }; void CStick:: DispStick() { m_nStickNum=GetPos(); // 調用基類CMeter的成員函數 cout<<m_nStickNum<<' '; } int main() { CMeter oMeter(20); CStick oStick; cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oMeter.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oStick.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oStick.DispStick(); oStick.StepIt(); oStick.DispStick(); return 0; }
程序運行結果如下:
CMeter:20,CStick:10
CMeter:21,CStick:10
CMeter:21,CStick:11
11 12
需要注意的是:派生類中或派生類的對象可以使用基類的公有成員(包括保護成員),例如CStick的成員函數DispStick中調用了基類CMeter的GetPos函數,oStick對象調用了基類的StepIt成員函數;但基類或基類的對象卻不可以使用派生類的成員。
2.私有繼承(private)
私有繼承的特點是基類的公有成員和保護成員都作為派生類的私有成員,并且不能被這個派生類的子類所訪問。
【例Ex_PrivateDerived】 派生類的私有繼承示例
#include <iostream.h> class CMeter { public: CMeter(int nPos=10) { m_nPos=nPos; } ~CMeter(){ } void StepIt(){ m_nPos++; } int GetPos(){ return m_nPos;} protected: void SetPos(int nPos) { m_nPos = nPos; } private: int m_nPos; }; class CStick:private CMeter // 從CMeter派生,私有繼承 { int m_nStickNum; // 聲明一個私有數據成員 public: void DispStick(); // 聲明一個公有成員函數 void SetStick(int nPos) { SetPos(nPos); // 調用基類的保護成員 } int GetStick() { return GetPos(); // 調用基類的公有成員 } }; void CStick::DispStick() { m_nStickNum = GetPos(); // 調用基類CMeter的成員函數 cout<<m_nStickNum<<' '; } int main() { CMeter oMeter(20); CStick oStick; cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetStick()<<endl; oMeter.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetStick()<<endl; oStick.DispStick(); return 0; }
程序運行結果如下:
由于私有繼承的派生類對象不能訪問基類的所有成員,因此oStick不能調用基類的GetPos函數,但在派生類中是可以訪問的。注意CStick的GetStick函數實現,并與上例相比較,看看有什么不同。
CMeter:20,CStick:10
CMeter:21,CStick:10
10
3.保護繼承(protected)
保護繼承的特點是基類的所有公有成員和保護成員都成為派生類的保護成員,并且只能被它的派生類成員函數或友元訪問,基類的私有成員仍然是私有的。表2.1列出了三種不同的繼承方式的基類成員和其在派生類中的特性。
表2.1 不同繼承方式的基類成員和其在派生類中的特性

需要注意的是,一定要區分清楚派生類的對象和派生類中的成員函數對基類的訪問是不同的。例如,在公有繼承時,派生類的對象可以訪問基類中的公有成員,派生類的成員函數可以訪問基類中的公有成員和保護成員。在私有繼承和保護繼承時,基類的所有成員不能被派生類的對象訪問,而派生類的成員函數可以訪問基類中的公有成員和保護成員。
2.3.2 派生類的構造函數和析構函數
由于基類的構造函數和析構函數不能被派生類繼承,因此,若有:
CMeter oA(3);
是可以的,因為CMeter類有與之相對應的構造函數。而
CStick oB(3);
是錯誤的,因為CStick類沒有對應的構造函數。但
CStick oC;
是可以的,因為CStick類有一個隱含的不帶參數的默認構造函數。
當派生類的構造函數和析構函數被執行時,基類相應的構造函數和析構函數也會被執行。因而,在前面兩個例子中,CStick對象oStick在建立時還調用了基類的構造函數,使得oStick.GetPos返回的值為10。
需要注意的是,派生類對象在建立時,先執行基類的構造函數,然后執行派生類的構造函數。但對于析構函數來說,其順序剛好相反,先執行派生類的析構函數,而后執行基類的析構函數。
【例Ex_ClassDerived】 派生類的構造函數和析構函數的示例
#include <iostream.h> #include <string.h> class CAnimal { public: CAnimal(char *pName = "noname"); ~CAnimal(); void setName(char *pName) { strncpy(name, pName, sizeof(name)); } char*getName(void) {return name;} private: char name[20]; }; CAnimal::CAnimal(char *pName) { setName(pName); cout<<"調用CAnimal的構造函數!"<<endl; } CAnimal::~CAnimal() { cout<<"調用CAnimal的析構函數!"<<endl; } class CCat : public CAnimal { public: CCat() { cout<<"調用CCat的構造函數!"<<endl; } ~CCat() { cout<<"調用CCat的析構函數!"<<endl; } void DispName() { cout<<"貓的名字是:"<<getName()<<endl; } }; int main() { CCat cat; cat.DispName(); cat.setName("Snoopy"); cat.DispName(); return 0; }
程序運行結果如下:
調用CAnimal的構造函數!
調用CCat的構造函數!
貓的名字是:noname
貓的名字是:Snoopy
調用CCat的析構函數!
調用CAnimal的析構函數!
需要注意的是,在對派生類進行初始化時,如果需要對其基類設置初值,則可按下列格式進行:
<派生類名>(總參表):<基類1>(參數表1), <基類2>(參數表2),…, <基類n>(參數表n), 對象成員1(對象成員參數表1), 對象成員2(對象成員參數表2),…, 對象成員n(對象成員參數表n) { … }
其中,構造函數總參表后面給出的是需要用參數初始化的基類名、對象成員名及各自對應的參數表,基類名和對象成員名之間的順序可以是任意的,且對于使用默認構造函數的基類和對象成員,可以不列出基類名和對象成員名。這里所說的對象成員是指在派生類中新聲明的數據成員,它屬于另外一個類的對象。對象成員必須在初始化列表中進行初始化。
例如,在【例Ex_PublicDerived】中,CStick的構造函數可這樣定義:
class CStick : public CMeter { int m_nStickNum; public: CStick():CMeter(30) { } void DispStick(); void SetStick(int nPos) { SetPos(nPos); } };
此時再重新運行程序,結果就會變為:
CMeter:20,CStick:30
CMeter:21,CStick:30
CMeter:21,CStick:31
31 32
2.3.3 多繼承
前面所討論的是單繼承的基類和派生類之間的關系,實際在類的繼承中,還允許一個派生類繼承多個基類,這種多繼承的方式可使派生類具有多個基類的特性,因而不僅使程序結構清晰,且大大提高了程序代碼的可重用性。
多繼承下派生類的定義按下面的格式:
class <派生類名> : [<繼承方式1>] <基類名1>,[<繼承方式2>] <基類名2>,… { [<派生類的成員>] };
其中的繼承方式還是前面提到的3種:public、private和protected。例如:
class A { //… }; class B { //… }; class C : public A,private B { //…
};
由于派生類C繼承了基類A和B,具有多繼承性,因此派生類C的成員包含了基類A中成員和B中成員及該類本身的成員。
除了類的多繼承性以外,C++還允許一個基類有多個派生類(稱為多重派生),以及從一個基類的派生類中再進行多個層次的派生。總之,掌握了基類和派生類之間的關系,類的多種形式的繼承也就清楚了。
2.3.4 虛基類
一般說來,在派生類中對基類成員的訪問應該是唯一的。但是,由于在多繼承情況下可能造成對基類中某成員的訪問出現不唯一的情況,這種情況稱為基類成員調用的二義性。
【例Ex_Conflict】 基類成員調用的二義性
#include <iostream.h> class A { public: int x; A(int a = 0) { x = a; } }; class B1 : public A { public: int y1; B1( int a = 0, int b = 0) : A(b) { y1 = a; } }; class B2 : public A { public: int y2; B2( int a = 0, int b = 0) : A(b) { y2 = a; } }; class C : public B1, public B2 { public: int z; C(int a, int b, int d, int e, int m) : B1(a,b), B2(d,e) { z = m; } void print() { cout<<"x="<<x<<endl; // 編譯出錯的地方 cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; } }; int main() { C c1(100,200,300,400,500); c1.print(); return 0; }
程序中,派生類B1和B2都從基類A繼承,這時在派生類中就有兩個基類A的拷貝。當編譯器編譯到“cout<<"x = "<<x<<endl;”語句時,因無法確定成員x是從類B1中繼承來的,還是從類B2繼承來的產生了二義性,從而出現編譯錯誤。
解決這個問題的方法之一是使用域作用運算符“ ::”來消除二義性,例如若將print函數實現代碼變為:
void print() { cout<<"B1::x = "<<B1::x<<endl; cout<<"B2::x = "<<B2::x<<endl; cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; }
重新運行,結果為:
B1::x = 200
B2::x = 400
y1 = 100, y2 = 300
z = 500
實際上,還有另一種更好的方法,即使用虛基類(或稱為虛繼承)。使用虛基類的目的是在多重派生的過程中,使公有的基類在派生類中只有一個拷貝,從而解決上述這種二義性問題。
【例Ex_VirtualBase】 基類成員調用的二義性
#include <iostream.h> class A { public: int x; A(int a = 0) { x = a; } }; class B1:virtual public A // 聲明虛繼承 { public: int y1; B1( int a = 0, int b = 0):A(b) { y1=a; } void print(void) { cout<<"B1:x="<<x<<",y1="<<y1<<endl; } }; class B2:virtual public A // 聲明虛繼承 { public: int y2; B2( int a = 0, int b = 0) : A(b) { y2 = a; } void print(void) { cout<<"B2:x="<<x<<",y2="<<y2<<endl; } }; class C : public B1, public B2 { public: int z; C(int a, int b, int d, int e, int m) : B1(a,b), B2(d,e) { z = m; } void print() { B1::print(); B2::print(); cout<<"z = "<<z<<endl; } }; int main() { C c1(100,200,300,400,500); c1.print(); c1.x = 400; c1.print(); return 0; }
程序運行結果如下:
B1: x = 0, y1 = 100
B2: x = 0, y2 = 300
z = 500
B1: x = 400, y1 = 100
B2: x = 400, y2 = 300
z = 500
從程序中可以看出:
(1)聲明一個虛基類的格式如下:
virtual <繼承方式><基類名>
其中,virtual是聲明虛基類的關鍵字。聲明虛基類與聲明派生類一道進行,寫在派生類名的后面。
(2)在派生類B1和B2中只有基類A的一個拷貝,當改變成員x的值時,由基類B1和B2中的成員函數輸出的成員x的值是相同的。
(3)虛基類的構造函數的調用方法與一般基類的構造函數的調用方法是不同的。C++規定,由虛基類經過一次或多次派生出來的派生類,在其每一個派生類的構造函數的成員初始化列表中必須給出對虛基類的構造函數的調用,如果未列出,則調用虛基類的默認構造函數。在這種情況下,虛基類的定義中必須要有默認的構造函數。程序中,類C的構造函數盡管分別調用了其基類B1和B2的構造函數,但由于虛基類A在類C中只有一個拷貝,所以編譯器無法確定應該由類B1的構造函數還是由類B2的構造函數來調用基類A的構造函數。在這種情況下,C++規定,執行類B1和B2的構造函數都不調用虛基類A的構造函數,而是在類C的構造函數中直接調用虛基類A的默認構造函數。這樣也就說明了運行結果中成員x的初始值為什么為“0”了。若將A的構造函數改為:
A(int a = 100) { x = a; }
則成員x的初始值為100。當然,不能僅變成:
A(int a) { x = a; }
因為類A中沒有定義默認構造函數,因此會出現編譯錯誤。與“A(int a = 100) { x = a; }”構造函數等價的是:
A(int a) { x = a; } A():x(100){} // 添加默認構造函數的定義
- Mastering NetBeans
- Android應用程序開發與典型案例
- LabVIEW2018中文版 虛擬儀器程序設計自學手冊
- Learning Apex Programming
- 算法精粹:經典計算機科學問題的Java實現
- 精通API架構:設計、運維與演進
- 大學計算機基礎(第2版)(微課版)
- Service Mesh實戰:基于Linkerd和Kubernetes的微服務實踐
- Mastering Akka
- 創意UI:Photoshop玩轉APP設計
- Python Machine Learning Blueprints:Intuitive data projects you can relate to
- Arduino機器人系統設計及開發
- Python機器學習與量化投資
- Microsoft Dynamics GP 2013 Cookbook
- Learning Shiny