vptr虚指针被定义在对象首地址的前4个字节处
指针的类型决定了普通函数的调用,指针指向的实际类型决定了虚函数的调用
构造函数不能是虚函数,析构函数最好设置为虚函数
对象的虚表指针初始化是通过编译器在构造函数内插入代码来完成的
对象首地址为this指针
在构造函数和析构函数中调用虚函数会使多态性失效
实例化一个对象时,优先调用父类的构造函数,并以子类对象的首地址作为this指针传递给父类构造函数。在父类的构造函数中,首先会初始化子类虚表指针为父类的虚表首地址(会将虚表指针修改为当前类的虚表指针),接着在子类的构造函数中,会重新写入子类虚表指针为子类的虚表首地址
接着是子类的析构函数,和父类的析构函数,同样会重写子类虚表指针(单一继承时,父类和子类的虚表指针位置一样,即对象首地址的前4字节处),重写虚表指针是为了防止在构造或析构函数中调用子类或父类的虚函数,比如在父类的构造函数中,调用虚函数,如果不更新虚表指针为父类虚表,那么调用了子类的虚函数,但是子类构造函数还没执行,没有初始化虚表指针,这样就会访问错误,在析构函数中原理一样。
总结下构造函数和析构函数的调用顺序,若存在以下继承关系:
1 | class Bird : public Mammal |
构造:Bird -> Mammal - > Bat
析构:Bat -> Mammal - >Bird
在单一继承中,其子类对象内存首先存放的是父类的数据成员
Q:为什么析构函数要定义为虚函数
因为可以父类指针保存子类对象,如 Mammal *a = new Bird();,如果析构函数没定义为虚函数,释放对象空间时就会直接调用父类的析构函数,子类和父类对象的内存大小不同,释放内存就会产生问题
虚表在内存中的结构
1 | +-------------+ |
offset_to_top
将对象从当前这个类型转换为该对象的实际类型的地址偏移量,是对象内存中的偏移量,不是虚表内存中的偏移量
1 | 继承关系:Bat : Bird, Mammal |
用父类指针保存子类对象,这时在子类对象的内存布局,
1 | 0 | class Bat |
this指向0,但是Mammal对象在偏移8处,需要修正this的位置,查看汇编:
1 | push ebx ; this |
此时this指向Mammal对象,调用方法:
1 | mov [ebp+var_1C], eax |
Bat虚表:
1 | .rodata:08048C68 ; `vtable for'Bat |
可以看到次表的offset to top值为-8,即为Bat对象到Mammal对象偏移的负值
在多继承中,由于不同的基类起点可能处于不同的位置,因此当需要将它们转化为实际类型时,this 指针的偏移量也不相同,且由于多态的特性,bat 的实际类型在编译时期是无法确定的;那必然需要一个东西帮助我们在运行时期确定 bat 的实际类型,这个东西就是offset_to_top
。通过让this指针
加上offset_to_top
的偏移量,就可以让 this 指针指向实际类型的起始地址
https://www.cnblogs.com/xhb19960928/p/11720314.html
https://www.zhihu.com/question/23971699
https://www.freebuf.com/articles/system/123821.html
https://blog.iret.xyz/article.aspx/cpp_vfunc_reversing_2
https://www.anquanke.com/post/id/85585
https://alschwalm.com/blog/static/2017/01/24/reversing-c-virtual-functions-part-2-2/