浅谈浅谈C++ 虚函数分析虚函数分析
虚函数调用属于运行时多态,在类的继承关系中,通过父类指针来调用不同子类对象的同名方法,而产生不同的效果。
C++ 中的多态是通过晚绑定(对象构造时)来实现的。
用法用法
在函数之前声明关键字 virtual 表示这是一个虚函数,在函数后增加一个 = 0 表示这是一个纯虚函数,纯虚函数的类不能创建具
体实例。
该示例作后文分析使用,一个包含纯虚函数的父类,一个重写了父类方法的子类,一个无继承的类。
struct Base {
Base() : val(7777) {}
virtual int fuck(int a) = 0;
int val;
};
struct Der : public Base {
Der() = default;
int fuck(int a) override { return val + 4396; }
};
struct A {
A() = default;
void funny(int a) {}
};
int main() {
Der der;
Base *pbase = &der;
pbase->fuck(sizeof(Der)); // 调用 Der::fuck(int a);
A a;
a.funny(sizeof(A)); // A::funny(int a);
return 3;
}
实现实现
原来就了解虚函数是通过虚表的偏移来获取实际调用函数地址来实现的,但是在何时确定这个偏移和具体的偏移细节也没有说
明,今儿个来探探究竟。
拿上面的代码进行反汇编获提取部分函数,main,Base::Base(), Base::fuck(), Der::Der(), Der::fuck, A::funny() 如下:
_ZN4BaseC2Ev:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -8(%rbp) // 还是 main 函数的栈帧 -32(%rpb) 的地址
leaq 16+_ZTV4Base(%rip), %rdx // 关键点来了,取虚表偏移 16 的地址也就是 __cxa_pure_virtual,这里是没有意义的
movq -8(%rbp), %rax
movq %rdx, (%rax) // 将 __cxa_pure_virtual 的地址存放在 地址rax 的内存中(这个例子中也就是main 函数的栈帧 -32(%rpb) 的地方),
movq -8(%rbp), %rax // 然后往后偏移 8 个字节,也就是跳过虚表指针,对成员变量 val 初始化。
movl $7777, 8(%rax)
nop // 注:上面是用这个示例中实际的地址带入的,实际上对于一个有的类的处理是一个通用逻辑的,构造函数传入的第一个参数 rdi 是 this 指针,由于有
虚表存在的影响,这里会修改 this 指针所在地址的内容,也就是虚表的偏移地址(非起始地址)
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size _ZN4BaseC2Ev, .-_ZN4BaseC2Ev
.weak _ZN4BaseC1Ev
.set _ZN4BaseC1Ev,_ZN4BaseC2Ev
.section .text._ZN3Der4fuckEi,"axG",@progbits,_ZN3Der4fuckEi,comdat
.align 2