从汇编看c++中的多态
作者:网络转载 发布时间:[ 2013/5/31 10:37:22 ] 推荐标签:
从汇编码可以看出,a,b虚函数在子类vtable和父类table中的位置是一样的(从它们相对于自己所在vtable的偏移量可以看出)。这保证了不论对象实际的类型是什么,编译器总能使用同样的偏移量来调用虚函数。假如不这么做,也是说虚函数a,b在子类Y的vtable中的位置和在父类X的vtable中的位置不一样,由于向上转型,编译器只针对父类工作,也是对虚函数a,b的调用只会根据父类X的vtable来确定偏移量,那么在实际运行的时候会出错,实际的子对象根本调用不到正确的函数,多态失效。
在上面的例子中,如果将yp转为实际的类型调用c,我们会看到编译器形成的偏移量为8byte,汇编代码如下:
; 32 : yp->c();
mov ecx, DWORD PTR _yp$[ebp];将yp所指向的堆对象的首地址给ecx
mov edx, DWORD PTR [ecx];将堆对象首地址的内容给edx,即将子类vptr指向的vtable首地址给edx
mov ecx, DWORD PTR _yp$[ebp];将yp所指向的堆对象首地址给ecx,作为隐含参数传递给虚成员函数c
mov eax, DWORD PTR [edx+8];将偏移子类vtable首地址8byte处内存的内容给eax,即将虚函数c的地址给eax(这里,虚函数b的地址同样位于偏移子类Y的vtable首地址8byte处)
call eax;调用虚成员函数c
对象切片
如果进行向上转型的时候不是用传地址或者引用,而是用传值,那么会发生对象切片,即派生类对象中原有的部分被切除,只保留了基类的部分。
下面是c++源码:
class X {
private:
int i;
public:
virtual void a() {
i = 1;
}
virtual void b() {
i = 2;
}
};
class Y : public X {
private:
int i;
public:
virtual void c() {//新定义的虚函数
i = 3;
}
void b() {//重写父类中的虚函数
i = 4;
}
};
void f(X x) {//用传值的形式进行向上转换
x.b();
}
int main() {
Y y;
f(y);
}
下面是main函数的汇编码:
; 28 : int main() {
push ebp
mov ebp, esp
sub esp, 16 ; 为对象y预留16byte的空间
; 29 : Y y;
lea ecx, DWORD PTR _y$[ebp];将y的首地址给ecx,转为隐含参数传递给y的构造函数
call ??0Y@@QAE@XZ;调用y的构造函数
; 30 : f(y);
sub esp, 8;//由于对象传值,要进行拷贝,产生临时对象,这里为临时对象预留8byte的空间(类X的大小)
mov ecx, esp;//将临时对象的首地址给ecx,作为隐含参数传递给拷贝函数
lea eax, DWORD PTR _y$[ebp];将对象y的首地址给eax,作为参数给拷贝函数
push eax;压栈,传递参数
call 0X@@QAE@ABV0@@Z;调用类X的拷贝函数
call f@@YAXVX@@@Z ; 调用函数f
add esp, 8;释放刚才的临时对象占用的8byte空间
; 31 : }
xor eax, eax
mov esp, ebp
pop ebp
ret 0
从汇编吗中可以看出,临时对象的大小为父类X的大小,调用的拷贝函数也是父类X的拷贝函数。
下面是父类X的拷贝函数汇编码:
??0X@@QAE@ABV0@@Z PROC ; X::X, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx;压栈,为存对象首地址预留4byte空间
mov DWORD PTR _this$[ebp], ecx;ecx中保存临时对象首地址,放到刚才预留的空间
mov eax, DWORD PTR _this$[ebp];将临时对象首地址给ecx
mov DWORD PTR [eax], OFFSET ??_7X@@6B@;将类X的vtable首地址存到临时对象首地址所指向的内存 即初始化临时对象的vptr指针
mov ecx, DWORD PTR _this$[ebp];将临时对象的首地址给ecx
mov edx, DWORD PTR ___that$[ebp];将y的首地址给edx
mov eax, DWORD PTR [edx+4];将偏移y首地址4byte处内存内容给edx,即将y包含的父对象中的成员变量i的值给edx
mov DWORD PTR [ecx+4], eax;将eax的值给偏移临时对象首地址4byte处内存,即将eax的值给临时对象的成员变量i
mov eax, DWORD PTR _this$[ebp];将临时对象的首地址给eax,作为返回值。构造函数总是返回对象首地址
mov esp, ebp
pop ebp
ret 4
从拷贝函数可以看出,临时对象只拷贝了y的所包含的的父对象部分(y被切片了),并且临时对象的vptr指针也初始化为类X的vtable首地址。
下面是函数f的汇编码:
; 24 : void f(X x) {
push ebp
mov ebp, esp
; 25 : x.b();
lea ecx, DWORD PTR _x$[ebp];将参数x的首地址给ecx,作为隐含参数传递给成员函数b
call ?b@X@@UAEXXZ ; 调用x中的成员函数b 这里是用对象直接调用,因此没有访问vtable
这里调用的是类X里面的成员函数,并且没有访问虚表vtable
下面是类X里面的虚成员函数b的汇编码:
?b@X@@UAEXXZ PROC ; X::b, COMDAT
; _this$ = ecx
; 8 : virtual void b() {
push ebp
mov ebp, esp
push ecx;为保存对象首地址预留4byte空间
mov DWORD PTR _this$[ebp], ecx;ecx中保存有对象x的首地址,放到刚才预留的空间
; 9 : i = 2;
mov eax, DWORD PTR _this$[ebp];将x首地址给eax
mov DWORD PTR [eax+4], 2;将2写给偏移x首地址4byte处,即将2赋给x的成员变量i
; 10 : }
mov esp, ebp
pop ebp
ret 0
?b@X@@UAEXXZ ENDP
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11