C++,继承、虚函数解惑!
本文作者:sodme
本文出处:http://blog.csdn.net/sodme
C++要实现虚函数, 归纳起来, 其实只用干两件事:
1. 根据派生和继承关系, 生成虚函数表;
2. 将代码中对虚函数的调用, 转化成对虚函数表中各虚函数指针的间接调用.
虽然在上文中的那个小例子中, 我们通过反汇编出来的asm弄明白了以下事实:
1. 每个"类"所拥有的虚函数表的个数: 每个类都会对应"唯一的一份"虚函数表;
2. "对象"中虚函数表指针的保存位置: 在调用对象的构造函数时, 会把虚函数表指针放到this处(即: 对象首地址处), 所以属于同一个类的每个对象的this地址所指向的单元处存放的是同一个值;
3. 虚函数的调用转化: 编译器编译c++代码时, 根据类的相关信息, 判断此函数是不是虚函数, 如果是, 则从虚函数表中取出真正的虚函数地址, 生成真正的调用语句;
那么, 现在看来, 问题的核心就变成了: 这个统领全局的虚函数表到底是如何产生? 在生成虚函数表的过程中, 遵守了哪些规则呢? 带着这些疑问, 我们将开始今天的探索.
本文所需代码可从以下地址获得( 此地址含有单继承c++和asm代码 ):
http://sodme.dev.googlepages.com/kyj_04_code.txt
为了研究的方便, 此次对代码结构进行了较大规模的调整, 主要是:
1. 去除了对输入输出流的引用, 只保存逻辑代码, 不再include任何多余代码;
2. 声明了两个基类, 它们在单继承和多继承方式下会被拿来作不同的整合: 在单继承下, Base2Class派生于Base1Class, 而MyClass类派生于Base2Class类; 而在多继承下, 两个基类相互之间不再有派生关系, 它们会被MyClass一个类多继承.
先看单继承的情况. 三个类的派生关系如下:
Base1Class -> Base2Class -> MyClass, 其派生规则为: 多层单继承.
其中,
Base1Class的虚函数表为:
.long _ZN10Base1Class14virtual_test_3Ev ;Base1Class::virtual_test_3()
Base2Class的虚函数表为:
.long _ZN10Base2Class14virtual_test_3Ev ;Base2Class::virtual_test_3()
.long _ZN10Base2Class14virtual_test_2Ev ;Base2Class::virtual_test_2()
MyClass的虚函数表为:
.long _ZN10Base2Class14virtual_test_3Ev ;Base2Class::virtual_test_3()
.long _ZN10Base2Class14virtual_test_2Ev ;Base2Class::virtual_test_2()
.long _ZN7MyClass15virtual_test_myEv ;MyClass::virtual_test_my()
由这三个虚函数表, 我们发现了以下的规律:
1. 派生类会继承基类的所有非同名虚函数, 存放顺序同基类;
如: 在Base2Class中, virtual_test_1() 是 Base1Class的虚函数, 且在Base2Class中没有定义同名函数, 它的顺序是1, 因为在基类Base1Class中, 它的顺序就是1;
2. 当派生类与基类有同名虚函数时, 派生类的虚函数表中存放派生类的虚函数, 存放顺序与基类同名虚函数的存放位置相同;
如: 在Base2Class中, virtual_test_3() 在 Base1Class和Base2Class中都有, 那么在Base2Class的虚函数表中, 就只会存放Base2Class自己的虚函数, 但存放的顺序, 与它的基类Base2Class的存放顺序相同! 而"存放顺序必须相同", 这一点, 是C++实现动态重载的重要方法! 这一点, 后面分析构造函数运行流程时会说到.
3. 存放完所有与基类同名的虚函数 以及 基类有但派生类没有的所有虚函数后, 开始存放派生类的其它虚函数.
如: 在Base2Class的virtual_test_2() 和 MyClass的virtual_test_my(), 这两个函数, 都是派生类中存在而基类中不存在的, 它们存放的顺序都在上述两类虚函数的后面.
以上, 是关于单继承情况下虚函数表的静态结构分析. 那么, 我们来看看实际运行时, 构造函数又是如何利用这些数据将对象进行初始化的.
MyClass构造函数的主要语句:
movl %eax, (%esp)
call _ZN10Base2ClassC2Ev ;调用Base2Class的构造函数
movl $_ZTV7MyClass+8, %edx ;将MyClass的虚函数表地址放在edx中, 以便放在this处
movl 8(%ebp), %eax
movl %edx, (%eax) ;将虚表地址放在this处
movl 8(%ebp), %eax
movl $1, 12(%eax) ;[this+12] = 1 <==> data1 = 1
movl 8(%ebp), %eax
movl $2, 16(%eax) ;[this+16] = 2 <==> data2 = 2
可以看到, 在MyClass的构造函数中, 首先是调用了Base2Class的构造函数: __ZN10Base2ClassC2Ev.
Base2Class的构造函数:
movl %eax, (%esp)
call _ZN10Base1ClassC2Ev ;调用Base1Class的构造函数
movl $_ZTV10Base2Class+8, %edx ;将Base2Class虚表地址放在this处
movl 8(%ebp), %eax
movl %edx, (%eax)
movl 8(%ebp), %eax
movl $44, 8(%eax) ;[this+8] = 44 <==> base_2_data = 44
而在Base2Class中, 又调用了Base1Class的构造函数:
movl 8(%ebp), %edx
movl %eax, (%edx)
movl 8(%ebp), %eax
movl $33, 4(%eax) ;[this+4] = 33 <==> base_1_data = 33
从这三个构造函数的执行过程来看, 在Base1Class的构造函数中, 将Base1Class的虚函数表地址传到了this地址处, 在Base2Class的构造函数中, 将Base2Class的虚函数表地址也传到了this地址处, 同样的, 在MyClass的构造函数中, 把MyClass的虚函数表地址还是放在this地址处. 换句话说, 针对于同一个this地址处的这个虚函数表地址的赋值, C++竟然重复作了三次! 而最后一次的赋值, 才是我们真正需要的结果! 没错, C++在这里, 确实是作了两次"无用功". 我在想, 有没有办法避免这样的重复赋值呢? 有一种方法: 在调用构造函数时, 传个参数进来, 表示是不是派生类调用它的, 由个参数来决定是不是将虚函数表地址拿到this, 但是, 仔细算一下: "传递参数+判断" 这样的逻辑, 似乎还没有不管三七二十一直接赋值来得更为简单一些, 所以, 这样看来, C++选择这样作, 也是有充分理由的.
前面反复提到过虚函数的存放顺序. 那么, 这个顺序, 与C++的动态重载到底有多大关系呢? 我们先看这条语句:
pMyClass->virtual_test_2();
C++在分析这条语句时, 首先会判断pMyClass是哪种类型, 此例中是MyClass, 然后判断它调用的函数是不是虚函数, 如果是虚函数, 则将调用形式转化为针对于虚函数表的间接调用.
而前面提到, 派生类中存在且在基类中也存在的虚函数, 在派生类虚函数表中的位置与基类位置相同. 所以, 不管是MyClass还是Base2Class的对象, 它们调用virtual_test_2()的汇编代码都是相同的:
pBaseClass->virtual_test_2():
movl (%eax), %eax ; 取虚表
addl $8, %eax ; 取virtual_test_2()存放地址
movl (%eax), %edx ; 取virtual_test_2()地址
movl -12(%ebp), %eax
movl %eax, (%esp) ; 将this指针放栈顶传递
call *%edx
pMyClass->virtual_test_2():
movl (%eax), %eax
addl $8, %eax
movl (%eax), %edx
movl -16(%ebp), %eax
movl %eax, (%esp)
call *%edx
经过比对, 除了取this指针的局部变量地址不同, 其余代码均相同, 取虚函数地址时, 都是在this指针的基础上加8. 之所以可以作到这样, 就是因为有这个约定: 针对于同名函数, 它们在虚函数表中的位置相同. 当然, 这个约定并不一定要所有的编译器都来遵守, 针对于同一个编译器而言, 有一套约定就行了, 关键是要有约定, 而具体如何约定倒各有各的作法.
除此之外, 我们还发现, 在Base2Class类的virtual_test_2()函数中, 对于类Base2Class 数据成员base_2_data的访问, 已经被修正为this+8. 这是因为, 经过一系列的派生后, pMyClass对象结构变成如下的方式:
| |
|-----------------------------|
this -> | 类MyClass虚函数表地址 | 0
|-----------------------------|
| Base1Class::base_1_data | +4
|-----------------------------|
| Base2Class::base_2_data | +8
|-----------------------------|
| MyClass::data1 | +12
|-----------------------------|
| MyClass::data2 | +16
|-----------------------------|
| |
其规则是: this地址处存放最上层类的虚函数表地址, 然后向下依次存放各基类的数据成员, 其顺序按类中的声明顺序排列. 需要指出的是, 这里存放的数据, 不仅包括public的数据, 也同时包括private的数据. 道理很简单: 在调用某层类的函数时, 该函数中仍有可能要引用到私有数据, 尽管此私有数据不能被外部函数访问. 所以, 从这个层面上来说, 私有数据仅仅是C++自己的"君子"约定, 我们完全使用比较流氓的方法来访问它. 比如我们在类Base1Class增加一private数据, 修改为如下形式:
{
public:
Base1Class(){ base_1_data = 33; };
~Base1Class(){};
int base_1_data;
virtual void virtual_test_1(){ base_1_data = 10; };
virtual void virtual_test_3(){ base_1_data = 11; };
private:
int pri_base_1_data;
};
pri_base_1_data会被放在base_1_data和base_2_data之间. 如果要访问pri_base_1_data, 在取得this指针后, 我们就可以通过this+8来访问, 具体的c++代码可以是这样:
*( (char*)pMyClass + 8 ) = (int)3333;
那么, C++如何区别这两条语句呢?
pBase2Class->virtual_test_2():
movl (%eax), %eax
addl $8, %eax
movl (%eax), %edx
movl -12(%ebp), %eax
movl %eax, (%esp)
call *%edx
((MyClass*)pBase2Class)->virtual_test_2():
movl (%eax), %eax
addl $8, %eax
movl (%eax), %edx
movl -12(%ebp), %eax
movl %eax, (%esp)
call *%edx
pMyClass->virtual_test_2():
movl (%eax), %eax
addl $8, %eax
movl (%eax), %edx
movl -16(%ebp), %eax
movl %eax, (%esp)
call *%edx
((Base2Class*)pMyClass)->virtual_test_2():
movl (%eax), %eax
addl $8, %eax
movl (%eax), %edx
movl -16(%ebp), %eax
movl %eax, (%esp)
call *%edx
比较完这四条语句的代码之后, 我们发现: 不管作不作类型转换, 其编译出的汇编代码全是一样的! 其实, 前面的类型转换只是后面是否可以调用某函数的类型约定, 至于此函数的具体执行行为, 则完全看对象本身的虚函数表是哪份以及this指针指向的数据是什么而定了, this指针指向的虚函数表决定了函数的最终行为归属, 而此表地址在对象初始化时被放在了对象首地址处, 这样想来, 好像前后的知识可以贯通了:
对于一个pBase2Class而言, 我如何知道形如这样的调用:
pBase2Class->virtual_test_2();
它调用的到底是类Base2Class的, 还是MyClass的? 一切答案皆在虚函数表中. 对象与"某个类的虚函数表"绑定, 而virtual_test_2()则与"虚函数表地址+8"绑定, 要想知道调用的是哪个, 就看此时的this到底是哪个类的this.
也就是说, 在对virtual_test_2()此函数的调用上, 不管是Base2Class类的对象还是MyClass类的对象, 其调用的汇编代码完全一样, 唯一可以让它们有不同执行行为的, 就是"addl $8, &eax"后取出的虚函数地址. 比如:
pBase2Class = new Base2Class; 则:
((MyClass*)pBase2Class)->virtual_test_my() 必定是非法的, 它会被转化成以下语句:
movl (%eax), %eax
addl $12, %eax ;根据类型为MyClass及函数为virtual_test_my()转化为"虚函数表地址+12"
movl (%eax), %edx
movl -12(%ebp), %eax
movl %eax, (%esp)
call *%edx
由此, 可以看出, 编译器会根据当前对象的类型, 决定如何编译virtual_test_my(), 这里的"类型", 指的是"调用时类型", 而不是简单的指"定义时类型", 因为调用时, 可能会进行强制类型转换. 这里, 尽管virtual_test_my()函数可以编译通过, 但通过"this->虚函数表+12"找出来的这个地址, 根据不是正常的函数地址, 此时this指向实际对象所拥有的虚函数表中最多只有2个虚函数(+8).
C++,继承、虚函数解惑!相关推荐
- 【足迹C++primer】52、,转换和继承虚函数
转换和继承,虚函数 Understanding conversions between base and derived classes is essential to understanding h ...
- 继承虚函数单层需继承的内存图(VC6.0)
上班之余抽点时间出来写写博文,希望对新接触的朋友有帮助.今天在这里和大家一起学习一下继承虚函数 继承关系图 class A {virtual aa(){}; };class B : public vi ...
- 虚函数 虚继承 抽象类
虚函数.纯虚函数.虚基类.抽象类.虚函数继承.虚继承 虚函数:虚函数是C++中用于实现多态(polymorphism)的机制.核心理念就是通过基类访问派生类定义的函数.是C++中多态性的一个重要体现, ...
- 虚函数继承与虚函数表-汇编码分析
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考:https://www.equestionanswers.com/cpp/vptr-and-vta ...
- 菱形继承,多继承,虚继承、虚表的内存结构全面剖析(逆向分析基础)
// 声明:以下代码均在Win32_Sp3 VC6.0_DEBUG版中调试通过.. 在逆向还原代码的时候,必须得掌握了菱形继承,多继承,虚继承虚函数的内存虚表结构.所以,这篇文章献给正在学习C++ ...
- C++学习笔记——虚函数
2019独角兽企业重金招聘Python工程师标准>>> 基本概念 虚函数是在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为: virtual ...
- 【C++基础之十一】虚函数的用法
虚函数的作用和意义,就不进行说明了,这里主要讨论下虚函数的用法. 1.典型的虚函数用法 可以看到,只有标识为virtual的函数才会产生多态的效果,而且是编译多态.它只能借助指针或者引用来达到多态的效 ...
- 【虚基类、虚函数及应用】
虚基类 1.虚基类存在的意义 当在多条继承路径上有一个公共的基类,在这些路径中的某几条汇合处,这个公共的基类就会产生多个实例(或多个副本),若只想保存这个基类的一个实例,可以将这个公共基类说明为虚基类 ...
- 空类,虚函数类,虚继承类的空间大小
//此代码在32位win下运行成功 #include<iostream> using namespace std; class A//A是空类,编译器会用一个char类型标记这个类,大小为 ...
最新文章
- asp.net gridview 模板列 弹出窗口编辑_连云港各种新型铝模板设计软件,哪家强_威尔达建材...
- sqlliet 创建多表查询的视图_第4关 复杂查询
- python3.1.1_python 3.1.1 with--enable shared:将不会构建任何扩展
- C# CKEditor、CKFinder集成使用
- mysql 优化器提示_Mysql查询优化器
- Android手机录制音频
- 计算机科学与技术专接本的历年真题,10年计算机专业专接本真题
- 操作系统课程设计(linux操作系统)
- Tp5.0完全开发手册学习(第六章 请求)之一 (request 和input)
- 【微分方程数值解】常\偏微分方程及其常用数值解法概述
- 华为 IPD 集成产品开发流程的缺点和适用局限性
- 基于WKT标准的空间参考系字符串及prj文件生成样例
- 手机浏览器打开微信小程序,支持外部浏览器跳转到小程序
- 3500字专家访谈,探访汽车零部件企业争相迈步数字化背后的故事
- 计算机学硕研究计划,博士研究生学习计划和研究计划
- SAT数学公式之几何图形
- 批量修改WORD文档密码
- [JZOJ5442]【NOIP2017提高A组冲刺11.1】荒诞([BZOJ3060]【POI2012】Tour de Byteotia)
- C语言switch语句用法详解
- Linux下安装软件的几种方法
热门文章
- 【云原生Docker系列项目实战第一篇】dockerfile+lnmp+workpress(星星温柔泛滥,人间至善)
- anasys hpc集群_ANSYS高性能计算(HPC)
- 滚珠螺杆的四种安装方式
- 身份证识别(java)
- 二分法python上机实验报告_数值分析上机实验报告..doc
- 项目工期所需人数的的计算
- 七年级下学期计算机考试题,七年级信息技术试卷及答案
- 转 : Squareup刷卡器,音频读卡识别android/iOS源码API
- java 可见_java 可见性简单总结
- Spring和Ehcache整合详解