由extern "C"引申出C++、C動態庫調用的一些注意事項
extern "C"和__stdcall/__cdecl這兩個概念都是C和C++語言混用時需要關注的。extern "C"是代碼段的修飾, 既可以單獨對函數進行修飾也可以放在代碼片段前對整段代碼進行修飾;是告知編譯器接下來的代碼中所有的函數名要以C語言的方式進行解析;_stdcall和_cdecl則是對函數名進行修飾,告知編譯器函數名應該按何種方式進行解析。
1. extern C
1.1 為什么要加extern C
工程中經常會遇到C和C++混合編程的情況,有時是C++的工程中需要使用C語言編寫的庫,有時是C的工程需要使用C++;但如果不加任何修飾,直接調用的話,就會遇到鏈接問題,提示找不到函數名稱。
這是由於C++在編譯時候,會將函數名做一些修飾,在函數名前加上函數名的長度,在函數名后面加上參數類型。鏈接的時候也使用相同的策略。C++這么做是由於C++語言支持函數重載,在函數名相同參數不同的幾個函數也可以共存。C語言不支持重載,所以編譯和鏈接時對函數名不會加修飾。加extern ”C“就是為了讓編譯器以C語言的函數名處理方式來編譯。
使用場景主要有兩種:
(1)C++的模塊調用C語言寫的庫
C語言test.h, test.c創建的動態庫libtest.so
#### test.h文件如下
1 #include "stdio.h"
2
3 void test_add(int a, int b);
#### test.c文件如下
1 #include "test.h"
2
3 void test_add(int a, int b)
4 {
5 int num = a + b;
6 printf("%d + %d = %d\n", a, b, num);
7 }
#### 編譯命令
gcc test.c -fPIC -shared -o libtest.so
#### C++的文件caller_cplusplus.cpp調用
1 #ifdef __cplusplus
2 extern "C"
3 {
4 #endif
5 #ifdef __cplusplus
6 #include "test.h"
7 }
8 #endif
9 int main()
10 {
11 test_add(3, 4);
12 return 0;
13 }
#### 編譯命令
g++ -o callerCpp caller_cplusplus.cpp -L. -ltest
(2)C++頭文件聲明接口函數
在C中引用C++語言中的函數和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明了extern "C"的該頭文件,應該僅將C文件中將C++中定義的extern "C"函數聲明為extern類型。
筆者編寫的C引用C++函數例子工程中包含的三個文件的源代碼如下:
//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H
#define CPP_EXAMPLE_H
extern "C" int add( int x, int y );
#endif
//C++實現文件 cppExample.cpp
#include "cppExample.h"
int add( int x, int y )
{
return x + y;
}
/* C實現文件 cFile.c*/
/* 這樣會編譯出錯:#include "cExample.h" */
extern int add( int x, int y );
int main( int argc, char* argv[] )
{
add( 2, 3 );
return 0;
}
上面介紹的是extern "C"的含義和使用方法,主要是跨C和C++的庫調用時需要注意,那么除了extern "C"之外還有哪些是在動態庫調用過程中經常遇到的坑呢?接下來的部分是個人總結的幾個需要注意的點。
2. 動態庫調用的注意事項
2.1 不同版本的VS編譯器編譯的庫
VS是比較常用的IDE,在公司中也經常要調用其他事業部提供的動態庫;當不同組之間使用的VS版本不同時,如果接口中使用了不通用的特性,就會導致動態庫無法調用或者調用過程中編譯失敗
比如說A.dll庫是由VS2013開發的,A.dll的接口中包含了C++11的獨特的數據結構或者新特性。VS2008開發的B.exe想要調用A.dll就會出現問題。另外不同版本的VS支持的MFC庫也不同,如果調用的庫在編譯時依賴了MFC,那么自己的程序也要和調用庫使用相同版本的VS
2.2 __cdecl、__stdcall
這兩個是函數名修飾規則定義,也定義了參數的壓棧方式和清理棧的規則;這里注意到和extern "C"不同,extern "C"是告知編譯器如何解析函數名,主要用途是解決跨語言調用(C和C++)而__cdecl/__stdcall則是規定了函數名應該如何修飾,以及參數壓棧方式,棧由誰清理
函數名修飾規則:
__stdcall :約定在輸出函數名前加上一個下划線前綴,后面加上一個“@”符號和其參數的字節數,格式為_functionname@number(number即參數棧長度)
__cdecl :約定僅在輸出函數名前加上一個下划線前綴,格式為_functionname
參數棧的管理:
兩種方式定義的參數傳遞方式都是從右至左壓,區別在於清理棧的角色不同。
__stdcall:是被調用函數清理(即函數自己清理)
__cdecl:是調用者清理;
所以__stdcall修飾函數名時加上參數棧的大小可以讓函數自己進行棧的清理;對於可變參數的函數,無法在函數定義時確認參數長度,只能讓調用者去清理參數棧,所以對於可變參數的函數應該用__cdecl修飾(可變參數的函數exp:printf)
2.3 CRT鏈接選項(C運行時庫的鏈接選擇)
一個原則,調用者和庫的CRT鏈接選項要相同,盡量都使用/MD選項 (多線程動態鏈接)。
遇到過一個坑,調用的庫和自己程序的CRT選項不同,庫函數中返回了一個字符串指針,內存在庫中申請,而釋放時需要在程序中釋放;由於程序和庫使用的CRT不同,導致內存操作的堆不同,庫函數申請的堆空間無法在自己程序中釋放掉,出現了崩潰。
引用:
https://blog.csdn.net/weiwangchao_/article/details/4681813
https://www.cnblogs.com/jueyunqi/p/4140141.html