C++ | 动多态的发生时机
探究动多态的发生时机
有了虚函数和虚函数表为动多态提供支持,从而可以实现C++语言的动多态。那么,问题又来了。
动多态的发生时机是什么?
或者说,动多态发生有哪些条件与限制呢?
下面让我们一起来探究动多态的秘密,揭示动多态的发生时机。
详细步骤:
1、虚函数与普通函数的调用
2、利用汇编代码分析动多态
3、初步探究动多态调用方式
4、深入探究动多态发生时机
5、总结
1、虚函数与普通函数的调用
我们已经知道,在调用虚函数时会通过虚表中保存的虚函数入口地址来调用。那么试想一下,如果类中既含有虚函数又含有普通函数,他们调用的方式又有何不同呢?
现有以下代码,基类中既有虚函数又有普通的类成员函数。
#include <iostream>
class Base //定义基类
{
public:
Base(int a) :ma(a) {}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
void Print()
{
std::cout << "Base: This is print " << std::endl;
}
protected:
int ma;
};
class Deriver : public Base //派生类
{
public:
Deriver(int b) :mb(b), Base(b) {}
void Show()
{
std::cout << "Deriver: mb = " << mb << std::endl;
}
protected:
int mb;
};
int main()
{
Base* pb = new Deriver(10);
pb->Show();
pb->Print();
return 0;
}
输出结果:

2、利用汇编代码分析动多态
输出结果毫无疑问是正确的,要想理解程序在运行时究竟做了什么,我们需要在汇编层次上进行探究
/* 以下代码段为:
pb->Show();
pb->Print();
return 0;
*/
pb->Show();
00766512 mov eax,dword ptr [pb]
00766515 mov edx,dword ptr [eax]
pb->Show();
00766517 mov esi,esp
00766519 mov ecx,dword ptr [pb]
0076651C mov eax,dword ptr [edx]
0076651E call eax // ⑴
00766520 cmp esi,esp
00766522 call __RTC_CheckEsp (07612DFh)
pb->Print();
00766527 mov ecx,dword ptr [pb]
0076652A call Base::Print (07614BFh) // ⑵
return 0;
0076652F xor eax,eax
先不看其他汇编代码具体有什么含义,在上述 ⑴ 、⑵ 标出的位置上,都执行了 call 指令(call指令是计算机转移到调用的子程序)。
- 在 ⑴ 处,
call eax,eax寄存器是在运行阶段暂存了某个变量的值,结合 call 指令我们可以得知,eax 中应该存放的就是 Deriver::Show() 的入口地址。通过在运行阶段确定函数的入口地址,进行动态的绑定 - 在 ⑵处,
call Base::Print (07614BFh)直接 call 了 Base::Print() 的入口地址,说明在编译阶段已经确定了函数的调用,直接写入到指令中,进行了一个静态的绑定。
3、初步探究动多态调用方式
上述中通过简单判断 call 指令从而判断函数是否发生了动多态,下面为了更加深入的探究动多态发生原理,我们稍加修改一下源码测试:
3.1 指针方式调用
/* 修改main 函数中内容如下: */
int main()
{
Base b(10);
Deriver d(20);
Base* pb1 = &b; //基类指针 指向 基类
Base* pb2 = &d; //基类指针 指向 派生类
Deriver* pd = &d; //派生类指针 指向 派生类
pb1->Show();
pb2->Show();
pd->Show();
b.Show();
d.Show();
return 0;
}
汇编分析:
pb1->Show();
004365B4 call eax /* 动态绑定 */
pb2->Show();
004365C9 call eax /* 动态绑定 */
pd->Show();
004365DE call eax /* 动态绑定 */
b.Show();
004365EA call Base::Print (043144Ch)
d.Show();
004365F2 call Base::Show (04314B5h)
在结果中我们发现:动多态发生在指针调用虚函数时
3.2 引用方式调用
我们说在C++中,引用的底层实现是依靠指针来做支持的,理论上引用与指针访问是没有多大区别的。那么我们再来测试一下以引用的方式来访问,探究动多态的发生时机。修改代码如下:
/* 修改main 函数中内容如下: */
int main()
{
Base b(10);
Deriver d(20);
Base& rb1 = b;
Base& rb2 = d;
Deriver& rd = d;
rb1.Show();
rb2.Show();
rd.Show();
return 0;
}
汇编分析:
rb1.Show();
006965B4 call eax /* 动态绑定 */
rb2.Show();
006965C9 call eax /* 动态绑定 */
rd.Show();
006965DE call eax /* 动态绑定 */
通过上述实验,我们初步得出结论:动多态发生在指针调用和引用调用的虚函数上
4、深入探究动多态发生时机
思考:那么。是否所有的动多态都可以通过指针或引用的方式调用实现呢,又或者通过引用调用或指针调用的方式就一定会发生动多态吗?
对于第一问,答案是显而易见的,当然是。调用函数无非就是拿到函数的入口地址,而能发生动多态的只能是(成为虚函数的)类成员函数,无论是通过 this->Show() 、.*运算符、->* 运算符(类成员函数指针) 访问,实质上都是普通的类成员方法访问,而要实现动多态就要具备有以基类指针形式存在,而又可以访问派生类的函数的的特点。因此,动多态只能通过指针或引用的方式实现。
ps:理论上通过类成员函数指针的方式模拟实现C++的动多态,这里引用CSDN的一篇博客函数指针实现多态,有兴趣的可以看看。
4.1 在构造和析构中是否可以发生动多态
对于第二问,我们需要进行以下实验才可得出结论。
在C++或者说在C/C++语言中,一个函数可以调用另一个函数,甚至有自身调用自身的递归调用存在。在C++中,类的构造函数和析构函数也支持这一特点,那么我们猜想在构造或者析构中调用函数能否发生动多态。
探究构造函数:
/* 在构造函数中调用 Show() */
#include <iostream>
class Base //定义基类
{
public:
Base(int a) :ma(a)
{
this->Show(); /* 在基类的构造函数中调用 Show()*/
}
virtual void Show()
{
std::cout << "Base: ma = " << ma << std::endl;
}
void Print()
{
std::cout << "Base: This is print " << std::endl;
}
protected:
int ma;
};
class Deriver : public Base //派生类
{
public:
Deriver(int b) :mb(b), Base(b) {}
void Show()
{
std::cout << "Deriver: mb = " << mb << std::endl;
}
protected:
int mb;
};
int main()
{
Base b(10);
Deriver d(20);
return 0;
}
汇编分析:
Base(int a) :ma(a)
00021F46 mov eax,dword ptr [this]
00021F49 mov ecx,dword ptr [a]
00021F4C mov dword ptr [eax+4],ecx
this->Show();
00021F4F mov ecx,dword ptr [this]
00021F52 call Base::Show (0213E3h)
}
我们可以看到 call Base::Show (0213E3h) 在汇编代码中,发生的是静态的绑定,编译时直接把代码写死在指令段中。
探究析构函数:
/* 添加虚析构函数, 注:在探究析构函数时应取消构造函数中的Show()调用 */
virtual ~Base()
{
this->Show();
}
/* 汇编分析 */
this->Show();
00812295 mov ecx,dword ptr [this]
00812298 call Base::Show (08113E3h)
可以看到,在析构函数中也无法实现函数的动多态调用。
4.2 在普通类成员函数中实现动多态
在普通类成员函数也可以调用函数,下面测试在类成员方法中是否可以实现动多态
/* 在基类的 Print() 函数中调用Show()
在main 函数中添加 b.Print() */
void Print()
{
this->Show();
std::cout << "Base: This is print " << std::endl;
}
/* 汇编分析 */
this->Show();
007D2689 call eax
可以看到在普通的类成员方法中是可以实现动多态的。
5、总结
分析:
构造函数内不能发生动多态:构造函数开始工作时对象正在生成,对象不完整
析构函数内不能发生动多态:析构函数开始工作时对象正在销毁,对象不完整
总结: 动多态发生条件
- 1、指针或引用调用虚函数
- 2、对象完整
满足以上条件可以发生动多态。
动多态发生时机为: 在基类指针访问派生类对象中的虚函数时,并且该访问满足动多态的发生条件,即可发生动多态。
附:
虚函数产生条件:https://blog.csdn.net/weixin_43919932/article/details/104388194
C++ | 动多态的发生时机的更多相关文章
- C++ | 虚函数产生条件
虚函数产生的条件 能否成为虚函数主要有以下两种判断依据,如果以下两种条件均满足,则具有成为虚函数的条件. 1.虚函数机制为动多态提供支持,而虚函数表中存放着虚函数的地址.因此虚函数必须是可以取地址的函 ...
- Linux用户抢占和内核抢占详解(概念, 实现和触发时机)--Linux进程的管理与调度(二十)
1 非抢占式和可抢占式内核 为了简化问题,我使用嵌入式实时系统uC/OS作为例子 首先要指出的是,uC/OS只有内核态,没有用户态,这和Linux不一样 多任务系统中, 内核负责管理各个任务, 或者说 ...
- 【JVM】类加载时机与过程
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制.下面来总结梳理类加载的五个阶段. 类加载发生在 ...
- SWFUpload
引用:http://www.cnblogs.com/2050/archive/2012/08/29/2662932.html SWFUpload是一个flash和js相结合而成的文件上传插件,其功能非 ...
- Unity3D重要知识点
数据结构和算法很重要!图形学也很重要!大的游戏公司很看重个人基础,综合能力小公司看你实际工作能力,看你的Demo. 1.什么是渲染管道? 是指在显示器上为了显示出图像而经过的一系列必要操作. 渲染管道 ...
- Unity3D 面试题汇总
最先执行的方法是: 1.(激活时的初始化代码)Awake,2.Start.3.Update[FixUpdate.LateUpdate].4.(渲染模块)OnGUI.5.再向后,就是卸载模块(TearD ...
- WPF拖放功能实现zz
写在前面:本文为即兴而作,因此难免有疏漏和词不达意的地方.在这里,非常期望您提供评论,分享您的想法和建议. 这是一篇介绍如何在WPF中实现拖放功能的短文. 首先要读者清楚的一件事情是:拖放主要分为拖放 ...
- Android UI 绘制过程浅析(三)layout过程
前言 上一篇blog中,了解到measure过程对View进行了测量,得到measuredWidth/measuredHeight.对于ViewGroup,则计算出全部children的宽高进行求和. ...
- kafka概念
一.结构与概念解释 1.基础概念 topics: kafka通过topics维护各类信息. producer:发布消息到Kafka topic的进程. consumer:订阅kafka topic进程 ...
随机推荐
- hdfs刷新节点失败,未显示Refresh nodes successful
使用命令:hdfs dfsadmin -refreshNodes 没有显示Refresh nodes successful,而是:refreshNodes: /opt/module/hadoop-2. ...
- AcWing 289. 环路运输
传送门 思路: 一个环路上的问题,考虑拆环为链然后复制一倍接在后面.那么对于Ai与Aj,不妨设j<i,如果i-j>N/2则两者距离在新的链上就是i-j,而如果i-j<=N/2那么两者 ...
- Visual Studio 文字样式背景设置
菜单栏 "工具"->选项->环境->常规
- deepin下安装ESP32的开发环境
deepin下安装ESP32的开发环境 本文章用于记录在deepin下安装ESP32的开发环境,前几日在CentOS7之下实现过,但CentOS7对于我这个Linux新手太不友好,于是重装deepin ...
- JavaScript面向对象—继承的实现
JavaScript面向对象-继承的实现 前言 面向对象的三大特性:封装.继承和多态.上一篇我们简单的了解了封装的过程,也就是把对象的属性和方法封装到一个函数中,这一篇讲一下JavaScript中继承 ...
- 二级py--day2
二级py day2-3 1.进程至少活动情况分为:运行状态.就绪状态.等待状态(阻塞状态).创建状态.终止状态 2.进程的特性包括: 并发性和动态性 3.计算机地址位数决定了内存的最大容量,决定了虚拟 ...
- CSS样式下划线
样式之文字下划线 第一 text-decoration: underline; 缺点是不好修改颜色 不要修改下划线的位置,优点就是一行代码解决 可以使用line-height: 20px; 也能调整位 ...
- LGP5341题解
SAM一道很裸的题... 题意很明确,不再阐述. 做法很简单:找到所有出现次数为 \(k\) 的子串,然后统计. 怎么找到这些字符串呢?SAM 只能找出等价类啊. 注意 parent tree 的父亲 ...
- 如何用three.js(webgl)搭建3D粮仓、3D仓库、3D物联网设备监控-第十二课
序: 最近因为疫情,居家办公,索性翻翻之前的项目案例,总结总结. 这次疫情,深圳停摆,群众也挺恐慌的,封闭前一天,超市被抢购一空,虽然官方媒体一再强调,材米油盐蔬菜肉类管够,但是任然挡不住群众们的抢购 ...
- Spring——自动装配的三种实现方式
依赖注入的本质是装配,装配是依赖注入的具体行为 spring会在上下文中自动寻找,并自动给bean装配属性 自动装配的三种方式 (1).在xml中显式的装配 (2).在java中显式的装配 (3).隐 ...