我們知道,調用函數時,計算機常用棧來存放函數執行需要的參數,由於棧的空間大小是有限的,在windows下棧是向低地址擴展的數據結構,是一塊連續的內存區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,windows下棧的大小是2M(也有的說是1M),如果申請的空間超過棧的剩余空間時,將提示overflow。
在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
在參數傳遞中,有兩個重要的問題必須要明確說明:
1. 當參數個數多於一個時,按照什么順序把參數壓入堆棧;
2. 函數調用后,由誰來把堆棧恢復原狀。
在高級語言中,就是通過函數的調用方式來說明這兩個問題的。常見的調用方式有:
stdcall
cdecl
fastcall
thiscall
naked call
下面就分別介紹這幾種調用方式:
1. stdcall
stdcall調用方式又被稱為Pascal調用方式。在Microsoft C++系列的C/C++編譯器中,使用PASCAL宏,WINAPI宏和CALLBACK宏來指定函數的調用方式為stdcall。
stdcall調用方式的函數聲明為:
int _stdcall function(int a, int b);
stdcall的調用方式意味着:
(1) 參數從右向左依次壓入堆棧
(2) 由被調用函數自己來恢復堆棧
(3) 函數名自動加前導下划線,后面緊跟着一個@,其后緊跟着參數的尺寸
上面那個函數翻譯成匯編語言將變成:
push b 先壓入第二個參數
push a 再壓入第一個參數
call function 調用函數
在編譯時,此函數的名字被翻譯為_function@8
2. cdecl
cdecl調用方式又稱為C調用方式,是C語言缺省的調用方式,它的語法為:
int function(int a, int b) // 不加修飾符就是C調用方式
int _cdecl function(int a, int b) // 明確指定用C調用方式
cdecl的調用方式決定了:
(1) 參數從右向左依次壓入堆棧
(2) 由調用者恢復堆棧
(3) 函數名自動加前導下划線
由於是由調用者來恢復堆棧,因此C調用方式允許函數的參數個數是不固定的,這是C語言的一大特色。
此方式的函數被翻譯為:
push b // 先壓入第二個參數
push a // 在壓入第一個參數
call funtion // 調用函數
add esp, 8 // 清理堆棧 。。。。。需要熟悉一下esp寄存器的功能,建議看一下匯編有關的書,基本都有講
在編譯時,此方式的函數被翻譯成:_function
3. fastcall
fastcall 按照名字上理解就可以知道,它是一種快速調用方式。此方式的函數的第一個和第二個DWORD參數通過ecx和edx傳遞,
后面的參數從右向左的順序壓入棧。
被調用函數清理堆棧。
函數名修個規則同stdcall
其聲明語法為:
int _fastcall function(int a, int b);
4. thiscall
thiscall 調用方式是唯一一種不能顯示指定的修飾符。它是c++類成員函數缺省的調用方式。由於成員函數調用還有一個this指針,因此必須用這種特殊的調用方式。
thiscall調用方式意味着:
參數從右向左壓入棧。
如果參數個數確定,this指針通過ecx傳遞給被調用者;如果參數個數不確定,this指針在所有參數壓入棧后被壓入棧。
參數個數不定的,由調用者清理堆棧,否則由函數自己清理堆棧。
可以看到,對於參數個數固定的情況,它類似於stdcall,不定時則類似於cdecl。
5. naked call
是一種比較少見的調用方式,一般高級程序設計語言中不常見。
函數的聲明調用方式和實際調用方式必須一致,必然編譯器會產生混亂。
函數名字修改規則:
1. C編譯時函數名修飾約定規則:
__stdcall調用約定在輸出函數名前加上一個下划線前綴,后面加上一個“@”符號和其參數的字節數,格式為_function@8。
__cdecl調用約定僅在輸出函數名前加上一個下划線前綴,格式為_function。
__fastcall調用約定在輸出函數名前加上一個“@”符號,后面也是一個“@”符號和其參數的字節數,格式為@function@8。
它們均不改變輸出函數名中的字符大小寫,這和PASCAL調用約定不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。
2. C++編譯器的函數名修飾規則 以上的截圖為c++
C++的函數名修飾規則有些復雜,但是信息更充分,通過分析修飾名不僅能夠知道函數的調用方式,返回值類型,參數個數甚至參數類型。
不管__cdecl,__fastcall還是__stdcall調用方式,函數修飾都是:? + 函數的名字 + 參數表的開始標識 + 按照參數類型代號拼出的參數表。
參數表的開始標識:
對於__stdcall方式是“@@YG”,
對於__cdecl方式是“@@YA”,
對於__fastcall方式是“@@YI”。
參數表的拼寫代號如下所示:
X--void
D--char
E--unsigned char
F--short
H--int
I--unsigned int
J--long
K--unsigned long(DWORD)
M--float
N--double
_N--bool
U--struct
....
指針的方式有些特別,用PA表示指針,用PB表示const類型的指針,后面的代號表明指針類型,如果相同類型的指針連續出現,以“0”代替,一個“0”代表一次重復。
U表示結構類型,通常后跟結構體的類型名,用“@@”表示結構類型名的結束。
函數的返回值不作特殊處理,它的描述方式和函數參數一樣,緊跟着參數表的開始標志,也就是說,函數參數表的第一項表示函數的返回值類型。
參數表后以“@Z”標識整個名字的結束,如果該函數無參數,則以“Z”標識結束。
下面舉兩個例子,假如有以下函數聲明:
int Function1 (char *var1,unsigned long);
其函數修飾名為“?Function1@@YGHPADK@Z”,
而對於函數聲明:
void Function2();
其函數修飾名則為“?Function2@@YGXXZ” 。
對於C++的類成員函數(其調用方式是thiscall),函數的名字修飾與非成員的C++函數稍有不同,
首先就是在函數名字和參數表之間插入以“@”字符引導的類名;
其次是參數表的開始標識不同,public成員函數的標識是“@@QAE”;protected成員函數的標識是“@@IAE”;private成員函數的標識是“@@AAE”,
如果函數聲明使用了const關鍵字,則相應的標識應分別為“@@QBE”,“@@IBE”和“@@ABE”。
如果參數類型是類實例的引用,則使用“AAV1”,對於const類型的引用,則使用“ABV1”。
下面就以類CTest為例說明C++成員函數的名字修飾規則:
class CTest
{
......
private:
void Function(int);
protected:
void CopyInfo(const CTest &src);
public:
long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
long InsightClass(DWORD dwClass) const;
......
};
對於成員函數Function,其函數修飾名為“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示這是一個私有函數。
成員函數CopyInfo只有一個參數,是對類CTest的const引用參數,其函數修飾名為“?CopyInfo@CTest@@IAEXABV1@@Z”。(#add 末尾怎么有兩個@?)
DrawText是一個比較復雜的函數聲明,不僅有字符串參數,還有結構體參數和HDC句柄參數,需要指出的是HDC實際上是一個HDC__結構類型的指針,這個參數的表示就是“PAUHDC__@@”,其完整的函數修飾名為“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。
InsightClass是一個public const函數,它的成員函數標識是“@@QBE”,完整的修飾名就是“?InsightClass@CTest@@QBEJK@Z”。
無論是C函數名修飾方式還是C++函數名修飾方式均不改變輸出函數名中的字符大小寫,這和PASCAL調用約定不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。
class A{
public:
A():num(0){}
void func()
};
假設你有兩個返回值不同的函數,比如
int getvalue(viod) {return value1;}
float getvalue(viod) {return value;}
那么當你去調用他們的時候,由於你調用的時候
寫的是
getvalue();
於是你的編譯器就無法知道 你調用的是上面哪個 函數(因為兩個函數都不用傳參數,編譯器無法區分它們), 所以就會報錯。
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
如果你寫成這樣 (函數重載)
int getvalue(int i, int j) {return value1;}..................1
float getvalue(viod) {return value;}......................2
那么當你當你調用
getvalue()編譯器就會知道 你調用的是第2個函數
getvalue(2, 3) 編譯器就會知道 你調用的是第1個函數
就不會出錯了。
總結:重構僅返回類型不同的函數不允許,c++程序中函數都會有一個唯一的修飾函數名,之所以說它唯一,是因為此修飾過的函數名因函數名,(所在類或所在空間),(訪問級別),返回值,參數值不同而不同。因此函數調用時便會根據具體的調用環境找到與之對應的修飾過的函數名。
脈絡:函數調用時發生什么?->五種調用方式調用時入棧,銷毀棧操作->最常用的_cbecl 和thiscall調用,掌握其修飾規則,並結合理解重構函數,類成員函數。
從上節的分析中可以看出,對象的內存中只保留了成員變量,除此之外沒有任何其他信息,程序運行時不知道 stu 的類型為 Student,也不知道它還有四個成員函數 setname()、setage()、setscore()、show(),C++ 究竟是如何通過對象調用成員函數的呢?
C++函數的編譯
C++和C語言的編譯方式不同。C語言中的函數在編譯時名字不變,或者只是簡單的加一個下划線_
(不同的編譯器有不同的實現),例如,func() 編譯后為 func() 或 _func()。
而C++中的函數在編譯時會根據它所在的命名空間、它所屬的類、以及它的參數列表(也叫參數簽名)等信息進行重新命名,形成一個新的函數名。這個新的函數名只有編譯器知道,對用戶是不可見的。對函數重命名的過程叫做名字編碼(Name Mangling),是通過一種特殊的算法來實現的。
Name Mangling 的算法是可逆的,既可以通過現有函數名計算出新函數名,也可以通過新函數名逆向推演出原有函數名。Name Mangling 可以確保新函數名的唯一性,只要函數所在的命名空間、所屬的類、包含的參數列表等有一個不同,最后產生的新函數名也不同。
如果你希望看到經 Name Mangling 產生的新函數名,可以只聲明而不定義函數,這樣調用函數時就會產生鏈接錯誤,從報錯信息中就可以看到新函數名。請看下面的代碼:
- #include <iostream>
- using namespace std;
- void display();
- void display(int);
- namespace ns{
- void display();
- }
- class Demo{
- public:
- void display();
- };
- int main(){
- display();
- display(1);
- ns::display();
- Demo obj;
- obj.display();
- return 0;
- }
該例中聲明了四個同名函數,包括兩個具有重載關系的全局函數,一個位於命名空間 ns 下的函數,以及一個屬於類 Demo 的函數。它們都是只聲明而未定義的函數。
在 VS 下編譯源代碼可以看到類似下面的錯誤信息:
小括號中就是經 Name Mangling 產生的新函數名,它們都以?
開始,以區別C語言中的_
。
上圖是 VS2010 產生的錯誤信息,不同的編譯器有不同的 Name Mangling 算法,產生的函數名也不一樣。
__thiscall、cdecl 是函數調用慣例,有興趣的讀者可以猛擊《 函數調用慣例》一文深入了解。
除了函數,某些變量也會經 Name Mangling 算法產生新名字,這里不再贅述。
成員函數的調用
從上圖可以看出,成員函數最終被編譯成與對象無關的全局函數,如果函數體中沒有成員變量,那問題就很簡單,不用對函數做任何處理,直接調用即可。
如果成員函數中使用到了成員變量該怎么辦呢?成員變量的作用域不是全局,不經任何處理就無法在函數內部訪問。
C++規定,編譯成員函數時要額外添加一個參數,把當前對象的指針傳遞進去,通過指針來訪問成員變量。
假設 Demo 類有兩個 int 型的成員變量,分別是 a 和 b,並且在成員函數 display() 中使用到了,如下所示:
- void Demo::display(){
- cout<<a<<endl;
- cout<<b<<endl;
- }
那么編譯后的代碼類似於:
- void new_function_name(Demo * const p){
- //通過指針p來訪問a、b
- cout<<p->a<<endl;
- cout<<p->b<<endl;
- }
使用obj.display()
調用函數時,也會被編譯成類似下面的形式:
new_function_name(&obj);
這樣通過傳遞對象指針就完成了成員函數和成員變量的關聯。這與我們從表明上看到的剛好相反,通過對象調用成員函數時,不是通過對象找函數,而是通過函數找對象。
這一切都是隱式完成的,對程序員來說完全透明,就好像這個額外的參數不存在一樣。
最后需要提醒的是,Demo * const p
中的 const 表示指針不能被修改,p 只能指向當前對象,不能指向其他對象。