《C++ Primer》学习笔记(第十八章)——多继承与虚继承

多继承与虚继承

<本章内容位于《C++ Primer》第十八章第3小结(p.710页),因为也是关于继承的知识,因此在复习完第十五章关于单继承的知识后,顺便也把多继承的知识单拎出来进行复习归纳>

一、多重继承
单继承就是只有一个直接基类,而多继承就是多个直接基类。每个基类都有一个访问权限说明符,和单继承一样,class默认是private继承,而struct默认是public继承。派生类派生列表中必须是被定义过的类,同一个基类只能出现一次。

class B1{ // };
class B2{ // };
class A:public B1, public B2{ // };  //多继承

在上述代码中,A类对象包含B1类的子对象、B2类的子对象以及自身的非静态数据成员,A类对象的结构如下所示:
在这里插入图片描述
1.1 派生类的构造函数
多继承派生类的构造函数也是只能初始化它的直接基类,如果某一个基类有它自己的基类,那么该基类负责初始化它自己的基类,基类的初始化顺序按基类在派生类列表中出现的顺序相同,而与派生类构造函数初始化列表中基类出现的顺序无关。举个栗子:

class Base {
public: Base() { cout << "Base构造函数被执行" << endl; }
};

class B1:public Base {
public: B1() { cout << "B1构造函数被执行" << endl; }
};
class B2 {
public: B2() { cout << "B2构造函数被执行" << endl; }
};

class A :public B2,public B1{
public: A() { cout << "A构造函数被执行" << endl; }
};
//
A a;   //构造函数执行顺序:①B2、②Base、③B1、④A

如上述代码所示:B1继承了Base,而A继承了B2和B1。当创建一个A类对象时,A类首先按继承顺序初始化直接基类,因此先初始化类B2,由于类B2没有基类,因此直接执行B2的构造函数(①),然后又按A的继承顺序初始化类B1,由于B1有自己的基类,因此类B1先调用基类的构造函数(②)初始化基类Base,然后在调用自己B1的构造函数(③),当A类的基类都初始化完成后,最后在调用A类自己的构造函数(④)。

1.2 派生类的析构函数
派生类的析构函数与构造函数执行顺序恰好相反,以上述例子为例,析构函数的执行顺序为①A、②B1、③Base、④B2

1.3 多继承下的二义性
如果派生类的某两个或多个基类含有相同名字的函数时,即使形参列表不同返回类型不同,那么通过派生类对象调用该函数就会产生二义性。举个栗子:

class B1 {
public: 
	void show() { cout << "B1" << endl; }
};
class B2 {
public: 
    void show(int i) { cout << "B2" << endl; }
};

class A :public B2, public B1{
};
//
A a;
a.show(10)//错误,产生二义性,即使show含有一个实参

如上述代码所示:基类B1和B2中都含有一个名字为show的函数,即使两个函数的形参列表不同,当我们使用A类对象调用show函数时,就会产生二义性,哪怕我们显式的为show函数传入一个实参,也会发生二义性。why?

二义性产生原因分析:
二义性产生的原因主要有两个,一个原因是名字查找先于类型检查,另一个原因是在多继承下,成员的查找过程在所有直接基类中同时进行,如果某个名字在多个基类中同时被发现,就直接报告二义性错误。
基于以上原因,我们再来分析一下上述代码发生二义性错误的过程:
①、首先,编译器会在派生类A中查找是否含有名字为show的成员;
②、编译器发现在派生类中没有名字为show的成员后,就会同时在其直接基类中进行查找,由于基类B1和B2中都含有名字为show的成员,因此编译器直接报告二义性的错误,而不会再进行类型检查。即使我们希望通过传入一个形参来调用B2类的show函数,但是由于名字查找先于类型检查,也就是当编译器发现有两个名字为show的成员时,直接报告错误压根就不会再进行类型检查,不管你传入什么类型的参数,编译器看都不看。

解决二义性的方法
1、 一种办法是通过作用域限定符来调用指定类的成员,如:

a.B1::show(); //显式调用B2类的show函数

2、另一种更好的办法就是在派生类中定义该函数的新版本,如:

class A :public B2,public B1{
public: 
       void show(double d) { cout << "A" << endl; }
};
//
a.show(3.14);  //调用派生类自己版本的show函数,注意掺入适合的形参

当在派生类中定义自己版本的show函数后,编译器先在派生类中进行名字检查,找到名字为show的成员后,再进行类型检查,因此此时传入show函数的实参必须与派生类中show函数的形参一致。

二、虚继承
1、前面提到过,同一个基类在派生列表中只能出现一次,但是实际上同一个基类可能会间接地在派生列表中出现两次,如以下两种情况:
在这里插入图片描述
如图所示,通过间接继承,派生类中可能包含两个相同的基类。如果发生这种情况,一个最直接的问题就是二义性的问题,因为类B1和类B2中同时包含类Base相同的成员,那么通过类A 调用这些成员就会发生二义性。

2、解决上述问题的办法就是采用虚继承。=,即在派生列表中添加关键字virtual。声明虚继承的基类称为虚基类,不管虚基类在继承体系中出现多少次,对于派生类来说都只包含唯一一个共享的虚基类子对象。如:

class Base {
public:
	void print() { cout << "Base" << endl; }
};
class B1:public virtual Base {  //虚继承,virtual和public的顺序可以互换
public: 
	void show1() { cout << "B1" << endl; }
};
class B2 :public virtual Base {  //虚继承,基类Base为虚基类
public: void show2(int i) { cout << "B2" << endl; }
};

class A :public B2,public B1{  //派生类A中只有唯一一份Base类的子对象
};
A a;
a.print();  //正确,不会出现二义性错误

3、无论基类是不是虚基类,我们都可以将派生类对象绑定到基类的指针或引用上(多态的基本,动态绑定)

4、虚基类成员的可见性
如果Base类中含有一个名为x成员,类B1和B2虚继承了Base,类A继承了B1和B2,那么 虚基类Base中的成员可见性分为四种情况:
①。如果派生类中定义了成员x,那么无论基类B1和B2中是否有x的定义,都使用派生类版本的x定义;
②、如果派生类中没有x的定义,而且基类B1和B2中也没有x的定义,那么使用虚基类版本的x定义;
③、如果派生类中没有x的定义,但是基类B1或者B2中有一个定义了x,那么使用B1或B2类版本的x定义;
④、如果派生类中没有x的定义,但是基类B1和B2中都定义了x,那么就会产生二义性错误。

5、虚基类的构造和析构
在虚派生中,虚基类有最底层的派生类初始化。编译器先按照直接基类的声明顺序依次检查,以确定其中是否含有虚基类,如果有,派生类必须先初始化虚基类,然后再按派生列表中的继承顺序初始化其他基类。析构顺序与构造顺序相反。比如在上面的栗子中:

class A:public B1,public B2{  //  };

先调用虚基类Base的构造函数,然后调用B1的构造函数,接着调用B2的构造函数,最后在调用A的构造函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值