2020-C++高级程序设计-C++ 多态
多态
- 通用概念:同一论域中一个元素可有多种解释。
- 提高面向对象设计的语言灵活性
- 程序设计语言:OO程序设计
- 多态形式
- 函数重载:(静态多态),和虚函数的动态多态不同(一名多用):函数重载包含操作符重载
- 类属多态:模板:template
1. 操作符重载
- 函数重载
- 名同、参数不同,返回值不同没有用的:参数顺序、参数类型匹配(找到最佳匹配)
- 静态绑定
- 歧义控制:
- 顺序:
- 最佳匹配:
- 原则一:这个匹配每一个参数不必其他的匹配更差
- 原则二:这个匹配有一个参数更精确匹配
- 整形提升:更好的,标准转换(标准转换都是一视同仁的)
- 窄转换?允许的,大->小
- 操作符重载(变为一种函数)
- 动机:操作符语义
- built_in 类型
- 自定义数据类型
- 作用:
- 提高可读性
- 提供可扩充性
- 动机:操作符语义
- 重点记忆返回的变量
1.1. 操作符 + 的重载
- 重载第一步
1 |
|
- 自增和自减的问题
1 |
|
- 重载++函数:封装SAT的问题
- 返回值引用或者是值是有区别的
1 |
|
1.2. 可以重载的操作符
- 不可以重载的操作符:
.
(成员访问操作符)、.*
(成员指针访问运算符,如下)、::
(域操作符)、?:
(条件操作符)、sizeof
:也不重载- 原因:前两个为了防止类访问出现混乱
- ::后面是名称不是变量
- ?:条件运算符涉及到跳转,如果重载就影响了理解
1 |
|
- 重载基本原则:
- 方法:(大多数都支持,但是有的不支持)
- 类成员函数
- 带有类参数的全局函数
- 遵循原有语法
- 单目/双目:一一对应
- 优先级
- 结合性
- 方法:(大多数都支持,但是有的不支持)
- 永远不要重载&&和||:会造成极大的问题
1.3. 双目操作符的重载
1.3.1. 类成员函数(双目操作符)
- 类成员函数:
- 格式:
<ret type>operator #(<arg>)
- this: 隐含,必然是第一个参数
- 格式:
- 使用:
1 |
|
1.3.2. 全局函数
- 友元:
friend <ret type> operator #(<arg1>,<arg2>)
- 格式:
<ret type> operator #(<arg1>,<arg2>)
- 注意:
=
、()
、[]
、->
不可以作为全局函数重载- 大体上来讲,C++ 一个类本身对这几个运算符就已经有了相应的解释了。
- 如果将这四种符号进行友元全局重载,则会出现一些冲突
- 下标和箭头运算符为什么?有保留调用顺序,我们希望能保留原来的顺序,而全局不能要求,而成员函数的this就可以解决这个问题
- 参考
- 全局函数作为补充:
- 单目运算符最好重载为类的成员函数
- 双目运算符最好重载为类的友元函数
1 |
|
- 永远不要重载 && 和 ||:逻辑与和逻辑或
- 原因:短路,类似?:
- 虽然绝大多数都没有问题,但是如果有逻辑短路容易出现问题
1 |
|
- 返回类型的问题:如果没有&的时候,第一个return出现了对象拷贝,避免:临时变量不能返回拷贝
1 |
|
- 操作符重载的哲理:尽量让事情有效率,但不是过度有效率(返回引用)
- 结论:每次就是返回一个拷贝,而不是引用
1.4. 返回值总结
- 加减乘除:就是拷贝,不是引用,效率不太高?为了解决这个问题:可以返回值优化,第一个return没有拷贝,直接返回的是一个对象(无拷贝),先计算,最后生成一个对象返回。
1.5. 单目操作符的重载
- 类成员函数:
- this:隐含
- 格式:
<ret type> operator#()
:this的隐含
- 全局函数:
<ret type> operator#(<arg>)
- 参数必须为自定义类型
- 单目操作符在绝大多数情况下重载为类的成员函数
- 15min没了
- a++ 和 ++ a
1 |
|
1.6. 操作符 = 的重载
- 默认赋值操作符重载函数
- 逐个成员赋值
- 对含有对象成员的类,该定义是递归的
- 赋值操作符的重载不可以被继承:因为拷贝构造,派生出来的类有一些新的部分
- 返回引用类型:返回*this的引用,支持链式赋值
- this引用应该是非常量引用,返回出来的是作为右值进行计算
- a = b = c:不要求非常量引用
- (a = b).f():要求非常量引用
- 例一:
1 |
|
1 |
|
1 |
|
- 注意:避免自我赋值(因为是相同的内存地址)
- Sample: class string
- s = s
class {... A void f(A& a);...}
void f (A& a1, A& a2)
int f2(Derived &rd,Base& rb);
- Object identity
- Content
- Same memory location
- Object identifier
1 |
|
1 |
|
1.7. 操作符 [] 的重载
1 |
|
1.8. 多维数组 class Array2D
1 |
|
1.8.1. 多维数组的最终版本
1 |
|
1.9. 操作符 () 的重载
- ()的意义
- 函数调用
- 类型转换操作符
1.9.1. 函数调用
1 |
|
1.9.2. 操作符类型转换的重载
- 基本数据类型
- 自定义类
1 |
|
1 |
|
- 问题:为什么禁止在类外禁止重载赋值操作符?
- 如果没有类内提供一个赋值操作符,则编译器会默认提供一个类内的复制操作符
- 查找操作符优先查找类内,之后查找全局,所以全局重载赋值操作符不可能被用到
1.10. 操作符 -> 的重载
- ->为二元运算符,重载的时候按照一元操作符重载描述。
1 |
|
- 例子:画图板程序
1 |
|
- Prevent memory Leak:需要符合compiler控制的生命周期
1 |
|
1.11. 操作符 new 和 delete 的重载
- 频繁调用系统的存储管理,影响效率。
- 程序自身管理内存,提高效率
- 方法:
- 调用系统存储分配,申请一块较大的内存
- 针对该内存,自己管理存储分配、去配
- 通过重载new与delete来实现
- 重载的new与delete是静态成员(隐式的,不需要额外声明,不允许操作任何类的数据成员)
- 重载的new与delete遵循类的访问控制,可继承(注意派生类和继承类的大小问题,开始5min左右)
- 我们想要对某些程序进行自己的资源管理的话,可以重载这两个操作符。
- 有些我们重复新建销毁的,比如Restful的可以单独管理
- new构造和返回指针
- delete析构和释放内存
- 可以重载成全局函数,也可以重载成类成员函数
1.11.1. 重载 new
void *operator new (size_t size, s...)
- 名:operator new
- 返回类型:void *
- 第一个参数:size_t(unsigned int)
- 系统自动计算对象的大小,并传值给size
- 其他参数:可有可无
A *p = new (...) A
,表示传给new的
- new的重载可以有多种
- 如果重载一个new,那么通过new动态创建该类的对象时将不再调用内置的(预定义的)new
- 允许进行全局重载,但是不推荐使用全局重载
1 |
|
- new也可以new在栈上
1 |
|
1.11.2. 重载 delete
void operator delete(void *,size_t size)
- 名:operator delete
- 返回类型:void
- 第一个参数:void *(必须):被撤销对象的地址
- 第二个参数:可有可无;如果有,则必须为size_t类型:被撤销对象的大小
- delete 的重载只能有一个
- 如果重载了delete,那么通过 delete 撤消对象时将不再调用内置的(预定义的)delete
- 动态删除其父类的所有的。
- 如果子类中有一个虚继承函数,则size_t大小会根据继承情况进行确定大小
1.11.3. new和delete考试
- 主要考核集中在这些上面
2. 模板 template
2.1. 模板
- 模板是一种源代码复用机制
- 参数化模块:
- 对程序模块(如:类、函数)加上类型参数
- 对不同类型的数据实施相同的操作
- 实例化:生成具体的函数/类
- 模板定义了若干个类,需要显式实例化
- 编译系统自动实例化函数模板:是否实例化模板的某个实例由使用点来决定;如果未使用到一个模板的某个实例,则编译系统不会生成相应实例是的代码。
2.2. 类属函数 templat function
- 同一函数对不同类型的数据完成相同的操作
- 宏实现:
#define max(a,b) ((a)>(b)?(a):(b))
- 缺陷:
- 只能实现简单的功能
- 没有类型检查
2.3. 函数重载
1 |
|
- 缺陷:
- 需要定义的重载函数太多
- 定义不全
- 不可以只有返回值不同
2.4. 函数指针
1 |
|
- 缺陷:
- 需要定义额外参数
- 大量指针运算
- 实现起来复杂
- 可读性差
- template更加结构清晰,实现简单
2.5. 函数模板
1 |
|
- 必须重载操作符 >
- 函数模板定义了一类重载的函数
- 函数模板的实例化:
- 隐式实现
- 根据具体模板函数调用
- 函数模板的参数
- 可有多个类型参数,用逗号分隔
- 可带普通参数
- 必须列在类型参数之后
- 调用时需显式实例化,使用默认参数值可以不显式实例化
1 |
|
- 函数模板与函数重载配合使用(编译器优先使用没有使用模板的函数)
1 |
|
2.6. 类模板
- 类定义带有类型参数,类属类需要显式实例化
- 类模板中的静态成员属于实例化后的类
- 类模板的实例化:创建对象时显式指定
1 |
|
2.7. 模板例子
1 |
|
- 类型参数也可以给出初始值,模板类如果不按照从右往左指定默认值参数,会导致编译错误
- 函数模板的默认值不一定是从右向左的,C++11之后函数模板才接受默认值参数。
- 总而言之从右向左给出默认值总是没有问题的。
2.8. C++中模板的完整定义通常出现在头文件中
- 如果在模块A中要使用模块B中定义的某模板的实例,而在B中未使用这个实例,则模板无法使用这个实例
- 为什么C++中模板的完整定义常常出现在头文件中?
1 |
|
- 解决方案:将file1.cpp中的代码放置到头文件中
- 连接器可以去掉多重定义
2.9. Template MetaProgramming 元编程
- 元程序就是编写一些直接可以生成或者操作其他程序的程序,要在更高层次上。
- 编写元程序就是元编程(两级编程),在编译的时候就已经完成编程
1 |
|
- 元编程的特点
- 输入就是模板中的参数(int N)
- 返回值往往是enum、static、final等等
- 往往是只支持整数,但是浮点数也是可以的
- 选择和循环语句如何操作?
- 选择可以通过特殊实例化实现:
class isTen<N==10>
:模板的特例化 - 递归的调用模板就提供了循环的能力
- 选择可以通过特殊实例化实现:
- 模板元编程是图灵完备的
- 不作为考核内容
3. 参考
2020-C++高级程序设计-C++ 多态
https://spricoder.github.io/2020/07/01/2020-C-plus-plus-advanced-programming/C++-OOP/2020-C-plus-plus-advanced-programming-C++%20%E5%A4%9A%E6%80%81/