前言:
假設你的應用程序引用的一個庫某天更新了,雖然 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| 00 | &A::vfunc11 | &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| 00 | &A::vfunc11 | &C2::vfunc3C2::$vftable@B@:| -80 | &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兼容性的關鍵問題