溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C/C++函數(shù)的編譯方式與調用約定以及extern “C”的使用

發(fā)布時間:2021-10-14 15:37:39 來源:億速云 閱讀:135 作者:柒染 欄目:編程語言

C/C++函數(shù)的編譯方式與調用約定以及extern “C”的使用,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。

C/C++:函數(shù)的編譯方式與調用約定以及extern “C”的使用

函數(shù)在C++編譯方式與C編譯方式下的主要不同在于:由于C++引入了函數(shù)重載(overload),因此編譯器對同名函數(shù)進行了名稱重整(name mangle)。因此,在C++中引

用其他C函數(shù)庫時,需要對聲明使用的函數(shù)做適當?shù)奶幚恚愿嬷幾g器做出適應的名稱處理。

函數(shù)的調用約定涉及了函數(shù)參數(shù)的入棧順序、清棧主體(負責清理棧的主體:函數(shù)自身還是調用函數(shù)者?)、部分名稱重整。

如,在C編譯方式下有_stdcall、_cdecl等調用約定,在C++編譯方式下也有_stdcall、_cedecl等調用約定。

兩個復雜修飾的例子:

extern "C" _declspec(dllexport) int __cdecl Add(int a, int b); //C編譯方式導出_cdecl調用約定函數(shù)

typedef int (__cdecl*FunPointer)(int a, int b);


1.編譯方式

c編譯時函數(shù)名修飾約定規(guī)則:

__stdcall調用約定在輸出函數(shù)名前加上一個下劃線前綴,后面加上一個“@”符號和其參數(shù)的字節(jié)數(shù),格式為_functionname@number。 

__cdecl調用約定僅在輸出函數(shù)名前加上一個下劃線前綴,格式為_functionname。 

__fastcall調用約定在輸出函數(shù)名前加上一個“@”符號,后面也是一個“@”符號和其參數(shù)的字節(jié)數(shù),格式@functionname@number。

它們均不改變輸出函數(shù)名中的字符大小寫,這和pascal調用約定不同,pascal約定輸出的函數(shù)名無任何修飾且全部大寫。 

c++編譯時函數(shù)名修飾約定規(guī)則:

__stdcall調用約定:

1、以“?”標識函數(shù)名的開始,后跟函數(shù)名;

2、函數(shù)名后面以“@@yg”標識參數(shù)表的開始,后跟參數(shù)表;

3、參數(shù)表以代號表示:

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--表示指針,后面的代號表明指針類型,如果相同類型的指針連續(xù)出現(xiàn),以“0”代替,一個“0”代表一次重復;

4、參數(shù)表的第一項為該函數(shù)的返回值類型,其后依次為參數(shù)的數(shù)據(jù)類型,指針標識在其所指數(shù)據(jù)類型前; 

5、參數(shù)表后以“@z”標識整個名字的結束,如果該函數(shù)無參數(shù),則以“z”標識結束。

其格式為“?functionname@@yg*****@z”或“?functionname@@yg*xz”,例如 
int test1-----“?test1@@yghpadk@z” 
void test2-----“?test2@@ygxxz”

__cdecl調用約定:

規(guī)則同上面的_stdcall調用約定,只是參數(shù)表的開始標識由上面的“@@yg”變?yōu)椤癅@ya”。

__fastcall調用約定:

規(guī)則同上面的_stdcall調用約定,只是參數(shù)表的開始標識由上面的“@@yg”變?yōu)椤癅@yi”。

2.調用約定

調用約定(Calling Convention)是指在程序設計語言中為了實現(xiàn)函數(shù)調用而建立的一種協(xié)議。這種協(xié)議規(guī)定了該語言的函數(shù)中的參數(shù)傳送方

式、參數(shù)是否可變和由誰來處理堆棧等問題。不同的語言定義了不同的調用約定。

在C++中,為了允許操作符重載和函數(shù)重載,C++編譯器往往按照某種規(guī)則改寫每一個入口點的符號名,以便允許同一個名字(具有不同的參

數(shù)類型或者是不同的作用域)有多個用法,而不會打破現(xiàn)有的基于C的鏈接器。這項技術通常被稱為名稱改編(Name Mangling)或者名稱修

飾(Name Decoration)。許多C++編譯器廠商選擇了自己的名稱修飾方案。

因此,為了使其它語言編寫的模塊(如Visual Basic應用程序、Pascal或Fortran的應用程序等)可以調用C/C++編寫的DLL的函數(shù),必須使

用正確的調用約定來導出函數(shù),并且不要讓編譯器對要導出的函數(shù)進行任何名稱修飾。

調用約定用來:(一)處理決定函數(shù)參數(shù)傳送時入棧和(二)出棧的順序(由調用者還是被調用者把參數(shù)彈出棧),以及(三)編譯器用來識別函數(shù)名

稱的名稱修飾約定等問題。

1、__cdecl

__cdecl是C/C++和MFC程序默認使用的調用約定,也可以在函數(shù)聲明時加上__cdecl關鍵字來手工指定。采用__cdecl約定時,函數(shù)參數(shù)按

照從右到左的順序入棧,并且由調用函數(shù)者把參數(shù)彈出棧以清理堆棧。因此,實現(xiàn)可變參數(shù)的函數(shù)只能使用該調用約定。由于每一個使用

__cdecl約定的函數(shù)都要包含清理堆棧的代碼,所以產(chǎn)生的可執(zhí)行文件大小會比較大。__cdecl可以寫成_cdecl。


2、__stdcall

__stdcall調用約定用于調用Win32 API函數(shù)。采用__stdcal約定時,函數(shù)參數(shù)按照從右到左的順序入棧,被調用的函數(shù)在返回前清理傳送參

數(shù)的棧,函數(shù)參數(shù)個數(shù)固定。由于函數(shù)體本身知道傳進來的參數(shù)個數(shù),因此被調用的函數(shù)可以在返回前用一條ret n指令直接清理傳遞參數(shù)的堆

棧。__stdcall可以寫成_stdcall。

3、__fastcall

__fastcall約定用于對性能要求非常高的場合。__fastcall約定將函數(shù)的從左邊開始的兩個大小不大于4個字節(jié)(DWORD)的參數(shù)分別放在

ECX和EDX寄存器,其余的參數(shù)仍舊自右向左壓棧傳送,被調用的函數(shù)在返回前清理傳送參數(shù)的堆棧。__fastcall可以寫成_fastcall。


關鍵字__cdecl、__stdcall和__fastcall可以直接加在要輸出的函數(shù)前,也可以在編譯環(huán)境的Setting...->C/C++->Code Generation項選

擇。它們對應的命令行參數(shù)分別為/Gd、/Gz和/Gr。缺省狀態(tài)為/Gd,即__cdecl。當加在輸出函數(shù)前的關鍵字與編譯環(huán)境中的選擇不同時,直

接加在輸出函數(shù)前的關鍵字有效。

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函數(shù)都是__stdcall類型的,為什么?

首先,我們談一下兩者之間的區(qū)別:WINDOWS的函數(shù)調用時需要用到棧(STACK,一種先入后出的存儲結構)。當函數(shù)調用

完成后,棧需要清除,這里就是問題的關鍵,如何清除?如果我們的函數(shù)使用了__cdecl,那么棧的清除工作是由調用者,用

COM的術語來講就是客戶來完成的。這樣帶來了一個棘手的問題,不同的編譯器產(chǎn)生棧的方式不盡相同,那么調用者能否正常

的完成清除工作呢?答案是不能。如果使用__stdcall,上面的問題就解決了,函數(shù)自己解決清除工作。所以,在跨(開發(fā))平

臺的調用中,我們都使用__stdcall(雖然有時是以WINAPI的樣子出現(xiàn))。那么為什么還需要_cdecl呢?當我們遇到這樣的函

數(shù)如fprintf()它的參數(shù)是可變的,不定長的,被調用者事先無法知道參數(shù)的長度,事后的清除工作也無法正常的進行,因此,這

種情況我們只能使用_cdecl。


注意:

1、_beginthread需要__cdecl的線程函數(shù)地址,_beginthreadex和CreateThread需要__stdcall的線程函數(shù)地址。

2、一般WIN32的函數(shù)都是__stdcall。而且在Windef.h中有如下的定義:

#define CALLBACK __stdcall

#define WINAPI __stdcall

3、復雜函數(shù)聲明或指針的修飾符示例:

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 __cplusplus

  extern "C" {

  #endif

  /*...*/

  #ifdef __cplusplus

  }

  #endif

  #endif /* __INCvxWorksh */

顯然,頭文件中的編譯宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止該頭文件被重復引用。

那么

#ifdef __cplusplus

extern "C" {

#endif

#ifdef __cplusplus

}

#endif

的作用又是什么呢?

答:被extern "C" 修飾的變量和函數(shù)是按照C語言方式編譯和連接的;即為實現(xiàn)C++與C語言的混合編程。

明白了C++中extern "C"的設立動機,我們下面來具體分析extern "C"通常的使用技巧。

extern "C"的慣用法:

(1)在C++中引用C語言中的函數(shù)和變量,在包含C語言頭文件(假設為cExample.h)時,需進行下列處理:

extern "C"

{

#include "cExample.h"

}

而在C語言的頭文件中,對其外部函數(shù)只能指定為extern類型,C語言中不支持extern "C"聲明,在.c文件中包含了extern "C"時會出現(xiàn)編

譯語法錯誤。

以C++引用C函數(shù)例子工程中包含的三個文件的源代碼如下:

  /* c語言頭文件:cExample.h */

  #ifndef C_EXAMPLE_H

  #define C_EXAMPLE_H

  extern int add(int x,int y);

  #endif

  /* c語言實現(xiàn)文件:cExample.c */

  #include "cExample.h"

  int add( int x, int y )

  {

  return x + y;

  }

 


  // c++實現(xiàn)文件,調用add:cppFile.cpp

  extern "C"

  {

  #include "cExample.h"

  }

  int main(int argc, char* argv[])

  {

  add(2,3);

  return 0;

  }

如果C++調用一個C語言編寫的.DLL時,當包括.DLL的頭文件或聲明接口函數(shù)時,應加extern "C" { }。

(2)在C中引用C++語言中的函數(shù)和變量時,C++的頭文件中的函數(shù)聲明需添加前綴extern "C",但是在C語言中不能直接引用

已由extern "C"修飾過的函數(shù)聲明或變量的頭文件(因為C編譯方式不支持extern “C” 關鍵字),應該在C中將需要引用的C++

函數(shù)的聲明為extern類型。

以C引用C++函數(shù)例子工程中包含的三個文件的源代碼如下:

  //C++頭文件 cppExample.h

  #ifndef CPP_EXAMPLE_H

  #define CPP_EXAMPLE_H

  extern "C" int add( int x, int y );

  #endif

  //C++實現(xiàn)文件 cppExample.cpp

  #include "cppExample.h"

  int add( int x, int y )

  {

  return x + y;

  }

 

  /* C實現(xiàn)文件 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來修飾函數(shù),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程序和調用其輸出函數(shù)的程序的關系:

1)、DLL與進程、線程之間的關系

DLL模塊被映射到調用它的進程的虛擬地址空間。

DLL使用的內存從調用進程的虛擬地址空間分配,只能被該進程的線程所訪問。

DLL的句柄可以被調用進程使用;調用進程的句柄可以被DLL使用。

DLL可以有自己的數(shù)據(jù)段,但沒有自己的堆棧,使用調用進程的棧,與調用它的應用程序相同的堆棧模式。

2)、關于共享數(shù)據(jù)段

DLL定義的全局變量可以被調用進程訪問;DLL可以訪問調用進程的全局數(shù)據(jù)。使用同一DLL的每一個進程都有自己的DLL全局

變量實例。如果多個線程并發(fā)訪問同一變量,則需要使用同步機制;對一個DLL的變量,如果希望每個使用DLL的線程都有自己

的值,則應該使用線程局部存儲(TLS,Thread Local Strorage).


關于C/C++函數(shù)的編譯方式與調用約定以及extern “C”的使用問題的解答就分享到這里了,希望以上內容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關注億速云行業(yè)資訊頻道了解更多相關知識。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。

AI