#include <iostream> #include <cstdio> void foo(int x, int y, int* z) { printf("x = %d at [0x%x]\n", x, &x); printf("y = %d at [0x%x]\n", y, &y); printf("z = 0x%x at [0x%x]\n", z, &z); } int main(int argc, char *argv[]) { int i = 300; printf("i at [0x%x]\n", &i); foo(100, 200, &i); return 0; }
i at [0x28feec] x = 100 at [0x28fed0] y = 200 at [0x28fed4] z = 0x28feec at [0x28fed8]
這兒其實還涉及到C語言中調用約定所採用的方式,下面簡單的介紹一下: ios
__stdcall與C調用約定(__cdecl)的區別 程序員
__stdcall與__cdecl 是函數名字修飾(Decorated Name)的方式。 編程
C調用約定在返回前,要做一次堆棧平衡,也就是參數入棧了多少字節,就要彈出來多少字節.這樣很安全. 安全
有一點須要注意:stdcall調用約定若是採用了不定參數,即VARARG的話,則和C調用約定同樣,要由調用者來做堆棧平衡. 函數
(1)_stdcall是 Pascal方式清理,C方式壓棧,一般用於Win32 Api中,函數採用從右到左的壓棧方式,本身在退出時清空堆棧。VC將函數編譯後會在函數名前面加上下劃線前綴,在函數名後加上"@"和參數的字節數。 int f(void *p) -->> _f@4(在外部彙編語言裏能夠用這個名字引用這個函數) 工具
在WIN32 API中,只有少數幾個函數,如wspintf函數是採用C調用約定,其餘都是stdcall this
(2)C調用約定(即用 __cdecl關鍵字說明)(The C default calling convention)按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於傳送參數的內存棧是由調用者來維護的(正由於如此,實現可變參數 vararg的函數(如printf)只能使用該調用約定)。另外,在函數名修飾約定方面也有所不一樣。 _cdecl是C和C++程序的缺省調用方式。每個調用它的函數都包含清空堆棧的代碼,因此產生的可執行文件大小會比調用_stdcall函數的大。函數採用從右到左的壓棧方式。VC將函數編譯後會在函數名前面加上下劃線前綴。 spa
(3)__fastcall調用的主要特色就是快,由於它是經過寄存器來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字(DWORD)或更小的參數,剩下的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的內存棧),在函數名修飾約定方面,它和前二者均不一樣。__fastcall方式的函數採用寄存器傳遞參數,VC將函數編譯後會在函數名前面加上"@"前綴,在函數名後加上"@"和參數的字節數。 .net
(4)thiscall僅僅應用於"C++"成員函數。this指針存放於CX/ECX寄存器中,參數從右到左壓。thiscall不是關鍵詞,所以不能被程序員指定。 命令行
(5)naked call。當採用1-4的調用約定時,若是必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。
(這些代碼稱做 prolog and epilog code,通常,ebp,esp的保存是必須的).
可是naked call不產生這樣的代碼。naked call不是類型修飾符,故必須和_declspec共同使用。
關鍵字 __stdcall、__cdecl和__fastcall能夠直接加在要輸出的函數前。它們對應的命令行參數分別爲/Gz、/Gd和/Gr。缺省狀態爲/Gd,即__cdecl。
要徹底模仿PASCAL調用約定首先必須使用__stdcall調用約定,至於函數名修飾約定,能夠經過其它方法模仿。還有一個值得一提的是WINAPI宏,Windows.h支持該宏,它能夠將出函數翻譯成適當的調用約定,在WIN32中,它被定義爲__stdcall。使用 WINAPI宏能夠建立本身的APIs。
綜上,其實只有PASCAL調用約定的從左到右入棧的.並且PASCAL不能使用不定參數個數,其參數個數是必定的。
簡單總結一下上面的幾個調用方式:
調用約定 |
堆棧清除 |
參數傳遞 |
__cdecl |
調用者 |
從右到左,經過堆棧傳遞 |
__stdcall |
函數體 |
從右到左,經過堆棧傳遞 |
__fastcall |
函數體 |
從右到左,優先使用寄存器(ECX,EDX),而後使用堆棧 |
thiscall |
函數體 |
this指針默認經過ECX傳遞,其餘參數從右到左入棧 |
一.函數修飾符:
函數名字修飾(Decorated Name) 方式
函數的名字修飾(Decorated Name)就是編譯器在編譯期間建立的一個字符串,用來指明函數的定義或原型。LINK程序或其餘工具備時須要指定函數的名字修飾來定位函數的正確位置。多數狀況下程序員並不須要知道函數的名字修飾,LINK程序或其餘工具會自動區分他們。固然,在某些狀況下須要指定函數的名字修飾,例如在C++程序中,爲了讓LINK程序或其餘工具可以匹配到正確的函數名字,就必須爲重載函數和一些特殊的函數(如構造函數和析構函數)指定名字裝飾。另外一種須要指定函數的名字修飾的狀況是在彙編程序中調用C或C++的函數。若是函數名字,調用約定,返回值類型或函數參數有任何改變,原來的名字修飾就再也不有效,必須指定新的名字修飾。C和C++程序的函數在內部使用不一樣的名字修飾方式,下面將分別介紹這兩種方式。
1._cdecl
按從右至左的順序壓參數入棧,由調用者把參數彈出棧。對於「C」函數或者變量,修飾名是在函數名前加下劃線。對於「C++」函數,有所不一樣。
如 函數void test(void)的修飾名是_test;對於不屬於一個類的「C++」全局函數,修飾名是?test@@ZAXXZ。
這 是MFC缺省調用約定。因爲是調用者負責把參數彈出棧,因此能夠給函數定義個數不定的參數,如printf函數。
2._stdcall
按從右至左的順序壓參數入棧,由被調用者把參數彈出棧。對於「C」函數或者變量,修飾名如下劃線爲前綴,而後是函數名,而後是符號「@」及參數的字節數,如函數int func(int a, double b)的修飾名是_func@12。 對於「C++」函數,則有所不一樣。
所 有的Win32 API函數都遵循該約定。
3._fastcall
頭兩 個DWORD類型或者佔更少字節的參數被放入ECX和EDX寄存器,其餘剩下的參數按從右到左的順序壓入棧。由被調用者把參數彈出棧,對於「C」函數或者變量,修飾名以「@」爲前綴,而後是函數名,接着是符號「@」及 參數的字節數,如函數int func(int a, double b)的 修飾名是@func @12。對於「C++」函數,有所不一樣。
將來的編譯器可能使用不一樣的寄存器來存放參數。
4.thiscall
僅僅應 用於「C++」成員函數。this指 針存放於CX寄存器,參數從右到左壓棧。thiscall不是關鍵詞,所以不能被程序員指定。
5.naked call
採用1-4的 調用約定時,若是必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。naked call不產生這樣的代碼。
naked call不是類型修飾符,故必須和_declspec共同使用,以下:
__declspec( naked ) int func( formal_parameters )
{
// Function body
}
二.函數調用
千萬要注意,C不支持默認參數
----------------------------------------------------------------------------------------------------------------
註釋:默認參數:好比說下面的函數
int fun(int a,int b,int c=3)
{
}
c就是指定的默認實參,一般在函數原型中指定。這裏給了3做爲默認參數。用日常的時候調用這個函數fun(4,5,6);那麼就是a=4,b=4,c=6。若是這樣調用fun(1,2)那 麼就是a=1,b=2,c=3,這裏c沒有指定,由於c是默認實參,已經有了默認值,這裏c就 是採用默認值3。
爲何默認實參必須是函數參數表中最右邊的參數。把上面的函數改下
int fun(int a=3,int b,int c)
{
}
這樣調用fun(1,2), 這樣就是a=1,b=2,而c根本就沒有賦到值,就出錯了。這些參數都是一一對應的。
--------------------------------------------------------------------------------------------------------------------
C/C++支持可變參數個數的函數定義,這一點與C/C++語言函數參數調用時入棧順序有 關,
首先引用其餘網友的一段文字,來描述函數調用,及參數入棧:
C支持可變參數的函數, 這裏的意思是C支持函數帶有可變數量的參數,最多見的例子就
是 咱們十分熟悉的printf()系列函數。咱們還知道在函數調用 時參數是自右向左壓棧的。若是可變參數函數的通常形式是:
f(p1, p2, p3, …)
那麼參數進棧(以及出棧)的順序是:
…
push p3
push p2
push p1
call f
pop p1
pop p2
pop p3
…
我能夠獲得這樣一個結論:若是支持可變參數的函數,那麼參數進棧的順序幾乎必然是自右向左 的。而且,參數出棧也不能由函數本身完成,而應該由調用者完成。
這個結論的後半部分是不難理解的, 由於函數自身不知道調用者傳入了多少參數,可是調用者知道,因此調用者應該負責將全部參數 出棧。
在可變參數函數的通常形式中,左邊是已經肯定的參數,右邊省略號表明未知參數部分。對於已經肯定的參數,它在棧上的位置也必須是肯定的。不然意味着已經肯定的參數是不能定位和找到的,這樣是沒法保證函數正確執行的。衡量參數在棧上的位置,就是離開確切的 函數調用點(call f)有多遠。已經肯定的參數,它在棧上的位置,不該該依賴參數的具體數量,由於參數的數量是未知的!
因此,選擇只能是,已經肯定的參 數,離開函數調用點有肯定的距離(較近)。知足這個條件,只有參數入棧聽從自右向左規則。 也就是說,左邊肯定的參數後入棧,離函數調用點有肯定的距離(最左邊的參數最後入棧,離函 數調用點最近)。
這樣,當函數開始執行後,它能找到 全部已經肯定的參數。根據函數本身的邏輯,它負責尋找和解釋後面可變的參數(在離開調用點 較遠的地方),一般這依賴於已經肯定的參數的值(典型的如prinf()函 數的格式解釋,遺憾的是這樣的方式具備脆弱性)。
聽說在pascal中參數是自左向右壓棧的,與C的相反。對於pascal這 種只支持固定參數函數的語言,它沒有可變參數帶來的問題。所以,它選擇哪一種參數進棧方式都 是能夠的。甚至,其參數出棧是由函數本身完成的,而不是調用者,由於函數的參數的類型和數 量
是徹底已知的。這種方式比採用C的 方式的效率更好,由於佔用更少的代碼量(在C中,函 數每次調用的地方,都生成了參數出棧代碼)。
C++爲了兼容C,因此仍然支持函數帶有可變的參數。可是在C++中 更好的選擇經常是函數重載。