基本的实现原理可以参考链接,不同编译器实现可以不同。
gcc version 11.2.0 (Homebrew GCC 11.2.0)测试
#include <iostream>
class A1 {
public:
virtual void a() { std::cout << "A1::a" << std::endl; }
virtual void b() { std::cout << "A1::b" << std::endl; }
virtual void c() { std::cout << "A1::c" << std::endl; }
int a1 = 1;
};
class A2 {
public:
virtual void d() { std::cout << "A2::d" << std::endl; }
virtual void e() { std::cout << "A2::e" << std::endl; }
int a2 = 2;
};
// A1::a B::b A1::c [[B::f B::g]]
// B::d A2::e
// ==>
// A1::a [B::b] A1::c [B:d B:e] [[B:f B:g]]
// thunk:B:d thunk:B:e
class B : public A1, public A2 {
public:
virtual void b() { std::cout << "B::b" << std::endl; }
virtual void d() { std::cout << "B::d" << std::endl; }
virtual void e() { std::cout << "B::e" << std::endl; }
virtual void f() { std::cout << "B::f" << std::endl; }
virtual void g() { std::cout << "B::g" << std::endl; }
int b3 = 3;
};
int main() {
B b;
long *pvptr = (long *)&b;
long *vptr = (long *)*pvptr;
typedef void (*pfunc)();
for (unsigned i = 0; i < 7; ++i) {
std::cout << std::hex << vptr + i << " " << vptr[i] << std::endl;
pfunc pfun = pfunc(vptr[i]);
pfun();
}
std::cout << "---------------------" << std::endl;
long *vptr2 = (long *)*(long *)((char *)&b + sizeof(A1));
for (unsigned i = 0; i < 2; ++i) {
std::cout << std::hex << vptr2 + i << " " << vptr2[i] << std::endl;
pfunc pfun = pfunc(vptr2[i]);
pfun();
}
return 0;
}
汇编后,类B的虚函数表结构:
vtable for B:
.quad 0
.quad typeinfo for B
.quad A1::a()
.quad B::b()
.quad A1::c()
.quad B::d()
.quad B::e()
.quad B::f()
.quad B::g()
.quad -16
.quad typeinfo for B
.quad non-virtual thunk to B::d()
.quad non-virtual thunk to B::e()
其中虚函数表中指向的B类 typeinfo
结构:
typeinfo for B:
.quad vtable for __cxxabiv1::__vmi_class_type_info+16
.quad typeinfo name for B
.long 0
.long 2
.quad typeinfo for A1
.quad 2
.quad typeinfo for A2
.quad 4098
在 GCC 实现中:
继承多个有虚函数的父类,派生类就有多个虚表指针,每个虚函数指针都位于基类存储位置中的起始地址(派生类对象中)。所以使用派生类对象向上转型的基类指针可以方便地调用虚函数-找到对应的虚指针。
虚表指针指向的虚函数表大致是连续的,每个虚表指针指向的前一个元素是type_info
结构体指针,用于实现RTTI(类名和子类信息),两个表之间是分隔元素(编译器决定)这里是-16。
第一个虚表指针指向的虚函数表很重要
其余虚表类似,包含没有被覆盖的虚函数,如果其虚函数被改写了,对应槽位置是一个 thunk 函数,作用是跳转到改写的虚函数地址执行;
想一想为什么这么实现:
可以方便地使用基类的指针来调用虚函数,在派生类被再次继承时,只需要修改它的第一个虚表的内容就作为新的派生类的虚表,实现更加方便。
注意:
存在多个基类时,直接使用派生类指针向上转型时,指针会赋值到该派生对象中该基类的起始地址,因此调用虚函数时,会找到它的虚表指针对应的虚表中的对应位置,如果被override改写会跳转到真实的函数地址实现多态效果。这里的多个虚表指针可以便捷编译器确定在该基类指针下找到对应函数的下标位置(生成虚表时就可以确定),进而可以调用到实际的函数。
由派生类对象地址向上转型可以确定基类的起始地址,从而可以调用对应的虚函数。如果使用:
void* p = &b; // 派生类地址赋值给void*
A1* pa = p; // 再赋值给基类指针
这样会使得基类指针指向的位置直接变成派生类的起始地址,如果不是单继承或者第一个基类会出现问题,必须直接使用派生类地址向上转型得到对应基类地址指针。
在继承体系中,派生类会保存基类的原始布局(包括大小);如果在满足每个基类的内存对齐情况下gcc允许紧密布局,例如上述类B占用32个字节。
// A1: vptr:8B + sizeof(a1):4B + padding:4B total:16B
// A2: vptr:8B + sizeof(a2):4B + [sizeof(b3)] total:16B
// B: total:32B (b3直接和A2中的数据紧密布局)
哪些函数不能定义为虚函数?
友元函数,它不是类的成员函数
全局函数
静态成员函数,它没有this指针
构造函数,拷贝构造函数以及赋值运算符重载(可以但是一般不建议作为虚函数)
构造函数/析构函数中可以调用虚函数吗?
不能;构造派生类对象时,首先调用基类构造函数初始化对象的基类部分。在执行基类构造函数时,对象的派生类部分是未初始化的。析构派生类对象时,首先析构他的派生类部分,然后按照与构造顺序的逆序析构他的基类部分。 在运行构造函数或者析构函数时,对象都是不完整的。为了适应这种不完整,编译器将对象的类型视为在调用构造/析构函数时发生了变换,即:视对象的类型为当前构造函数/析构函数所在的类的类类型。由此造成的结果是:在基类构造函数或者析构函数中,会将派生类对象当做基类类型对象对待。 即如果在构造函数或者析构函数中调用虚函数,运行的都将是为构造函数或者析构函数自身类类型定义的虚函数版本。对象的虚函数表地址在对象的构造和析构过程中会随着部分类的构造和析构而发生变化,这一点应该是编译器实现相关的。
RTTI,即运行时类型检查,其主要作用:
相关的运算符:
typeid: 返回type_info结构体的对象(如果没有虚函数实现多态,返回的是静态类型而不是运行时类型的type_info结构体)
dynamic_cast: 向下类型转换,成功返回子类指针,失败返回nullptr(如果没有实现多态,编译器报错)
#include <iostream>
class Base {
public:
virtual ~Base() {}
};
class Derive : public Base {};
int main() {
Base* pBase = new Derive();
std::cout << typeid(*pBase).name() << std::endl;
Derive* pDerive = nullptr;
if ((pDerive = dynamic_cast<Derive*>(pBase)) != nullptr) {
std::cout << "dynamic_cast success" << std::endl;
} else {
std::cout << "dynamic_cast failed" << std::endl;
}
return 0;
}
RTTI的实现原理
虚函数表中vptr[-1]是type_info
结构体指针,dynamic_cast
也要依据type_info
运行,所以使用typeid
和dynamic_cast
都要有虚函数表,也就是虚函数。
本文链接: C++对象模型-虚函数
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
发布日期: 2022-04-19
最新构建: 2024-12-26
欢迎任何与文章内容相关并保持尊重的评论😊 !