C++多重继承与虚继承

多重继承的原理

多重继承(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 { //声明为基类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: 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部分。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/496896.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

452. 用最少数量的箭引爆气球(力扣LeetCode)

文章目录 452. 用最少数量的箭引爆气球贪心算法代码 452. 用最少数量的箭引爆气球 有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points &#xff0c;其中points[i] [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确…

花了100块大洋搞懂 ipv6的用户如何访问ipv4 服务器

大家好&#xff0c;今天蓝胖子花了100多块搞懂了 ipv6的用户如何访问ipv4 服务器&#xff0c;将收获与大家分享下。 ipv4和ipv6的协议栈不同&#xff0c;这意味着&#xff0c;其对应的ip包的封装和解析不同&#xff0c;那么只支持ipv4的机器就无法直接与ipv6的服务器进行通信。…

kubernetes K8s的监控系统Prometheus安装使用(一)

简单介绍 Prometheus 是一款基于时序数据库的开源监控告警系统&#xff0c;非常适合Kubernetes集群的监控。Prometheus的基本原理是通过HTTP协议周期性抓取被监控组件的状态&#xff0c;任意组件只要提供对应的HTTP接口就可以接入监控。不需要任何SDK或者其他的集成过程。这样做…

网络服务练习题

综合练习&#xff1a;请给 openlab 搭建 web 网站 网站需求&#xff1a; 1. 基于域名 www.openlab.com 可以访问网站内容为 welcome to openlab!!! 2. 给该公司创建三个子界面分别显示学生信息&#xff0c;教学资料 和缴费网站&#xff0c;基于&#xff0c; www.openlab.c…

【Linux 驱动基础】Linux platform平台设备驱动

# 前置知识 总线驱动模型简介&#xff1a; 总线是处理器与一个或者多个设备之间的通道&#xff0c;在设备模型中&#xff0c;所有的设备都是通过总线相连&#xff0c;当然也包括虚拟的 platform 平台总线。 总线驱动模型中有三要素&#xff1a; 1. 总线 /*** struct bus_ty…

C语言书籍——B/陷阱之处(2)

文章参考于文献&#xff1a;《C陷阱与缺陷》[美]Andrew Koening &#x1f308;个人主页&#xff1a;慢了半拍 &#x1f525; 创作专栏&#xff1a;《史上最强算法分析》 | 《无味生》 |《史上最强C语言讲解》 | 《史上最强C练习解析》 &#x1f3c6;我的格言&#xff1a;一切只…

Obsidian插件-高亮块(Admonition)

在插件市场里面搜索Admonition并安装插件&#xff0c;就可以使用高亮块了。 添加高亮块 用法稍微有一些不同。按照下面的格式&#xff0c;输入Markdown就可以创建一个高亮块。 内容内容内容输入*ad-*会出现相应的类型可以选择

Dubbo管理控制台

1.将资料中的dubbo-admin-2.6.0.war文件复制到tomcat的webapps目录下 2.启动tomcat,修改WEB-INF下的dubbo.properties文件 #如果Zookeeper是安装在虚拟机上的那么注册中心的地址需要修改为虚拟机的ip地址 dubbo.registry.addresszookeeper://192.168.100.110:2181 dubbo.admin…

对象存储服务MinIO快速入门

对象存储服务MinIO快速入门 MinIO简介开箱使用快速入门封装MinIO为starter1 创建模块heima-file-starter2 配置类3 封装操作minIO类4 对外加入自动配置5 其他微服务使用 MinIO简介 官网文档 开箱使用 docker run -p 9000:9000 --name minio -d --restartalways -e "MINIO…

ocr之opencv配合paddleocr提高识别率

背景1&#xff1a;在这篇文章编写之前使用到的工具并不是opencv&#xff0c;而是java原有的工具BufferedImage。但因为在使用过程中会频繁切图&#xff0c;放大&#xff0c;模糊&#xff0c;所以导致的jvm内存使用量巨大&#xff0c;分秒中都在以百兆的速度累加内存空间。这种情…

WIFI驱动移植实验: openssl库的移植(wpa_supplicant 依赖库)

一. 简介 前面实现了WIFI驱动的移植&#xff0c;而连接某个WIFI热点上就需要用到 wpa_supplicant 工具&#xff0c;所以&#xff0c;本文开始为 移植 wpa_supplicant 工具做准备。 wpa_supplicant 依赖于 openssl库 与 libnl库&#xff0c;因此&#xff0c;需要移植一下open…

鸿蒙hdc使用指导

简介 hdc&#xff08;HarmonyOS Device Connector&#xff09;是HarmonyOS为开发人员提供的用于调试的命令行工具&#xff0c;通过该工具可以在windows/linux/mac系统上与真实设备或者模拟器进行交互。 环境准备 hdc工具通过HarmonyOS SDK获取&#xff0c;存放于SDK的toolch…

蓝桥杯练习题总结(三)线性dp题(摆花、数字三角形加强版)

目录 一、摆花 思路一&#xff1a; 确定状态&#xff1a; 初始化&#xff1a; 思路二&#xff1a; 确定状态&#xff1a; 初始化&#xff1a; 循环遍历&#xff1a; 状态转移方程&#xff1a; 二、数字三角形加强版 一、摆花 题目描述 小明的花店新开张&#xff0c;为了吸…

VBA技术资料MF134:单值匹配查找

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

【消息队列开发】 实现 MqClientTests 类——测试客户端

文章目录 &#x1f343;前言&#x1f333;所需属性&#x1f334;BeforeEach&#x1f332;AfterEach&#x1f38d;API测试⭕总结 &#x1f343;前言 本次开发任务 测试客户端接口 &#x1f333;所需属性 所需要一共三个属性 BrokerServer&#xff1a;服务器 ConnectionFa…

C++进阶之路---C++11新特性 | lambda表达式

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 前言&#xff1a;简介lambda 在C中&#xff0c;lambda表达式是一种匿名函数的方式&#xff0c;它可以用来解决以下问题&a…

免费|Python|【需求响应】一种新的需求响应机制DR-VCG研究

目录 1 主要内容 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序对应文章《Contract Design for Energy Demand Response》&#xff0c;电力系统需求响应&#xff08;DR&#xff09;用来调节用户对电能的需求&#xff0c;即在预测的需求高于电能供应时&#xff0c;希…

TTS 文本转语音模型综合简述

本文参考文献&#xff1a; [1] Kaur N, Singh P. Conventional and contemporary approaches used in text ot speech synthesis: A review[J]. Artificial Intelligence Review, 2023, 56(7): 5837-5880. [2] TTS | 一文了解语音合成经典论文/最新语音合成论文篇【20240111更新…

使用certbot为网站启用https

1. 安装certbot客户端 cd /usr/local/bin wget https://dl.eff.org/certbot-auto chmod ax ./certbot-auto 2. 创建目录和配置nginx用于验证域名 mkdir -p /data/www/letsencryptserver {listen 80;server_name ~^(?<subdomain>.).ninvfeng.com;location /.well-known…

大数据之scala

为什么学习scala spark是新一代内存级大数据计算框架&#xff0c;是大数据的重要内容 spark就是使用scala编写的&#xff0c;因此为了更好的学习spark&#xff0c;需要掌握scala这门语言 spark的兴起&#xff0c;带动scala语言的发展 scala发展历史 联邦理工学院的马丁 奥德…