当前课程知识点:基于Linux的C++ > 第九讲 类与对象 > 9.10 继承(二) > LinuxCPP0910
继承的时候有可能导致基类的成员函数
和派生类的成员函数具有同名的情况
在这种情况下边就有可能发生函数的覆盖
调用的时候有可能会发生二义性
我们在编程的时候必须要能够处理它
我们看这样的一个例子
class Point 里面有一个函数 假设叫Print
而在Point3D里边
它不仅继承了Point类的Print函数
它自己又实现了一个
这实际上意味着Point3D这个对象
具有两个Print函数 一个是它自己的
一个是它从基类继承下来的
这两个函数在Point3D这样的对象上边
怎么访问呢
pt是Point类的一个对象
当我们用pt.Print来调用它的时候
我们实际上调用的是Point这个类的
那个Print函数
当我们在pt3d.Print调用这个Print的时候
注意pt3d因为是Point3D类的一个对象pt3d的话
那么它的调用
是调用Point3D这个类的Print成员函数
它自己内部的那个实现
它不会调用Point那个类的Print函数
也就是继承下来的那个函数它没有被调用
缺省的时候它调用它自己实现的那个Print函数
如果你真地想在Point3D类里边
去调用它基类的那个Print函数
那么你就必须明确地指定那个Print函数
是从属于基类的 而不属于派生类的特点
你得必须把这个特征给我体现出来
怎么办呢 要用名解析的方式
所以要写pt3d.Point::Print
很怪异的写法 但它能够工作
就能够在Point3D类的对象里边正确地调用
从基类Point类里边继承下来的那个Print函数
而不是调用它自己的那个Print函数
这一点在继承函数中
需要调用基类的那个对应成员函数
去做基础处理的时候尤为重要
在派生类需要调用基类的同名函数
去做一些基础处理的时候
显得尤其如此
单继承 它描述了类库的基本组织方式
理论上来讲 它其实能够解决任何问题
但是C++本身
它还提供了特殊的多继承的机制
也就是说 一个派生类
可以从好几个基类中同时继承数据
这个就叫多继承
多继承的基本的语法格式就是这个:
class派生类的名字
冒号后边跟着逗号分隔开的不同的基类
每一个基类都有一个对应的
派生类型的保留字
确定它到底是按照什么样的方式去派生
同样地 public、protect、private
三个格式去派生
每一个基类都可以不一样
你比如说 我们这样的例子
我定义一个ClassA 又定义一个classB
然后定义一个classC
公有派生A 保护派生B
这样的话 C类将具有
A类和B类的全部的属性和行为
在C类里边 A类的public成员还是public
A类protected成员还是protected
A类的private看不见
而在C类里边 从B类继承下来的
那些public的东西和protected的东西
全变成了protected
而B类private成员
同样地在C类里面还是看不见
这就是多重继承
多重继承导致的问题
比它能够带来的好处还要多
所以我们一定要慎用
就像我刚才讲的
理论上一个单继承能够解决我们任何问题
所以使用多继承在绝大多数情况下
其实是毫无必要的
所以我们不建议同学们
在实际编程的时候使用多继承的机制
它导致什么问题呢 它导致的问题就是
一个派生类里面
可能包含很多个基类的副本
这些基类副本的位置
你没有办法去决定它
因为对于一个单继承来讲
它一个基类的一个数据对象
它的存储布局是基本是这样一个模式:
按照它的数据成员的顺序
一个接着一个分配好 放在那个地方
构造这个对象的时候
就按照这个方式来构造
当我派生的时候
首先会继承基类这些数据成员
它的架构 包括它的存储布局
一模一样地全都继承下来
然后在后边继续添加自己的数据
这就形成一个非常明确的
不断扩展的数据存储布局
这是一个单继承的模式
如果多继承
那么它的第二个基类就只能写在
第一个基类的后边
第三个基类就写在第二个基类的后边
这个存储布局 你看上去也是顺序的
但是如果它(注:指基类)有多个副本
就可能导致它(注:指派生类)
在后续某个位置还有基类的数据
访问的时候是非常讨厌的
操作起来很麻烦 一不注意就能搞错
所以大部分情况下面
理论上我们应该慎用它
我们看这个例子
我定义classA 定义classB
从A公有派生 定义class C
从A类公有派生 从B类保护派生
这个时候 在C类里边
上来就会放A类的一个存储空间
接下来放B类的存储空间
B类的存储空间是什么呢
B类的存储空间上来就是A类的存储空间
然后是B类新添的数据成员
在此之后 才是C类自己定义的数据成员
你看到了吧
这里有A类的数据成员的两个副本
而且第一个副本是公有派生下来的
第二个副本是保护派生下来的
访问控制还不一样 所以在这种情况下
取的是A类第一个副本呢还是第二个副本呢
你编程的时候就需要特别小心
一不注意就容易搞错 另外那我还要问了
保存A类的两个副本有意义吗
大部分情况下边
在C类保存A类的两个副本是没有意义的
所以多继承问题很多
在这一点上面尤其需要注意
当采用多继承的时候
派生类的那个成员函数名字
和原来的函数(基类成员函数)的名字
相同的情况更多了
所以使用的时候也需要注意
解析方式和以前类似
但是会更麻烦一点
我们看一个例子 classA有一个f函数
classB有一个f函数
classC从A类公有继承、从B类公有继承
然后自己又写了一个f函数
你注意到这里面有几个f函数
三个 A类一个 B类一个 C类一个
那么c.f这样的一个调用
调用的就是c的f函数
如果是c.A::f 调用的就是C类
从A类继承下来的那个函数
如果c.B::f
调用的就是从B类继承的那个函数
所以名解析一样是可以工作
但是肯定就不像原来那么方便
就在这儿 你看这个例子
A类f函数 B类从A类公有派生f函数
C类从A类公有派生f函数
D类从B、C类公有派生f函数
这里面多少个f函数啊 太多了
A类有一个 B类有一个 C类有一个
D类有几个 D类自己定义了一个
从B类继承了几个 从B类继承了两个
因为B类本身有A类一个 自己的一个
从C类继承了几个 从C类继承了两个
那么你想调用它的时候 d.f
调用的就是D类f函数
d.B::f 调用的就是从B类继承下来的f函数
d.C::f 调用的就是从C类继承下来的f函数
d.B::A::f 调用的就是从B类继承的
A类的那个f函数
沿着它那个继承的树往上回溯
找到它对应的f函数
才能够明确地解析出来它
这样才能够准确地调用
当然在大部分情况下边
我从B类和C类继承的A的f函数有差别吗
当然没差别 有意义吗
很多时候 这么写其实是没有意义的
我们刚才讲了
一旦在一个多继承的机制下面
派生类就可能保留基类的很多个副本
大部分情况下它是无效的 没有意义的
那么我们怎么限制这一点
让它只保留基类的唯一一个副本呢
后续的同样的副本我都删掉 我不要了
那我们怎么保证这一点呢
那么我们就需要虚继承
取消继承时派生类中公共基类的多个副本
只保留唯一的一份就叫虚继承
派生的时候填上virtual这样一个关键字
就能够保证它是虚继承的
classA 这是一个类的定义
classB从A类虚继承 classC从A类虚继承
D类从B、C类继承
D类不需要写virtual public B,virtual public C
virtual写在virtual public A里边
就表示B类的这一个类的对象上
所有的A类的副本只有一个
C类的上面 所有的A类的副本只有一个
所以当D类里边既从B类继承
又从C类继承的时候
那么它首先继承B类 保留一个A类的副本
当它再继承C类的时候
发现D类这个对象已经有了一个A类的副本
那么它的自己A类的那个副本它就不要了
这个就是虚继承的目的
整个D类将只有A类的一个副本
而不会有很多个
virtual和public这种写法 前后颠倒没关系
它只和多继承有关 对于单继承
因为所有的继承都是单一的一条线
所以它不会产生多继承的情况
基类的副本不会有两个
那么虚继承就不需要
你写了虚继承 从C++的实现上来讲
它需要做一些额外的操作
而这个额外的操作实际上
是会降低我们程序的效率的
所以在单继承模式下
你可以认为虚基类这种东西压根就不重要
不存在 不要管它
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口