我也要有自己的博客了吗!
随便放点东西测试一下。把最近上OOP的笔记更新上来吧。
Lec.9 多态与模板
纯虚函数与抽象类
虚函数还可以进一步声明为纯虚函数(如下所示),包含纯虚函数的类,通常被称为“抽象类”。
virtual 返回类型 函数名(形式参数) = 0;
抽象类不允许定义对象,定义基类为抽象类的主要用途是为派生类规定共性“接口”
特点:
•不允许定义对象。
•只能为派生类提供接口。
•能避免对象切片:保证只有指针和引用能被向上类型转换。
| class A { public: virtual void f() = 0; }; A obj;
|

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> using namespace std;
class Pet { public: virtual void motion()=0; }; void Pet::motion(){ cout << "Pet motion: " << endl; } class Dog: public Pet { public: void motion() override {Pet::motion(); cout << "dog run" << endl; } }; class Bird: public Pet { public: void motion() override {Pet::motion(); cout << "bird fly" << endl; } }; int main() { Pet* p = new Dog; p->motion(); p = new Bird; p->motion(); return 0; }
|
基类纯虚函数被派生类重写覆盖之前仍是纯虚函数。因此当继承一个抽象类时,除纯虚析构函数外(后面解释),必须实现(重写覆盖)所有纯虚函数,否则继承出的类也是抽象类。
why?
对于纯虚析构函数而言,即便派生类中不显式实现,编译器也会自动合成默认析构函数。因此,即使派生类不显式覆盖纯虚析构函数,只要派生类覆盖了其他纯虚函数,该派生类就不是抽象类,可以定义派生类对象。
纯虚析构函数
- 纯虚析构函数仍然需要函数体
- 目的:使基类成为抽象类,不能创建基类的对象。如果有其他函数是纯虚函数,则析构函数无论是否为纯虚的,基类均为抽象类。
(why? 虽然是纯虚函数,但名字不同,故派生类中没必要再写一个名字相同的)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream> using namespace std; class Base{ public: virtual ~Base()=0; }; Base::~Base() {cout<<"Base destroyed"<<endl;}
class Derive1: public Base {}; class Derive2: public Base { public: virtual ~Derive2() {cout<<"Derive2 destroyed"<<endl;} }; int main() { Base* p1 = new Derive1; Base* p2 = new Derive2; delete p1; cout << “------” << endl; delete p2; return 0; }
|

向下类型转换
回顾:向上类型转换:
转换为基类指针或引用,则对应虚函数表仍为派生类的虚函数表(晚绑定)。
转换为基类对象,产生对象切片,调用基类函数(早绑定)。
基类指针/引用转换成派生类指针/引用,则称为向下类型转换。(类层次中向下移动)
- 当我们用基类指针表示各种派生类时(向上类型转换),保留了他们的共性,但是丢失了他们的特性。如果此时要表现特性,则可以使用向下类型转换。
- 比如我们可以使用基类指针数组对各种派生类对象进行管理,当具体处理时我们可以将基类指针转换为实际的派生类指针,进而调用派生类专有的接口
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| #include <iostream> using namespace std;
class Pet { public: virtual ~Pet() {} }; class Dog : public Pet { public: void run() { cout << "dog run" << endl; } }; class Bird : public Pet { public: void fly() { cout << "bird fly" << endl; } };
void action(Pet* p) { auto d = dynamic_cast<Dog*>(p); auto b = dynamic_cast<Bird*>(p); if (d) d->run(); else if(b) b->fly(); } int main() { Pet* p[2]; p[0] = new Dog; p[1] = new Bird; for (int i = 0; i < 2; ++i) { action(p[i]); } return 0; }
|
多重继承中的虚函数

多重继承的问题:
- 二义性:如果派生类D继承的两个基类A,B,有同名成员a,则访问D中a时,编译器无法判断要访问的哪一个基类成员。
- 钻石型继承树(DOD:Diamond Of Death)带来的数据冗余:右图中如果 InputFile 和 OutputFile 都含有继承自 File 的 filename 变量,则 IOFile 会有两份独立的 filename,而这实际上并不需要。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream> using namespace std;
class WhatCanSpeak { public: virtual ~WhatCanSpeak() {} virtual void speak() = 0; }; class WhatCanMotion { public: virtual ~WhatCanMotion() {} virtual void motion() = 0; }; class Human : public WhatCanSpeak, public WhatCanMotion { void speak() { cout << "say" << endl; } void motion() { cout << "walk" << endl; } };
void doSpeak(WhatCanSpeak* obj) { obj->speak(); } void doMotion(WhatCanMotion* obj) { obj->motion(); } int main() { Human human; doSpeak(&human); doMotion(&human); return 0; }
|
使用虚函数实现动态的多态行为!隔离开“变”与“不变”
派生类改变,接口不变
多态 Polymorphism
- 按照基类的接口定义,调用指针或引用所指对象的接口函数,函数执行过程因对象实际所属派生类的不同而呈现不同的效果(表现),这个现象被称为“多态”。
- 当利用基类指针/引用调用函数时
虚函数在运行时确定执行哪个版本,取决于引用或指针对象的真实类型
非虚函数在编译时绑定
- 当利用类的对象直接调用函数时
无论什么函数,均在编译时绑定
- 产生多态效果的条件:继承 && 虚函数 && (引用 或 指针)

典例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| #include <iostream> using namespace std;
class Animal{ public: void action() { speak(); motion(); } virtual void speak() { cout << "Animal speak" << endl; } virtual void motion() { cout << "Animal motion" << endl; } };
class Bird : public Animal { public: void speak() { cout << "Bird singing" << endl; } void motion() { cout << "Bird flying" << endl; } };
class Fish : public Animal { public: void speak() { cout << "Fish cannot speak ..." << endl; } void motion() { cout << "Fish swimming" << endl; } };
int main() { Fish fish; Bird bird; fish.action(); bird.action();
Animal *pBase1 = new Fish; Animal *pBase2 = new Bird; pBase1->action(); pBase2->action(); return 0; }
|

应用: TEMPLATE METHOD 模板方法设计模式
- 在接口的一个方法中定义算法的骨架
- 将一些步骤的实现延迟到子类中
- 使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
- 模板方法是一种源代码重用的基本技术,在类库的设计实现中应用十分广泛,因为这个设计模式能有效地解决 “类库提供公共行为”与“用户定制特殊细节”之间的折中平衡。
有效框架:


可以通过继承和组合重用对象代码
可以通过模板特征重用源代码
模板
定义
- 有些算法实现与类型无关,所以可以将函数的参数类型也定义为一种特殊的“参数”,这样就得到了“函数模板”。
- 定义函数模板的方法
template ReturnType 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| #include <iostream> #include <algorithm>
template<class T> void sort(T* data, int len) { for(int i = 0; i < len; i++){ for(int j = i + 1; j < len; j++) { if(data[i] > data[j]) std::swap(data[i], data[j]); } } }
template<class T> void output(T* data, int len) { for(int i = 0; i < len; i++) std::cout << data[i] << " "; std::cout << std::endl; } int main() { int arr_a[] = {3,2,4,1,5}; sort(arr_a, 5); output(arr_a, 5); float arr_b[] = {3.2, 2.1, 4.3, 1.5, 5.7}; sort(arr_b, 5); output(arr_b, 5); return 0; }
|
模板生成的原理
对模板的处理是在编译期进行的,每当编译器发现对模板的一种参数的使用,就生成对应参数的一份代码。
也带来了问题:
模板库必须在头文件中实现,不可以分开编译(请思考为什么?)
因为模板的原理是:在编译时,每发现一种模板参数的模板实例,就生成对应模板参数的代码。
如果使用源代码分开编译,则编译模板库的源代码时,编译器并不知道这一模板库有哪些模板实例;而编译模板实例时,又没有模板库的源代码来作生成。
因此会产生链接错误,没有生成对应模板参数的源代码。——不知道如何实现,无法链接
类模板
在定义类时也可以将一些类型信息抽取出来,用模板参数来替换,从而使类更具通用性。这种类被称为“类模板”。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #include <iostream> using namespace std;
template <typename T> class A { T data; public: A(T _data): data(_data) {} void print() { cout << data << endl; } }; template<typename T> void A<T>::print() { cout << data << endl; } int main() { A<int> a(1); a.print(); return 0; }
|
类模板的“模板参数”
类型参数:使用typename或class标记
非类型参数:整数,枚举,指针(指向对象或函数),引用(引用对象或引用函数)。无符号整数(unsigned)比较常用。如:
template<typename T, unsigned size>
class array {
T elems[size];
};
array<char, 10> array0;
所有模板参数必须在编译期确定,不可以使用变量!!
1 2 3 4 5 6 7 8 9 10 11 12 13
| template<typename T, unsigned size> class array { T elems[size]; };
int main(){ int n = 5; const int m = 5; array<char, m> array1; array<char, 5> array2; return 0; }
|
模板与多态
- 模板使用泛型标记,使用同一段代码,来关联不同但相似的特定行为,最后可以获得不同的结果。模板也是多态的一种体现。
- 但模板的关联是在编译期处理,称为静多态。(编译期多态)
- 往往和函数重载同时使用
- 高效,省去函数调用
- 编译后代码增多
- 基于继承和虚函数的多态在运行期处理,称为动多态
std标准模板库
成员函数模板