您好,登錄后才能下訂單哦!
C/C++函數的編譯方式與調用約定以及extern “C”的使用,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。
函數在C++編譯方式與C編譯方式下的主要不同在于:由于C++引入了函數重載(overload),因此編譯器對同名函數進行了名稱重整(name mangle)。因此,在C++中引
用其他C函數庫時,需要對聲明使用的函數做適當的處理,以告知編譯器做出適應的名稱處理。
函數的調用約定涉及了函數參數的入棧順序、清棧主體(負責清理棧的主體:函數自身還是調用函數者?)、部分名稱重整。
如,在C編譯方式下有_stdcall、_cdecl等調用約定,在C++編譯方式下也有_stdcall、_cedecl等調用約定。
兩個復雜修飾的例子:
extern "C" _declspec(dllexport) int __cdecl Add(int a, int b); //C編譯方式導出_cdecl調用約定函數
typedef int (__cdecl*FunPointer)(int a, int b);
1.編譯方式
c編譯時函數名修飾約定規則:
__stdcall調用約定在輸出函數名前加上一個下劃線前綴,后面加上一個“@”符號和其參數的字節數,格式為_functionname@number。
__cdecl調用約定僅在輸出函數名前加上一個下劃線前綴,格式為_functionname。
__fastcall調用約定在輸出函數名前加上一個“@”符號,后面也是一個“@”符號和其參數的字節數,格式@functionname@number。
它們均不改變輸出函數名中的字符大小寫,這和pascal調用約定不同,pascal約定輸出的函數名無任何修飾且全部大寫。
c++編譯時函數名修飾約定規則:
__stdcall調用約定:
1、以“?”標識函數名的開始,后跟函數名;
2、函數名后面以“@@yg”標識參數表的開始,后跟參數表;
3、參數表以代號表示:
x--void ,
d--char,
e--unsigned char,
f--short,
h--int,
i--unsigned int,
j--long,
k--unsigned long,
m--float,
n--double,
_n--bool,
....
pa--表示指針,后面的代號表明指針類型,如果相同類型的指針連續出現,以“0”代替,一個“0”代表一次重復;
4、參數表的第一項為該函數的返回值類型,其后依次為參數的數據類型,指針標識在其所指數據類型前;
5、參數表后以“@z”標識整個名字的結束,如果該函數無參數,則以“z”標識結束。
其格式為“?functionname@@yg*****@z”或“?functionname@@yg*xz”,例如
int test1-----“?test1@@yghpadk@z”
void test2-----“?test2@@ygxxz”
__cdecl調用約定:
規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@yg”變為“@@ya”。
__fastcall調用約定:
規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@yg”變為“@@yi”。
2.調用約定
調用約定(Calling Convention)是指在程序設計語言中為了實現函數調用而建立的一種協議。這種協議規定了該語言的函數中的參數傳送方
式、參數是否可變和由誰來處理堆棧等問題。不同的語言定義了不同的調用約定。
在C++中,為了允許操作符重載和函數重載,C++編譯器往往按照某種規則改寫每一個入口點的符號名,以便允許同一個名字(具有不同的參
數類型或者是不同的作用域)有多個用法,而不會打破現有的基于C的鏈接器。這項技術通常被稱為名稱改編(Name Mangling)或者名稱修
飾(Name Decoration)。許多C++編譯器廠商選擇了自己的名稱修飾方案。
因此,為了使其它語言編寫的模塊(如Visual Basic應用程序、Pascal或Fortran的應用程序等)可以調用C/C++編寫的DLL的函數,必須使
用正確的調用約定來導出函數,并且不要讓編譯器對要導出的函數進行任何名稱修飾。
調用約定用來:(一)處理決定函數參數傳送時入棧和(二)出棧的順序(由調用者還是被調用者把參數彈出棧),以及(三)編譯器用來識別函數名
稱的名稱修飾約定等問題。
1、__cdecl
__cdecl是C/C++和MFC程序默認使用的調用約定,也可以在函數聲明時加上__cdecl關鍵字來手工指定。采用__cdecl約定時,函數參數按
照從右到左的順序入棧,并且由調用函數者把參數彈出棧以清理堆棧。因此,實現可變參數的函數只能使用該調用約定。由于每一個使用
__cdecl約定的函數都要包含清理堆棧的代碼,所以產生的可執行文件大小會比較大。__cdecl可以寫成_cdecl。
2、__stdcall
__stdcall調用約定用于調用Win32 API函數。采用__stdcal約定時,函數參數按照從右到左的順序入棧,被調用的函數在返回前清理傳送參
數的棧,函數參數個數固定。由于函數體本身知道傳進來的參數個數,因此被調用的函數可以在返回前用一條ret n指令直接清理傳遞參數的堆
棧。__stdcall可以寫成_stdcall。
3、__fastcall
__fastcall約定用于對性能要求非常高的場合。__fastcall約定將函數的從左邊開始的兩個大小不大于4個字節(DWORD)的參數分別放在
ECX和EDX寄存器,其余的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的堆棧。__fastcall可以寫成_fastcall。
關鍵字__cdecl、__stdcall和__fastcall可以直接加在要輸出的函數前,也可以在編譯環境的Setting...->C/C++->Code Generation項選
擇。它們對應的命令行參數分別為/Gd、/Gz和/Gr。缺省狀態為/Gd,即__cdecl。當加在輸出函數前的關鍵字與編譯環境中的選擇不同時,直
接加在輸出函數前的關鍵字有效。
3._stdcall與_cdecl調用約定對比
在“windef.h”頭文件中可找到:
#define CALLBACK __stdcall#define WINAPI __stdcall#define WINAPIV __cdecl#define APIENTRY WINAPI#define APIPRIVATE __stdcall#define PASCAL __stdcall#define cdecl _cdecl#ifndef CDECL#define CDECL _cdecl#endif
幾乎我們寫的每一個WINDOWS API函數都是__stdcall類型的,為什么?
首先,我們談一下兩者之間的區別:WINDOWS的函數調用時需要用到棧(STACK,一種先入后出的存儲結構)。當函數調用
完成后,棧需要清除,這里就是問題的關鍵,如何清除?如果我們的函數使用了__cdecl,那么棧的清除工作是由調用者,用
COM的術語來講就是客戶來完成的。這樣帶來了一個棘手的問題,不同的編譯器產生棧的方式不盡相同,那么調用者能否正常
的完成清除工作呢?答案是不能。如果使用__stdcall,上面的問題就解決了,函數自己解決清除工作。所以,在跨(開發)平
臺的調用中,我們都使用__stdcall(雖然有時是以WINAPI的樣子出現)。那么為什么還需要_cdecl呢?當我們遇到這樣的函
數如fprintf()它的參數是可變的,不定長的,被調用者事先無法知道參數的長度,事后的清除工作也無法正常的進行,因此,這
種情況我們只能使用_cdecl。
注意:
1、_beginthread需要__cdecl的線程函數地址,_beginthreadex和CreateThread需要__stdcall的線程函數地址。
2、一般WIN32的函數都是__stdcall。而且在Windef.h中有如下的定義:
#define CALLBACK __stdcall
#define WINAPI __stdcall
3、復雜函數聲明或指針的修飾符示例:
extern "C" _declspec(dllexport) int __cdecl Add(int a, int b);
typedef int (__cdecl*FunPointer)(int a, int b);
4、extern ”C” 的作用(參考:http://hi.baidu.com/qinfengxiaoyue/item/8bd89e81d1cbeb5226ebd9b4)
為什么標準頭文件都有類似以下的結構?
#ifndef __INCvxWorksh#define __INCvxWorksh#ifdef __cplusplusextern "C" {#endif/*...*/#ifdef __cplusplus}#endif#endif /* __INCvxWorksh */顯然,頭文件中的編譯宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止該頭文件被重復引用。
那么
#ifdef __cplusplus
extern "C" {
#endif
#ifdef __cplusplus
}
#endif
的作用又是什么呢?
答:被extern "C" 修飾的變量和函數是按照C語言方式編譯和連接的;即為實現C++與C語言的混合編程。
明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧。
extern "C"的慣用法:
(1)在C++中引用C語言中的函數和變量,在包含C語言頭文件(假設為cExample.h)時,需進行下列處理:
extern "C"
{
#include "cExample.h"
}
而在C語言的頭文件中,對其外部函數只能指定為extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現編
譯語法錯誤。
以C++引用C函數例子工程中包含的三個文件的源代碼如下:
/* c語言頭文件:cExample.h */#ifndef C_EXAMPLE_H#define C_EXAMPLE_Hextern int add(int x,int y);#endif/* c語言實現文件:cExample.c */#include "cExample.h"int add( int x, int y ){return x + y;}
// c++實現文件,調用add:cppFile.cppextern "C"{#include "cExample.h"}int main(int argc, char* argv[]){add(2,3);return 0;}如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數時,應加extern "C" { }。
(2)在C中引用C++語言中的函數和變量時,C++的頭文件中的函數聲明需添加前綴extern "C",但是在C語言中不能直接引用
已由extern "C"修飾過的函數聲明或變量的頭文件(因為C編譯方式不支持extern “C” 關鍵字),應該在C中將需要引用的C++
中函數的聲明為extern類型。
以C引用C++函數例子工程中包含的三個文件的源代碼如下:
//C++頭文件 cppExample.h#ifndef CPP_EXAMPLE_H#define CPP_EXAMPLE_Hextern "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",因為C編譯不支持extern "C" 關鍵字 */extern int add( int x, int y );int main( int argc, char* argv[] ){add( 2, 3 );return 0;}
5、MFC提供了一些宏,可以使用AFX_EXT_CLASS來代替__declspec(DLLexport),并修飾類名,從而導出類,
AFX_API_EXPORT來修飾函數,AFX_DATA_EXPORT來修飾變量
AFX_CLASS_IMPORT:__declspec(DLLexport)
AFX_API_IMPORT:__declspec(DLLexport)
AFX_DATA_IMPORT:__declspec(DLLexport)
AFX_CLASS_EXPORT:__declspec(DLLexport)
AFX_API_EXPORT:__declspec(DLLexport)
AFX_DATA_EXPORT:__declspec(DLLexport)
AFX_EXT_CLASS:#ifdef _AFXEXT
AFX_CLASS_EXPORT
#else
AFX_CLASS_IMPORT
6、DLLMain負責初始化(Initialization)和結束(Termination)工作,每當一個新的進程或者該進程的新的線程訪問DLL時,或
者訪問DLL的每一個進程或者線程不再使用DLL或者結束時,都會調用DLLMain。但是,使用TerminateProcess或
TerminateThread結束進程或者線程,不會調用DLLMain。
7、一個DLL在內存中只有一個實例
DLL程序和調用其輸出函數的程序的關系:
1)、DLL與進程、線程之間的關系
DLL模塊被映射到調用它的進程的虛擬地址空間。
DLL使用的內存從調用進程的虛擬地址空間分配,只能被該進程的線程所訪問。
DLL的句柄可以被調用進程使用;調用進程的句柄可以被DLL使用。
DLL可以有自己的數據段,但沒有自己的堆棧,使用調用進程的棧,與調用它的應用程序相同的堆棧模式。
2)、關于共享數據段
DLL定義的全局變量可以被調用進程訪問;DLL可以訪問調用進程的全局數據。使用同一DLL的每一個進程都有自己的DLL全局
變量實例。如果多個線程并發訪問同一變量,則需要使用同步機制;對一個DLL的變量,如果希望每個使用DLL的線程都有自己
的值,則應該使用線程局部存儲(TLS,Thread Local Strorage).
關于C/C++函數的編譯方式與調用約定以及extern “C”的使用問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業資訊頻道了解更多相關知識。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。