OOP随手记-Lec.9:多态与模板

我也要有自己的博客了吗!
随便放点东西测试一下。把最近上OOP的笔记更新上来吧。

Lec.9 多态与模板

纯虚函数与抽象类

  • 虚函数还可以进一步声明为纯虚函数(如下所示),包含纯虚函数的类,通常被称为“抽象类”
    virtual 返回类型 函数名(形式参数) = 0;

  • 抽象类不允许定义对象,定义基类为抽象类的主要用途是为派生类规定共性“接口

  • 特点:

    •不允许定义对象。

    •只能为派生类提供接口

    •能避免对象切片:保证只有指针和引用能被向上类型转换。

1
2
3
4
5
class A {
public:
virtual void f() = 0; /// 可在类外定义函数体提供默认实现。派生类通过 A::f() 调用
};
A obj; /// 不准抽象类定义对象!编译不通过!

image-20220418133937541

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();
//p = new Pet; /// 不允许定义抽象类对象
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;
}
/*Base destroyed(也调用了Derive1,只不过是隐式定义的)
------
Derive2 destroyed
Base destroyed
*/

image-20220425162411624

向下类型转换

回顾:向上类型转换:
转换为基类指针或引用,则对应虚函数表仍为派生类的虚函数表(晚绑定)。
转换为基类对象,产生对象切片,调用基类函数(早绑定)。

基类指针/引用转换成派生类指针/引用,则称为向下类型转换。(类层次中向下移动)

  • 当我们用基类指针表示各种派生类时(向上类型转换),保留了他们的共性,但是丢失了他们的特性。如果此时要表现特性,则可以使用向下类型转换。
  • 比如我们可以使用基类指针数组对各种派生类对象进行管理,当具体处理时我们可以将基类指针转换为实际的派生类指针,进而调用派生类专有的接口

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)避免对象之间的转换。

image-20220418140501192image-20220418140827399

应当清楚指向的是基类对象还是派生类对象

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;
}

多重继承中的虚函数

image-20220425164056101

多重继承的问题:

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

image-20220418142236127

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

  • 按照基类的接口定义,调用指针或引用所指对象的接口函数,函数执行过程因对象实际所属派生类的不同而呈现不同的效果(表现),这个现象被称为“多态”。
  • 当利用基类指针/引用调用函数时
    虚函数在运行时确定执行哪个版本,取决于引用或指针对象的真实类型
    非虚函数在编译时绑定
  • 当利用类的对象直接调用函数时
    无论什么函数,均在编译时绑定
  • 产生多态效果的条件:继承 && 虚函数 && (引用 或 指针)
  • image-20220418143002571

典例:

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;
}

image-20220418143338219

应用: TEMPLATE METHOD 模板方法设计模式

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

有效框架:

image-20220418143702100

image-20220418143709266

可以通过继承和组合重用对象代码

可以通过模板特征重用源代码

模板

定义

  • 有些算法实现与类型无关,所以可以将函数的参数类型也定义为一种特殊的“参数”,这样就得到了“函数模板”。
  • 定义函数模板的方法
    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); //调用int类型的sort
output(arr_a, 5); //调用int类型的output

float arr_b[] = {3.2, 2.1, 4.3, 1.5, 5.7};
sort(arr_b, 5); //调用float类型的sort
output(arr_b, 5); //调用float类型的output
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;
//array<char, n> array0; //不能使用变量
const int m = 5;
array<char, m> array1; //可以使用常量
array<char, 5> array2; //或具体数值
return 0;
}

模板与多态

  • 模板使用泛型标记,使用同一段代码,来关联不同但相似的特定行为,最后可以获得不同的结果。模板也是多态的一种体现。
  • 但模板的关联是在编译期处理,称为静多态。(编译期多态)
    • 往往和函数重载同时使用
    • 高效,省去函数调用
    • 编译后代码增多
  • 基于继承和虚函数的多态在运行期处理,称为动多态
    • 运行时,灵活方便
    • 侵入式,必须继承
    • 存在函数调用

std标准模板库

成员函数模板


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!