C++类继承基础6——多重继承与虚基类

多重继承的原理

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

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值