OOP随手记-Lec.9:多态与模板
我也要有自己的博客了吗!
随便放点东西测试一下。把最近上OOP的笔记更新上来吧。
Lec.9 多态与模板
纯虚函数与抽象类
虚函数还可以进一步声明为纯虚函数(如下所示),包含纯虚函数的类,通常被称为“抽象类”。
virtual 返回类型 函数名(形式参数) = 0;抽象类不允许定义对象,定义基类为抽象类的主要用途是为派生类规定共性“接口”
特点:
•不允许定义对象。
•只能为派生类提供接口。
•能避免对象切片:保证只有指针和引用能被向上类型转换。
1 |
|
1 |
|
基类纯虚函数被派生类重写覆盖之前仍是纯虚函数。因此当继承一个抽象类时,除纯虚析构函数外(后面解释),必须实现(重写覆盖)所有纯虚函数,否则继承出的类也是抽象类。
why?
对于纯虚析构函数而言,即便派生类中不显式实现,编译器也会自动合成默认析构函数。因此,即使派生类不显式覆盖纯虚析构函数,只要派生类覆盖了其他纯虚函数,该派生类就不是抽象类,可以定义派生类对象。
纯虚析构函数
- 纯虚析构函数仍然需要函数体
- 目的:使基类成为抽象类,不能创建基类的对象。如果有其他函数是纯虚函数,则析构函数无论是否为纯虚的,基类均为抽象类。
(why? 虽然是纯虚函数,但名字不同,故派生类中没必要再写一个名字相同的)
1 |
|
向下类型转换
回顾:向上类型转换:
转换为基类指针或引用,则对应虚函数表仍为派生类的虚函数表(晚绑定)。
转换为基类对象,产生对象切片,调用基类函数(早绑定)。
基类指针/引用转换成派生类指针/引用,则称为向下类型转换。(类层次中向下移动)
- 当我们用基类指针表示各种派生类时(向上类型转换),保留了他们的共性,但是丢失了他们的特性。如果此时要表现特性,则可以使用向下类型转换。
- 比如我们可以使用基类指针数组对各种派生类对象进行管理,当具体处理时我们可以将基类指针转换为实际的派生类指针,进而调用派生类专有的接口
dynamic_cast
- C++提供了一个特殊的显式类型转换,称为dynamic_cast,是一种安全的向下类型转换。
- 使用dynamic_cast的对象必须有虚函数,因为它使用了存储在虚函数表中的信息判断实际的类型。
- 使用方法:
obj_p,obj_r分别是T1类型的指针和引用
T2* pObj = dynamic_cast<T2*>(obj_p);
//转换为T2指针,运行时失败返回nullptr
T2& refObj = dynamic_cast<T2&>(obj_r);
//转换为T2引用,运行时失败抛出bad_cast异常
在向下转换中,T1必须是多态类型(声明或继承了至少一个虚函数的类),否则不过编译
static_cast
- 如果我们知道正在处理的是哪些类型,可以使用static_cast来避免这种开销。
static_cast在编译时静态浏览类层次,只检查继承关系。没有继承关系的类之间,必须具有转换途径才能进行转换(要么自定义,要么是语言语法支持),否则不过编译。运行时无法确认是否正确转换。 - static_cast使用方法:
obj_p,obj_r分别是T1类型的指针和引用
T2* pObj = static_cast<T2*>(obj_p);
//转换为T2指针
T2& refObj = static_cast<T2&>(obj_r);
//转换为T2引用
不安全:不保证指向目标是T2对象,可能导致非法内存访问
dynamic_cast与static_cast
- 相同点:
都可完成向下类型转换。 - 不同点:
static_cast在编译时静态执行向下类型转换。 - dynamic_cast会在运行时检查被转换的对象是否确实是正确的派生类。额外的检查需要 RTTI (Run-Time Type Information),因此要比static_cast慢一些,但是更安全。
一般使用dynamic_cast进行向下类型转换
Summary: 判断指针指向的是否是所需的真正对象
1)指针或引用的向上转换总是安全的;
2)向下转换时用dynamic_cast,安全检查;
3)避免对象之间的转换。
应当清楚指向的是基类对象还是派生类对象
1 |
|
多重继承中的虚函数
多重继承的问题:
- 二义性:如果派生类D继承的两个基类A,B,有同名成员a,则访问D中a时,编译器无法判断要访问的哪一个基类成员。
- 钻石型继承树(DOD:Diamond Of Death)带来的数据冗余:右图中如果 InputFile 和 OutputFile 都含有继承自 File 的 filename 变量,则 IOFile 会有两份独立的 filename,而这实际上并不需要。
1 |
|
使用虚函数实现动态的多态行为!隔离开“变”与“不变”
派生类改变,接口不变
多态 Polymorphism
- 按照基类的接口定义,调用指针或引用所指对象的接口函数,函数执行过程因对象实际所属派生类的不同而呈现不同的效果(表现),这个现象被称为“多态”。
- 当利用基类指针/引用调用函数时
虚函数在运行时确定执行哪个版本,取决于引用或指针对象的真实类型
非虚函数在编译时绑定 - 当利用类的对象直接调用函数时
无论什么函数,均在编译时绑定 - 产生多态效果的条件:继承 && 虚函数 && (引用 或 指针)
典例:
1 |
|
应用: TEMPLATE METHOD 模板方法设计模式
- 在接口的一个方法中定义算法的骨架
- 将一些步骤的实现延迟到子类中
- 使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 模板方法是一种源代码重用的基本技术,在类库的设计实现中应用十分广泛,因为这个设计模式能有效地解决 “类库提供公共行为”与“用户定制特殊细节”之间的折中平衡。
有效框架:
可以通过继承和组合重用对象代码
可以通过模板特征重用源代码
模板
定义
- 有些算法实现与类型无关,所以可以将函数的参数类型也定义为一种特殊的“参数”,这样就得到了“函数模板”。
- 定义函数模板的方法
templateReturnType Func(Args); - 如:任意类型两个变量相加的“函数模板”
template
T sum(T a, T b) { return a + b; }//需要加法运算符被重载过
注:typename也可换为class
调用
- 函数模板在调用时,编译器能自动推导出实际参数的类型(这个过程叫做实例化)。
所以,形式上调用一个函数模板与普通函数没有区别,如
cout << sum(9, 3);
cout << sum(2.1, 5.7); - 调用类型需要满足函数的要求。本例中,要求类型 T 定义了加法运算符。
当多个参数的类型不一致时,无法推导:
cout << sum(9, 2.1); //编译错误 - 可以手工指定调用类型:sum
(9, 2.1)
以排序为例:
1 |
|
模板生成的原理
对模板的处理是在编译期进行的,每当编译器发现对模板的一种参数的使用,就生成对应参数的一份代码。
也带来了问题:
模板库必须在头文件中实现,不可以分开编译(请思考为什么?)
因为模板的原理是:在编译时,每发现一种模板参数的模板实例,就生成对应模板参数的代码。
如果使用源代码分开编译,则编译模板库的源代码时,编译器并不知道这一模板库有哪些模板实例;而编译模板实例时,又没有模板库的源代码来作生成。
因此会产生链接错误,没有生成对应模板参数的源代码。——不知道如何实现,无法链接
类模板
在定义类时也可以将一些类型信息抽取出来,用模板参数来替换,从而使类更具通用性。这种类被称为“类模板”。例如:
1 |
|
类模板的“模板参数”
类型参数:使用typename或class标记
非类型参数:整数,枚举,指针(指向对象或函数),引用(引用对象或引用函数)。无符号整数(unsigned)比较常用。如:
template<typename T, unsigned size>
class array {
T elems[size];
};
array<char, 10> array0;
所有模板参数必须在编译期确定,不可以使用变量!!
1 |
|
模板与多态
- 模板使用泛型标记,使用同一段代码,来关联不同但相似的特定行为,最后可以获得不同的结果。模板也是多态的一种体现。
- 但模板的关联是在编译期处理,称为静多态。(编译期多态)
- 往往和函数重载同时使用
- 高效,省去函数调用
- 编译后代码增多
- 基于继承和虚函数的多态在运行期处理,称为动多态
- 运行时,灵活方便
- 侵入式,必须继承
- 存在函数调用
std标准模板库
成员函数模板
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!