




已阅读5页,还剩5页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
C+基本概念在编译器中的实现对于C+对象模型,相信很多程序员都耳熟能详。 本文试图通过一个简单的例子演示一些C+基本概念在编译器中的实现,以期达到眼见为实的效果。 1、对象空间和虚函数1.1 对象空间在我们为对象分配一块空间时,例如: CChild1 *pChild = new CChild1();这块空间里放着什么东西?在CChild1没有虚函数时,CChild1对象空间里依次放着其基类的非静态成员和其自身的非静态成员。没有任何非静态成员的对象,会有一个字节的占位符。 如果CChild1有虚函数,VC6编译器会在对象空间的最前面加一个指针,这就是虚函数表指针(Vptr:Virtual function table pointer)。我们来看这么一段代码:class CMember1 public:CMember1()a=0x5678;printf(构造 CMember1n);CMember1()printf(析构 CMember1n);int a;class CParent1 public:CParent1()parent_data=0x1234;printf(构造 CParent1n);virtual CParent1()printf(析构 CParent1n);virtual void test()printf(调用CParent1:test()nn);void real()printf(调用CParent1:test()nn);int parent_data;class CChild1 : public CParent1 public:CChild1()printf(构造 CChild1n);virtual CChild1()printf(析构 CChild1n);virtual void test()printf(调用CChild1:test()nn);void real()printf(调用CChild1:test()nn);CMember1 member;static int b;CChild1对象的大小是多少?以下是演示程序的打印输出:-派生类对象对象地址 0x00370FE0对象大小 12对象内容00370FE0: 00410104 00001234 00005678vptr内容00410104: 004016a0 00401640 00401f70CChild1对象的大小是12个字节,包括:Vptr、基类成员变量parent_data、派生类成员变量member。Vptr指向的虚函数表(VTable)就是虚函数地址组成的数组。1.2 Vptr和VTable如果我们用VC自带的dumpbin反汇编Debug版的输出程序:dumpbin /disasm test_vc6.exea.txt可以在a.txt中找到:?testCChild1UAEXXZ:00401640: 55 push ebp.?_ECChild1UAEPAXIZ: 004016A0: 55 push ebp可见VTable中的两个地址分别指向CChild1的析构函数和CChild1的成员函数test。这两个函数是CChild1的虚函数。如果打印两个CChild1对象的内容,可以发现它们Vptr是相同的,即每个有虚函数的类有一个VTable,这个类的所有对象的Vptr都指向这个VTable。这里的函数名是不是有点奇怪,附录二简略介绍了C+的Name Mangling。1.3 静态成员变量在C+中,类的静态变量相当于增加了访问控制的全局变量,不占用对象空间。它们的地址在编译链接时就确定了。例如:如果我们在项目的Link设置中选择“Generate mapfile”,build后,就可以在生成的map文件中看到:0003:00002e18 ?bCChild12HA 00414e18 test1.obj从打印输出,我们可以看到CChild1:b的地址正是0x00414E18。其实类定义中的对变量b的声明仅是声明而已,如果我们没有在类定义外 (全局域) 定义这个变量,这个变量根本就不存在。 1.4 调用虚函数通过在VC调试环境中设置断点,并切换到汇编显示模式,我们可以看到调用虚函数的汇编代码:16: pChild-test();(1) mov edx,dword ptr pChild(2) mov eax,dword ptr edx(3) mov esi,esp(4) mov ecx,dword ptr pChild(5) call dword ptr eax+4语句(1)将对象地址放到寄存器edx,语句(2)将对象地址处的Vptr装入寄存器eax,语句(5)跳转到Vptr指向的VTable第二项的地址,即成员函数test。语句(4)将对象地址放到寄存器ecx,这就是传入非静态成员函数的隐含this指针。非静态成员函数通过this指针访问非静态成员变量。1.5 虚函数和非虚函数在演示程序中,我们打印了成员函数地址:printf(CParent1:test地址 0x%08pn, &CParent1:test);printf(CChild1:test地址 0x%08pn, &CChild1:test);printf(CParent1:real地址 0x%08pn, &CParent1:real);printf(CChild1:real地址 0x%08pn, &CChild1:real);得到以下输出:CParent1:test地址 0x004018F0CChild1:test地址 0x004018F0CParent1:real地址 0x00401460CChild1:real地址 0x00401670两个非虚函数的地址很容易理解,在dumpbin的输出中可以找到它们:?realCParent1QAEXXZ: 00401460: 55 push ebp.?realCChild1QAEXXZ: 00401670: 55 push ebp为什么两个虚函数的“地址”是一样的?其实这里打印的是一段thunk代码的地址。通过查看dumpbin的输出,我们可以看到:_9$B3AE:(6) mov eax,dword ptr ecx(7) jmp dword ptr eax+4如果我们在跳转到这段代码前将对象地址放到寄存器ecx,语句(6)就会将对象地址处的Vptr装入寄存器eax,语句(7)跳转到Vptr指向的VTable第二项的地址,即成员函数test。基类和派生类VTable的虚函数排列顺序是相同的,所以可以共用一段thunk代码。这段thunk代码的用途是通过函数指针调用虚函数。如果我们不取函数地址,编译器就不会产生这段代码。请注意不要将本节的thunk代码与VTable中虚函数地址混淆起来。Thunk代码根据传入的对象指针决定调用哪个函数,VTable中的虚函数地址才是真正的函数地址。1.6 指向虚函数的指针我们试验一下通过指针调用虚函数。非静态成员函数指针必须通过对象指针调用:typedef void (Parent:*PMem)();printf(n-通过函数指针调用n);PMem pm = &Parent:test;printf(函数指针 0x%08pn, pm);(pParent-*pm)();得到以下输出:-通过函数指针调用函数指针 0x004018F0调用CChild1:test()我们从VC调试环境中复制出这段汇编代码:13: (pParent-*pm)();(8) mov esi,esp(9) mov ecx,dword ptr pParent(10) call dword ptr pm语句(9)将对象指针放到寄存器ecx中,语句(10)调用函数指针指向的thunk代码,就是1.5节的语句(6)。下面会发生什么,前面已经说过了。1.7 多态的实现经过前面的分析,多态的实现应该是显而易见的。当用指向派生类对象的基类指针调用虚函数时,因为派生类对象的Vptr指向派生类的VTable,所以调用的当然是派生类的函数。通过函数指针调用虚函数同样要经过VTable确定虚函数地址,所以同样会发生多态,即调用当前对象VTable中的虚函数。2、构造和析构2.1 构造函数下面的语句:printf(-构造派生类对象n); CChild1 *pChild = new CChild1();产生以下输出:-构造派生类对象构造 CParent1构造 CMember1构造 CChild1编译器会在用户定义的构造函数中加一些代码:先调用基类的构造函数,然后构造每个成员对象,最后才是程序中的构造函数代码(以下称用户代码)。下面这段汇编代码就是编译器修改过的CChild1类的构造函数:0CChild1QAEXZ:004014D0 push ebp.(11) call CParent1:CParent1 (004013b0).(12) call CMember1:CMember1 (00401550)(13) mov eax,dword ptr this(14) mov dword ptr eax,offset CChild1:vftable (00410104)(15) push offset string xb9xb9xd4xec CChild1n (004122a0)call printf (004022e0).ret语句(11)调用基类的构造函数,语句(12)构造成员对象,语句(15)以后是用户代码。语句(13)和(14)也值得一提:语句(13)将对象地址放到寄存器eax,语句(14)将CChild1类的VTable指针放到对象地址(eax)的起始处。它们建立的正是对象的Vptr。如果对象是通过new操作符构造的,编译器会先调用new函数分配对象空间,然后调用上面这个构造函数。2.2 析构函数删除指向派生类对象的指针产生以下输出:-删除指向派生类对象的基类指针析构 CChild1析构 CMember1析构 CParent1编译器会在用户定义的析构函数中加一些代码:即先调用用户代码,然后析构每个成员对象,最后析构基类的构造函数。下面这段汇编代码就是编译器修改过的CChild1类的析构函数:?1CChild1UAEXZ:00401590 push ebp.push offset string xcexf6xb9xb9 CChild1n (004122c0) call printf (004022e0) .(16) call CMember1:CMember1 (00401610).(17) call CParent1:CParent1 (004013f0).ret前面是用户代码,语句(16)调用成员对象的析构函数,语句(17)调用基类的析构函数。细心的朋友会发现这里的析构函数的地址与前面VTable中析构函数地址不同。其实,它们的名字也不一样,它们是两个函数:_ECChild1UAEPAXIZ:004016A0 push ebp.(18) call CChild1:CChild1 (00401590).(19) call operator delete (004023a0).ret 4如果在调试器中看(或者用dem工具Demangling),第二个析构函数的名字是CChild1:scalar deleting destructor,前一个析构函数的名字是CChild1:CChild1。函数CChild1:scalar deleting destructor在语句(18)上调用前面的析构函数,在语句(19)上调用delete函数释放对象空间。在通过delete删除对象指针时,需要在析构后释放对象空间,所以编译器合成了第二个析构函数。通过VTable调用析构函数,肯定是delete对象指针引发的,所以VTable中放的是第二个析构函数。在析构堆栈上的对象时,只要调用第一个析构函数就可以了。2.3 虚析构函数千万不要将析构函数和虚函数混淆起来。不管析构函数是不是虚函数,编译器都会按照2.2节的介绍合成析构函数。将析构函数设为虚函数是希望在通过基类指针删除派生类对象时调用派生类的析构函数。如果析构函数不是虚函数,派生类对象没有Vptr,编译器会调用基类的析构函数(在编译时就确定了)。这样,用户在派生类析构函数中填写的代码就不会被调用,派生类成员对象的析构函数也不会被调用。不过,派生类对象空间还是会被正确释放的,堆管理程序知道对象分配了多少空间。3、不同的实现本文的目的只是通过对编译器内部实现的适当了解,加深对C+基本概念的理解,我们的代码不应该依赖可能会改变的内部机制。其实各个编译器对相同机制的实现也会有较大差异。例如:Vptr的位置就可能有多种方案:VC的编译器把Vptr放在对象头部 BCB的编译器将Vptr放在继承体系中第一个有Vptr的对象头部Dev-C+的编译器以前将Vptr放在继承体系中第一个有Vptr的对象尾部 Dev-C+的最新版本(4.9.9.2)也将Vptr放在对象头部。其实第1个方案有一个小问题:如果基类对象没有Vptr,而派生类对象有Vptr,让基类指针指向派生类对象时,编译器不得不调整基类指针的地址,让其指向Vptr后的基类非静态成员。以后如果通过基类指针delete派生类对象,由于delete的地址与分配地址不同,就会发生错误。读者可以在演示程序中找到研究这个问题的代码(其实是CSDN上一个网友的问题)。将Vptr放在其它两个位置,因为不用调整基类指针,就可以避免这个问题。 g+编译器(v3.4.2)产生的程序在打印虚函数地址时会输出:CParent1:test地址 0x00000009CChild1:test地址 0x00000009在通过函数指针调用函数时,编译器会通过这个数字9在对象的虚函数表中找到虚函数test。附录1 增量链接和ILT为了简化表述,演示程序的VC6项目设置(Debug版)关闭了“Link Incrementally”选项。如果打开这个选项,编译器会通过一个叫作ILT的数组间接调用函数。数组ILT的每个元素是一条5个字节的jmp指令,例如:ILT+170(?testCChild2QAEXXZ): 004010AF: E9 1C 10 00 00 jmp ?testCChild2QAEXXZ编译器调用函数时:call ILT+170(?testCChild2QAEXXZ)通过ILT跳转到函数的实际地址。这样,在函数地址变化时,编译器只需要修改ILT表,而不用修改每个引用函数的语句。ILT是编译器开发者起的变量名,据网友Cody2k3猜测,可能是Incremental
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 猪肉直供店培训
- 肾内科诊疗与健康管理
- IQC图纸识图培训
- 培训课件的版权申请
- 水利培训课件封面图片
- 建筑经理考试题及答案
- 监理师考试题及答案
- 煤矿培训宣传课件模板
- 教育培训机构课件
- 几何画板考试题及答案
- 顺丰运作主管
- 万达入职在线测评题
- 2024年吉林省长春市中考物理试题(含解析)
- 多校联考高一下学期语文期末考试试卷
- 品管圈PDCA提高手卫生依从性
- 2025高考物理步步高同步练习选修1练透答案精析
- TGDNAS 043-2024 成人静脉中等长度导管置管技术
- 陕西省西安市雁塔区2024年五年级数学第二学期期末综合测试试题含解析
- 2024年黔东南州能源投资限公司招聘(高频重点提升专题训练)共500题附带答案详解
- CJJ181-2012 城镇排水管道检测与评估技术规程
- 人音版六年级下册音乐教案及反思
评论
0/150
提交评论