十七.多态

17 多态

多态是 C++ 面向对象三大特性之一,它让同一接口可以表现出多种不同的形态,底层原理是虚函数。多态分两类:

  • 静态多态:函数重载和运算符重载,属于编译阶段就确定的绑定,又称编译时多态(早绑定)
  • 动态多态:派生类和虚函数实现,属于运行阶段才确定的绑定,又称运行时多态(晚绑定)

17.1 多态的基本概念

17.1.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
34
35
36
37
38
#include <iostream>
using namespace std;

class Animal
{
public:
//虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};

class Cat : public Animal
{
public:
//重写父类的虚函数
void speak()
{
cout << "小猫在说话" << endl;
}
};

//动态多态:父类指针或引用指向子类对象
void doSpeak(Animal& animal)
{
animal.speak();
}

int main() {

Cat cat;
doSpeak(cat);

system("pause");

return 0;
}

输出结果为「小猫在说话」。如果去掉 virtual 关键字,输出就会变成「动物在说话」——因为此时 animal.speak() 在编译阶段就被绑定到了 Animal::speak,属于静态绑定。

17.1.2 多态的原理

当类中声明了虚函数后,类内部会多出一个虚函数指针 vfptr,它指向一张虚函数表 vftable,表中记录了各个虚函数的入口地址。

  • 子类重写父类虚函数后,子类虚函数表中记录的地址被替换为子类函数的地址
  • 当父类指针或引用指向子类对象时,调用虚函数会通过表中的地址去调用,于是调用到了子类函数

示例:用 sizeof 验证虚函数表指针的存在

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
#include <iostream>
using namespace std;

class Animal
{
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};

class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};

int main() {

cout << "sizeof(Animal) = " << sizeof(Animal) << endl; //4,虚函数指针
cout << "sizeof(Cat) = " << sizeof(Cat) << endl; //4,继承的虚函数指针

system("pause");

return 0;
}

注意:示例中多了虚函数指针,空类原本只占 1 字节,所以判断类的大小时要把虚函数指针也算进去。

17.2 多态案例一:计算器类

分别使用普通写法和多态技术,设计两个操作数进行运算的计算器类。

  • 普通写法:需要为每个运算写独立的类的成员函数,后期维护扩展困难
  • 多态写法:先写一个抽象计算器基类,再派生出各运算类,后期新增运算只需加一个子类,符合开闭原则(对扩展开放,对修改关闭)

示例:普通写法

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
41
42
43
44
45
46
47
48
49
50
#include <iostream>
using namespace std;
#include <string>

class Calculator
{
public:

int getResult(string oper)
{
if (oper == "+")
{
return m_Num1 + m_Num2;
}
else if (oper == "-")
{
return m_Num1 - m_Num2;
}
else if (oper == "*")
{
return m_Num1 * m_Num2;
}
//如果想扩展新的功能,需要修改源码
//在真实开发中提倡开闭原则:对扩展进行开放,对修改进行关闭
return 0;
}

int m_Num1;
int m_Num2;
};

void test01()
{
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 10;

cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}

int main() {

test01();

system("pause");

return 0;
}

示例:多态写法

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include <iostream>
using namespace std;
#include <string>

//抽象计算器基类
class AbstractCalculator
{
public:

virtual int getResult()
{
return 0;
}

int m_Num1;
int m_Num2;
};

//加法计算器
class AddCalculator : public AbstractCalculator
{
public:

int getResult()
{
return m_Num1 + m_Num2;
}
};

//减法计算器
class SubCalculator : public AbstractCalculator
{
public:

int getResult()
{
return m_Num1 - m_Num2;
}
};

//乘法计算器
class MulCalculator : public AbstractCalculator
{
public:

int getResult()
{
return m_Num1 * m_Num2;
}
};

void test01()
{
//创建加法计算器对象
AbstractCalculator* abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;

cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc; //堆区数据,用完要销毁

//换成减法计算器
abc = new SubCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;

cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;

//乘法
abc = new MulCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 10;

cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}

int main() {

test01();

system("pause");

return 0;
}

总结:多态的优点——组织结构清晰、可读性强、前期和后期扩展以及维护性高。

17.3 纯虚函数和抽象类

纯虚函数:在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。因此可以将虚函数改为纯虚函数。

语法:virtual 返回值类型 函数名 (参数列表) = 0;

当类中有了纯虚函数,这个类也称为抽象类。

抽象类特点:

  • 无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

示例:

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
#include <iostream>
using namespace std;

class Base
{
public:

//纯虚函数
virtual void func() = 0;
};

class Son : public Base
{
public:

void func()
{
cout << "Son 中 func 的调用" << endl;
}
};

void test01()
{
//Base b; //错误,抽象类无法实例化对象
//new Base; //错误,抽象类无法实例化对象

//子类必须重写父类中的纯虚函数,否则无法实例化对象
Son s;
s.func();
}

int main() {

test01();

system("pause");

return 0;
}

17.4 多态案例二:制作饮品

制作饮品的流程大体为:煮水 → 冲泡 → 倒入杯中 → 加入辅料。

利用多态技术实现,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。

示例:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <iostream>
using namespace std;

//抽象制作饮品
class AbstractDrinking
{
public:

//烧水
virtual void Boil() = 0;

//冲泡
virtual void Brew() = 0;

//倒入杯中
virtual void PourInCup() = 0;

//加入辅料
virtual void PutSomething() = 0;

//规定流程
void MakeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};

//制作咖啡
class Coffee : public AbstractDrinking
{
public:

//烧水
void Boil()
{
cout << "煮农夫山泉" << endl;
}

//冲泡
void Brew()
{
cout << "冲泡咖啡" << endl;
}

//倒入杯中
void PourInCup()
{
cout << "倒入杯中" << endl;
}

//加入辅料
void PutSomething()
{
cout << "加入糖和牛奶" << endl;
}
};

//制作茶叶
class Tea : public AbstractDrinking
{
public:

//烧水
void Boil()
{
cout << "煮矿泉水" << endl;
}

//冲泡
void Brew()
{
cout << "冲泡茶叶" << endl;
}

//倒入杯中
void PourInCup()
{
cout << "倒入杯中" << endl;
}

//加入辅料
void PutSomething()
{
cout << "加入枸杞" << endl;
}
};

//制作饮品函数
void doWork(AbstractDrinking* abs)
{
abs->MakeDrink();
delete abs; //释放
}

void test01()
{
//制作咖啡
doWork(new Coffee);

cout << "----------------------" << endl;

//制作茶叶
doWork(new Tea);
}

int main() {

test01();

system("pause");

return 0;
}

17.5 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码,会造成内存泄漏。

解决办法:将父类中的析构函数改为虚析构或者纯虚析构。

虚析构语法:virtual ~类名(){}

纯虚析构语法:virtual ~类名() = 0;,且需要类外实现 类名::~类名(){}

示例:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <iostream>
using namespace std;
#include <string>

class Animal
{
public:

Animal()
{
cout << "Animal 构造函数调用" << endl;
}

//纯虚析构
//有了纯虚析构之后,这个类也属于抽象类,无法实例化对象
virtual ~Animal() = 0;
};

//纯虚析构需要类外实现
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用" << endl;
}

class Cat : public Animal
{
public:

Cat(string name)
{
cout << "Cat 构造函数调用" << endl;
m_Name = new string(name);
}

~Cat()
{
if (m_Name != NULL)
{
cout << "Cat 析构函数调用" << endl;
delete m_Name;
m_Name = NULL;
}
}

string* m_Name;
};

void test01()
{
Animal* animal = new Cat("Tom");
cout << *animal->m_Name << endl;

//父类指针在析构时,不会调用子类中析构函数
//导致子类如果有堆区属性,会出现内存泄漏
//解决办法:父类析构函数改为虚析构或纯虚析构
delete animal;
}

int main() {

test01();

system("pause");

return 0;
}

总结:

  • 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象的问题
  • 如果子类中没有堆区数据,可以不写虚析构或纯虚析构
  • 拥有纯虚析构函数的类也属于抽象类