十四、繼承與組合(Inheritance & Composition)
引言
- C++最引人注目的特性之一是代碼復用。
- 組合:在新類中創建已有類的對象。
- 繼承:將新類作為已有類的一個類型來創建。
14.1 組合的語法
Useful.h
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{int i;
public:X(){i = 0;}void set(int ii){i = ii;}int read() const {return i;}int permute(){return i = i * 47;}
};
#endif
Composition,cpp
#include "Useful.h"
class Y{int i;
public:X x;//嵌入對象,子對象Y(){ i = 0;}void f(int ii) {i = ii;}int g() cosnt{return i;}
};void main()
{Y y;y.f(47);y.x.set(37);
}
這里
Y y;
語句執行的時候,y
里面的x
是利用構造函數進行初始化的
14.2 繼承的語法
Useful.h
//C14:Useful.h
#ifndef USEFUL_H
#define USEFUL_H
class X{int i;
public:X (){i = 1;}void set(int ii){i = ii;}int read() const{return i;}int permute() {return i = i*47;}
};
#endif
Inheritance.cpp
//C14:Inheritance.cpp
#include "Useful.h"
#include <iostream>
using namespace std;
class Y:public X
{int i;//不是X的i
public:Y(){i = 2;}int change(){ i = permute();//調用不同名稱的函數return i;}void set(int ii){i = ii;X::set(ii);//調用同名函數,需要加::}
};int main(){cout << "sizeof(X) = " << sizeof(X) << endl;cout << "sizeof(Y) = " << sizeof(Y) << endl;Y D;//X::i = 1,Y::i = 2D.change();//return Y::i = 47(X::i = 47)D.read(); //X::i = 47D.permute();//X::i = 47 * 47D.set(15);//Y::i = 12,X::i = 12return 0;
}
這里對y里面的x初始化也是通過X的無形參構造函數。
輸出
sizeof(X) = 4
sizeof(Y) = 8
-
Y
繼承自X
,這意味著Y
內將包含一個X
類型的子對象,就像在Y
內部直接創建了一個X
成員對象一樣。無論是成員對象還是基類所占的存儲都稱為子對象。 -
Y
是X
的派生類,X
是基類。派生類繼承基類的屬性,這種關系稱為繼承。 -
X
的所有私有成員在Y
中仍然是私有的(因此Y
里面不能訪問X
的私有成員,只能通過X
的函數)。通過public
繼承,基類的所有公有成員在派生類也保持公有(后面還會有private
繼承、protected
繼承),也就是說public
繼承,X
中的私有在Y
中仍私有,公有仍公有,protected
仍protected
。protected
是指派生類可以訪問,外部代碼不可以訪問。
-
將一個類用作基類相當于聲明了一個該類的(未命名)對象。因此。必須先定義這個類才能將其用作基類。
class X; class Y:public X{………… };
14.3 構造函數初始化列表
- 在組合和繼承中,確保**子對象被正確初始化**非常重要。
- 構造函數和析構函數不會被繼承(賦值運算符也不會被繼承)。因此派生類的構造函數無法直接初始化基類的成員。
- 新類的構造函數無法訪問子對象的私有數據元素。
- 如果不使用默認構造函數,該如何初始化子對象的私有數據元素。
解決方法
- 在構造函數初始化列表中調用子對象的構造函數。
- 構造函數初始化列表允許顯式調用成員對象的構造函數。其原理是:在進入新類構造函數的函數體之前,所有成員對象的構造函數都會被調用。
- 內置類型的變量也可以在構造函數初始化列表中初始化。而且初始化列表會自動幫忙初始化(避免垃圾值)。
注意:在非虛繼承(普通繼承) 中,派生類只需要構造它的直接基類,間接基類會自動由中間類網上構造,構造鏈自動完成。
示例1
#include <iostream>
using namespace std;
class X{int a;
public:X(int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y{int b;
public:Y(int i,int j):b(i),x(j){cout << "Constructor Y:" << b << endl;}X x;
};int main(){Y y(1,2);return 0;
}
該實例中
x
是y
的成員
輸出
Constructor X:2
Constructor Y:1
示例2
#include <iostream>
using namespace std;
class X {int a;
public:X(int i = 7) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y {int b;
public:Y(int i) :b(i) { cout << "Constructor Y:" << b << endl; }X x;
};int main() {Y y(1);return 0;
}
該實例中
x
是y
的成員
輸出
Constructor X:7
Constructor Y:1
示例3
#include <iostream>
using namespace std;
class X{int a;
public:X (int i):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X
{int b;
public:Y(int i,int j):b(i),X(j){cout << "Constructor Y:" << b << endl;}
}int main(){Y y(1,2);return 0;
}
輸出
Constructor X:2
Constructor Y:1
示例4
#include <iostream>
using namespace std;
class X {int a;
public:X(int i = 9) :a(i) { cout << "Constructor X:" << a << endl; }
};
class Y :public X
{int b;
public:Y(int i, int j) :b(i){ cout << "Constructor Y:" << b << endl; }
};int main() {Y y(1, 2);return 0;
}
輸出
Constructor X:9
Constructor Y:1
14.4 組成與繼承結合
- 當創建一個派生類對象時,可能會創建以下對象:基類對象、成員對象和派生類對象本身。構造順序自下而上:首先構造基類、然后構造成員對象,最后構造派生類自身。
- 基類或成員對象構造函數的調用順序以它們在派生類中聲明的順序為準,而不是它們在初始化列表中出現的順序。
- 默認構造函數可以被隱式調用。
示例
#include <iostream>
using namespace std;
class X{int a;
public:X(int i = 0):a(i){cout << "Constructor X:" << a << endl;}
};
class Y:public X{int b;X x1,x2;
public:Y(int i,int j,int m,int n):b(i),x2(j),x1(m),X(n){cout << "Constructor Y:" << b << endl;}
};
int main(){Y y(1,2,3,4);return 0;
}
輸出
Constructor X:4
Constructor X:3
Constructor X:2
Constructor Y:1
14.5 名字隱藏
? 先介紹下面三種相關機制
- 重載(Overload):發生在同一作用域(類)內
- 重新定義(Redefining):繼承關系中的普通成員函數
- 重寫(Overriding):繼承關系中的虛成員函數
? 名字隱藏:在繼承關系中 ,如果派生類定義了一個與基類中同名的成員(不論是函數還是變量),那么這個基類的同名成員(函數)就會被“隱藏”,即使參數不同也無法通過派生類對象直接訪問。上面三種機制中,重載并非是名字隱藏,其余兩個是名字隱藏。
概念 | 英文術語 | 適用情形 | 是否是名字隱藏 | 說明 |
---|---|---|---|---|
重載 | Overload | 同一個類或者同一作用域 | 否 | 函數名相同,但參數不同(返回值不同不算重載啊) |
重新定義 | Redifining | 繼承中的普通函數 | 是 | 派生類中定義了同名函數(或變量),會隱藏基類中所有同名函數(變量),需要顯示訪問 |
重寫 | Overriding | 繼承中的虛函數 | 是 | 派生類用virtual 修飾的函數覆蓋基類中的虛函數 |
虛函數會在后續的章節繼續提到
- 在派生類中,只要重新定義了基類中重載的函數名(假設基類有重載函數),基類中 該名字的其他版本 在 派生類中都會被自動隱藏。
示例
//C14:NameHidding.cpp
#include <iostream>
#include <string>
using namespace std;class Base{
public:int f() const{cout << "Base::f()" << endl;return 1;}int f(string) const {return 1;}void g(){}
};class Derived1:public Base
{
public://重新定義void g() const{};
};class Derived2:public Base{
public://重定義int f() const {cout << "Derived2::f()" << endl;return 2;}
};class Derived3:public Base{
public://改變return的類型void f() const{cout << "Derived3::f()" << endl;}
};class Derived4:public Base{
public://改變return的類型int f(int) const{cout << "Derived4::f()" << endl;return 4;}
};void main(){string s("hello");Derived1 d1;int x = d1.f();d1.f(s);Derived2 d2;x = d2.f();//!d2.f(s);//string版本被隱藏Derived3 d3;//!x = d3.f();return int 版本被隱藏Derived4 d4;//!x = d4.f();f()版本被隱藏x = d4.f(1);
}
輸出
Base::f()
Derived2::f()
Derived4::f()
14.6 不會自動繼承的函數
以下函數不會被自動繼承
- 構造函數
- 析構函數
- 賦值運算符函數
繼承和靜態成員函數(Inheritance and static member funcitons)
- 靜態成員函數可以被繼承到派生類中。
- 如果在派生類中重新定義了一個靜態成員函數,那么基類中所有同名的重載函數也會被隱藏。
- 靜態成員函數只能訪問靜態數據成員。
- 靜態成員函數不能是是
virtual
(虛函數)。 - 靜態成員函數沒有
this
指針。
14.7 選擇組合還是繼承
- 共同點:組合(Composition)和繼承(Inheritance)都會在新類中放置子對象(subojects)。它們都使用構造函數初始化列表(intializer list) 來構造這些子對象。
- 當我們希望在新類中包含某個已有類的功能(作文數據成員),但不想繼承它的接口時,通常使用組合。
- 當我們希望新類具有與現有類完全相同的接口時,使用繼承。這被稱為子類型化。
14.8 基類的子類型化
-
如果一個派生類繼承自一個
public
的基類,那么該派生類就繼承了基類的所有成員,但只能訪問基類中的public
和protected
成員。派生類就是基類的子類型,基類是派生類的超類型從外部使用者的角度來看,這個派生類就具有與基類相同的
public
接口(可以再加自己的新接口),因此它可以在需要基類對象的地方替代使用,這就是**子類型(subtyping)**的概念。 -
通過子類型化,當我們使用指針或引用操作派生類對象時,它可以被當作基類對象來處理(即:可以指向或引用基類)
#include <iostream> using namespace std; class Base { public:void speak() { cout << "Base speaking" << endl; } };class Derived : public Base { public:void shout() { cout << "Derived shouting" << endl; } };int main() {Derived d;// 子類型化:Base* 指向 Derived 對象Base* ptr = &d;ptr->speak(); // OK:Base 的函數可調用// ptr->shout(); // 錯誤:Base* 看不到 Derived 的接口// 同樣適用于引用Base& ref = d;ref.speak(); // OK }
這在后續還會提到。
14.9 繼承的訪問控制
- 訪問說明符:
public
private
protected
- 訪問說明符 用于控制派生類對基類的成員的訪問,以及從派生類到基類的指針和引用轉換的權限。(無論哪種訪問,基類的所有成員都會被繼承,派生類的內存里都會有它們,只是能不能訪問的差別。)
下面展示三種訪問控制的不同
public
繼承
如果一個派生類使用public
繼承:
- 派生類的成員函數及其類內部可以訪問其基類中的
public
和protected
成員。private
不可以訪問。 - 派生類的對象可以訪問其基類中的
public
成員。
說明:
public
繼承會讓基類的public
成員在派上類里面仍是public
,protected
成員仍是protected
。- 可以形成”子類型關系“,即能用
Base*
指向Derived
。
示例
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};class manager :public employee {
public:void meeting(int n) {print(); //oknumber = n;//ok//name = "Jhoo" //error}
private:short level;
};
int main() {employee E;manager M;E.print();//ok//E.number = 3; //error//M.name = "Jhoo"; //error//M.number = 3; //errorM.print(); //okM.meeting(1); //okreturn 0;
}
可見protected
和 private
的區別就是:
- 派上類里面可以訪問
protected
,而不可以訪問private
,而派生類的對象兩個都不可以訪問
private
繼承
如果一個類使用private
說明符繼承:
- 派生類的成員函數及其類內部可以訪問其基類中的
public
和protected
成員。private
不可以訪問。 - 派生類的對象不能訪問基類中的任何成員。
說明:
private
繼承會把基類的public
和protected
成員變成派生類中的private
成員。- 所以 只有派生類內部能訪問 基類的
public
和protected
成員,類外(包括派生類對象)都不能訪問。 - 同時,不會形成”子類型關系“,即不能用
Base*
指向Derived
。
示例
#include <iostream>
using namespace std;
class employee {
public: void print() {}
protected: short number;
private: string name;
};class manager :private employee {
public:void meeting(int n) {print(); //oknumber = n;//ok//name = "Jhoo" //error}
private:short level;
};
int main() {employee E;manager M;E.print();//ok//E.number = 3; //error//M.name = "Jhoo"; //error//M.number = 3; //error//M.print(); //errorM.meeting(1); //okreturn 0;
}
protected
繼承
如果一個類使用protected
說明符進行繼承:
- 派生類的成員函數及其類內部可以訪問其基類中的
public
和protected
成員。private
不可以訪問。 - 派生類的對象不能訪問基類中的任何成員。
說明
protected
繼承會將基類的public
和protected
成員變成派生類中的protected
;- 所以,只有派生類的內部能用,類外部(包括對象)都無法訪問;
- 同時不形成“子類型關系”,不能用
Base*
指向Derived
。
protected
繼承和private
繼承當派生類再次被繼承時,會體現出來差別
總結
三種繼承方式對比
方面 | public 繼承 | protected 繼承 | private 繼承 |
---|---|---|---|
基類的public 成員在派生類中變成 | public | protected | private |
基類的proitected 成員在派生類中變成 | protected | protected | ptivate |
基類的private 成員在派生類中變成 | 不可訪問 | 不可訪問 | 不可訪問 |
派生類成員函數能訪問哪些基類成員 | public 和protected | public 和protected | public 和protected |
派生類對象能訪問哪些基類成員 | public | 無法訪問任何成員 | 無法訪問熱河成員 |
是否支持子類型轉換(Base* = new Derived ) | 是 | 不支持 | 不支持 |
適用場景 | 接口繼承、支持多態、面向對象設計 | 實現復用,不暴露接口 | 強封裝 |
類中成員訪問權限對比
訪問標識符 | 類中是否可以訪問 | 派生類是否可以訪問 | 類外部(對象)是否可以訪問 | 是否可以繼承 |
---|---|---|---|---|
public | 可以 | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 不可以 | 可以 |
private | 可以 | 不可以 | 不可以 | 可以(但不可見) |
示例
Example1
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :public Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y);W = w;H = h;
}int main() {Rectangle rect;//派生類的對象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
輸出
5,5,20,10
Example2
這里在Example1里面添加一個V
類。
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :public Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y);W = w;H = h;
}
// 派生類
class V : public Rectangle {
public:void Function() {Move(3, 2); // 來自基類的函數}
};
-
如果這里的
clas V :public Rectangle
繼承改為private
繼承,那么Move(3,2)
還能用嗎?解答:
- 如果
Rectangle
是以public
或protected
方式繼承Location
,
那么類V
無論使用哪種繼承方式(public
/protected
/private
),都可以訪問Move()
函數。 - 如果
Rectangle
是以private
方式繼承Location
,
那么類V
無論如何繼承Rectangle
,都無法訪問Move()
函數。 - 因為:
public
和protected
繼承會讓基類的public
/protected
成員保留可見性(對類內仍可訪問); - 而private
繼承會將基類的public
/protected
成員都變為private
,對子類完全不可見。
- 如果
Example3
這里將Example1 的public
繼承改為private
繼承
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :private Location {
public:void InitR(int x, int y, int w, int h);int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y); //OKW = w;H = h;
}
那么Example1里面的主函數需要修改
int main() {Rectangle rect;//派生類的對象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
rect.GetX()
和 rect.GetY()
以及rect.Move(3,2)
會報錯,因為它們在Rectangle
里面已經是private
。所以Rectangle
對象無法訪問它們。
所以我們將Exampel1修改成如下
#include <iostream>
using namespace std;
class Location {
public:void InitL(int xx, int yy) {X = xx;Y = yy;}void Move(int xOff, int yOff) {X += xOff;Y += yOff;}int GetX() { return X; }int GetY() { return Y; }
private:int X, Y;
};
class Rectangle :private Location {
public:void InitR(int x, int y, int w, int h);void Move(int xOff, int yOff) {Location::Move(xOff,yOff);}int GetX() {return Location::GetX();}int GetY() {return Location::GetY();}int GetH() { return H; }int GetW() { return W; }
private:int W, H;
};void Rectangle::InitR(int x, int y, int h, int w)
{InitL(x, y); //OKW = w;H = h;
}int main() {Rectangle rect;//派生類的對象rect.InitR(2, 3, 20, 10);rect.Move(3, 2);cout << rect.GetX() << "," << rect.GetY() << "," << rect.GetH() << "," << rect.GetW() << endl;return 0;
}
這里在 Rectangle
里面重新定義Move()
、GetX()
、GetY()
。
輸出
5,5,20,10
Example4
class Base {
public: void f1() {}
protected: void f3() {}
};
class Derived1 : protected Base {};class Derived2 : public Derived1 {public:void fun() {f1(); //okf3(); //ok}
};
int main() {Derived1 d;//d.f1(); //error//d.f3(); //errorreturn 0;
}
這里如果將class Derived2 : public Derived1
改為private
或者protected
,那么
void fun() {f1(); //okf3(); //ok
}
正確嗎?
是正確的,因為,private
或者protected
僅僅是改變了Derived1
的成員在Derived1
里面是什么訪問權限(public
/ protectecd
/ private
),而基類的public
和 protected
成員在派生類的內部是都可以訪問的。
14.10 運算符重載與繼承
- 除了賦值運算符(
=
)之外,其他運算符會自動被繼承到派生類中。
14.11 多重繼承
- 多重繼承是指:一個派生類可以擁有多個直接基類。
class A{//……
}
class B{//……
}
class C:access A,access B{};
”access“是占位詞,代表public
、 protected
、 private
中任意的一種訪問方式。
Base classes: A B↖ ↗C ← Derived class
示例
#include <iostream>
using namespace std;
class B1 {
public:B1(int i) {b1 = i;cout << "Constructor B1:" << b1 << endl;}void Print() { cout << b1 << endl; }
private:int b1;
};class B2 {
public:B2(int i){b2 = i;cout << "Constructor B2:" << b2 << endl;}void Print() { cout << b2 << endl; }
private:int b2;
};class B3 {
public:B3(int i) {b3 = i;cout << "Constructor B3:" << b3 << endl;}int Getb3() { return b3; }
private:int b3;
};class A :public B2, public B1//多重繼承
{
public:A(int i, int j, int k, int l);void Print();
private:B3 bb;int a;
};A::A(int i, int j, int k, int l) :a(l), bb(k), B2(j), B1(i) {cout << "Constructor A:" << a << endl;
}void A::Print() {B1::Print();B2::Print();cout << bb.Getb3() << endl << a << endl;
}
int main() {A aa(1, 2, 3, 4);aa.Print();return 0;
}
輸出
Constructor B2:2
Constructor B1:1
Constructor B3:3
Constructor A:4
1
2
3
4
14.12 增量式開發
- 增量式開發:在不破壞已有代碼的前提下添加新代碼。
- 繼承和組合的一個優點是:它們支持增量式開發。
歧義問題:
- 歧義1:當多個基類中擁有同名成員函數時(多重繼承情況下),可能會發生名字沖突。
- 歧義2:如果一個派生類有兩個基類,而這兩個基類又都繼承自一個類,那么就可能觸發歧義(即,一個類在繼承鏈中被“繼承了兩次”)。也就是”菱形繼承問題“。
消除歧義1
歧義1:
當多個基類中擁有同名成員函數時(多重繼承情況下),可能會發生名字沖突。
歧義1示例
class A{
public:void f(){}
};
class B{
public:void f(){}void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){C c;//c.f(); //error:編譯器不知道式A還是B的f()c.g(); //okc.B::g(); //ok
}
解決方法:
- 使用作用域解析運算符
::
(比如c.A::f();
) - 在派生類中定義一個新的函數(以覆蓋或隱藏同名函數)
方法一:使用作用域解析運算符::
class A{
public:void f(){}
};
class B{
public:void f(){}void g(){}
};
class C:public A,public B{
public: void g(){}
};
void main(){C c;c.A::f(); //okc.g(); //okc.B::g(); //ok
}
方法二:在派生類中定義一個新的函數
class A {
public:void f() {}
};
class B {
public:void f() {}void g() {}
};
class C :public A, public B {
public:void g() {}void f() { A::f(); }
};
void main() {C c;c.f(); //okc.g(); //okc.B::g(); //ok
}
上述講的只是第一種歧義:當多個基類中擁有同名成員函數時(多重繼承情況下),可能會發生名字沖突。
接下來講述第二種歧義:如果一個派生類又有兩個基類,且這兩個基類又都繼承自同一個類,就可以出現歧義。(即,同一個類被繼承了兩次)
消除歧義2
歧義2:
如果一個派生類有兩個基類,且這兩個基類又都繼承自同一個類,就可能出現歧義。(即,同一個類被繼承了兩次)
歧義2示例
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.f(); //error
}
解決方法:
- 使用作用域解析運算符
::
- 在派生類中定義一個新函數來隱藏或重寫沖突函數
- 使用虛基類(virtual base class) 避免重復繼承
方法一:使用作用域解析運算符::
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.B::f(); //ok
}
方法二:在派生類中重新定義一個新的函數
class A
{public:void f(){};
};class B:public A{//……
};class C:public A{//……
};class D:public B,public C{
public:void f(){}
};void main(){D d;//A,B,A,C,Dd.f(); //ok
}
方法三:使用虛基類
- 關鍵字
virtual
只作用于其后緊跟的基類。
class D:virtual public A,public B,virtual public C{//…………
};
上述中A和C都是虛基類,B不是虛基類
class A
{public:void f(){};
};class B:virtual public A{//……
};class C:virtual public A{//……
};class D:public B,public C{//……
};void main(){D d;//A,B,A,C,Dd.f(); //ok
}
采用虛基類后,A,B,C,D的關系就變為
虛基類與非虛基類的內存比較
-
虛基類
-
非虛基類
虛基類只存在一份,而非虛基類會重復拷貝。
虛基類的構造函數
-
它只會被調用一次。
-
它是由最底層派生類的構造函數調用的(可以是顯示也可以是隱式調用)。
-
它會在非基類的構造函數之前被調用。
-
虛基類的構造函數會出現在所有派生類的構造函數的成員初始化列表中。
-
如果沒有顯示調用,則會自動調用它的默認構造函數。
下面解釋顯示和隱式調用
//顯示調用
class A {
public:A(int x) { cout << "A(" << x << ")\n"; }
};class B : virtual public A {
public:B() : A(1) { cout << "B\n"; }
};
//隱式調用
class A {
public:A() { /*...*/ } // 默認構造函數
};class B : virtual public A {
public:B() { // 隱式調用 A()}
};
下面解釋最底層派生類
最底層派生類:最終的創建那個類對象,那個類就是最底層派生類,也就是最終構造函數調用者。
示例
#include <iostream>
using namespace std;
class A {
public:A(int i) { cout << "A" << i << endl; }
};class B : virtual public A {
public:B(int i = 1):A(i) { cout << "B\n"; }
};class C : virtual public A {
public:C(int i = 2):A(i) { cout << "C\n"; }
};class D : public B, public C {
public:D(int i = 3):A(i) { cout << "D\n"; }
};
? 現在看兩種對象的構造
int main(){B b;return 0;
}
? 此時構造的是 B
, B
是最底層派生類,所以它負責構虛基類A
。
? 輸出
A1
B
int main(){D d;return 0;
}
? 此時構造的是 D
,D
是最底層派生類,所以它負責構造虛基類A
,即使 B
和C
也繼承了 A
。
? 輸出
A3
B
C
D
-
A
只被調用一次,由D
構造; -
B
和C
如果重新定義了A
的初始化,那也會被忽略
依舊是對最底層派生類的解釋
假設有如下圖的類
最底層派生類:
E e;//E是最底層派生類
D d;//D是最底層派生類
B b;//B是最底層派生類
C c;//C是最底層派生類
構造函數:
B(……):A(……){……}
C(……):A(……){……}
D(……):B(……),C(……),A(……){……}
E(……):D(……),A(……){……}
E
不需要構造B
和C
,是因為它們不是E
的直接基類;
但E
必須構造A
,是因為A
是一個虛基類,虛基類總是由“最底層派生類”負責構造,即使它不是直接基類。
示例
#include <iostream>
using namespace std;
class A {
public:A(const char* s) { cout << "Class A:" << s << endl; }~A() {}
};class B :virtual public A
{
public:B(const char* s1, const char* s2) :A(s1) {cout << "Class B:" << s2 << endl;}
};class C :virtual public A
{
public:C(const char* s1, const char* s2) :A(s1) {cout << "Class C:" << s2 << endl;}
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2), A(s1) {cout << "Class D:" << s4 << endl;}
};
void main() {D* ptr = new D("str1", "str2", "str3", "str4");delete ptr;
}
輸出
Class A:str1
Class B:str2
Class C:str3
Class D:str4
這里先構造
B
再C
的原因是class D:public B,public C
,先繼承B
再繼承C
示例(A
不是虛函數)
#include <iostream>
using namespace std;
class A {
public:A(const char* s) { cout << "Class A:" << s << endl; }~A() {}
};class B :public A
{
public:B(const char* s1, const char* s2) :A(s1) {cout << "Class B:" << s2 << endl;}
};class C :public A
{
public:C(const char* s1, const char* s2) :A(s1) {cout << "Class C:" << s2 << endl;}
};class D :public B, public C
{
public:D(const char* s1, const char* s2, const char* s3, const char* s4) :C(s2, s3), B(s4, s2) {cout << "Class D:" << s4 << endl;}
};
void main() {D* ptr = new D("str1", "str2", "str3", "str4");delete ptr;
}
輸出
Class A:str4
Class B:str2
Class A:str2
Class C:str3
Class D:str4
這里
D
就不需要再構造A
了,簡潔基類會自動構造。
14.13 向上轉型(Upcasing)
- 繼承最重要的方面,并不是它為了新類提供了成員函數。
- 更關鍵的是:它表達了新類和基類之間的關系。
- 這種關系可以總結為這樣的一句話:新類是已有類的一種類型。
向上轉型(Upcasting): 將一個 派生類 類型的引用或指針轉換為
其 基類 的引用或指針,就叫做“向上轉型”。
示例
class Instrument {
public:void play() const {}
};
//Wind是Instrument的派生類
class Wind :public Instrument {};
void tune(Instrument& i) { i.play(); }
void main() {Wind flute;tune(flute); //向上轉型(Upcasting)Instrument* p = &flute;//UpcastingInstrument& l = flute;//Upcasting
}
- 當通過指針或引用(指向或引用基類)來操作派生類對象時,派生類對象可以被當作其基類對象來處理。
在第十五章還會接著講述“Upcasting” 。
14.14 總結
- 繼承和組合
- 多重繼承
- 訪問控制
- 向上轉型