虚表生成
C++的类只要有一个虚函数,就会生成一张虚表
class A
{
};
class B
{
public:
virtual void vfunc1();
}
sizeof(A) = 1 // 空类1个字节用于地址定位
sizeof(B) = 4 // 有虚表指针,占sizeof(void*)字节
子类的虚表
VisualStudio可以使用 cl Test.cpp -d1 reportSingleClassLayoutC 查看内存布局 reportSingleClassLayout<类名>,例如 reportSingleClassLayoutC,是查看类C 的布局
class A
{
public:
virtual void vfunc1();
private:
int a;
};
class B
{
public:
virtual void vfunc2();
private:
int b;
};
class C1 : public A
{
public:
virtual void vfunc3();
private:
int c;
};
class C2 : public A, public B
{
public:
virtual void vfunc3();
private:
int c;
};
类 C1 的内存布局
class C1 size(12):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | a
| +---
8 | c
+---
C1::$vftable@:
| &C1_meta
| 0
0 | &A::vfunc1
1 | &C1::vfunc3
类 C2 的内存布局
class C2 size(20):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | a
| +---
8 | +--- (base class B)
8 | | {vfptr}
12 | | b
| +---
16 | c
+---
C2::$vftable@A@:
| &C2_meta
| 0
0 | &A::vfunc1
1 | &C2::vfunc3
C2::$vftable@B@:
| -8
0 | &B::vfunc2
C++类虚表中函数顺序规则
- 从基类开始,按照申明顺序每遇到一个不是重写的虚函数,就记录在表中
- 如果有重载,则提前重载的虚函数
- 依次循环遍历子类,如果遇到重写,则替换相应的虚函数
举例
class A
{
public:
virtual void vfunc1() = 0;
virtual void vfunc2() = 0;
virtual void vfunc1(int x) = 0;
virtual void vfunc3() = 0;
virtual void vfunc1(int x, int y) = 0;
};
class B : public A
{
public:
virtual void vfunc1(int x) = 0;
virtual void vfunc4() = 0;
virtual void vfunc2(int x) = 0
}
请问B的虚表是应该是什么样的?
- 遍历A中的虚函数
void A::vfunc1();
由于 vfunc1 有两个重载,按照第 2 条规则,依次提前重载函数
void A::vfunc1();
void A::vfunc1(int x);
void A::vfunc1(int x, int y);
- 继续遍历A中的虚函数
void A::vfunc1();
void A::vfunc1(int x);
void A::vfunc1(int x, int y);
void A::vfunc2();
void A::vfunc3();
- 由于
B 重写了 A 的 void vfunc1(int x) 函数,所以将表中对应的函数替换
void A::vfunc1();
void B::vfunc1(int x);
void A::vfunc1(int x, int y);
void A::vfunc2();
void A::vfunc3();
- 添加
B::vfunc4() 到虚表中
void A::vfunc1();
void B::vfunc1(int x);
void A::vfunc1(int x, int y);
void A::vfunc2();
void A::vfunc3();
void B::vfunc4();
- 由于
B::vfunc2(int x) 没有重写A中的函数,按照规则 1 添加到虚表中
void A::vfunc1();
void B::vfunc1(int x);
void A::vfunc1(int x, int y);
void A::vfunc2();
void A::vfunc3();
void B::vfunc4();
void B::vfunc2(int x);
搞清楚虚表有什么用?
答:ABI兼容
举例
你写了这样一个SDK
class IAwesomeSDK
{
public:
virtual void foo() = 0;
virtual void bar(int x) = 0;
};
extern "C" {
// 创建SDK实例
IAwesomeSDK *createAwesomeInstance();
// 销毁SDK实例
void destroyAwesomeInstance();
} // extern "C"
调用方使用时一般这么写
int main(int argc, char **argv)
{
IAwesomeSDK *sdk = createAwesomeInstance();
sdk->foo();
sdk->bar();
destroyAwesomeInstance();
return 0;
}
如果保证新发布的动态库可以兼容之前的程序(集成DLL的程序不需要重新编译,就可以使用新DLL),那么动态库中添加功能需要注意
- 只能在类最后添加新函数
class IAwesomeSDK
{
public:
virtual void feature1() = 0; // 错误
virtual void foo() = 0;
virtual void bar(int x) = 0;
};
- 添加的新函数不可以与旧函数重名(重载)
class IAwesomeSDK
{
public:
virtual void foo() = 0;
virtual void bar(int x) = 0;
virtual void bar() = 0; // 错误
};
- 不可以修改旧函数的签名(参数,返回值,限定符等)
class IAwesomeSDK
{
public:
virtual void foo(int x = 0) = 0; // 错误
virtual void bar(int x) = 0;
};
- 不可以重新排序旧函数
class IAwesomeSDK
{
public:
virtual void bar(int x) = 0; // 错误
virtual void foo() = 0; // 错误
};
这时你要添加一个新功能,还希望旧程序可以不重新编译替换新DLL,你可以这么做
class IAwesomeSDK
{
public:
virtual void foo() = 0;
virtual void bar(int x) = 0;
virtual void feature() = 0; // 正确
};
导出DLL注意事项
- 申请和释放保持在同一模块
最好不要在接口处使用STL库,除非编译器选项一致、STL实现一致、系统平台一致
class IAwesomeSDK
{
public:
virtual void foo() = 0;
virtual void bar(int x) = 0;
virtual std::string feature() = 0; // 错误,模块内申请,模块外释放
};
|