【C++】-- 多态(二)

简介: 【C++】-- 多态

三、多态原理

1.虚函数表

了解多态原理前需要了解虚函数表

【C++】-- 类和对象一文中,讲过类的大小如何计算,只包含成员变量的大小,不会包含成员函数的大小,那么下面的代码应该打印4

1. #include<iostream>
2. using namespace std;
3. 
4. class Animal
5. {
6. public:
7.  virtual void Speak()//父类虚函数
8.  {
9.    cout << "speak" << endl;
10.   }
11. public:
12.   int legs;
13. };
14. 
15. int main()
16. {
17.   Animal a;
18.   cout << "sizeof(a) = " << sizeof(a) << endl;//Animal类对象的大小
19.   return 0;
20. }

但是结果却打印8:

对象a只有一个成员变量legs,占4个字节。通过监视看到,对象a里面包含两个成员 ,那么另外4个字节一定是_vfptr占用的,且_vfptr里面存放的是一个地址,那么_vfptr一定是个指针:

_vfptr叫做虚函数表指针,其中v是virtual的缩写,f是function的缩写。

虚函数表也简称虚表。

由于虚函数的地址要被放到虚函数表中,因此一个含有虚函数的类中都至少有一个虚函数表指针,这个虚函数表指针指向一个虚函数。虚函数表指针用来实现多态。

那么子类的虚表中都存放了什么呢?对于如下代码

1. #include<iostream>
2. using namespace std;
3. 
4. class Animal
5. {
6. public:
7.  virtual void Speak()//父类虚函数
8.  {
9.    cout << "speak" << endl;
10.   }
11. 
12.   virtual void run()//父类虚函数
13.   {
14.     cout << "run" << endl;
15.   }
16. 
17.   void jump()//父类普通函数
18.   {
19.     cout << "jump" << endl;
20.   }
21. public:
22.   int legs;
23. };
24. 
25. class Bird :public Animal
26. {
27. public:
28.   virtual void Speak()//子类重写父类虚函数
29.   {
30.     cout << "chirp" << endl;
31.   }
32. public:
33.   string color;
34. };
35. 
36. int main()
37. {
38.   Animal a;
39.   Bird b;
40. 
41.   return 0;
42. }

监视:

数组也叫做表

从监视可以发现:

1.子类对象b中也有一个虚表指针,b对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。

2.父类a对象和子类对象b虚表是不一样的,Speak完成了重写,所以b的虚表中存的是重写的Bird::Speak,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. Run继承下来后是虚函数,所以放进了虚表,Jump也继承下来了,但是不是虚函数,所以不会放进虚表。

4.虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

5.总结一下派生类的虚表生成:

       a.先将基类中的虚表内容拷贝一份到派生类虚表中

       b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

       c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

6.虚函数存在哪的?虚表存在哪的?

       虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。虚表在vs下存在代码段里。

2.原理

(1)构成多态

对于如下代码,子类重写了父类虚函数,且通过父类指针调用虚函数,这就满足了多态的两个条件

1. #include<iostream>
2. using namespace std;
3. 
4. class Animal
5. {
6. public:
7.  virtual void Speak()//父类虚函数
8.  {
9.    cout << "speak" << endl;
10.   }
11. };
12. 
13. class Bird :public Animal
14. {
15. public:
16.   virtual void Speak()//子类重写父类虚函数
17.   {
18.     cout << "chirp" << endl;
19.   }
20. };
21. 
22. void func(Animal* pa)
23. {
24.   pa->Speak();//通过父类指针调用虚函数
25. }
26. 
27. int main()
28. {
29.   Animal a;
30.   func(&a);
31. 
32.   Bird b;
33.   func(&b);
34. 
35.   return 0;
36. }

打印结果:

为什么引用是父类就调父类的Speak,是子类就调子类的Speak呢?

对象a和对象b里面都没有其他成员,只有虚表指针,都是4字节。子类完成父类虚函数重写以后,子类的虚表指针指向的是重写了的子类虚函数:

指针或引用调用虚函数是怎么调的呢?

指针或引用调用虚函数时,不是编译时确定,而是运行时才到指向的对象的虚表中找对应的虚函数调用,当指针或引用指向父类对象时,调用的就是父类的虚表中的虚函数,当指针或引用指向子类对象时,调用的就是子类虚表中的虚函数

(2)不构成多态

① 如果子类没有重写父类虚函数:

1. #include<iostream>
2. using namespace std;
3. 
4. class Animal
5. {
6. public:
7.  virtual void Speak()//父类虚函数
8.  {
9.    cout << "speak" << endl;
10.   }
11. 
12. };
13. 
14. class Bird :public Animal
15. {
16. public:
17. };
18. 
19. void func(Animal* pa)
20. {
21.   pa->Speak();
22. }
23. 
24. int main()
25. {
26.   Animal a;
27.   func(&a);
28. 
29.   Bird b;
30.   func(&b);
31. 
32.   return 0;
33. }

这时就破坏了多态的条件,那么子类也有虚表,但是子类虚表里的指针存的是Animal的虚函数,而不是Bird的虚函数。不满足多态条件时(子类没有重写父类虚函数/通过父类指针或引用调用虚函数),就不会到虚表里面去找,决定调哪个函数是在编译时确定这个函数的形参是哪个类型,而跟对象没有关系。

总结:

(1)构成多态,指向谁就调用谁的虚函数,跟对象有关

(2)当子类没有重写父类虚函数时,不构成多态,调用函数的入参类型是什么,调用的就是哪个的虚函数,跟对象无关,跟入参类型有关

② 不是通过父类指针或引用,而是通过父类对象调用虚函数

1. #include<iostream>
2. using namespace std;
3. 
4. class Animal
5. {
6. public:
7.  virtual void Speak()//父类虚函数
8.  {
9.    cout << "speak" << endl;
10.   }
11. 
12. public:
13.   int legs = 4;
14. };
15. 
16. class Bird :public Animal
17. {
18. public:
19.   virtual void Speak()//子类重写父类虚函数
20.   {
21.     cout << "chirp" << endl;
22.   }
23. public:
24.   string color;
25. };
26. 
27. void func(Animal pa)//入参类型为父类对象
28. {
29.   pa.Speak();
30. }
31. 
32. int main()
33. {
34.   Animal a;
35.   func(a);
36. 
37.   Bird b;
38.   b.legs = 2;
39.   func(b);
40. 
41.   return 0;
42. }

对象是无法实现出多态的,因为如果入参是子类对象,那么指针和引用会把父类那部分切出来,切出来后不是赋值,而是让指针指向子类里面父类的那部分,这个指针无论指向的是父类还是子类,看到的都是父类对象,给父类引用的就是父类对象,给子类引用的是切片出来的父类对象。

而构成多态时,引用和指针本身并不知道自己指向或引用的是父类对象还是子类对象,指向父类对象,那就指向或引用整个父类对象,指向子类对象,那就那看到的就是子类对象中父类那一部分,对于

a.Speak();

编译完成的指令是一样的,虽然传入的实参不同,但是看到的都是父类部分或者子类切片出来的父类部分,都是一样的动作,到对应的地方去找。

如果是对象的时候为什么不行,如果是对象涉及到切片问题,这个时候的切片不是让我窒息那个你,你给我的是一个父类对析那个,那就把这个父类对象给你,你给我的是个子类对象,就把子类对象中的父类部分切片后给你,调用拷贝构造函数把父类部分切片出来,把父类成员给你;父类对象不会把虚表指针给过去,两者的虚表指针是一样的

父类对象调用完func后,pa的虚表指针存放的是父类的虚函数地址:

子类对象调用完func后,pa的虚表指针存放的还是父类的虚函数地址,但是成员变量被修改了:

这是因为多个同类型对象,只有一份虚表,因此虚表当中的内容是一样的,它们的虚表指针都指向这个虚表。

当是子类切片的时候,会把子类切出来的成员变量给func的形参(即父类对象),但不会把_vfptr给过去,因为只有一份虚表,假如切片后把虚表指针也给过去了,会出现混乱,它的虚表指针到底是父类的还是子类的,如果是直接定义出来的,那就是父类的,如果经过子类赋值,那就是子类的,但是父类对象的虚表里面怎么会有子类的虚函数呢?这显然不合理,因此不会把虚表给func的形参(父类对象)。

指针和引用是指向的,指向父类就是父类对象,指向子类就是子类当中切片出来的父类部分,让指针和引用去指向。而对象要拷贝构造,只是把值给过去。

总结:当通过父类对象调用虚函数,切片只会拷贝成员变量,不会拷贝虚表指针

(3)汇编层面看多态

不构成多态时,编译时直接调用函数的地址

构成多态时,运行时到指向的对象的虚表中找到要调用的虚函数

四、单继承和多继承关系的虚函数表

1.单继承的虚函数表

(1)虚表初始化的时机

对象中虚表指针是在什么阶段初始化的?虚表在哪个阶段生成?

对于如下代码:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Animal
6. {
7. public:
8.  virtual void Speak()//父类虚函数
9.  {
10.     cout << "Animal::speak" << endl;
11.   }
12. 
13.   virtual void Run()//父类虚函数
14.   {
15.     cout << "Animal::run" << endl;
16.   }
17. 
18.   void Jump()//父类普通函数
19.   {
20.     cout << "Animal::jump" << endl;
21.   }
22. public:
23.   int legs = 4;
24. };
25. 
26. class Bird :public Animal
27. {
28. public:
29.   virtual void Speak()//子类重写父类虚函数
30.   {
31.     cout << "Bird::chirp" << endl;
32.   }
33. public:
34.   string color = "Yellow";
35. };
36. 
37. int main()
38. {
39.   Animal a;
40.   Bird b;
41. 
42.   return 0;
43. }

通过监视F11逐语句查看执行过程发现,定义对象a时,执行步骤如下:

(1)开始执行Animal的构造函数

(2)初始化Animal的成员

(3)将Animal构造函数执行完毕

发现执行完以上3步之后,虚表指针已经初始化了:

因此,虚表指针是在构造函数初始化列表阶段初始化的,虚表在编译时就已经生成了。

一个类中所有的虚函数地址,都会放到虚表中。虚表里面存放的是虚函数地址,虚函数和普通函数一样, 编译完成后,都放在代码段。

(2)子类虚表的生成过程

子类的虚表是如何生成的呢?

父类的虚表中存的是Aniaml的Speak( )和Run( )的地址。生成子类虚表时,会单独开辟一块空间,拷贝一份父类虚表过程中,会将对应虚函数位置覆盖成子类重写了父类的虚函数,如果子类没有重写,那么父类的虚函数就不会被覆盖,保留。所以子类虚表的生成过程是一个拷贝+覆盖的过程。

监视如上代码:

(1)子类重写了父类的Speak( )虚函数,所以子类会覆盖父类Speak( )位置;

(2)子类没有重写父类的Run( )虚函数,子类不会覆盖父类Run( )位置;

(3)父类的Jump( )不是虚函数, 不会出现在虚表中:

虚函数的重写是语法层的概念,覆盖是虚表实现层的概念。

在内存窗口输入虚表地址,发现里面存的是虚函数的地址,虚表作为数组,是如何知道数组结束的呢?VS在虚表结束位置放空指针,表示虚表结束了:

假如子类还有虚函数:

1.  virtual void Fly()//飞
2.  {
3.    cout << "virtual Bird::fly" << endl;
4.  }
5. 
6.  virtual void Sing()//唱歌
7.  {
8.    cout << "virtual Bird::sing" << endl;
9.  }

这两个虚函数既不是继承父类虚函数,也没有重写父类虚函数,通过监视看不到子类的这两个虚函数,但是通过内存可以看到:

也可以打印一下虚表中调用的函数:

1. typedef void(*VFunc)();//为虚表指针定义简洁的名称
2. 
3. void PrintVFT(VFunc* ptr)//传参虚函数指针数组
4. {
5.  printf("虚表地址:%p\n",ptr);
6.  for (int i = 0; ptr[i] != nullptr; i++)
7.  {
8.    printf("VFT[%d] : %p->", i,ptr[i]);
9.    ptr[i]();
10.   }
11.   printf("\n");
12. }
13. 
14. int main()
15. {
16.   Animal a;
17.   PrintVFT((VFunc*)(*((int*)&a)));
18. 
19.   Bird b;
20.   PrintVFT((VFunc*)(*((int*)&b)));
21. 
22.   //(int*)&a -- 将a的虚表指针强转为int型
23.   //*((int*)&a)) -- 解引用得到虚表指针指向的第一个虚函数地址
24.   //(VFunc*)(*((int*)&a)) -- 将第一个虚函数地址强转为(VFunc*)
25. 
26.   return 0;
27. }

2.多继承的虚函数表

以上是单继承,对于多继承,如何打印虚表函数:

1. #define  _CRT_SECURE_NO_WARNINGS  1
2. #include<iostream>
3. using namespace std;
4. 
5. class Animal
6. {
7. public:
8.  virtual void Color()//颜色
9.  {
10.     cout << "virtual Animal::color" << endl;
11.   }
12. 
13.   virtual void Name()//名称
14.   {
15.     cout << "virtual Animal::name" << endl;
16.   }
17. };
18. 
19. class Plant
20. {
21. public:
22.   virtual void Color()//颜色
23.   {
24.     cout << "virtual Plant::color" << endl;
25.   }
26. 
27.   virtual void Name()//名称
28.   {
29.     cout << "virtual Plant::name" << endl;
30.   }
31. };
32. 
33. class Coral :public Animal, public Plant
34. {
35. public:
36.   virtual void Color()//子类重写Animal类虚函数
37.   {
38.     cout << "virtual Coral::color" << endl;
39.   }
40. 
41.   virtual void Shape()//子类重写Plant类虚函数
42.   {
43.     cout << "virtual Coral::shape" << endl;
44.   }
45. };
46. 
47. typedef void(*VFunc)();//为虚表指针定义简洁的名称
48. 
49. void PrintVFT(VFunc* ptr)//传参虚函数指针数组
50. {
51.   printf("虚表地址:%p\n", ptr);
52.   for (int i = 0; ptr[i] != nullptr; i++)
53.   {
54.     printf("VFT[%d] : %p->", i, ptr[i]);
55.     ptr[i]();
56.   }
57.   printf("\n");
58. }
59. 
60. int main()
61. {
62.   //c继承了两个类,有两个虚表
63.   //c的两张虚表,先继承了Animal,Animal在前面,正好Animal的头4个字节是虚表指针,Plant挨着Animal,Animal完了就是Plant
64.   Coral c;
65.   PrintVFT((VFunc*)(*((int*)&c)));
66.   PrintVFT((VFunc*)(*(int*)((char*)&c + sizeof(Animal))));
67. 
68.   //(char*)&c -- 取c的地址,强转成char*
69.   //(char*)&c + sizeof(Animal) -- 取c的地址,强转成char*,再跨越一个Animal类的大小
70. 
71.   return 0;
72. }

打印发现:

(1)两张虚表都重写了Color( )函数

(2)但两张虚表都没有重写Name( )函数,都直接继承了Name( )函数

(3)Shape( )虚函数只放在了第一张虚表中,第二张虚表没有放


相关文章
|
24天前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
25 1
|
1月前
|
C++
【C++】从零开始认识多态(一)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
38 4
|
1月前
|
C++
【C++】从零开始认识多态(二)
面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。 封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。 继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。
29 1
|
28天前
|
C++
c++多态
c++多态
25 0
|
1月前
|
编译器 C++
c++的学习之路:22、多态(1)
c++的学习之路:22、多态(1)
28 0
c++的学习之路:22、多态(1)
|
3天前
|
C++
C++一分钟之-继承与多态概念
【6月更文挑战第21天】**C++的继承与多态概述:** - 继承允许类从基类复用代码,增强代码结构和重用性。 - 多态通过虚函数实现,使不同类对象能以同一类型处理。 - 关键点包括访问权限、构造/析构、菱形问题、虚函数与动态绑定。 - 示例代码展示如何创建派生类和调用虚函数。 - 注意构造函数初始化、空指针检查和避免切片问题。 - 应用这些概念能提升程序设计和维护效率。
16 2
|
26天前
|
存储 编译器 C语言
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(下)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
33 1
|
26天前
|
存储 编译器 Linux
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题(中)
从C语言到C++_23(多态)抽象类+虚函数表VTBL+多态的面试题
34 1
|
6天前
|
C++
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性
C++ 是一种面向对象的编程语言,它支持对象、类、继承、多态等面向对象的特性
|
1月前
|
存储 设计模式 编译器
【C++】继承和多态常见的问题
【C++】继承和多态常见的问题