前言:
假设你的应用程序引用的一个库某天更新了,虽然 API 和调用方式基本没变,但你需要重新编译你的应用程序才能使用这个库,那么一般说这个库是源码兼容(Source compatible);反之,如果不需要重新编译应用程序就能使用新版本的库,那么说这个库跟它之前的版本是二进制兼容的(Binary compatible)。
👉👉👉
而影响ABI兼容中,最重要的部分涉及到虚表机制,这块我们重点来谈下它们之间的关系。
文章目录
- 虚表生成
- 子类的虚表
- C++ 类虚表中函数顺序规则
- 搞清楚虚表有什么用?
- 导出DLL注意事项
- C++ 虚析构函数在虚表中位置说明
- 更多ABI 相关文章
虚表生成
C++的类只要有一个虚函数,就会生成一张虚表:
class A
{
};
class B
{
public:
virtual void vfunc1();
}
sizeof(A) = 1 // 空类1个字节用于地址定位
sizeof(B) = 4 // 有虚表指针,占sizeof(void*)字节
子类的虚表
Visual Studio 可以使用自带的命令行工具查看类的内存布局。在 Visual Studio 2022 中是如下工具:
命令是:cl /d1 reportSingleClassLayout<ClassName> xxx.cpp
例如:cl /d1 reportSingleClassLayoutA demo.cpp
即,在 demo.cpp
中查看 class A 的内存布局。
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;
};
class 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
class 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;
void vfunc4();
void vfunc4(int x);
virtual void vfunc1(int x, int y) = 0;
};
class B : public A
{
public:
virtual void vfunc1(int x) = 0;
virtual void vfunc4() = 0;
void vfunc5();
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:
// awesome.h
class IAwesomeSDK
{
public:
virtual void foo() = 0;
virtual void bar(int x) = 0;
};
extern "C" {
// 创建SDK实例
IAwesomeSDK *createAwesomeInstance();
// 销毁SDK实例
void destroyAwesomeInstance();
} // extern "C"
// 二次开发用户这样对其进行使用:
// demo.cpp
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; // 错误,模块内申请,模块外释放
};
C++ 虚析构函数在虚表中位置说明
先说结论:如果类中含有虚析构函数,其受约束和普通虚函数一致:
- 从基类开始,按照申明顺序每遇到一个不是重写的虚函数,就记录在表中
- 如果有重载,则提前重载的虚函数
- 依次循环遍历子类,如果遇到重写,则替换相应的虚函数
数据测试如下:(环境:Visual Studio 2022,默认配置)
-
测试项1:没有虚析构函数时,虚表中的排布情况如下图:
-
测试项2:虚析构函数位于类首时:
-
测试项3:虚析构函数位于有重载的虚函数前面时:
-
测试项4:虚析构函数位于非重载的虚函数前面时:
更多ABI 相关文章
- 【1】C++ 编程必看!超万字深度解析API与ABI兼容性的关键问题