C++多重继承与虚继承

news2024/12/22 15:07:27

多重继承的原理

多重继承(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部分。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

任务2.1 一元二次方程(顺序结构版)

在这个任务中&#xff0c;我们编写了一个Java程序来解决一元二次方程。程序接受用户输入的系数a、b、c&#xff0c;并计算出方程的根。通过计算判别式delta的值&#xff0c;我们可以确定方程有两个不相等实根、两个相等实根还是没有实数根。这个程序遵循了IPO模式&#xff0c;即…

Linux split分割xls或csv文件

文件名&#xff1a;test.xls split -a 2 -d -l 100 test.xls test-a 2&#xff1a;后缀是2位 -d&#xff1a;后缀数字 -l 100 &#xff1a;每100行一个文件 test.xls&#xff1a;需要分割的文件名 test&#xff1a;分割后的文件前缀批量修改文件后缀 for i in test*; do mv $…

探索多种数据格式:JSON、YAML、XML、CSV等数据格式详解与比较

title: 探索多种数据格式&#xff1a;JSON、YAML、XML、CSV等数据格式详解与比较 date: 2024/3/28 17:34:03 updated: 2024/3/28 17:34:03 tags: 数据格式JSONYAMLXMLCSV数据交换格式比较 1. 数据格式介绍 数据格式是用于组织和存储数据的规范化结构&#xff0c;不同的数据格…

蓝桥杯每日一题(floyd算法)

4074 铁路与公路 如果两个城市之间有铁路t11&#xff0c;公路就会t2>1,没铁路的时候t1>1,公路t21。也就是公路铁路永远都不会相等。我们只需要计算通过公路和铁路从1到n最大的那个即可。 floyd是直接在数组上更新距离。不需要新建dis数组。另外一定要记得把邻接矩阵初始…

探索云原生时代:技术驱动的业务架构革新

一、引言 在数字化浪潮中&#xff0c;云原生技术已成为推动企业快速创新的重要动力。本文将深入探讨云原生的核心理念、技术架构以及其在实际业务环境中的应用&#xff0c;带领读者深入理解云原生技术的复杂性和优势。 云原生技术的定义 云原生&#xff08;Cloud Native&#x…

慧天[HTWATER]:采用CUDA框架实现耦合模型并行求解

慧天[HTWATER]软件简介 针对城市排水系统基础设施数据管理的需求&#xff0c;以及水文、水力及水质模拟对数据的需求&#xff0c;实现了以数据库方式对相应数据的存储。可以对分流制排水系统及合流制排水系统进行地表水文、管网水力、水质过程的模拟计算。可以对城市低影响开发…

“人工智能+”国家战略会带来哪些机会?

一、“人工智能”战略背景 2024年的中国政府工作报告首次引入了“人工智能”的概念&#xff0c;这是国家层面对于人工智能技术和各行业深度融合的重要战略举措。这一概念的提出意味着我国将进一步深化人工智能技术的研发应用&#xff0c;并积极推动人工智能与经济社会各领域的…

ida调试技巧-通过修改zf寄存器的值绕过简单反调试

参考本篇->OllyDbg笔记-对标志寄存器中ZF的理解&#xff08;逆向方面&#xff09;_零标志位zf怎么判断-CSDN博客 不想看也没关系&#xff0c;蒟蒻博主概述一下&#xff0c;总之&#xff0c;在机器执行汇编指令时&#xff0c;标志&#xff08;flag&#xff09;寄存器中的一个…

C++自主点餐系统

一、 题目 设计一个自助点餐系统&#xff0c;方便顾客自己点餐&#xff0c;并提供对餐厅销售情况的统计和管理功能。 二、 业务流程图 三、 系统功能结构图 四、 类的设计 五、 程序代码与说明 头文件1. SystemMap.h #pragma once #ifndef SYSTEMMAP #define SYSTEMMAP #in…

kubernetes(K8S)学习(一):K8S集群搭建(1 master 2 worker)

K8S集群搭建&#xff08;1 master 2 worker&#xff09; 一、环境资源准备1.1、版本统一1.2、k8s环境系统要求1.3、准备三台Centos7虚拟机 二、集群搭建2.1、更新yum&#xff0c;并安装依赖包2.2、安装Docker2.3、设置hostname&#xff0c;修改hosts文件2.4、设置k8s的系统要求…

常见的三种办公室租赁方式各自优缺点

商业办公的租赁市场。找商业办公地点&#xff0c;跟找住宅租房有点像&#xff0c;但目的和要求不同。主要也是三种方式&#xff1a;直接找房东租、接手别人的转租&#xff0c;或者找中介帮忙。每种方式都有它的小窍门和注意事项。 直租 直租商业办公&#xff0c;就是直接和办公…

YoloV5改进策略:Neck和Head改进|ECA-Net:用于深度卷积神经网络的高效通道注意力|多种改进方法|附结构图

摘要 本文使用ECA-Net注意力机制加入到YoloV5Neck和Head中。我尝试了多种改进方法&#xff0c;并附上改进结果&#xff0c;方便大家了解改进后的效果&#xff0c;为论文改进提供思路。&#xff08;改进中。。。。&#xff09; 论文&#xff1a;《ECA-Net&#xff1a;用于深度…

Python实现一个简单的银行管理系统GUI应用

介绍 在本教程中&#xff0c;我们将创建一个基本的银行管理系统GUI应用&#xff0c;用户可以通过图形界面执行各种银行操作。我们将使用Python编程语言和Tkinter库来实现此应用。 使用说明 需要安装Python解释器&#xff0c;以及PythonCharm &#x1f449; 点我去下载 效果图…

Mysql的连接与存储

目录 一、mysql的连接查询 1、连接的分类&#xff1a; 1.1 内连接 1.2左连接 1.3 右连接 二、存储过程 1、简介 2、优点 3、语法 4、参数分类 5、带参数的存储过程 6、修改存储过程 7、删除存储过程 三、总结 一、mysql的连接查询 mysql的连接查询&#xff0c;通…

tomcat和Servlet开发小案例

在上篇文章中,我已经正确安装了tomcat和利用servlet写了个基础的hello world程序,明白了这两个东西到底是啥玩意. 接下来,需要写个登录的小案例来进一步熟悉下基于servlet开发的流程. 一,新建项目 我们新建的maven项目其实里面是空的。所以我们需要给他变成一个servlet项目。 …

vue3全局引入element-plus使用Message教程

文章目录 安装引入 Element Plus和组件样式示例注意安装与引入&#xff1a;按需引入&#xff1a;API 使用&#xff1a;样式问题&#xff1a;组件上下文&#xff1a;版本兼容性&#xff1a;错误处理&#xff1a; 这是 Element UI 的 Vue 3 版本。ElMessage 是 Element Plus 中的…

编程语言|C语言——C语言变量的存储方式

前言 变量是程序中数据的存储空间的抽象。变量的存储方式可分为静态存储和动态存储两种。 静态存储变量通常是在程序编译时就分配一定的存储空间并一直保持不变&#xff0c;直至整个程序结束。在上一部分中介绍的全局变量的存储方式即属于此类存储方式。 动态存储变量是在程序执…

黑马鸿蒙笔记1

这里与前端类似。

跨境智星教你如何批量注册阿里国际站买家账号?

随着全球化的加速和跨境电商的兴起&#xff0c;越来越多的海外消费者开始关注并尝试在阿里国际站进行购物。然而&#xff0c;对于初次接触阿里国际站的买家来说&#xff0c;如何注册一个有效的买家账号却是一个不小的难题。 首先&#xff0c;要注册阿里国际站买家账号&#xff…

蓝桥杯(3):python搜索DFS

目录 1 DFS简介 1.1 DFS与n重循环 1.2 代码实现 1.3 例题 1.3.1 分糖果 1.3.2 买瓜 2 回溯 2.1 定义 2.2 代码实例 2.1.1 排列数 2.1.2 找子集 2.3 例题 2.3.1 N皇后 2.3.2 小朋友崇拜圈 2.3.3 全球变暖 3 剪枝 3.1 定义 3.2 分类 3.3 例子 3.3.1 数字王国之…