C++ 入门基础之九

多态的原理

多态的实现原理

  • 当类中声明了虚函数时,编译器会在类中生成一个虚函数表
  • 虚函数表是一个存储类成员函数指针的数据结构
  • 虚函数表是由编译器自动生成和维护的
  • 虚函数(virtual)会被编译器放入虚函数表中
  • 当存在虚函数时,每个对象中都有一个指向虚函数表的指针(C++ 编译器给父类对象、子类对象提前设置了 VPTR 虚函数表指针,因此 C++ 编译器不需要区分子类对象或者父类对象,只需要在 base 指针中,找 VPTR 指针即可)
  • VPTR 虚函数表指针一般作为类对象的第一个成员

多态的实现原理图解

  • a) 多态实现原理的图解 如图 所示
  • b) 通过 VPTR 虚函数表指针调用重写函数的过程是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数,而普通成员函数是在编译时就确定了调用的函数
  • c) 在效率上,虚函数的效率要低很多,因此出于效率考虑,没有必要将所有成员函数都声明为虚函数,即使 C++ 编译器允许这么做
  • d) 由于有了虚函数表,C++ 编译器不再需要知道是子类对象还是父类对象,这往往会给我们造成一种假象:C++ 编译器能识别子类对象或者父类对象

证明 VPTR 指针的存在

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 Parent1 {
public:
Parent1(int a) {
this->a = a;
}

// 不声明虚函数
void print() {
cout << "I'm parent1" << endl;
}

private:
int a;
};

class Parent2 {
public:
Parent2(int a) {
this->a = a;
}

// 声明虚函数
virtual void print() {
cout << "I'm parent2" << endl;
}

private:
int a;
};

int main() {
// 由于指针也是一种数据类型,由于在Parent2类中声明了虚函数,若Parent2类里存在VPTR指针,那么下面两个类的大小应该是不一样的
cout << "sizeof(Parent1): " << sizeof(Parent1) << endl;
cout << "sizeof(Parent2): " << sizeof(Parent2) << endl;
return 0;
}

程序运行的输出结果如下:

1
2
sizeof(Parent1): 4
sizeof(Parent2): 8

父类指针和子类指针的步长可能是不一样的

  • a) 指针也只一种数据类型,对 C++ 类对象的指针执行 ++-- 运算符仍然是合法的
  • b) "多态是用父类的指针指向子类的对象""父类指针步长的自加(++)" 是两个完全不同的概念
  • c) 当子类继承父类后,没有添加任何自己的成员变量和成员函数,那么此时父类指针和子类指针的步长才是一样的
  • d) 指针运算是按照指针所指的类型进行的,父类指针和子类指针的步长可能是不一样的,不要用父类指针自加(++)、自减(--)的方式来操作子类的对象数组
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
#include <iostream>
using namespace std;

class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
}

virtual void print()
{
cout << "I'm parent" << endl;
}

private:
int a;
};

class Child : public Parent
{
public:

Child(int b, int c) :Parent(0)
{
this->b = b;
this->c = c;
}

virtual void print()
{
cout << "I'm child" << endl;
}
private:
int b;
int c;
};

int main()
{
Parent* parent = NULL;
Child* child = NULL;

Child array[] = { Child(1, 2), Child(3,4), Child(5, 6) };
parent = array;
child = array;

// 指针自加运算后运行可能会出错,这里父类指针和子类指针的步长是不一样的,不要用父类指针自加(`++`)、自减(`--`)的方式来操作子类的对象数组
parent++;
child++;

parent++;
child++;

return 0;
}

在父类的构造函数中调用虚函数,不能实现多态

子类的 VPTR 指针是分步完成初始化的,当执行父类的构造函数时,子类 的 VPTR 指针指向父类的虚函数表,当父类的构造函数执行完毕后,才会把子类的 VPTR 指针指向子类的虚函数表。因此,在父类的构造函数中调用虚函数,不能实现多态

  • a) 分析图解 如图 所示
  • b) 对象在创建的时,由编译器对 VPTR 指针进行初始化
  • c) 只有当对象的构造全部完成后,VPTR 指针的指向才能最终确定
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 Parent {

public:
Parent(int a) {
this->a = a;
// 在父类的构造函数中调用虚函数
print();
}

virtual void print() {
cout << "I'm parent, a = " << a << endl;
}

private:
int a;
};

class Child : public Parent {
public:

Child(int a, int c) : Parent(a) {
this->c = c;
}

virtual void print() {
cout << "I'm child, c = " << c << endl;
}

private:
int c;
};

int main() {
Child child(5, 8);
return 0;
}

程序运行的输出结果如下:

1
I'm parent, a = 5

纯虚函数和抽象类

纯虚函数和抽象类的基本概念

基本概念:

  • a) 纯虚函数是一个在基类中说明的虚函数,且在基类中没有被定义,要求任何派生类都定义自己的版本
  • b) 纯虚函数为各派生类提供一个公共界面,可以实现接口的封装和设计、软件的模块功能划分
  • c) 纯虚函数的声明形式: virtual 类型 函数名 ( 参数表 ) = 0;
  • d) 一个具有纯虚函数的基类称为抽象类

使用限制:

  • a) 可以声明抽象类的指针和引用
  • b) 抽象类不能创建对象(实例化)
  • c) 抽象类不能作为函数的参数类型和返回值类型

纯虚函数和抽象类的应用案例

定义一个图形抽象类 Figure,并声明了负责计算图形面积的纯虚函数 getArea(),然后再定义 Circle、Triangle、Squre 派生类,并各自实现了纯虚函数 getArea() 来计算不同图形的面积。

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

using namespace std;

// 抽象类
class Figure {

public:
// 声明纯虚函数,计算面积
virtual double getArea() = 0;
};

class Circle : public Figure {

public:

Circle(double r) {
this->r = r;
}

// 计算圆的面积
virtual double getArea() {
double area = 3.14 * r * r;
cout << "圆的面积: " << area << endl;
return area;
}

private:
double r;
};

class Triangle : public Figure {

public:
Triangle(double a, double b) {
this->a = a;
this->b = b;
}

// 计算三角形的面积
virtual double getArea() {
double area = a * b / 2;
cout << "三角形的面积: " << area << endl;
return area;
}

private:
double a;
double b;
};

class Square : public Figure {

public:
Square(double a, double b) {
this->a = a;
this->b = b;
}

// 计算四边形的面积
virtual double getArea() {
double area = a * b;
cout << "四边形的面积: " << area << endl;
return area;
}

private:
double a;
double b;
};

void printArea(Figure* base) {
base->getArea();
}

int main() {
// Figure f; // 错误写法,抽象类不能实例化

Triangle Triangle(20, 30);
Circle circle(6.8);
Square square(50, 60);

// 可以声明抽象类的指针
Figure* pBase = new Circle(5.3);
pBase->getArea();

// 可以声明抽象类的引用
Figure& base = square;
base.getArea();

printArea(&Triangle);

return 0;
}

程序运行的输出结果如下:

1
2
3
圆的面积: 88.2026
四边形的面积: 3000
三角形的面积: 300

纯虚函数和抽象类在多继承中的应用案例

C++ 中没有 Java 中的接口概念,但可以使用抽象类和纯虚函数模拟 Java 中的接口(代码如下)。值得一提的是,C++ 中的接口类只有函数原型定义,没有任何数据的定义,同时继承多个接口类不会带来二义性和复杂性等问题。C++ 面向抽象类编程(Java 面向接口编程)是项目开发中重要技能之一。

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

using namespace std;

// 定义接口类一
class Interface1 {

public:
virtual int add(int a, int b) = 0;
virtual void print() = 0;
};

// 定义接口类二
class Interface2 {

public:
virtual int mult(int a, int b) = 0;
virtual void print() = 0;
};

// 定义父类
class Parent {

public:
Parent() {
this->a = 8;
}

virtual ~Parent() {

}

virtual int getA() {
return a;
}

private:
int a;
};

// 定义子类,首先继承父类,然后继承多个接口类
class Child : public Parent, public Interface1, public Interface2 {

public:
int add(int a, int b) {
return a + b;
}

int mult(int a, int b) {
return a * b;
}

void print() {
cout << "Child::print() 函数被执行" << endl;
}

};

int main() {
Child child;
child.print();

Parent* parent = &child;
cout << "a = " << parent->getA() << endl;

Interface1* interface1 = &child;
int result1 = interface1->add(2, 5);
cout << "2 + 5 = " << result1 << endl;

Interface2* interface2 = &child;
int result2 = interface2->mult(3, 6);
cout << "3 * 6 = " << result2 << endl;

return 0;
}

程序运行的输出结果如下:

1
2
3
a = 8
2 + 5 = 7
3 * 6 = 18

纯虚函数和抽象类在多继承中的使用总结

C++ 中没有 Java 中的接口概念:

  • 绝大多数面向对象语言都不支持多继承
  • 绝大多数面向对象语言都支持接口的概念
  • C++ 中没有 Java 中的接口概念,但可以使用抽象类和纯虚函数模拟 Java 中的接口
  • C++ 中的接口类只有函数原型定义,没有任何数据的定义(代码如下)
    ★点击显示示例代码★
    1
    2
    3
    4
    5
    6
    7
    class Interface  
    {
    public:
    virtual void func1() = 0;
    virtual void func2(int i) = 0;
    virtual void func3(int i) = 0;
    };

工程上多继承的使用说明:

  • a) 多继承已经被实际开发经验所抛弃
  • b) 工程开发中真正意义上的多继承是几乎不被使用的
  • c) 多继承带来的代码复杂性远多于其带来的便利
  • d) 多继承对代码维护性上的影响是灾难性的
  • e) 在设计方法上,任何多继承都可以使用单继承代替
  • f) 在多继承中,使用虚继承不能完全解决二义性的问题

工程上继承多个接口类的使用说明:

  • a) 继承多个接口类不会带来二义性和复杂性等问题
  • b) 多继承可以通过精心设计的单继承和接口类来代替
  • c) 接口类只是一个功能说明,而不是功能实现,子类需要根据功能说明定义功能实现