C++多重继承与虚继承

news/2024/4/29 21:09:18

多重继承的原理

多重继承(multiple inheritance)是指从多个直接基类中产生派生类的能力。

多重继承的派生类继承了所有父类的属性。

在面向对象的编程中,多重继承意味着一个类可以从多个父类继承属性和方法。

就像你有一杯混合果汁,它是由多种水果榨取而来的,每种水果都为这杯果汁带来了独特的味道和营养。

假设我们有三个类:Animal(动物)、Bird(鸟类)和Swimmer(游泳者)。

Animal类有eat()方法,Bird类有fly()方法,而Swimmer类有swim()方法。

现在,我们想要创建一个新的类叫做SeaBird(海鸟),它既能飞,又能游泳,还像其他动物一样吃东西。这时候,我们就可以使用多重继承来实现这个需求。


多重继承

多重继承的声明格式如下

class 派生类名:访问说明符 基类1名,访问说明符 基类名2...
{
}或者
struct 派生类名:访问说明符 基类1名,访问说明符 基类名2...
{
}

在派生类的派生列表中可以包含多个基类

class A1:public A{...};
class AB:public A,public B{...};

每个基类包含一个可选的访问说明符。一如往常,如果访问说明符被忽略掉了,则关键字class对应的默认访问说明符是private,关键字struct对应的是public。

class A1:A{};
//和class A1:private A{}等价
//和struct A2:private A{}等价struct A2:A{};
//和struct A2:public A{}等价
//和class A1:public A{}等价

和只有一个基类的继承一样,多重继承的派生列表也只能包含已经被定义过的类,而且这些类不能是final的。

对于派生类能够继承的基类个数,C++没有进行特殊规定;但是在某个给定的派生列表中,同一个基类只能出现一次。

class A1:A,A{}//这是错误的

多重继承的派生类从每个基类中继承状态

在多重继承关系中,派生类的对象包含有每个基类的子对象。

就是说一个派生类对象不仅包含自己特有的部分,还包含它的基类部分

我们举个例子

class A{
int a;
}
class A1:A{
int b;
}

一个A1对象不仅含有它特有的部分(变量b),还含有它的基类部分(变量a) 

派生类构造函数初始化所有基类 

构造一个派生类的对象将同时构造并初始化它的所有基类子对象。

与从一个基类进行的派生一样,多重继承的派生类的构造函数初始值也只能初始化它的直接基类

class A1
{public:A1(int a_):a(a_){}int a;
};
class A2
{
public:int b;A2(int b_):b(b_){}
};
class AA1 :A1,A2
{int c;AA1(int a_,int b_,int c_):A1(a_),A2(b_),c(c_){}//显式地初始化所有基类
};
class A1
{public:A1(int a_):a(a_){}A1(){}int a;
};
class A2
{
public:int b;A2(int b_):b(b_){}
};
class AA1 :A1,A2
{int c;AA1(int a_,int b_,int c_):A2(b_),c(c_){}//隐式的使用A1的默认构造函数来初始化a
};

 多重继承派生类的构造函数

多重继承派生类的构造函数形式与单一继承时的构造函数形式基本相同,只是在派生类的构造函数初始化列表中调用多个基类构造函数。

一般形式为:

派生类名(形式参数列表):基类名1(基类1构造函数实参列表),基类名2(基类2构造函数实参列表),...,成员对象名1(子对象1属类构造函数实参列表),...,派生类初始化列表
{派生类初始化函数体
}

派生类的构造函数初始值列表将实参分别传递给每个直接基类。

其中基类的构造顺序与派生列表中基类的出现顺序保持一致,而与派生类构造函数初始值列表中基类的顺序无关。


其调用顺序是:

  1. 调用基类构造函数,各个基类按定义时的次序先后调用;
  2. 调用成员对象构造函数,各个子对象按声明时的次序先后调用;
  3. 执行派生类初始化列表;
  4. 执行派生类初始化函数体;

例:多重继承举例

#include<iostream> 
using namespace std; class Base1 {private:int b1;public:Base1() {b1=0;cout<<"默认构造Base1:"<<"b1="<<b1<<endl;}Base1(int i) {b1=i;cout<<"构造Base1:"<<"b1="<<b1<<endl;}
};class Base2 {private:int b2;public:Base2() {b2=0;cout<<"默认构造Base2:"<<"b2="<<b2<<endl;}Base2(int j) {b2=j;cout<<"构造Base2:"<<"b2="<<b2<<endl;}
};class Base3 {public:Base3() {cout<<"默认构造Base3:"<<endl;}
};class Derive : public Base1,public Base2,public Base3 {private:Base1 memberBase1;Base2 memberBase2;Base3 memberBase3;public:Derive() {cout<<"默认构造函数Derive."<<endl; }Derive(int a,int b,int c,int d): Base1(a),Base2(b),memberBase1(c),memberBase2(d) {cout<<"构造Derive."<<endl;}
};int main()
{cout<<endl<<"创建派生类对象obj1:"<<endl;Derive obj1;cout<<endl<<"创建派生类对象(1,2,3,4):"<<endl;Derive obj2(1,2,3,4);return 0;
}



运行结果:

创建派生类对象obj1:
默认构造Base1:b1=0//基类默认构造函数,下面2个也是
默认构造Base2:b2=0
默认构造Base3:
默认构造Base1:b1=0//成员对象的构造函数,下面2个也是
默认构造Base2:b2=0
默认构造Base3:
默认构造函数Derive,//派生类默认构造函数创建派生类对象(1.2.3.4):
构造Base1:b1=1   //基类构造函数,下面2个也是
构造Base2:b2=2
默认构造Base3:
构造Base1:b1=3  //成员对象的构造函数,下面2个也是
构造Base2:b2=4
默认构造Base3:
构造Derive.     //派生类构造函数

继承的构造函数的易错点

在C++11新标准中,允许派生类从它的一个或几个基类中继承构造函数。

但是如果从多个基类中继承了相同的构造函数(即形参列表完全相同)则程序将产生错误:

struct Base1 {Base1() = default;Base1(const std::string&) {}Base1(std::shared_ptr<int>) {}
};
struct Base2 {Base2() = default;Base2(const std::string&) {}Base2(int) {}
};
//错误:D1试图从两个基类中都继承D1::Dl(const string&)
struct D1 : public Base1, public Base2 {using Base1::Base1; //从Base1继承构造函数using Base2::Base2; // 从Base2继承构造函数
};

发生错误的原因是D1企图多个基类中继承了相同的构造函数(即形参列表完全相同),而且它没有定义自己版本的构造函数。

有两种解决办法

第一种方法

如果我们把Base1的第二个构造函数的函数参数改为int,就不会有错误了

struct Base1 {Base1() = default;Base1 (int&) {}Base1(std::shared_ptr<int>) {}
};
struct Base2 {Base2() = default;Base2(const std::string&) {}Base2(int) {}
};
//正确
struct D1 : public Base1, public Base2 {using Base1::Base1; //从Base1继承构造函数using Base2::Base2; // 从Base2继承构造函数
};

第二种方法

如果一个类从它的多个基类中继承了相同的构造函数,则这个类必须为该构造函数定义它自己的版本:

struct Base1 {Base1() = default;Base1 (const std::string&) {}Base1(std::shared_ptr<int>) {}
};
struct Base2 {Base2() = default;Base2(const std::string&) {}Base2(int) {}
};
//正确
struct D1 : public Base1, public Base2 {using Base1::Base1; //从Base1继承构造函数using Base2::Base2; // 从Base2继承构造函数D1(const std::string&a):Base1(a),Base2(a){}
};

注意不能是不带任何参数的默认构造函数

struct Base1 {Base1() = default;Base1 (const std::string&) {}Base1(std::shared_ptr<int>) {}
};
struct Base2 {Base2() = default;Base2(const std::string&) {}Base2(int) {}
};
//正确
struct D1 : public Base1, public Base2 {using Base1::Base1; //从Base1继承构造函数using Base2::Base2; // 从Base2继承构造函数D1(){}//错误
};

析构函数与多重继承

和往常一样,派生类的析构函数只负责清除派生类本身分配的资源,派生类的成员及基类都是自动销毁的。合成的析构函数体为空。

析构函数的调用顺序正好与构造函数相反。

我们可以来验证一番

#include<iostream>
using namespace std;
class A1
{public:A1(int a_) :a(a_) { cout << "A1的构造函数被调用" << endl; }A1(){}int a;~A1() { cout << "A1的析构函数被调用" << endl; }
};class A2
{
public:int b;A2(int b_):b(b_) { cout << "A2的构造函数被调用" << endl; }~A2() { cout << "A2的析构函数被调用"<<endl; }
};
class AA1 :A1,A2
{
public:int c;AA1(int a_,int b_,int c_):A1(a_),A2(b_),c(c_) { cout << "AA1的构造函数被调用" << endl; }~AA1() { cout << "AA1的析构函数被调用" << endl; }
};
int main()
{{AA1 a(1, 2, 3);}
}

事实确实如此

多重继承的派生类的拷贝与移动操作

与只有一个基类的继承一样,多重继承的派生类如果定义了自己的拷贝/赋值构造函数和赋值运算符,则必须在完整的对象上执行拷贝、移动或赋值操作。

只有当派生类使用的是合成版本的拷贝、移动或赋值成员时,才会自动对其基类部分执行这些操作。在合成的拷贝控制成员中,每个基类分别使用自己的对应成员隐式地完成构造、赋值或销毁等工作。

例如

class Bear:public Zooanimal{}
class Panda:public Bear,public Endangered{}Panda ying_ yang("ying_yang");
panda ling_ling =ying_yang;// 使用拷贝构造函数


将调用Bear的拷贝构造函数,后者又在执行自己的迷贝任务之前先调用ZooAnimal的的拷贝构造函数。一旦ling_ling 的Bear 部分构造完成,接着就会调用 Endangered来创建对象相应的部分。最后,执行Panda的拷贝构造函数。合成的移动构造函数的工作机理与之类似。

合成的拷贝赋值运算符的行为与拷贝构造函数报相似,它首先球值Bear 部分(并且通过Bear赋值ZooAnimal部分),然后赋值Endanagered部分,最后是Panda部分。

类型转换与多个基类

在只有一个基类的情况下,公有继承派生类的指针或引用能自动转换成一个可访问基类的指针或引用。多个基类的情况与之类似。

我们可以令某个可访问基类的指针或引用直接指向一个派生类对象。

例如,一个ZooAnimal、Bear或Endangered类型的指针或引用可以绑定到Panda对象上://接受

Panda的基类引用的一系列操作
 

class Bear:public Zooanimal{}
class Panda:public Bear,public Endangered{}void print(const Bear&);
void highlight (const Endangered&) ;
ostream& operator<<(ostream&, const ZooAnimal&);Panda ying yang("ying_yang");
print(ying_yang); //把一个Panda对象传递给一个Bear的引用
highlight(ying_yang); // 把一个 Panda 对象传递给一个Endangered的引用
cout << ying yang << endl;// 把一个Panda 对象传递给一个ZooAnimal的引用

编译器不会在派生类向基类的几种转换中进行比较和选择,因为在它看来转换到任意一种基类都一样好。

例如,如果存在如下所示的print重载形式:

void print(const Bear&);
void print(const Endangered&);


则通过Panda对象对不带前缀限定符的print函数进行调用将产生编译错误:

Panda ying_yang("ying_yang");
print(ying_yang); // 二义性错误


基于指针类型或引用类型的查找

与只有一个基类的继承一样,对象、指针和引用的前态类里快定了我们能够使用哪些成员。

如果我们使用一个ZooAnimal指针,则只有定义在zooAnimal中的操作是可以使用的,Panda 接口中的 Bear、Panda和Endangered特有的部分都不可见。

类似的,一个 Bear 类型的指针或引用只能访问BearzooAnimal的成员,一个Endangered的指针或引用只能访问Endangered的成员

举个例子,已知我们的类已经定义了表中列出的虚函数,考虑下面的这些函数调用。

在ZooAnimal/Endangered中定义的虚函数
函数含有自定义版本的类
printZooAnimal::ZooAnimal
Bear::Bear
Endangered::Endangered
Panda::Panda
highlightEndangered::Endangered
Panda::Panda
toesBear::Bear
Panda::Panda
cuddlePanda::Panda
析构函数ZooAnimal::ZooAnimal
Endangered::Endangered

class Bear:public Zooanimal{}
class Panda:public Bear,public Endangered{}Bear *pb = new Panda("ying yang");
pb->print(); // 正确:Panda::print()
pb->cuddle(); // 错误:不属于Bear的接口
pb->highlight(); // 错误:不属于Bear的接口
delete pb; // 正确:Panda::~Panda()


当我们通过Endangered的指针或引用访问一个Panda 对象时,Panda 接口中Panda特有的部分以及属于Bear的部分都是不可见的:

class Bear:public Zooanimal{}
class Panda:public Bear,public Endangered{}Endangered *pe = new Panda("ying yang");
pe->print(); //正确:Panda::print()
pe->toes(); // 错误:不属于 Endangered的接口
pe->cuddle(); // 错误:不属于 Endangered的接口
pe->highlight(); // 正确:Panda::highlight()
delete pe; // 正确:Panda::~Panda()

a

二义性问题及名字支配规则

1.二义性问题

  1. 在只有一个基类的情况下,派生类的作用域嵌套在直接基类和间接基类的作用域中。查找过程沿着继承体系自底向上进行,直到找到所需的名字。派生类的名字将隐藏基类的同名成员。
  2. 在多重继承的情况下,相同的查找过程在所有直接基类中同时进行。如果名字在多个基类中都被找到,则对该名字的使用将具有二义性。

当一个类拥有多个基类时,有可能出现派生类从两个或更多基类中继承了同名成员的情况。此时,不加前缀限定符直接使用该名字将引发二义性。

多重继承时,多个基类可能出现同名的成员。在派生类中如果使用一个表达式的含义能解释为可以访问多个基类的成员,则这种对基类成员的访问就是不确定的,称这种访问具有二义性。C++要求派生类对基类成员的访问必须是无二义性的

例如:

class A {public:void fun() { cout<<"a.fun"<<endl; }
};
class B {public:void fun() { cout<<"b.fun"<<endl; }void gun() { cout<<"b.gun"<<endl; }
};
class C:public A,public B {public:void gun() { cout<<"c.gun"<<endl; } //重写gun()void hun() { fun(); } //出现二义性
};
int main()
{C c,*p=&c;return 0;
}


使用成员名限定可以消除二义性,例如:

//成员名限定消除二义性

c.A::fun();
c.B::fun();
p->A::fun();
p->B::fun();



基本形式为:

对象名.基类名::成员名
对象指针名->基类名::成员名


2.名字支配规则

C++对于在不同的作用域声明的名字,可见性原则是:如果存在两个或多个具有包含关系的作用域,外层声明了一个名字,而内层没有再次声明相同的名字,那么外层名字在内层可见;如果在内层声明了相同的名字,则外层名字在内层不可见,这时称内层名字隐藏(或覆盖)了外层名字,这种现象称为隐藏规则

在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域,二者的作用域是不同的:基类在外层,派生类在内层

如果派生类声明了一个和基类成员同名的新成员,派生的新成员就覆盖了基类同名成员,直接使用成员名只能访问到派生类的成员

如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数不同,从基类继承的同名函数的所有重载形式也都会被覆盖

如果要访问被覆盖的成员,就需要使用基类名和作用域限定运算符来限定

派生类D中的名字N覆盖基类B中同名的名字N,称为名字支配规则。如果一个名字支配另一个名字,则二者之间不存在二义性,当选择该名字时,使用支配者的名字,如:

c.gun(); //使用C::gun



如果要使用被支配者的名字,则应使用成员名限定,例如:

c.B::gun(); //使用B::gun



虚基类

C++引入虚基类的目的是为了解决多继承时可能出现的冲突问题。当一个类通过多个路径继承了同一个基类时,如果不使用虚基类,那么在派生类中就会存在多个基类子对象,这可能会导致数据重复和二义性的问题。

通过使用虚基类,可以确保在派生类中只有一个基类子对象,从而避免数据重复和二义性的问题。虚基类的成员在派生类中只有一个副本,不会重复出现。

另外,虚基类还可以实现多态性,能够让派生类对象按照基类的指针或引用进行使用,从而提高代码的灵活性和可扩展性。

1.虚基类的定义

虚基类是在派生类定义时,指定继承方式时声明的。声明虚基类的一般形式为:

class 派生类名: virtual 访问标签 虚基类名,... { //类体成员列表
};

还有一种形式是

class 派生类名: 访问标签 virtual 虚基类名,... { // 成员列表
};



需要注意,为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类。否则仍然会出现对基类的多次继承(我们下面会举例子)

例:虚基类举例

#include<iostream>
using namespace std; class A { //声明为基类Apublic:A(int n) { //A类的构造函数 nv=n;cout<<"Member of A"<<endl;} void fun() {cout<<"fun of A"<<endl;}private:int nv;
};class B1: virtual public A { //声明A为虚基类 public:B1(int a):A(a) { //B1类的构造函数 cout<<"Member of B1"<<endl;}private:int nv1; 
};class B2: virtual public A { //声明A为虚基类public:B2(int a):A(a) { //B2类的构造函数cout<<"Member of B2"<<endl; } private:int nv2;
};class C: public B1,public B2 {public://派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用C(int a):A(a),B1(a),B2(a) {cout<<"Member of C"<<endl;}void fund() {cout<<"fun of C"<<endl;}private:int nvd;
};int main()
{C c1(1);c1.fund();c1.fun(); //不会产生二义性return 0; 
}

 现在C对象只将包含A对象的一个副本。从更本质的说,继承的B1和B2对象共享一个A对象,而不是各种引入自己的A对象副本。这样子调用A类方法就不会有二义性了(不知道调用B!继承的还是B2继承的A类方法)。

如果我们不引入虚基类,再看看这个例子

#include<iostream>
using namespace std;class A { //声明为基类A
public:A(int n) { //A类的构造函数 nv = n;cout << "Member of A" << endl;}void fun() {cout << "fun of A" << endl;}
private:int nv;
};class B1 : virtual public A { //声明A为虚基类 
public:B1(int a) :A(a) { //B1类的构造函数 cout << "Member of B1" << endl;}
private:int nv1;
};class B2 :  public A {         //注意这里没有声明A为虚基类
public:B2(int a) :A(a) { //B2类的构造函数cout << "Member of B2" << endl;}
private:int nv2;
};class C : public B1, public B2 {
public://派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用C(int a) :A(a), B1(a), B2(a) {cout << "Member of C" << endl;}void fund() {cout << "fun of C" << endl;}
private:int nvd;
};int main()
{C c1(1);c1.fund();c1.fun(); //产生了二义性return 0;
}

我们将B2后面的virtual去掉,使A类失去虚基类的性质。发现上面这个程序出现了二义性,编译器不知道调用B1继承的A类方法还是B2继承的A类方法。 


2.虚基类的初始化

如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生和间接派生)中,都要通过构造函数的初始化表对虚基类进行初始化。例如:

class A { public: A(int) { } }; //定义构造函数有参数的基类
class B: virtual public A {public:B(int a):A(a) { } //对基类A初始化
};
class C: virtual public A {public:C(int a):A(a) { } //对基类A初始化
};
class D: public B,public C {public:D(int a):A(a),B(a),C(a) { }
};


在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化

关于虚基类的说明:

(1)一个类可以在一个类族中即被用作虚基类,也被用作非虚基类

(2)派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的默认构造函数

(3)在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行多重继承应用举例

#include<iostream>
using namespace std; enum Color { //颜色枚举类型 Red,Yellow,Green,White
};class Circle { //圆类Circle的定义private:float radius;public:Circle(float r) {radius=r;cout<<"Circle initialized!"<<endl;} ~Circle() {cout<<"Circle destroyed!"<<endl;}float Area() {return 3.1415926*radius*radius;}
};class Table { //桌子类Table的定义private:float height;public:Table(float h) {height=h;cout<<"Table initialized!"<<endl;}~Table() {cout<<"Table destroyed!"<<endl;}float Height() {return height;}
};class RoundTable: public Table,public Circle { //圆桌类的定义private:Color color;public:RoundTable(float h,float r,Color c);int GetColor() {return color;}~RoundTable() {cout<<"RoundTable destroyed!"<<endl;}
};RoundTable::RoundTable(float h,float r,Color c):Table(h),Circle(r) { //圆桌构造函数的定义color=c;cout<<"RoundTable initialized!"<<endl; 
}int main()
{RoundTable cir_table(15.0,2.0,Yellow);cout<<"The table properties are:"<<endl;cout<<"Height="<<cir_table.Height()<<endl; //调用Table类的成员函数cout<<"Area="<<cir_table.Area()<<endl; //调用circle类的成员函数cout<<"Color="<<cir_table.GetColor()<<endl; //调用RoundTable 类的成员函数return 0;
}



运行结果:

Table initialized!
Circle initialized!
RoundTable initialized!
The table properties are:
Height=15
Area=12.5664
Color=1
RoundTable destroyed!
Circle destroyed!
Table destroyed!

支持向基类的常规类型转换 

不论基类是不是虚基类,派生类对象都能被可访问基类的指针或引用操作。

例如,下面这些从Panda向基类的类型转换都是合法的:

class Raccoon:public virtual ZooAnimal{}
class Bear:virtual public ZooAnimal{}
class Panda:public Bear,public Raccoon,public Endangered{}void dance (const Bear&);
void rummage (const Raccoon&);
ostream& operator<<(ostream&, const ZooAnimal&);Panda ying_yang; // 正确:把一个Panda对象当成Bear传递
dance(ying_yang);//正确:把一个Panda对象当成Raccoon传递
rummage (ying_yang);// 正确:把一个 Panda 对象当成 ZooAnimal 传递
cout << ying_yang; 


虚基类成员的可见性

因为在每个共享的虚基类中只有唯一一个共享的子对象,所以该基类的成员可以被直接访问,并且不会产生二义性。

此外,如果虚基类的成员只被一条派生路径覆盖,则我们仍然可以直接访问这个被覆盖的成员。但是如果成员被多于一个基类覆盖,则一般情况下派生类必须为该成员自定义一个新的版本。

假定类B定义了一个名为x的成员,D1和D2都是从B虚继承得到的,D继承了D1和D2,则在D的作用域中,x通过D的两个基类都是可见的。如果我们通过D的对象使用x,有三种可能性

如果在D1和D2中都没有x的定义,则x将被解析为B的成员,此时不存在二义性,一个D的对象只含有x的一个实例。

class B
{
public:int x;B(int x_):x(x_){}B() {}
};
class D1:virtual public B
{
public:D1(int a_):B(a_){}
};
class D2 :virtual public B
{
public:D2(int a_):B(a_) {}
};
class D :public D1, public D2
{public:D(int a1,int a2):B(a1),D1(a1),D2(a2){}
};
int main()
{D a(1, 1);cout << a.x << endl;//解析为B.x
}


如果x是B的成员,同时是D1和D2中某一个的成员,则同样没有二义性,派生类的x比共享虚基类B的x优先级更高。

class B
{
public:int x;B(int x_):x(x_){}B() {}
};
class D1:virtual public B
{
public:D1(int a_):B(a_){}
};
class D2 :virtual public B
{
public:D2(int a_):B(a_),x(a_) {}int x;//定义了x
};
class D :public D1, public D2
{public:D(int a1,int a2,int a3):B(a1),D1(a2),D2(a3){}
};
int main()
{D a(1, 2,3);cout << a.x << endl;//结果是3
}

如果在D1和D2中都有x的定义,则直接访问x将产生二义性问题。

class B
{
public:int x;B(int x_):x(x_){}B() {}
};
class D1:virtual public B
{
public:D1(int a_) :B(a_), x(a_) {}int x;//定义了x
};
class D2 :virtual public B
{
public:D2(int a_):B(a_),x(a_) {}int x;//定义了x
};
class D :public D1, public D2
{public:D(int a1,int a2,int a3):B(a1),D1(a2),D2(a3){}
};
int main()
{D a(1, 2,3);cout << a.x << endl;
}


与非虚的多重继承体系一样,解决这种二义性问题最好的方法是在派生类中为成员自定义
新的实例。


构造函数与虚继承

在虚派生中,虚基类是由最低层的派生类初始化的。

class Raccoon:public virtual ZooAnimal{}
class Bear:virtual public ZooAnimal{}
class Panda:public Bear,public Raccoon,public Endangered{}

以我们的程序为例,当创建Panda对象时,由Panda的构造函数独自控制zooAnimal的初始化过程。

为了理解这一规则,我们不妨假设当以普通规则处理初始化任务时会发生什么情况

在此例中,虚基类将会在多条继承路径上被重复初始化。

以ZooAnimal为例,如果应用普通规则,则Raccoon和Bear都会试图初始化Panda对象的ZooAnimal部分

当然,继承体系中的每个类都可能在某个时刻成为“最低层的派生类”。 

只要我们能创建虚基类的派生类对象,该派生类的构造函数就必须初始化它的虚基类。

例如在我们的继承体系中,当创建一个Bear(或Raccoon)的对象时,它已经位于派生的最低层,因
此Bear(或Raccoon)的构造函数将直接初始化其ZooAnimal基类部分;

class Raccoon:public virtual ZooAnimal{}
class Bear:virtual public ZooAnimal{}
class Panda:public Bear,public Raccoon,public Endangered{}Bear::Bear(std::string name, bool onExhibit):ZooAnimal (name, onExhibit, "Bear"){}Raccoon::Raccoon(std::string name, bool onExhibit):ZooAnimal (name, onExhibit, "Raccoon") {}


而当创建一个Panda 对象时,Panda位于派生的最低层并由它负责初始化共享的ZooAnimal基类部分。即使ZooAnimal不是Panda的直接基类,Panda的构造函数也可以初始化ZooAnimal:
 

class Raccoon:public virtual ZooAnimal{}
class Bear:virtual public ZooAnimal{}
class Panda:public Bear,public Raccoon,public Endangered{}Panda::Panda(std::string name, bool onExhibit):ZooAnimal (name, onExhibit, "Panda"),Bear (name, onExhibit),Raccoon(name, onExhibit) Endangered(Endangered::critical)sleeping_flag(false) {}

虚继承的对象的构造方式

含有虚基类的对象的构造顺序与一般的顺序稍有区别:

首先使用提供给最低层派生类构造函数的初始值初始化该对象的虚基类子部分,接下来按照直接基类在派生列表中出现的次序依次对其进行初始化。

例如,当我们创建一个Panda对象时:

  1. 首先使用Panda 的构造函数初始值列表中提供的初始值构造虚基类ZooAnimal部分。
  2. 接下来构造Bear部分。
  3. 然后构造Raccoon部分。
  4. 然后构造第三个直接基类Endangered。
  5. 最后构造Panda部分。

如果Panda 没有显式地初始化ZooAnimal基类,则ZooAnimal的默认构造函数将被调用。如果ZooAnimal没有默认构造函数,则代码将发生错误。

虚基类总是先于非虚基类构造,与它们在继承体系中的求序和位置无关。

构造函数与析构函数的次序


一个类可以有多个虚基类。此时,这些虚的子对象按照它们在派生列表中出现的顺序从左向右依次构造。

例如,在下面这个稍显杂乱的Teddvpear派生关系中有两个虚基类:

ToyAnimal是直接虚基类,ZooAnima1是Bear的虚基类:
 

class Character {};
class BookCharacter:public Character{};
class ToyAnimal {};
class TeddyBear : public BookCharacter, public Bear, public virtual ToyAnimal{};


编译器按照直接基类的声明顺序对其依次进行检查,以确定其中是否含有虚基类。

如果有,则先构造虚基类,然后按照声明的顺序逐一构造其他非虚基类。

因此,要想创建一个TeddyBear对象,需要按照如下次序调用这些构造函数:

ZooAnimal(); // Bear的虚基类
ToyAnimal(); //直接虚基类
Character(); //第一个非虚基类的间接基类
BookCharacter(); // 第一个直接非虚基类
Bear();// 第二个直接非虚基类
TeddyBear(); //最低层的派生类


合成的拷贝和移动构造函数按照完全相同的顺序执行,合成的赋值运算符中的成员也按照该顺序赋值。

和往常一样,对象的销毁顺序与构造顺序正好相反,首先销毁TeddyBear 部分,最后销毁zooAnimal部分。


http://www.mrgr.cn/p/74844000

相关文章

Visio中存在问题的解决方法

公式缩放 mathtype公式在visio缩放之后&#xff0c;出现了变形。 解决方法&#xff1a;每次输入公式都通过 插入->对象->mathType Equation 新建一个公式。可以避免 注&#xff1a;网上有的说在word中使用mathtype编写公式&#xff0c;之后复制到visio中。 插入波形 选择…

python知识点总结(十)

python知识点总结十 1、装饰器的理解、并实现一个计时器记录执行性能&#xff0c;并且将执行结果写入日志文件中2、队列和栈的区别&#xff0c;并且用python实现3、设计实现遍历目录与子目录4、CPU处理进程最慢的情况通常发生在以下几种情况下&#xff1a;5、CPU处理线程最慢的…

C# 获取程序路径的几种方法及其区别【WPF】

遇到的问题 但最近发现一个问题:如果程序是由用户通过exe文件直接打开的,获取同目录下配置文件不会有问题;但如果程序是由第三方应用启动的,通过不同方式获取的“当前目录”会有不同。以下是常用的记住获取当前目的的方法: System.Environment.CurrentDirectory System.IO…

C语言分支循环语句详解

分支和循环语句是什么 在我们写程序的时候&#xff0c;总会遇到想一直循环执行某种语句的时候&#xff0c;这时候我们就要使用一种语句叫循环语句&#xff0c;或者带一些判断条件的语句&#xff0c;在C语言中提供了像这些的循环语句和分支语句 if else 语句 这是一种判断语句…

P8623 [蓝桥杯 2015 省 B] 移动距离 Python

[蓝桥杯 2015 省 B] 移动距离 题目描述 X 星球居民小区的楼房全是一样的&#xff0c;并且按矩阵样式排列。其楼房的编号为 $1,2,3, \cdots $ 。 当排满一行时&#xff0c;从下一行相邻的楼往反方向排号。 比如&#xff1a;当小区排号宽度为 6 6 6 时&#xff0c;开始情形如…

学习总结!

1.构造器&#xff1a; 构造器通常也叫构造方法、构造函数&#xff0c;构造器在每个项目中几乎无处不在。当你new一个对象时&#xff0c;就会调用构造器。构造器格式如下&#xff1a; [修饰符&#xff0c;比如public] 类名 (参数列表&#xff0c;可以没有参数){ //这里不能有r…

Unity PS5开发 天坑篇 之 URP管线与HDRP管线部署流程以及出包介绍04

目录 一, URP管线、HDRP管线下的Unity项目部署 1. PS5开发论坛关于Unity可支持的版本说明: 2. URP管线下的项目与部署 2.1 Build PS5 URP Project 2.2 运行画面 3. HDRP管线下的项目与部署 3.1 附上可以运行的画面: 4. PS5打包方式介绍 4.1 PC串流调试模式: Build Typ…

面试题--3.18

1. http与https的区别&#xff0c;以及https的认证过程及加密算法 &#xff1f; 区别&#xff1a; https协议需要到CA申请证书&#xff0c;一般免费证书较少&#xff0c;因而需要一定费用。 http是超文本传输协议&#xff0c;信息是明文传输&#xff0c;https则是具有安全性…

打造核心竞争力:高效Web系统数据中台的设计与实践_光点科技

在数字化的浪潮中&#xff0c;数据已经成为企业赖以生存和发展的核心资源。一个高效的Web系统数据中台&#xff0c;能够赋予企业在激烈的市场竞争中立于不败之地的能力。本文将深入探讨如何设计和实施一个能够提升企业数据管理水平和支持业务决策的高效数据中台架构。 数据中台…

接单收入超过主业!程序员必备的兼职接单平台汇总

这个月副业接单的款项到账了&#xff0c;由于接了个大单&#xff0c;这个月净收入5w&#xff0c;一下子副业收入超过了主业。不得不说&#xff0c;程序员接单确实是一条赚钱的新路子&#xff0c;都说节流不如开源&#xff0c;这下我是真的体会到了&#xff01; 今天&#xff0c…

计算机组成原理 — 指令系统

指令系统 指令系统指令的概述指令的格式指令的字长取决于 操作数类型和操作种类操作数的类型数据在存储器中的存放方式操作类型 寻址方式指令寻址数据寻址立即寻址直接寻址隐含寻址间接寻址寄存器寻址寄存器间接寻址基址寻址变址寻址堆栈寻址 RISC 和 CISC 技术RISC 即精简指令…

Camera subdev注册流程

参考资料: 极客笔记https://deepinout.com/camera-sensor-driver/camera-sensor-driver-sensor-subdev-register-flow.htmlCamera驱动模型:用户空间通过CSL协议层,才能访问到Camera subdev,/dev/v4l-subdevx都不是固定的 可以通过cat /sys/class/video4linux/v4l-subdevx/n…

Java设计模式之单例模式(多种实现方式)

虽然写了很多年代码&#xff0c;但是说真的对设计模式不是很熟练&#xff0c;虽然平时也会用到一些&#xff0c;但是都没有深入研究过&#xff0c;所以趁现在有空练下手 这章主要讲单例模式&#xff0c;也是最简单的一种模式&#xff0c;但是因为spring中bean的广泛应用&#…

基于SpringBoot后端实现连接MySQL数据库并存贮数据

目录 一、什么是MySQL数据库 二、基于SpringBoot框架连接MySQL数据库 1、首先添加MySQL依赖&#xff1a; 2、配置数据库连接&#xff1a; 3、创建实体类&#xff1a; 4、创建Repository接口&#xff1a; 5、使用Repository&#xff1a; 三、编写业务SQL语句 1、使用Spring Data…

微服务高级篇(三):分布式缓存+Redis集群

文章目录 一、单点Redis的问题及解决方案二、Redis持久化2.1 单机安装Redis2.2 RDB持久化2.3 AOF持久化2.4 RDB和AOF对比 三、Redis主从3.1 搭建Redis主从架构3.1.1 集群结构3.1.2 准备实例和配置3.1.3 启动3.1.4 开启主从关系3.1.5 测试 3.2 数据同步3.2.1 全量同步【建立连接…

FreeType Glyph Conventions 翻译(4) ——Kerning 字间距调整

原文地址 https://freetype.org/freetype2/docs/glyphs/glyphs-4.html 目录字间距调整对子 Kerning pairs应用字间距调整 Applying kerning The term kerning refers to specific information used to adjust the relative positions of successive glyphs in a string of text…

MHA高可用+VIP漂移

目录一、环境搭建1、关闭防火墙firewalld,selinux2、每台主机安装MySQL二、基于GTID的主从复制1、修改/etc/my.cnf文件2、检查GTID状态3、配置主从复制4、从库设置三、部署MHA1、准备环境(所有节点)2、部署管理节点(可以部署在任何机器上)3、配置ssh信任4、启动测试(mana…

iOS客户端自动化UI自动化airtest+appium从0到1搭建macos+脚本设计demo演示+全网最全最详细保姆级有步骤有图

Android客户端自动化UI自动化airtest从0到1搭建macos脚本设计demo演示全网最全最详细保姆级有步骤有图-CSDN博客 避坑系列-必读&#xff1a; 不要安装iOS-Tagent &#xff0c;安装appium -这2个性质其实是差不多的都是为了安装wda。注意安装appium最新版本&#xff0c;安装完…

计算机复试面试问答准备(未完)

目录 1、理解多态性2、怎么逆置⼀个链表3、顺序表和链表的区别4、树的存储结构5、什么是哈夫曼树&#xff1f;简述哈夫曼树的构造过程。介绍哈夫曼树的特性。6、哈夫曼编码的编码和解码过程7、图的遍历方式8、图的存储方式9、最小生成树10、迪杰斯特拉算法11、佛洛依德算法12、…

逆向中常见的加密算法

逆向中常见的加密算法 1.Base64 1) 原理与特征: ​ a.原理:将3个byte(即38=24bit)切割为46,然后根据6bit表示的数字在base64表(64byte的表)寻找对应的值;如果待加密字符串长度不为3的整数,则在末尾处补0对齐,其中0对应的字符为=。 ​ b.特征:在反汇编代码中会出现0x…