番外篇--C++中的代码重用
本文最后更新于:2023年1月1日 下午
- 实现代码重用的一些方法(这里并不是全部):
- 包含(组合、层次化):类包含另一个类的对象
- 使用私有继承或保护继承
- 以上两种方法都用于实现has-a关系,常用第一种方法
- 多重继承可以使多个基类派生出一个新类,将基类的功能组合到一起
包含对象成员的类
- 公有继承实现is-a模型,没有增加新接口,可增加新实现
- 包含(组合、层次化)、私有继承、保护继承实现has-a模型,没有增加新接口,可增加新实现;
- 接口:直接暴露在外的函数;实现:函数定义;注意:这两个概念不是互斥的
私有继承
要区分开 类成员的访问类型 和 类的继承类型 之间的关系 类成员的访问类型 决定 类成员的可访问性和可继承性:
访问类型 | 外部可访问性 | 可继承性 |
---|---|---|
public | 可访问 | 可继承 |
protect | 不可访问 | 可继承 |
private | 不可访问 | 不可继承 |
类的继承类型 决定 继承下来的成员 的访问类型会变成什么:
继承类型 | 变化 |
---|---|
公有继承 | 不改变继承下来内容的访问类型 |
保护继承 | 将继承下来的内容的访问类型都变为protect |
私有继承 | 将继承下来的内容的访问类型都变为private |
派生类的指针(引用)要赋值给私有继承下来的基类类型指针(引用),必要强转派生类的指针(引用)
包含将对象作为一个有名字的成员添加到类中,私有继承将一个没有名字的成员添加到类中;这些成员称为子对象(subobject)
私有继承是默认继承方式
访问基类成员的格式:类名::基类成员
ps:友元函数不是类成员
若要直接获得子对象,则只需要对this(*this)进行强制类型转换即可,转换成你想要的子对象类型的指针(引用)
- 包含 与 私有继承 实现has-a关系的对比:
- 包含易于理解,可包含多个同类子对象,不可访问子对象的保护成员、不可重新定义虚函数
- 私有继承比较抽象,不易包含多个同类子对象,容易出问题(多重继承);但是特性多:可访问子对象的保护成员、可重新定义虚函数
- 一般来说,能用包含就用包含,实在不行再用私有继承
- 如果想要改变私有继承(或保护继承(可以吗?))下来的基类成员的访问属性,可以使用using关键字,将那些成员强行引入到特性的访问区块:
1 |
|
多重继承
多重继承(MI):有多个直接基类的类,每一个基类都要写上自己的继承属性
以下面的继承关系图为例介绍MI:
如果上图中所有的继承关系都是公有继承,那么SingingWaiter实例就会有两个Worker基类对象:
多重继承引入的问题
- 派生类对象(SingingWaiter)指针(引用)无法自动上转为相同基类类型(Worker)的指针(引用)
- 解决方法:手动强转下派生类对象(SingingWaiter)指针(引用)到上一级基类(Singer或Waiter)就好了
- 从不同的基类继承同名的方法
- 解决方法:可在调用同名函数时,通过类名指定
从不同继承路线上继承相同的基类
为了删除多个相同基类,有点像融合,C++引入了新技术— 虚基类 ;
要指定哪条继承线的相同基类进行融合,则继承这个相同类(Worker)的直接派生类(Singer和Waiter)都要在继承时加上virtual关键字:
1 |
|
然后派生类(SingingWaiter)的基类对象情况就会变成这样:
引入虚基类的多重继承需要解决的问题
构造函数的问题
只能在类(SingingWaiter)在构造函数 初始化列表 中调用虚基类(Worker)的构造函数,类(SingingWaiter)的所有基类(Singer和Waiter)都不能在构造函数 初始化列表 中调用虚基类(Worker)的构造函数;
只能由类(SingingWaiter)来 自动 调用虚基类(Worker)的构造函数,类(SingingWaiter)的所有基类(Singer和Waiter)都不能再 自动 调用虚基类(Worker)的构造函数;
总的来说就是:类(SingingWaiter)的所有基类(Singer和Waiter)已无权调用虚基类(Worker)的构造函数,只能由类(SingingWaiter)调用虚基类(Worker)的构造函数;
虚基类中虚函数的问题
如果一个类(SingingWaiter)继承了虚基类(Worker)但是没有实现虚基类(Worker)的虚函数vfun,会出现下面两种情况:
如果有两个以上 虚基类(Worker)的派生类(Singer和Waiter) 都实现了虚基类(Worker)的虚函数vfun;这个时候如果通过这个类(SingingWaiter)的对象指针去调用虚函数vfun,那么将出现二义性;因为虚函数从虚基类(Worker)开始往下走会有两个最新版本出现;因此这个类(SingingWaiter)最好重写(override)虚函数vfun,否则多态性就会异常;
如果只有一个 虚基类(Worker)的派生类(Singer或Waiter) 实现了虚基类(Worker)的虚函数vfun;这个时候如果通过这个类(SingingWaiter)的对象指针去调用虚函数vfun,那么不会有任何问题;
重写虚函数的问题
在重写(override)虚基类(Worker)的虚函数vfun时最好不要以 递增 的方式去写;所谓的 递增 指的是:在重写(override)的时候调用了基类的虚函数vfun;
后记
虚函数的调用规则不受访问类型的影响,该调用哪个就调用哪个,如果调到了私有函数或者保护函数,那运行时就报错;
基类普通函数的调用规则也不受访问类型的影响,如果基类出现两个同名普通函数函数,即使一个是私有的、一个是公有的,那么一样存在调用的二义性;
类模板
类模板的声明与定义
1 |
|
类模板的使用
1 |
|
数组模板实例和非类型参数
非类型(non-type)参数or表达式(expression)参数:
1 |
|
- 非类型参数的限制:
- 类型只能是整形、枚举、引用、指针
- 模板中的代码不能修改非类型参数的值,也不能获取参数的地址(引用也不行吧?)(因为非类型参数没有内存空间?)
- 在实例化模板的时候,用作非类型参数的值必须是常量表达式
- 非类型参数数组模板的优点:
- 使用非类型参数来创建数组速度更快,因为数组是自动变量;而且不需要手动new和delete;
- 非类型参数数组模板的缺点:
- 不同的非类型参数数值都会生成自己的模板实例,占用内存增加;
- 无法将一种尺寸的数组模板实例赋给另一种尺寸的数组模板实例;
模板多功能性
类模板也可以用作基类、派生类、组件类、其它模板的类型参数:
1 |
|
递归使用类模板:一个类模板的类型参数还是类模板,例如:vector<vector<int,6>,7>
模板可以包含多个类型参数:
1 |
|
默认模板参数:
1 |
|
模板的具体化
隐式实例化(implicit instantiation) 、 显式实例化(explicit instantiation) 、 显式具体化(explicit specialization) 统称为 具体化(specialization)
假设有这样一个类模板:
1 |
|
隐式实例化
1 |
|
这种实例化在函数模板中是显式实例化的一种形式
显式实例化
1 |
|
显式具体化
1 |
|
当同时存在具体化模板和通用模板时,编译器优先选择具体化模板;如果还存在普通函数,是不是普通函数最优先
部分具体化
1 |
|
部分具体化的更多例子:
1 |
|
函数调用时,模板匹配优先度:显式具体化>部分具体化>实例化
指针(引用)类型优先与T*(T&)匹配,非指针(引用)类型优先与T匹配
成员模板
老编译器不接受模板成员
可以在类模板中定义类模板或者函数模板:
1 |
|