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);


轉自:http://blog.csdn.net/H2SO2H2SO2/article/details/4207127

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 __cplusplus
  extern "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_H
  extern 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.cpp
  extern "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_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",因爲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).

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章