c++远征05之多态篇

C++远征之多态篇_C++视频教程-慕课网

本课程将带领大家体会面向对象三大特性中的多态特性,讲述了虚函数、抽象类和接口类等概念,以及多态的实现原理,课程的最后引入RTTI及异常处理,使整个多态篇更加完整,更具实战指导性,本门课程是C++远征课程的高潮和经典,对于面向对象的语言的学习将大有裨益。

1 c++ 多态概述

多态知识点地位

多态衍生的知识点

2 虚函数及实现原理

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

/*
* 静态多态(函数重载)
*/

class Rect
{
public:
/*
* 计算正方形面积
*/

int calcArea(int width)
{

return width * width;
}

/*
* 计算一般矩形面积
*/

int calcArea(int width, int height)
{

return width * height;
}
};

int main(void)
{

Rect rect;
rect.calcArea(10);
rect.calcArea(10, 20);

return 0;
}

动态多态(晚绑定)

说明: 动态多态具体到语法中是指,使用父类指针指向子类指针,并可以通过该指针调用子类的方法。

  • 产生多态的基础是继承关系,没有继承就没有多态。
  • 多态的语法核心是 virtual 关键字,必须使用 virtual 才能使多个类间建立多态关系。
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
#include <iostream>

using namespace std;

// 形状
class Shape
{
public:
Shape()
{
cout << "Shape()" << endl;
}
~Shape()
{
cout << "~Shape()" << endl;
}

// 父类中该方法定义为虚函数
virtual double calcArea()
{

cout << "calcArea" << endl;
return 0;
}
};

// 圆形
class Circle:public Shape
{
public:
Circle(double r)
{
cout << "Circle(double r)" << endl;
m_dR = r;
}
~Circle()
{
cout << "virtual ~Circle()" << endl;
}
// 由于父类同名函数已经使用了 virtual 修饰,这里的 virtual 计算机会自动添加,因此可以参略,
virtual double calcArea() {
cout << "Circle::calcArea()" << endl;
return 3.14 * m_dR * m_dR;
}
private:
double m_dR;
};

// 矩形
class Rect:public Shape
{
public:
Rect(double width, double height)
{
cout << "Rect(double width, double height)" << endl;
m_dWidth = width;
m_dHeight = height;
}
virtual ~Rect()
{
cout << "virtual ~Rect()" << endl;
}
virtual double calcArea()
{

cout << "Rect::calcArea()" << endl;
return m_dWidth * m_dHeight;
}
private:
double m_dWidth;
double m_dHeight;
};

int main(void)
{

// 父类型指针指向子类实例
Shape *shape1 = new Circle(4.0);
Shape *shape2 = new Rect(3.0, 5.0);

// 调用的是子类定义的函数
shape1->calcArea();
shape2->calcArea();

delete shape1;
delete shape2;

shape1 = NULL;
shape2 = NULL;
return 0;
}

2.2 虚析构函数

2.2.1 虚析构函数

说明: 使用 virtual 关键字修饰符修饰析构函数,就构成了虚析构函数。

  • 虚函数使用 virtual 关键字定义,但时用 virtual 关键字时,并非全部是虚函数。
  • 虚函数特性可以被继承,当子类中定义的函数与父类中虚函数的声明相同时,该函数也是虚函数。
  • 虚析构函数是为了避免使用父类指针释放子类对象时造成内存泄漏。

用途: 将父类的析构函数定义为虚析构函数,从而当通过指向子类对象的父类型指针销毁对象时,子类的析构函数也会被自动调用。
注意: 父类的析构函数定义为虚析构函数,则编译器会自动为子类的析构函数添加 virtual修饰,但仍然建议手动为子类定义虚析构函数,代码的可读性更好。

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
#include <iostream>

using namespace std;
// 坐标
class Coordinate
{
public:
Coordinate(int x, int y):m_iX(x), m_iY(y)
{
cout << "Coordinate()" << endl;
}
~Coordinate()
{
cout << "~Coordinate()" << endl;
}
private:
int m_iX;
int m_iY;
};

// 形状
class Shape
{
public:
Shape()
{
cout << "Shape()" << endl;
}
virtual ~Shape()
{
cout << "virtual ~Shape()" << endl;
}

// 父类中该方法定义为虚函数
virtual double calcArea()
{

cout << "calcArea" << endl;
return 0;
}
};

// 圆形
class Circle:public Shape
{
public:
Circle(int x, int y, double r)
{
cout << "Circle(double r)" << endl;
m_pCenter = new Coordinate(x, y);
m_dR = r;
}

virtual ~Circle()
{
delete m_pCenter;
m_pCenter = NULL;
cout << "virtual ~Circle()" << endl;
}
// 由于父类同名函数已经使用了 virtual 修饰,这里的 virtual 计算机会自动添加,因此可以参略,
virtual double calcArea() {
cout << "Circle::calcArea()" << endl;
return 3.14 * m_dR * m_dR;
}
private:
double m_dR;
Coordinate *m_pCenter;
};

int main(void)
{

// 父类型指针指向子类实例
Shape *shape1 = new Circle(0, 5, 3.5);

// 调用的是子类定义的函数
shape1->calcArea();

delete shape1;

shape1 = NULL;
return 0;
}

2.2.2 不能被 virtual 修饰的函数

说明:并不是所有类型的函数都能构用 virtual修饰。

不能用 virtual 修饰的函数|编译器行为
—|—
全局函数|报错
静态成员函数|报错
内联函数|忽略 virtual 修饰符
构造函数|报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 普通函数(全局函数)
virtual int test() {}

class Animal
{
public:
// 静态成员函数
virtual static int getCount()

// 内联函数
inline virtual int eat()

// 构造函数
Animal(){}

};

2.3 虚函数与虚析构函数

2.3.1 函数指针

说明: 函数的本质其实就是一段二进制代码,函数指针保存着函数二进制代码存储区域的首地址。

2.3.2 虚函数原理

虚函数表: 关于虚函数表的事实如下

  • 类中含有虚析构函数或其它虚函数就能产生虚析函数表。
  • 每个类只有一份虚函数表,所有该类的对象公用同一张虚函数表。
  • 两张虚函数表中的函数指针可能指向同一个函数(指父类定义了虚函数而子类没有对应的虚函数的情况)。

说明: 在 c++ 中,多态是通过虚函数表来实现的。具体来说,父类的 A 函数定义为虚函数,那么分两种情况:

子类是否定义了 A 函数|父类指针调用 A 函数
—|—
是|通过查找虚函数表,调用的是子类各自的 A 函数(构成多态)
否|通过查找虚函数表,调用的是父类中定义的 A 函数(不构成多态)

其中第一种情况,事实上就是利用 c++ 函数的覆盖与隐藏 实现了多态。

例子

情形1:子类没定义对应的虚函数

情形2:子类定义了相应的虚函数

2.3.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
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
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;

/**
* 定义动物类:Animal
* 成员函数:eat()、move()
*/

class Animal
{
public:
// 构造函数
Animal(){cout << "Animal" << endl;}
// 析构函数
virtual ~Animal(){cout << "~Animal" << endl;}
// 成员函数eat()
virtual void eat(){cout << "Animal -- eat" << endl;}
// 成员函数move()
virtual void move(){cout << "Animal -- move" << endl;}
};

/**
* 定义狗类:Dog
* 此类公有继承动物类
* 成员函数:父类中的成员函数
*/

class Dog : public Animal
{
public:
// 构造函数
Dog(){cout << "Dog" << endl;}
// 析构函数
virtual ~Dog(){cout << "~Dog" << endl;}
// 成员函数eat()
virtual void eat(){cout << "Dog -- eat" << endl;}
// 成员函数move()
virtual void move(){cout << "Dog -- move" << endl;}
};

int main(void)
{

// 通过父类对象实例化狗类
Animal *animal = new Dog();
// 调用成员函数
animal->eat();
animal->move();
// 释放内存
delete animal;
animal = NULL;

return 0;
}
1
2
3
4
5
6
Animal
Dog
Dog -- eat
Dog -- move
~Dog
~Animal

2.3.4 深入了解内存中的对象

说明: 除了前面的虚函数和虚析构函数,我们还想通过这个例子深入了解以下知识点

  • 对象的大小

组成部分|大小/字节(64位机器)|存在条件
—|—|—
占位|1|仅当既没有任何数据成员,也没有虚函数表的情况下
虚函数表指针|8|定义了虚函数或虚析构函数,实际上是一个
数据成员|所有数据成员大小之和|存在数据成员

  • 对象的地址
  • 对象成员的地址
  • 虚函数表指针
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;

// 形状
class Shape
{
public:
Shape()
{
cout << "Shape()" << endl;
}
// 虚析构函数
virtual ~Shape()
{
cout << "virtual ~Shape()" << endl;
}
// 定义为虚函数(实现多态)
virtual double caleArea()
{

cout << "virtual double caleArea()" << endl;
return 0;
}
};

// 圆
class Circle:public Shape
{
public:
Circle(int r):m_iR(r)
{
cout << "Circle(int r)" << endl;
}
virtual ~Circle()
{
cout << "virtual ~Circle()" << endl;
}
private:
int m_iR;
};

int main(void)
{

// 虚函数表指针(8)
Shape shape;
cout << sizeof(shape) << endl;

// 虚函数表指针(8) + 数据成员(8)
Circle circle(100);
cout << sizeof(circle) << endl;

// shape 对象地址(用整数方式显示地址信息)
int *p = (int *)&shape;
cout << p << endl;

// circle 对象地址
int *q = (int *)&circle;
cout << q << endl;

// shape 对象首地址存储的数据(虚函数表地址)
cout << (unsigned int)(*p) << endl;

// circle 对象首地址存储的数据(虚函数表地址)
cout << (unsigned int)(*q) << endl;

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
Shape()
8
Shape()
Circle(int r)
16
0x7fff5fbff450
0x7fff5fbff430
8432
8488
virtual ~Circle()
virtual ~Shape()
virtual ~Shape()

3 纯虚函数和抽象类

3.1 纯虚函数和抽象类

纯虚函数: 定义为 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
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;

/**
* 定义动物类:Animal
* 虚函数:eat()
* 纯虚函数:move()
* 数据成员:m_strName
*/

class Animal
{
public:
// 默认构造函数
Animal(){}
// 含参构造函数
Animal(string name){m_strName = name; cout << "Animal" << endl;}
// 虚析构函数
virtual ~Animal(){cout << "~Animal" << endl;}
// 虚成员函数
virtual void eat(){cout << "Animal--" << m_strName << "-- eat" << endl;}
// 纯虚函数
virtual void move() = 0;
public:
// 数据成员
string m_strName;
};

/**
* 定义狗类:Dog
* 公有继承动物类
* 虚成员函数:eat()、move()
*/

class Dog : public Animal
{
public:
// 默认构造函数
Dog(){}
// 含参构造函数
Dog(string name){m_strName = name; cout << "Dog" << endl;}
// 虚析构函数
virtual ~Dog(){cout << "~Dog" << endl;}
// 虚成员函数eat()
virtual void eat(){cout << "Dog--" << m_strName << " -- eat" << endl;}
// 虚成员函数move()
virtual void move(){cout << "Dog--" << m_strName << " -- move" << endl;}
};

int main(void)
{

// 通过动物类实例化狗类
Animal *dog = new Dog("Watson");
// 调用成员函数
dog->eat();
// 释放内存
delete dog;
dog = NULL;

return 0;
}

3.2 接口类

说明: 仅含有纯虚函数的类称为接口类

  • 接口类中仅有纯虚函数,不能有其它函数,也不能有数据成员。
  • 可以使用接口类指针指向其子类对象,并调用子类对象中实现的接口类中的纯虚函数。
  • 一个类可以继承一个接口类,也可以继承多个接口类。
  • 一个类可以继承接口类的同时也继承非接口类。

DEMO1: 即继承接口,也继承类

DEMO2

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
#include <iostream>
#include <stdlib.h>
#include <string>
using namespace std;

/**
* 定义射击类:CanShut
* 定义纯虚函数:aim、reload
*/

class CanShut
{
public:
virtual void aim() = 0;
virtual void reload() = 0;
};

/**
* 定义枪类:Gun
* 公有继承射击类
* 实现成员函数:aim、reload
*/

class Gun : public CanShut
{
public:
virtual void aim()
{

cout << "Gun -- aim" << endl;
}
virtual void reload()
{

cout << "Gun -- reload" << endl;
}
};

/**
* 定义含参函数射击:hunting
* 调用参数的aim与reload函数
*/

void hunting(CanShut *s)
{

s->aim();
s->reload();
}

int main(void)
{

// 实例化枪对象
CanShut *gun = new Gun();
// 调用含参函数hunting,将对象枪传入函数中
hunting(gun);
// 释放内存
delete gun;
gun = NULL;

return 0;
}

4 运行时类型识别

说明:RTTI(Run-Time Type Identification)技术可以通过父类指针识别其所指向对象的真实数据类型。与虚拟概念是类型识别必须建立在虚函数的基础上,否则无需 RTTI 技术。
原理: 利用 typeid函数 和 dynamic_cast函数

4.1 dynamic_cast 函数

描述: dynamic_cast 可以将一种类型的指针或引用转换为另外一种。
参数:指针或引用(且要转换的类型中必须包含虚函数)
返回值: 转换成功则返回子类的地址,失败返回 NULL

1
2
Flyable *p = new Bird(); // 多态父类(有虚函数)指针指向子类实例
Bird *b = dynamic_cast<Bird *>p; // 将父类指针转换为子类指针

4.2 typeid 函数

描述: 可以用来获取任何类型的类型描述。
参数: 参数类型非常广泛,包括

  • 对象
  • 任意数据类型本身(比如 int、double、自定义的结构体、类、枚举类型等)
  • 指针
  • 引用

返回值:type_info 对象

1
2
3
4
5
6
7
8
9
10
11
class type_info
{
public:
const char* name() const;
bool operator == (const type_info& rhs) const; // 运算符重载
bool operator != (const type_info& rhs) const; // 运算符重载
int before(const type_info& rhs) const;
virtual ~type_info();
private:
...
}

注意

  • typeid 返回一个 type_info 对象的引用
  • 如果想通过基类的指针获得派生类的数据类型,基类必须带有虚函数
  • 只能获取对象的实际类型
1
2
3
4
5
6
// typeid 基本用法
Flyable *p = new Bird();
cout << typeid(int).name() << endl;// i
cout << typeid(Bird).name() << endl;// 4Bird
cout << typeid(p).name() << endl; // P7Flyable
cout << typeid(*p).name() << endl; // 4Bird

4.3 RTTI 示例

案例一(代码模块化)

.
├── Bird.cpp
├── Bird.h
├── Flyable.h
├── Plane.cpp
├── Plane.h
├── main.cpp
└── makefile

main.cpp: 核心代码

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
#include <iostream>
#include "Bird.h"
#include "Plane.h"
using namespace std;

// RTTI
void doSomething(Flyable *obj)
{

cout << typeid(*obj).name() << endl;
obj->takeOff();
// 如果对象是 Bird 类
if(typeid(*obj) == typeid(Bird))
{
// 将对象转换为 Bird 类
Bird *bird = dynamic_cast<Bird *>(obj);
bird->foraging();
}
// 如果对象是 Plane 类
if(typeid(*obj) == typeid(Plane))
{ // 将类型转换为 Plane 类
Plane *plane = dynamic_cast<Plane *>(obj);
plane->carry();
}
obj->land();
}

int main(void)
{

Bird b;
doSomething(&b);
return 0;
}
1
2
3
4
4Bird
Bird -- takeOff
Bird -- foraging
Bird -- land

案例二(all in one)

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
#include <iostream>
#include <stdlib.h>
#include <string>
#include <typeinfo> // 我用 g++ 编译发现并不需要手动 include 这个头文件
using namespace std;

/**
* 定义移动类:Movable
* 纯虚函数:move
*/

class Movable
{
public:
virtual void move() = 0;
};

/**
* 定义公交车类:Bus
* 公有继承移动类
* 特有方法carry
*/

class Bus : public Movable
{
public:
virtual void move()
{

cout << "Bus -- move" << endl;
}

void carry()
{

cout << "Bus -- carry" << endl;
}
};

/**
* 定义坦克类:Tank
* 公有继承移动类
* 特有方法fire
*/

class Tank : public Movable
{
public:
virtual void move()
{

cout << "Tank -- move" << endl;
}

void fire()
{

cout << "Tank -- fire" << endl;
}
};

/**
* 定义函数doSomething含参数
* 使用dynamic_cast转换类型
*/

void doSomething(Movable *obj)
{

obj->move();

if(typeid(*obj) == typeid(Bus))
{
Bus *bus = dynamic_cast<Bus *>(obj);
bus->carry();
}

if(typeid(*obj) == typeid(Tank))
{
Tank *tank = dynamic_cast<Tank *>(obj);
tank->fire();
}
}

int main(void)
{

Bus b;
Tank t;
doSomething(&b);
doSomething(&t);
return 0;
}
1
2
3
4
Bus -- move
Bus -- carry
Tank -- move
Tank -- fire

5 异常处理

说明: 对有可能发生异常的地方做出预见性的安排。

  • try-catch:尝试捕获
  • throw:抛出异常

基本思想: 主逻辑和异常处理分离。

5.1 try-catch 和 throw

  • 在 C++ 中,异常处理通常使用 try…catch…语法结构。
  • 一个 try 语句可以对应一个或多个 catch 语句,但不能没有 catch 语句。
  • C++ 中使用 throw 抛出异常,通过 catch 捕获异常

try 和 catch 是一对多的关系

1
2
3
4
5
6
7
try
{...}
catch(int) // 匹配 throw 的值为 double 的情况
{...}
catch(double) // 匹配 throw 的值为 double 的情况
{...}
catch(...) // 匹配所有类型(放在最后,作为兜底策略)

可以使用 throw 抛出一段字符串,并通过 try-catch 捕获并关打印出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
char getChar(const string& aStr, const int aIndex)
{
if(aIndex > aStr.size())
{
throw string("invalid index!);
}
return aStr[aIndex];
}

int main(void)
{
string str("hello world");
char ch;
try{
ch = getChar(str, 100);
cout << ch << endl;
}
catch(string& aval)
{
cout << aval << endl;
}
return 0;
}

5.2 常见的异常

  • 数组下标越界
  • 除数为0
  • 内存不足(现代计算机内存空间较大,较少遇到这种问题)

5.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
void fun1()
{

throw new SizeErr();
}

void new MemoryErr()
{

throw new MemoryErr();
}

int main(void)
{

try
{
fun1();
fun2();
}
// 可以捕获所有继承了 Exception 类的异常类型
catch(Exception &e)
{
e.xxx();
}

return 0;
}

5.4 异常综合 DEMO

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
#include <iostream>
#include <string>
#include <stdlib.h>
using namespace std;

/**
* 定义函数division
* 参数整型dividend、整型divisor
*/

int division(int dividend, int divisor)
{

if(0 == divisor)
{
// 抛出异常,字符串“除数不能为0”
throw string("除数不能为0");
}
else
{
return dividend / divisor;
}
}

int main(void)
{

int d1 = 0;
int d2 = 0;
int r = 0;
cin >> d1;
cin >> d2;
// 使用try...catch...捕获异常
try
{
division(d1, d2);
}
catch(string &str)
{
cout << str << endl;
}

return 0;
}