探究动多态的发生时机


有了虚函数和虚函数表为动多态提供支持,从而可以实现C++语言的动多态。那么,问题又来了。

动多态的发生时机是什么?

或者说,动多态发生有哪些条件与限制呢?

下面让我们一起来探究动多态的秘密,揭示动多态的发生时机。



详细步骤:

1、虚函数与普通函数的调用

2、利用汇编代码分析动多态

3、初步探究动多态调用方式

4、深入探究动多态发生时机

5、总结


1、虚函数与普通函数的调用

我们已经知道,在调用虚函数时会通过虚表中保存的虚函数入口地址来调用。那么试想一下,如果类中既含有虚函数又含有普通函数,他们调用的方式又有何不同呢?

现有以下代码,基类中既有虚函数又有普通的类成员函数。

  1. #include <iostream>
  2. class Base //定义基类
  3. {
  4. public:
  5. Base(int a) :ma(a) {}
  6. virtual void Show()
  7. {
  8. std::cout << "Base: ma = " << ma << std::endl;
  9. }
  10. void Print()
  11. {
  12. std::cout << "Base: This is print " << std::endl;
  13. }
  14. protected:
  15. int ma;
  16. };
  17. class Deriver : public Base //派生类
  18. {
  19. public:
  20. Deriver(int b) :mb(b), Base(b) {}
  21. void Show()
  22. {
  23. std::cout << "Deriver: mb = " << mb << std::endl;
  24. }
  25. protected:
  26. int mb;
  27. };
  28. int main()
  29. {
  30. Base* pb = new Deriver(10);
  31. pb->Show();
  32. pb->Print();
  33. return 0;
  34. }

输出结果:

2、利用汇编代码分析动多态

输出结果毫无疑问是正确的,要想理解程序在运行时究竟做了什么,我们需要在汇编层次上进行探究

  1. /* 以下代码段为:
  2. pb->Show();
  3. pb->Print();
  4. return 0;
  5. */
  6. pb->Show();
  7. 00766512 mov eax,dword ptr [pb]
  8. 00766515 mov edx,dword ptr [eax]
  9. pb->Show();
  10. 00766517 mov esi,esp
  11. 00766519 mov ecx,dword ptr [pb]
  12. 0076651C mov eax,dword ptr [edx]
  13. 0076651E call eax // ⑴
  14. 00766520 cmp esi,esp
  15. 00766522 call __RTC_CheckEsp (07612DFh)
  16. pb->Print();
  17. 00766527 mov ecx,dword ptr [pb]
  18. 0076652A call Base::Print (07614BFh) // ⑵
  19. return 0;
  20. 0076652F xor eax,eax

先不看其他汇编代码具体有什么含义,在上述 ⑴ 、⑵ 标出的位置上,都执行了 call 指令(call指令是计算机转移到调用的子程序)。

  • 在 ⑴ 处,call eax ,eax寄存器是在运行阶段暂存了某个变量的值,结合 call 指令我们可以得知,eax 中应该存放的就是 Deriver::Show() 的入口地址。通过在运行阶段确定函数的入口地址,进行动态的绑定
  • 在 ⑵处,call Base::Print (07614BFh) 直接 call 了 Base::Print() 的入口地址,说明在编译阶段已经确定了函数的调用,直接写入到指令中,进行了一个静态的绑定。

3、初步探究动多态调用方式

上述中通过简单判断 call 指令从而判断函数是否发生了动多态,下面为了更加深入的探究动多态发生原理,我们稍加修改一下源码测试:

3.1 指针方式调用
  1. /* 修改main 函数中内容如下: */
  2. int main()
  3. {
  4. Base b(10);
  5. Deriver d(20);
  6. Base* pb1 = &b; //基类指针 指向 基类
  7. Base* pb2 = &d; //基类指针 指向 派生类
  8. Deriver* pd = &d; //派生类指针 指向 派生类
  9. pb1->Show();
  10. pb2->Show();
  11. pd->Show();
  12. b.Show();
  13. d.Show();
  14. return 0;
  15. }

汇编分析:


  1. pb1->Show();
  2. 004365B4 call eax /* 动态绑定 */
  3. pb2->Show();
  4. 004365C9 call eax /* 动态绑定 */
  5. pd->Show();
  6. 004365DE call eax /* 动态绑定 */
  7. b.Show();
  8. 004365EA call Base::Print (043144Ch)
  9. d.Show();
  10. 004365F2 call Base::Show (04314B5h)

在结果中我们发现:动多态发生在指针调用虚函数时

3.2 引用方式调用

我们说在C++中,引用的底层实现是依靠指针来做支持的,理论上引用与指针访问是没有多大区别的。那么我们再来测试一下以引用的方式来访问,探究动多态的发生时机。修改代码如下:

  1. /* 修改main 函数中内容如下: */
  2. int main()
  3. {
  4. Base b(10);
  5. Deriver d(20);
  6. Base& rb1 = b;
  7. Base& rb2 = d;
  8. Deriver& rd = d;
  9. rb1.Show();
  10. rb2.Show();
  11. rd.Show();
  12. return 0;
  13. }

汇编分析:


  1. rb1.Show();
  2. 006965B4 call eax /* 动态绑定 */
  3. rb2.Show();
  4. 006965C9 call eax /* 动态绑定 */
  5. rd.Show();
  6. 006965DE call eax /* 动态绑定 */

通过上述实验,我们初步得出结论:动多态发生在指针调用引用调用的虚函数上

4、深入探究动多态发生时机

思考:那么。是否所有的动多态都可以通过指针或引用的方式调用实现呢,又或者通过引用调用或指针调用的方式就一定会发生动多态吗?

对于第一问,答案是显而易见的,当然是。调用函数无非就是拿到函数的入口地址,而能发生动多态的只能是(成为虚函数的)类成员函数,无论是通过 this->Show().*运算符、->* 运算符(类成员函数指针) 访问,实质上都是普通的类成员方法访问,而要实现动多态就要具备有以基类指针形式存在,而又可以访问派生类的函数的的特点。因此,动多态只能通过指针或引用的方式实现。

ps:理论上通过类成员函数指针的方式模拟实现C++的动多态,这里引用CSDN的一篇博客函数指针实现多态,有兴趣的可以看看。

4.1 在构造和析构中是否可以发生动多态

对于第二问,我们需要进行以下实验才可得出结论。

在C++或者说在C/C++语言中,一个函数可以调用另一个函数,甚至有自身调用自身的递归调用存在。在C++中,类的构造函数和析构函数也支持这一特点,那么我们猜想在构造或者析构中调用函数能否发生动多态。

探究构造函数:
  1. /* 在构造函数中调用 Show() */
  2. #include <iostream>
  3. class Base //定义基类
  4. {
  5. public:
  6. Base(int a) :ma(a)
  7. {
  8. this->Show(); /* 在基类的构造函数中调用 Show()*/
  9. }
  10. virtual void Show()
  11. {
  12. std::cout << "Base: ma = " << ma << std::endl;
  13. }
  14. void Print()
  15. {
  16. std::cout << "Base: This is print " << std::endl;
  17. }
  18. protected:
  19. int ma;
  20. };
  21. class Deriver : public Base //派生类
  22. {
  23. public:
  24. Deriver(int b) :mb(b), Base(b) {}
  25. void Show()
  26. {
  27. std::cout << "Deriver: mb = " << mb << std::endl;
  28. }
  29. protected:
  30. int mb;
  31. };
  32. int main()
  33. {
  34. Base b(10);
  35. Deriver d(20);
  36. return 0;
  37. }

汇编分析:

  1. Base(int a) :ma(a)
  2. 00021F46 mov eax,dword ptr [this]
  3. 00021F49 mov ecx,dword ptr [a]
  4. 00021F4C mov dword ptr [eax+4],ecx
  5. this->Show();
  6. 00021F4F mov ecx,dword ptr [this]
  7. 00021F52 call Base::Show (0213E3h)
  8. }

我们可以看到 call Base::Show (0213E3h) 在汇编代码中,发生的是静态的绑定,编译时直接把代码写死在指令段中。

探究析构函数:
  1. /* 添加虚析构函数, 注:在探究析构函数时应取消构造函数中的Show()调用 */
  2. virtual ~Base()
  3. {
  4. this->Show();
  5. }
  6. /* 汇编分析 */
  7. this->Show();
  8. 00812295 mov ecx,dword ptr [this]
  9. 00812298 call Base::Show (08113E3h)

可以看到,在析构函数中也无法实现函数的动多态调用。

4.2 在普通类成员函数中实现动多态

在普通类成员函数也可以调用函数,下面测试在类成员方法中是否可以实现动多态

  1. /* 在基类的 Print() 函数中调用Show()
  2. 在main 函数中添加 b.Print() */
  3. void Print()
  4. {
  5. this->Show();
  6. std::cout << "Base: This is print " << std::endl;
  7. }
  8. /* 汇编分析 */
  9. this->Show();
  10. 007D2689 call eax

可以看到在普通的类成员方法中是可以实现动多态的。

5、总结

分析:

构造函数内不能发生动多态:构造函数开始工作时对象正在生成,对象不完整

析构函数内不能发生动多态:析构函数开始工作时对象正在销毁,对象不完整

总结: 动多态发生条件

  • 1、指针或引用调用虚函数
  • 2、对象完整

满足以上条件可以发生动多态。

动多态发生时机为: 在基类指针访问派生类对象中的虚函数时,并且该访问满足动多态的发生条件,即可发生动多态。


附:

虚函数产生条件:https://blog.csdn.net/weixin_43919932/article/details/104388194

C++ | 动多态的发生时机的更多相关文章

  1. C++ | 虚函数产生条件

    虚函数产生的条件 能否成为虚函数主要有以下两种判断依据,如果以下两种条件均满足,则具有成为虚函数的条件. 1.虚函数机制为动多态提供支持,而虚函数表中存放着虚函数的地址.因此虚函数必须是可以取地址的函 ...

  2. Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)

    1 非抢占式和可抢占式内核 为了简化问题,我使用嵌入式实时系统uC/OS作为例子 首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 多任务系统中, 内核负责管理各个任务, 或者说 ...

  3. 【JVM】类加载时机与过程

    虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.下面来总结梳理类加载的五个阶段. 类加载发生在 ...

  4. SWFUpload

    引用:http://www.cnblogs.com/2050/archive/2012/08/29/2662932.html SWFUpload是一个flash和js相结合而成的文件上传插件,其功能非 ...

  5. Unity3D重要知识点

    数据结构和算法很重要!图形学也很重要!大的游戏公司很看重个人基础,综合能力小公司看你实际工作能力,看你的Demo. 1.什么是渲染管道? 是指在显示器上为了显示出图像而经过的一系列必要操作. 渲染管道 ...

  6. Unity3D 面试题汇总

    最先执行的方法是: 1.(激活时的初始化代码)Awake,2.Start.3.Update[FixUpdate.LateUpdate].4.(渲染模块)OnGUI.5.再向后,就是卸载模块(TearD ...

  7. WPF拖放功能实现zz

    写在前面:本文为即兴而作,因此难免有疏漏和词不达意的地方.在这里,非常期望您提供评论,分享您的想法和建议. 这是一篇介绍如何在WPF中实现拖放功能的短文. 首先要读者清楚的一件事情是:拖放主要分为拖放 ...

  8. Android UI 绘制过程浅析(三)layout过程

    前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...

  9. kafka概念

    一.结构与概念解释 1.基础概念 topics: kafka通过topics维护各类信息. producer:发布消息到Kafka topic的进程. consumer:订阅kafka topic进程 ...

随机推荐

  1. 通过修改profile 来修改账号的过期时间

    转至:https://blog.csdn.net/xxzhaobb/article/details/80026028 查看账号的过期时间 SYS@test>select username,exp ...

  2. Tableau绘图一热图、日历图、人口金字塔、标靶图、凹凸图、帕累托图

    Tableau绘图一热图.日历图.人口金字塔.标靶图.凹凸图.帕累托图 本文首发于博客冰山一树Sankey,去博客浏览效果更好.直接右上角搜索该标题即可 一.热图 例子:示例超市 可以通过更改颜色来改 ...

  3. WPF-ListView单元格设置文字换行

    第2-6行 1 <ListView Name="HumidifyEventLog" Style="{StaticResource ListViewStyle}&qu ...

  4. S5700上三层Vlan间隔离的例子

    转自:https://forum.huawei.com/enterprise/zh/forum.php?mod=viewthread&tid=247591 公司最近的无线覆盖做好了,但让人无语 ...

  5. 安装MYSQL8.0提示api-ms-win-crt-runtime-l1-1-0.dll 丢失

    Windows Server 2012 api-ms-win-crt-runtime-l1-1-0.dll 丢失 2017-11-06 11:11:37 Martin_Yelvin 阅读数 17015 ...

  6. C++高并发场景下读多写少的优化方案

    概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...

  7. bash shell 快捷键

     Bash Shell 快捷键: Ctrl + a - 跳到行首      Ctrl + e - 跳到行尾     Ctrl + k - 从光标处删除到行尾     Ctrl + l - 清屏,类似  ...

  8. 4月2日 python学习总结

    昨天内容回顾: 1.迭代器 可迭代对象: 只要内置有__iter__方法的都是可迭代的对象 既有__iter__,又有__next__方法 调用__iter__方法==>得到内置的迭代器对象 调 ...

  9. pip安装使用国内源的两种方法

    pip安装后使用pip安装第三方库默认是国外源,一般安装慢连接不稳定,等得花儿都谢了,结果还告诉你安装失败..../(ㄒoㄒ)/~~ 这时我们就要想想其它办法啦,毕竟不能强求 国外不行,就只有国内了赛 ...

  10. ssh端口转发学习笔记

    ssh端口转发学习笔记 ssh命令参数介绍 -C 压缩数据传输 -f 将 ssh 转到后台运行,即认证之后,ssh 自动以后台运行.不在输出信息 -n 将 stdio 重定向到 /dev/null,与 ...