当前课程知识点:C++语言程序设计进阶 > 第七章 继承与派生 > 派生类成员的标识与访问 > 虚基类
需要解决的问题
当派生类从多个基类派生,而这些基类又共同基类,则在访问此共同基类中的成员时,将产生冗余,并有可能因冗余带来不一致性
虚基类声明
以virtual说明基类继承方式
例:class B1:virtual public B
作用
主要用来解决多继承时可能发生的对同一基类继承多次而产生的二义性问题
为最远的派生类提供唯一的基类成员,而不重复产生多次复制
注意:
在第一级继承时就要将共同基类设计为虚基类。
#include <iostream> using namespace std; class Base0 { public: int var0; void fun0() { cout << "Member of Base0" << endl; } }; class Base1: virtual public Base0 { public: int var1; }; class Base2: virtual public Base0 { public: int var2; }; class Derived: public Base1, public Base2 { //定义派生类Derived public: int var; void fun() { cout << "Member of Derived" << endl; } }; int main() { Derived d; d.var0 = 2; //直接访问虚基类的数据成员 d.fun0(); //直接访问虚基类的函数成员 return 0; }
建立对象时所指定的类称为最远派生类。
虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。
在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化表中为虚基类的构造函数列出参数。如果未列出,则表示调用该虚基类的默认构造函数。
在建立对象时,只有最远派生类的构造函数调用虚基类的构造函数,其他类对虚基类构造函数的调用被忽略。
#include <iostream> using namespace std; class Base0 { public: Base0(int var) : var0(var) { } int var0; void fun0() { cout << "Member of Base0" << endl; } }; class Base1: virtual public Base0 { public: Base1(int var) : Base0(var) { } int var1; }; class Base2: virtual public Base0 { public: Base2(int var) : Base0(var) { } int var2; }; class Derived: public Base1, public Base2 { public: Derived(int var) : Base0(var), Base1(var), Base2(var) { } int var; void fun() { cout << "Member of Derived" << endl; } }; int main() { //程序主函数 Derived d(1); d.var0 = 2; //直接访问虚基类的数据成员 d.fun0(); //直接访问虚基类的函数成员 return 0; }
大家好
欢迎回来继续学习
C++语言程序设计
这一节我们来学习虚基类
虚基类是什么
为什么我们需要虚基类
现在我们考虑这样一个问题
是不是说
我们继承多个基类的时候
它们有重名的成员都没有关系
我们访问的时候呢
用类名限定就可以了
问题是不是就这么简单呢
大家有没有想过
如果某个派生类的多个基类
实际上呢
又曾经派生自
一个共同的最远基类的情况
这种情况下
这些基类里面
就会有同名的成员
不仅名字相同
它本质上就是相同的东西
最后到最远派生类中
又汇聚到一起了
这个时候带来的麻烦
好象不仅仅是说
区分一下名字
避免二异性的问题吧
对 这个时候麻烦会更多一些
我们就来看到底有什么麻烦
怎么解决这个麻烦
我们需要解决的问题
简单来讲就是
当派生类从多个基类派生
而这些基类
又有共同基类
那么这种情况下
我们要访问那些
从这个最远的共同基类中
继承过来的成员的时候
不仅仅是有二异性的问题
实际上它是产生了冗余
而这个冗余
不仅仅是浪费了存储空间的问题
很有可能会带来不一致性
那么怎么解决这个问题呢
在C++中
有虚基类这种语法机制
某种程度上可以解决这个问题
这个时候呢
就需要我们在继承基类的时候
用这个virtual关键字
说明这是一个虚继承
这种虚继承的作用呢
主要是用来解决多继承的时候
可能发生的对同一个基类
继承多次而产生的这种二异性
和冗余的问题
用了虚基类这种机制
就可以为最远派生类
提供唯一的基类成员
而不产生重复的多次复制
那这里要注意的就是说
要在
一定要在第一级继承的时候
就要将共同基类设计为虚基类
下面呢
我通过例子和图
给大家解释一下
大家就比较清楚了
这到底意味着什么
那避免二异性和冗余呢
我们可以采用虚基类
比如说
我们可以将Base0类
作为它直接派生类
Base1 Base2的虚基类
也就是说Base1虚继承Base0
Base2虚继承Base0
那么Derived
再继承Base1 Base2
这样呢
它最终的结果就是什么呢
在Derived对象里面
不会存在Base0类的双份的成员
那么看起来
它也是包含着
从Base1继承的成员
和从Base2继承的成员
但是从Base1继承的Base0成员
实际上这个地方放了一个指针
指向真正的Base0成员
Base2的也是
所以呢
实质上从最远的基类
继承过来的成员
在最远派生类中只有一份
我们看这个程序
我们只要在
Base1继承Base0的时候
加一个virtual
在Base2继承Base0的时候
也加一个virtual
在第一级继承的时候
就作为虚继承
然后呢
这种二异性和冗余的问题呢
在这个例题中就这样解决了
我们知道了虚基类
可以在某种程度上
解决这个二异性和冗余的问题
但是
虚基类也给我们带来了
另外一个方面的麻烦
就是这个派生类的构造函数
怎么写
我们将建立对象的时候
所指定的这个类型
称为最远派生类
它是目前最远的这一级
对吧
那么虚基类的成员呢
应该是由最远派生类的构造函数
通过调用虚基类的构造函数
进行初始化的
那么这个原则
就跟我们以前学的语法
不太一样了
以前我们是说
每个派生类
只负责给它的直接基类构造函数
传递参数
但是现在
这个最远派生类除此而外
还要给那个最远的
最顶上的那个基类
它的构造函数传递参数
就是那个虚基类
那么在整个继承结构中呢
实际上直接或间接
继承虚基类的所有派生类
都必须在构造函数的初始化
列表中
为虚基类的构造函数列出参数
如果说你没有列出
那就表示要调用
这个虚基类的默认构造函数了
可是这么多级的派生类
都给它传参数
用哪个呀
在构造对象的时候呢
只有最远派生类的构造函数
会真的去调用虚基类的构造函数
而其他类
对虚基类构造函数的调用呢
就都被忽略了
也就是中间各级传的参数
就都被忽略了
只收那个最远派生类
传给虚基类的参数
下面呢
我们通过一个例子来演示
有虚基类的时候
这个最远派生类的构造函数
该怎么写
有虚基类的时候呢
每一级的派生类
都要给最远的那个基类
负责传递参数
我们看
那么构造函数怎么写呢
直接的派生类Base1
当然它依然是给
它的直接基类构造函数
Base0传递参数
Base2也是
它是Base0的直接派生类
它也要给它的构造函数
传递参数
那我们来看
下级派生类Derived
如果不是虚继承的话
它是只要给自己的直接基类
构造函数传递参数就行了
现在由于我们使用了虚继承
所以Derived类
它的构造函数
还要负责给最远基类
传递参数
也要给Base0类传递参数
那么当构造
Derived类对象的时候
Base0的构造函数被调用几次呢
当然只被调用一次
只有这个最远派生类
传递这个参数被接收
用来调用Base0的构造函数
那么在中间的地方
其他的这些Base1 Base2
传递的参数呢
就都被忽略了
所以大家不用担心说
大家都给最远基类传参数
它的构造函数
会不会不知道接收哪个参数
会不会被调用执行多次啊
不会的
只有这个最远派生类对象
构造的时候
传的这个参数是起作用的
-导学
--导学
-继承的基本概念和语法
-第七章 继承与派生--继承的基本概念和语法习题
-继承方式
-第七章 继承与派生--继承方式
-基类与派生类类型转换
-第七章 继承与派生--基类与派生类类型转换
-派生类的构造和析构
--派生类的构造函数
--派生类的析构函数
--第七章 继承与派生--派生类的构造和析构
-派生类成员的标识与访问
--虚基类
-第七章 继承与派生--派生类成员的标识与访问
-小结
--小结
-综合实例
--第七章综合实例
-实验七
--实验七
-导学
--导学
-第八章 多态性--导学
-运算符重载
--运算符重载的规则
-第八章 多态性--运算符重载
-虚函数
--虚函数
--虚析构函数
--虚表与动态绑定
-第八章 多态性--虚函数
-抽象类
--抽象类
--第八章 多态性--抽象类
-override与final
-第八章 多态性--override与final
-小结
--第八章小结
-综合实例
--第八章综合实例
-实验八
--实验八
- 第八章讲义
-导学
--导学
-模板
--函数模板
--类模板
-第九章 模板与群体数据--模板
-线性群体
--线性群体的概念
-第九章 模板与群体数据--线性群体
-数组
--数组类模板
-链表
--链表类模板
-第九章 模板与群体数据--链表
-栈
--栈类模板
--栈类模板课后习题
--例9-9 栈的应用课后习题
-队列
--队列类模板
-第九章 模板与群体数据--队列
-排序
--排序概述
--插入排序
--选择排序
--交换排序
-第九章 模板与群体数据--排序
-查找
--查找
--查找课后习题
-小结
--小结
-综合实例
--综合实例
-实验九
--实验九
- 第九章讲义
-导学
--导学
-泛型程序设计及STL的结构
--STL简介
-第十章 泛型程序设计与C++标准模板库--泛型程序设计及STL的结构
-迭代器
--迭代器
-第十章 泛型程序设计与C++标准模板库--迭代器
-容器的基本功能与分类
-第十章 泛型程序设计与C++标准模板库--容器的基本功能与分类
-顺序容器
--顺序容器的特征
--第十章 泛型程序设计与C++标准模板库--顺序容器
-关联容器
--集合
--映射
-第十章 泛型程序设计与C++标准模板库--关联容器
-函数对象
--函数对象
--函数适配器
-算法
--算法
-小结
--第十章小结
-综合实例
--综合实例
-实验十
--实验十
- 第十章讲义
-导学
--导学
-I/O流的概念及流类库结构
-第十一章 流类库与输入/输出--I/O流的概念及流类库结构
-输出流
--输出流概述
--向文本文件输出
--向二进制文件输出
--向字符串输出
-第十一章 流类库与输入/输出--输出流
-输入流
--输入流概述
--输入流应用举例
--从字符串输入
-第十一章 流类库与输入/输出--输入流
-输入/输出流
--输入/输出流
-第十一章 流类库与输入/输出--输入/输出流
-小结
--小结
-综合实例
--综合实例
-实验十一
--实验十一
- 第十一章讲义
-导学
--第12章导学
-异常处理的思想与程序实现
-第十二章 异常处理--异常处理的思想与程序实现
-异常处理中的构造与析构
-第十二章 异常处理--异常处理中的构造与析构
-标准程序库异常处理
-第十二章 异常处理--标准程序库异常处理
-小结
--第12章小结
-综合实例
--综合实例
-实验十二
--实验十二
- 第十二章讲义