天天看點

__stdcall,__cdecl,_declspec,__fastcall的差別

DWORD WINAPI ThreadProc(LPVOID lpParameter);

不解為什麼要用WINAPI宏定義,查了後發現下面的定義。于是乎需要差別

__stdcall 和 __cdecl兩者的差別:

#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

1.幾乎我們寫的每一個WINDOWS API函數都是__stdcall類型的,首先,需要了解兩者之間的差別: WINDOWS的函數調用時需要用到棧(STACK,一種先入後出的存儲結構)。當函數調用完成後,棧需要清除,這裡就是問題的關鍵,如何清除??如果我們的函數使用了_cdecl,那麼棧的清除工作是由調用者,用COM的術語來講就是客戶來完成的。這樣帶來了一個棘手的問題,不同的編譯器産生棧的方式不盡相同,那麼調用者能否正常的完成清除工作呢?答案是不能。如果使用__stdcall,上面的問題就解決了,函數自己解決清除工作。是以,在跨(開發)平台的調用中,我們都使用__stdcall(雖然有時是以 WINAPI的樣子出現)。那麼為什麼還需要_cdecl呢?當我們遇到這樣的函數如fprintf()它的參數是可變的,不定長的,被調用者事先無法知道參數的長度,事後的清除工作也無法正常的進行,是以,這種情況我們隻能使用_cdecl。到這裡我們有一個結論,如果你的程式中沒有涉及可變參數,最好使用__stdcall關鍵字。

2.__cdecl,__stdcall是聲明的函數調用協定.主要是傳參和彈棧方面的不同.一般c++用的是__cdecl,windows裡大都用的是__stdcall(API)。

__cdecl 是C/C++和MFC程式預設使用的調用約定,也可以在函數聲明時加上__cdecl關鍵字來手工指定。采用__cdecl約定時,函數參數按照從右到左的順序入棧,并且由調用函數者把參數彈出棧以清理堆棧。是以,實作可變參數的函數隻能使用該調用約定。由于每一個使用__cdecl約定的函數都要包含清理堆棧的代碼,是以産生的可執行檔案大小會比較大。__cdecl可以寫成_cdecl。

__stdcall調用約定用于調用Win32 API函數。采用__stdcall約定時,函數參數按照從右到左的順序入棧,被調用的函數在傳回前清理傳送參數的棧,函數參數個數固定。由于函數體本身知道傳進來的參數個數,是以被調用的函數可以在傳回前用一條ret n指令直接清理傳遞參數的堆棧。__stdcall可以寫成_stdcall。

__fastcall 約定用于對性能要求非常高的場合。__fastcall約定将函數的從左邊開始的兩個大小不大于4個位元組(DWORD)的參數分别放在ECX和EDX寄存器,其餘的參數仍舊自右向左壓棧傳送,被調用的函數在傳回前清理傳送參數的堆棧。__fastcall可以寫成_fastcall

3. __stdcall:

_stdcall 調用約定相當于16位動态庫中經常使用的PASCAL調用約定。在32位的VC++5.0中PASCAL調用約定不再被支援(實際上它已被定義為 __stdcall。除了__pascal外,__fortran和__syscall也不被支援),取而代之的是__stdcall調用約定。兩者實質上是一緻的,即函數的參數自右向左通過棧傳遞,被調用的函數在傳回前清理傳送參數的記憶體棧,但不同的是函數名的修飾部分(關于函數名的修飾部分在後面将詳細說明)。

_stdcall是Pascal程式的預設調用方式,通常用于Win32 Api中,函數采用從右到左的壓棧方式,自己在退出時清空堆棧。VC将函數編譯後會在函數名前面加上下劃線字首,在函數名後加上"@"和參數的位元組數。

_cdecl:

_cdecl c調用約定, 按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對于傳送參數的記憶體棧是由調用者來維護的(正因為如此,實作可變參數的函數隻能使用該調用約定)。另外,在函數名修飾約定方面也有所不同。

_cdecl是C和C++程式的預設調用方式。每一個調用它的函數都包含清空堆棧的代碼,是以産生的可執行檔案大小會比調用_stdcall函數的大。函數采用從右到左的壓棧方式。VC将函數編譯後會在函數名前面加上下劃線字首。是MFC預設調用約定。

__fastcall:

__fastcall調用約定是"人"如其名,它的主要特點就是快,因為它是通過寄存器來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的參數,剩下的參數仍舊自右向左壓棧傳送,被調用的函數在傳回前清理傳送參數的記憶體棧),在函數名修飾約定方面,它和前兩者均不同。

_fastcall方式的函數采用寄存器傳遞參數,VC将函數編譯後會在函數名前面加上"@"字首,在函數名後加上"@"和參數的位元組數。

thiscall:

thiscall僅僅應用于"C++"成員函數。this指針存放于CX寄存器,參數從右到左壓。thiscall不是關鍵詞,是以不能被程式員指定。

naked call:

采用1-4的調用約定時,如果必要的話,進入函數時編譯器會産生代碼來儲存ESI,EDI,EBX,EBP寄存器,退出函數時則産生代碼恢複這些寄存器的内容。

naked call不産生這樣的代碼。naked call不是類型修飾符,故必須和_declspec共同使用。

備注:大多數API都采用__stdcall調用規範,這是因為幾乎所有的語言都支援__stdcall調用.相比之下,__cdecl隻有在C語言中才能用.但是__cdecl調用有一個特點,就是能夠實作可變參數的函數調用,比如printf,這用__stdcall調用是不可能的.

__fastcall這種調用規範比較少見,但是在Borland C++ Builder中比較多的采用了這種調用方式.

如果有共享代碼的需要,比如寫DLL,推薦的方法是用__stdcall調用,因為這樣适用範圍最廣.如果是C++語言寫的代碼供Delphi這樣的語言調用就必須聲明為__stdcall,因為Pascal不支援cdecl調用(或許Delphi的最新版本能夠支援也說不定,這個我不太清楚).在其他一些地方,比如寫COM元件,幾乎都用的是stdcall調用.在VC或Delphi或C++Builder裡面都可以從項目設定中更改預設的函數調用規範,當然你也可以在函數聲明的時候加入__stdcall,__cdecl,__fastcall關鍵字來明确的訓示本函數用哪種調用規範.

4. __declspec:

__declspec主要是用于說明DLL的引出函數的,在某些情況下用__declspec(dllexport)在DLL中生命引出函數,比用傳統的DEF檔案友善一些.在普通程式中也可以用__declspec(dllimport)說明函數是位于另一個DLL中的導出函數。

另附:

關鍵字 __stdcall、__cdecl和__fastcall可以直接加在要輸出的函數前,也可以在編譯環境的Setting.../C/C++ /Code Generation項選擇。當加在輸出函數前的關鍵字與編譯環境中的選擇不同時,直接加在輸出函數前的關鍵字有效。它們對應的指令行參數分别為/Gz、 /Gd和/Gr。預設狀态為/Gd,即__cdecl。

要完全模仿PASCAL調用約定首先必須使用__stdcall調用約定,至于函數名修飾約定,可以通過其它方法模仿。還有一個值得一提的是WINAPI宏,Windows.h支援該宏,它可以将出函數翻譯成适當的調用約定,在WIN32中,它被定義為__stdcall。使用WINAPI宏可以建立自己的APIs。

名字修飾約定

1、 修飾名(Decoration name)

“C” 或者“C++”函數在内部(編譯和連結)通過修飾名識别。修飾名是編譯器在編譯函數定義或者原型時生成的字元串。有些情況下使用函數的修飾名是必要的,如在子產品定義檔案裡頭指定輸出“C++”重載函數、構造函數、析構函數,又如在彙編代碼裡調用“C””或“C++”函數等。

修飾名由函數名、類名、調用約定、傳回類型、參數等共同決定。

2、名字修飾約定随調用約定和編譯種類(C或C++)的不同而變化。函數名修飾約定随編譯種類和調用約定的不同而不同,下面分别說明。

a、 C編譯時函數名修飾約定規則:

__stdcall調用約定在輸出函數名前加上一個下劃線字首,後面加上一個“@”符号和其參數的位元組數,格式為[email protected]。

__cdecl調用約定僅在輸出函數名前加上一個下劃線字首,格式為_functionname。

__fastcall調用約定在輸出函數名前加上一個“@”符号,後面也是一個“@”符号和其參數的位元組數,格式為@[email protected]。

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

b、 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(char *var1,unsigned long)-----“?Test1@@[email protected]”

void Test2() -----“?Test2@@YGXXZ”

__cdecl調用約定:

規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的“@@YG”變為“@@YA”。

__fastcall調用約定:

規則同上面的_stdcall調用約定,隻是參數表的開始辨別由上面的“@@YG”變為“@@YI”。

VC++對函數的省缺聲明是“__cedcl“,将隻能被C/C++調用.

CB在輸出函數聲明時使用4種修飾符号

//__cdecl

cb的預設值,它會在輸出函數名前加_,并保留此函數名不變,參數按照從右到左的順序依次傳遞給棧,也可以寫成_cdecl和cdecl形式。

//__fastcall

她修飾的函數的參數将盡肯呢感地使用寄存器來處理,其函數名前加@,參數按照從左到右的順序壓棧;

//__pascal

它說明的函數名使用Pascal格式的命名約定。這時函數名全部大寫。參數按照從左到右的順序壓棧;

//__stdcall

使用标準約定的函數名。函數名不會改變。使用__stdcall修飾時。參數按照由右到左的順序壓棧,也可以是_stdcall;

VC++對函數的省缺聲明是"__cedcl",将隻能被C/C++調用.

注意:

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"的作用:如果Add(int a, int b)是在c語言編譯器編譯,而在c++檔案使用,則需要在c++檔案中聲明:extern "C" Add(int a, int b),因為c編譯器和c++編譯器對函數名的解釋不一樣(c++編譯器解釋函數名的時候要考慮函數參數,這樣是了友善函數重載,而在c語言中不存在函數重載的問題),使用extern "C",實質就是告訴c++編譯器,該函數是c庫裡面的函數。如果不使用extern "C"則會出現連結錯誤。

一般象如下使用:

#ifdef _cplusplus

#define EXTERN_C extern "C"

#else

#define EXTERN_C extern

#endif

#ifdef _cplusplus

extern "C"{

#endif

 EXTERN_C int func(int a, int b);

#ifdef _cplusplus

}

#endif

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使用。

DLLDLL可以有自己的資料段,但沒有自己的堆棧,使用調用程序的棧,與調用它的應用程式相同的堆棧模式。

2)、關于共享資料段

DLL定義的全局變量可以被調用程序通路;DLL可以通路調用程序的全局資料。使用同一 DLL的每一個程序都有自己的DLL全局變量執行個體。如果多個線程并發通路同一變量,則需要使用同步機制;對一個DLL的變量,如果希望每個使用DLL的線程都有自己的值,則應該使用線程局部存儲(TLS,Thread Local Strorage)。

本文來自CSDN部落格,轉載請标明出處:http://blog.csdn.net/weide001/archive/2009/05/25/4214589.aspx

繼續閱讀