文章目录
- 1、虚函数
- 1.1、虚函数储存结构
- 1.2、子类重写虚函数
- 1.3、在栈上调用虚函数
- 1.4、在堆上调用虚函数(通过指针调用,多态)
本文用到的反汇编工具是objconv,使用方法可以看我另一篇文章https://blog.csdn.net/weixin_45001971/article/details/128660642。
其它文章:
从汇编的角度了解C++原理——类的储存结构和函数调用
从汇编的角度了解C++原理——new和malloc的区别
从汇编的角度了解C++原理——虚函数
1、虚函数
1.1、虚函数储存结构
反汇编。
main:
sub rsp, 56
lea rcx, [rsp+20H]
call ??0A@@QEAA@XZ //调用构造函数
mov eax, 4294967295
add rsp, 56
ret
??0A@@QEAA@XZ: //调用A类的构造函数
mov qword [rsp+8H], rcx
mov rax, qword [rsp+8H]
lea rcx, [rel ??_7A@@6B@] //获取虚表??_7A@@6B@的地址
mov qword [rax], rcx //把虚表地址放在对象的头部
mov rax, qword [rsp+8H]
mov dword [rax+8H], 10 //在对象首地址偏移8个字节的位置定义d1变量
mov rax, qword [rsp+8H]
ret
??_7A@@6B@: //A类虚表
dq ?func2@A@@UEAAXXZ //虚函数func2
以上例的汇编代码可以得出带虚函数的类的储存结构如下图所示。
带有虚函数的对象的头部会放置8个字节大小的虚表地址,有了虚表之后的对象会以8个字节为单位去对齐,如上例中的A类,如果没有虚函数,它的大小为4个字节,而加了虚函数之后,大小变为了16个字节。
1.2、子类重写虚函数
在代码中添加A的子类B,重写func2方法。
反汇编
main:
sub rsp, 56
lea rcx, [rsp+20H]
call ??0B@@QEAA@XZ //调用B类构造
mov eax, 4294967295
add rsp, 56
ret
??0A@@QEAA@XZ: //A类构造函数
mov qword [rsp+8H], rcx
mov rax, qword [rsp+8H]
lea rcx, [rel ??_7A@@6B@] //把A类虚表的地址放在头部
mov qword [rax], rcx
mov rax, qword [rsp+8H]
mov dword [rax+8H], 10
mov rax, qword [rsp+8H]
ret
??0B@@QEAA@XZ: //B类构造函数
mov qword [rsp+8H], rcx
sub rsp, 40
mov rcx, qword [rsp+30H]
call ??0A@@QEAA@XZ //调用A类构造
mov rax, qword [rsp+30H]
lea rcx, [rel ??_7B@@6B@] //把B类虚表的地址放在头部
mov qword [rax], rcx
mov rax, qword [rsp+30H]
add rsp, 40
ret
??_7A@@6B@: //A类虚表
dq ?func2@A@@UEAAXXZ //A::func2
dq ?func3@A@@UEAAXXZ //A::func3
??_7B@@6B@: //B类虚表
dq ?func2@B@@UEAAXXZ //B::func2,被替换为了B实现的func2
dq ?func3@A@@UEAAXXZ //A::func3
从该例中我们可以看到,父类有虚函数时,不光它自己有一张虚表,它的子子孙孙都会各带有一个自己的虚表,子类重写虚函数时,会把子类实现的函数指针替换上虚表,把原先父类的函数指针覆盖掉。
1.3、在栈上调用虚函数
在main里添加方法的调用。
反汇编。
main:
sub rsp, 56
lea rcx, [rsp+20H]
call ??0B@@QEAA@XZ
lea rcx, [rsp+20H]
call ?func1@A@@QEAAXXZ //调用A::func1
lea rcx, [rsp+20H]
call ?func2@B@@UEAAXXZ //调用B::func2
lea rcx, [rsp+20H]
call ?func3@A@@UEAAXXZ //调用A::func3
mov eax, 4294967295
add rsp, 56
ret
在栈上调用方法时,因为类型是确定的,所以编译器在编译阶段就会找到对应的函数去调用,调用过程与普通方法一样。
1.4、在堆上调用虚函数(通过指针调用,多态)
修改例程如下。
反汇编
main:
sub rsp, 72
mov ecx, 16
call ??2@YAPEAX_K@Z
mov qword [rsp+28H], rax
cmp qword [rsp+28H], 0
jz ?_001
mov rcx, qword [rsp+28H] //定义指针b
call ??0B@@QEAA@XZ
mov qword [rsp+30H], rax //rsp+30H指向对象
jmp ?_002 //跳到?_002
?_001: mov qword [rsp+30H], 0
?_002: mov rax, qword [rsp+30H]
mov qword [rsp+38H], rax //rsp+38H指向对象
mov rax, qword [rsp+38H] //rax指向对象
mov qword [rsp+20H], rax //rsp+20H指向对象
mov rcx, qword [rsp+20H] //rcx指向对象
call ?func1@A@@QEAAXXZ //调用A::func1
mov rax, qword [rsp+20H]
mov rax, qword [rax] //取虚表
mov rcx, qword [rsp+20H]
call near [rax] //执行虚表第一个函数,即B::func2
mov rax, qword [rsp+20H]
mov rax, qword [rax]
mov rcx, qword [rsp+20H]
call near [rax+8H] //执行虚表第二个函数,即A::func3
mov eax, 4294967295
add rsp, 72
ret
??_7B@@6B@:
dq ?func2@B@@UEAAXXZ
dq ?func3@A@@UEAAXXZ
从该例可以看到,通过指针来调用函数时。
如果是普通函数,编译器会直接根据指针类型,找到对应的的方法,而不是根据对象本身的类型,如本例中B类也实现了func1方法,但通过A类指针调用时,写到汇编里的时A::func1。
如果是虚函数,编译器不会根据名字来查找函数,而是让汇编代码通过虚表中的偏移量来调用,如本例中,b指针执行了func2和func3,这两个函数都没有被直接调用,而是以“call near [rax + 偏移量]”的形式调用了,这也是C++中父类指针指向子类对象的多态的实现原理。