十四、继承与组合(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.mzph.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 …