十四、繼承與組合(Inheritance Composition)

十四、繼承與組合(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 成員對象一樣。無論是成員對象還是基類所占的存儲都稱為子對象。

  • YX 的派生類,X基類。派生類繼承基類的屬性,這種關系稱為繼承。

  • X 的所有私有成員在 Y 中仍然是私有的(因此 Y 里面不能訪問 X 的私有成員,只能通過 X 的函數)。通過 public 繼承,基類的所有公有成員在派生類也保持公有(后面還會有 private 繼承、 protected 繼承),也就是說 public 繼承,X 中的私有在 Y 中仍私有,公有仍公有,protectedprotectedprotected

    是指派生類可以訪問,外部代碼不可以訪問。

  • 將一個類用作基類相當于聲明了一個該類的(未命名)對象。因此。必須先定義這個類才能將其用作基類。

    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;
}

該實例中 xy 的成員

輸出

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;
}

該實例中 xy 的成員

輸出

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 的基類,那么該派生類就繼承了基類的所有成員,但只能訪問基類中的 publicprotected 成員。派生類就是基類的子類型,基類是派生類的超類型

    外部使用者的角度來看,這個派生類就具有與基類相同的 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 繼承:

  • 派生類的成員函數及其類內部可以訪問其基類中的 publicprotected 成員。private不可以訪問。
  • 派生類的對象可以訪問其基類中的 public 成員。

說明:

  • public 繼承會讓基類的public 成員在派上類里面仍是publicprotected成員仍是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;
}

可見protectedprivate 的區別就是:

  • 派上類里面可以訪問protected ,而不可以訪問private,而派生類的對象兩個都不可以訪問

private 繼承

如果一個類使用private 說明符繼承:

  • 派生類的成員函數及其類內部可以訪問其基類中的 publicprotected 成員。private不可以訪問。
  • 派生類的對象不能訪問基類中的任何成員

說明:

  • private 繼承會把基類的publicprotected 成員變成派生類中的private 成員。
  • 所以 只有派生類內部能訪問 基類的publicprotected 成員,類外(包括派生類對象)都不能訪問。
  • 同時,不會形成”子類型關系“,即不能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 說明符進行繼承:

  • 派生類的成員函數及其類內部可以訪問其基類中的 publicprotected 成員。private不可以訪問。
  • 派生類的對象不能訪問基類中的任何成員

說明

  • protected 繼承會將基類的 publicprotected 成員變成派生類中的 protected
  • 所以,只有派生類的內部能用,類外部(包括對象)都無法訪問;
  • 同時不形成“子類型關系”,不能用 Base* 指向 Derived

protected繼承和private繼承當派生類再次被繼承時,會體現出來差別

總結

三種繼承方式對比
方面public繼承protected繼承private繼承
基類的public成員在派生類中變成publicprotectedprivate
基類的proitected成員在派生類中變成protectedprotectedptivate
基類的private成員在派生類中變成不可訪問不可訪問不可訪問
派生類成員函數能訪問哪些基類成員publicprotectedpublicprotectedpublicprotected
派生類對象能訪問哪些基類成員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 是以 publicprotected 方式繼承 Location
      那么類 V 無論使用哪種繼承方式(public / protected / private),都可以訪問 Move() 函數
    • 如果 Rectangle 是以 private 方式繼承 Location
      那么類 V 無論如何繼承 Rectangle,都無法訪問 Move() 函數
    • 因為:
      • publicprotected 繼承會讓基類的 public / protected 成員保留可見性(對類內仍可訪問); - 而 private 繼承會將基類的 public / protected 成員都變為 private,對子類完全不可見。
Example3

這里將Example1public繼承改為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),而基類的publicprotected 成員在派生類的內部是都可以訪問的。

14.10 運算符重載與繼承

  • 除了賦值運算符(=)之外,其他運算符會自動被繼承到派生類中。

14.11 多重繼承

  • 多重繼承是指:一個派生類可以擁有多個直接基類
class A{//……
}
class B{//……
}
class C:access A,access B{};

”access“是占位詞,代表publicprotectedprivate 中任意的一種訪問方式。

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
}
解決方法:
  1. 使用作用域解析運算符::(比如c.A::f();
  2. 在派生類中定義一個新的函數(以覆蓋或隱藏同名函數)
方法一:使用作用域解析運算符::
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
}
A
B
A
C
D
解決方法:
  1. 使用作用域解析運算符 ::
  2. 在派生類中定義一個新函數來隱藏或重寫沖突函數
  3. 使用虛基類(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的關系就變為

A
B
C
D
虛基類與非虛基類的內存比較
  • 虛基類

    image-20250507142259996

  • 非虛基類

    image-20250507142328204

    虛基類只存在一份,而非虛基類會重復拷貝

虛基類的構造函數
  • 它只會被調用一次。

  • 它是由最底層派生類的構造函數調用的(可以是顯示也可以是隱式調用)。

  • 它會在非基類的構造函數之前被調用。

  • 虛基類的構造函數會出現在所有派生類的構造函數的成員初始化列表中。

  • 如果沒有顯示調用,則會自動調用它的默認構造函數。

下面解釋顯示隱式調用

//顯示調用
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;
}

? 此時構造的是 DD最底層派生類,所以它負責構造虛基類A,即使 BC也繼承了 A

? 輸出

A3
B
C
D
  • A 只被調用一次,由 D 構造;

  • BC 如果重新定義了 A 的初始化,那也會被忽略


依舊是對最底層派生類的解釋

假設有如下圖的類

A
B
C
D
E

最底層派生類:

E e;//E是最底層派生類
D d;//D是最底層派生類
B b;//B是最底層派生類
C c;//C是最底層派生類

構造函數:

B(……):A(……){……}
C(……):A(……){……}
D(……):B(……),C(……),A(……){……}
E(……):D(……),A(……){……}

E 不需要構造 BC,是因為它們不是 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

這里先構造BC的原因是 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
}
Instrument
Wind
  • 當通過指針或引用(指向或引用基類)來操作派生類對象時,派生類對象可以被當作其基類對象來處理。

在第十五章還會接著講述“Upcasting” 。

14.14 總結

  • 繼承和組合
  • 多重繼承
  • 訪問控制
  • 向上轉型

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/80871.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/80871.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/80871.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

2025年5月-信息系統項目管理師高級-軟考高項一般計算題

決策樹和期望貨幣值 加權算法 自制和外購分析 溝通渠道 三點估算PERT 當其他條件一樣時&#xff0c;npv越大越好

OpenJDK 17 中線程啟動的完整流程用C++ 源碼詳解

1. 線程創建入口&#xff08;JNI 層&#xff09; 當 Java 層調用 Thread.start() 時&#xff0c;JVM 通過 JNI 進入 JVM_StartThread 函數&#xff1a; JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))// 1. 檢查線程狀態&#xff0c;防止重復啟動if (java_…

Spring MVC參數傳遞

本內容采用最新SpringBoot3框架版本,視頻觀看地址:B站視頻播放 1. Postman基礎 Postman是一個接口測試工具,Postman相當于一個客戶端,可以模擬用戶發起的各類HTTP請求,將請求數據發送至服務端,獲取對應的響應結果。 2. Spring MVC相關注解 3. Spring MVC參數傳遞 Spri…

Python面向對象編程(OOP)深度解析:從封裝到繼承的多維度實踐

引言 面向對象編程(Object-Oriented Programming, OOP)是Python開發中的核心范式&#xff0c;其三大特性——??封裝、繼承、多態??——為構建模塊化、可維護的代碼提供了堅實基礎。本文將通過代碼實例與理論結合的方式&#xff0c;系統解析Python OOP的實現機制與高級特性…

0.66kV0.69kV接地電阻柜常規配置單

0.66kV/0.69kV接地電阻柜是變壓器中性點接地電阻柜中的特殊存在&#xff0c;主要應用于低壓柴油發電機組220V、火力發電廠380V、煤炭企業660V/690V等電力系統或電力用戶1000V的低壓系統中。 我們來看看0.66kV0.69kV接地電阻柜配置單&#xff1a; 配置特點如下&#xff1a; 1…

矩陣短劇系統:如何用1個后臺管理100+小程序?深度解析多端綁定技術

短劇行業效率革命&#xff01;一套系統實現多平臺內容分發、數據統管與流量聚合 在短劇行業爆發式增長的今天&#xff0c;內容方和運營者面臨兩大核心痛點&#xff1a;多平臺運營成本高與流量分散難聚合。傳統模式下&#xff0c;每個小程序需獨立開發后臺&#xff0c;導致人力…

CSS可以繼承的樣式匯總

CSS可以繼承的樣式匯總 在CSS中&#xff0c;以下是一些常見的可繼承樣式屬性&#xff1a; 字體屬性&#xff1a;包括 font-family &#xff08;字體系列&#xff09;、 font-size &#xff08;字體大小&#xff09;、 font-weight &#xff08;字體粗細&#xff09;、 font-sty…

BFS算法篇——打開智慧之門,BFS算法在拓撲排序中的詩意探索(上)

文章目錄 引言一、拓撲排序的背景二、BFS算法解決拓撲排序三、應用場景四、代碼實現五、代碼解釋六、總結 引言 在這浩瀚如海的算法世界中&#xff0c;有一扇門&#xff0c;開啟后通向了有序的領域。它便是拓撲排序&#xff0c;這個問題的解決方法猶如一場深刻的哲學思考&#…

【Qt開發】信號與槽

目錄 1&#xff0c;信號與槽的介紹 2&#xff0c;信號與槽的運用 3&#xff0c;自定義信號 1&#xff0c;信號與槽的介紹 在Qt框架中&#xff0c;信號與槽機制是一種用于對象間通信的強大工具。它是在Qt中實現事件處理和回調函數的主要方法。 信號&#xff1a;窗口中&#x…

數據庫基礎:概念、原理與實戰示例

在當今信息時代&#xff0c;數據已經成為企業和個人的核心資產。無論是社交媒體、電子商務、金融交易&#xff0c;還是物聯網設備&#xff0c;幾乎所有的現代應用都依賴于高效的數據存儲和管理。數據庫&#xff08;Database&#xff09;作為數據管理的核心技術&#xff0c;幫助…

前端-HTML基本概念

目錄 什么是HTML 常用的瀏覽器引擎是什么&#xff1f; 常見的HTML實體字符 HTML注釋 HTML語義化是什么&#xff1f;為什么要語義化&#xff1f;一定要語義化嗎&#xff1f; 連續空格如何渲染&#xff1f; 聲明文檔類型 哪些字符集編碼支持簡體中文&#xff1f; 如何解…

Linux進程信號處理(26)

文章目錄 前言一、信號的處理時機處理情況“合適”的時機 二、用戶態與內核態概念重談進程地址空間信號的處理過程 三、信號的捕捉內核如何實現信號的捕捉&#xff1f;sigaction 四、信號部分小結五、可重入函數六、volatile七、SIGCHLD 信號總結 前言 這篇就是我們關于信號的最…

C++ 字符格式化輸出

文章目錄 一、簡介二、實現代碼三、實現效果 一、簡介 這里使用std標準庫簡單實現一個字符格式化輸出&#xff0c;方便后續的使用&#xff0c;它有點類似Qt中的QString操作。 二、實現代碼 FMTString.hpp #pragma once#include <cmath> #include <cstdio> #include…

python高級特性

json.dumps({a:1,n:2}) #Python 字典類型轉換為 JSON 對象。相當于jsonify data2 json.loads(json_str)#將 JSON 對象轉換為 Python 字典 異步編程&#xff1a;在異步編程中&#xff0c;程序可以啟動一個長時間運行的任務&#xff0c;然后繼續執行其他任務&#xff0c;而無需等…

ubuntu24離線安裝docker

一、確認ubuntu版本 root@dockerserver:/etc/pam.d# lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 24.04.2 LTS Release: 24.04 Codename: noble 根據codename確認。 docker官方網址下載 https://download.docker.com/linux/…

索尼(sony)攝像機格式化后mp4的恢復方法

索尼(sony)的Alpha 7 Ⅳ系列絕對稱的上是索尼的“全畫幅標桿機型”&#xff0c;A7M4配備了3300萬像素的CMOS&#xff0c;以及全新研發的全畫幅背照式Exmor R?CMOS影像傳感器&#xff0c;搭載BIONZ XR?影像處理器&#xff0c;與旗艦微單?Alpha 1如出一轍。下面我們來看看A7M4…

2025最新出版 Microsoft Project由入門到精通(七)

目錄 優化資源——在資源使用狀況視圖中查看資源的負荷情況 在資源圖表中查看資源的負荷情況 優化資源——資源出現沖突時的原因及處理辦法 資源過度分類的處理解決辦法 首先檢查任務工時的合理性并調整 增加資源供給 回到資源工作表中雙擊對應的過度分配資源 替換資…

最短路與拓撲(1)

1、找最長良序字符串 #include<bits/stdc.h> using namespace std; const int N105; int dis[N]; int vis[N]; int edge[N][N]; int n,m; int vnum;void dij(int u, int v) {// 初始化距離數組和訪問標記for(int i0; i<vnum; i) {vis[i] 0;dis[i] edge[u][i];}// D…

降低60.6%碰撞率!復旦大學地平線CorDriver:首次引入「走廊」增強端到端自動駕駛安全性

導讀 復旦大學&地平線新作-CorDriver: 首次通過引入"走廊"作為中間表征&#xff0c;揭開一個新的范式。預測的走廊作為約束條件整合到軌跡優化過程中。通過擴展優化的可微分性&#xff0c;使優化后的軌跡能無縫地在端到端學習框架中訓練&#xff0c;從而提高安全…

CSS flex:1

在 CSS 中&#xff0c;flex: 1 是一個用于彈性布局&#xff08;Flexbox&#xff09;的簡寫屬性&#xff0c;主要用于控制 flex 項目&#xff08;子元素&#xff09;如何分配父容器的剩余空間。以下是其核心作用和用法&#xff1a; 核心作用 等分剩余空間&#xff1a;讓 flex …