Assembly 重載函數(shù)和名字改編

2018-12-04 17:43 更新

C++編程語言是C語言的一種擴展形式。許多C語言和匯編語言接口的基本規(guī)則同樣適用于C++。但是,有一些規(guī)則需要修正。同樣,擁有一些匯編語言的知識,你能很容易理解C++中的一些擴展部分。這一節(jié)假定你已經(jīng)有一定的C++基礎知識。


兩個名為f()的函數(shù)


C++允許不同的函數(shù)(和類成員函數(shù))使用同樣的函數(shù)名來定義。當不止一個函數(shù)共享同一個函數(shù)名時,這些函數(shù)就稱為重載函數(shù)。在C語言中,如果定義的兩個函數(shù)使用的函數(shù)名是一樣,那么連接器將產(chǎn)生一個錯誤,因為在它連接的目標文件中,一個符號它將找到兩個定義。例如,考慮圖7.10中的代碼。等價的匯編代碼將定義兩個名為_f的標號,而這明顯是錯誤的。


C++使用和C一樣的連接過程,但是通過執(zhí)行名字改編或修改用來標記函數(shù)的符號來避免這個錯誤。在某種程序上,C也早已經(jīng)使用了名字改編。當創(chuàng)建函數(shù)的標號時,它在C函數(shù)名上增加了一條下劃線。但是,C語言將以同樣的方法來改編圖7.10中的兩個函數(shù)名,那么將會產(chǎn)生一個錯誤。C++使用一個更高級的改編過程:為這些函數(shù)產(chǎn)生兩個不同的標號。例如:圖7.10中的第一個函數(shù)將由DJGPP指定為標號_f_ _Fi,而第二個函數(shù),指定為_f __Fd。這樣就避免了任何的連接錯誤。


不幸的是,關于在C++中如何改編名字并沒有一個標準,而且不同的編譯器改編的名字也不一樣。例如,Borland C++將使用標號@f$qi和@f$qd來表示圖7.10中的兩個函數(shù)。但是,規(guī)則并不是完全任意的。改編后的名字編碼成函數(shù)的簽名。一個函數(shù)的簽名是通過它攜帶的參數(shù)的順序和類型來定義的。注意,,攜帶了一個int參數(shù)的函數(shù)在它的改編名字的末尾將有
一個i(對于DJGPP 和Borland都是一樣),而攜帶了一個double參數(shù)的函數(shù)在它的改編名字的末尾將有一個d。如果有一個名為f的函數(shù),它的原型如下:


void f ( int x, int y, double z );


DJGPP將會把它的名字改編成_f_ _Fiid而Borland將會把它改編成@f$qiid。函數(shù)的返回類型并不是函數(shù)簽名的一部分,因此它也不會編碼到它的改編名字中。這個事實解釋了在C++中的一個重載規(guī)則。只有簽名唯一的函數(shù)才能重載。就如你能看到的,如果在C++中定義了兩個名字和簽名都一樣的函數(shù),那么它們將得到同樣的簽名,而這將產(chǎn)生一個連接錯誤。

缺省情況下,所有的C++函數(shù)都會進行名字改編,甚至是那些沒有重載的函數(shù)。它編譯一個文件時,編譯器并沒有方法知道一個特定的函數(shù)重載與否,所以它將所有的名字改編。事實上,和函數(shù)簽名的方法一樣,編譯器同樣通過編碼變量的類型來改編全局變量的變量名。因此,如果你在一個文件中定義了一個全局變量為某一類型然后試圖在另一個文件中用一個錯誤的類型來使用它,那么將產(chǎn)生一個連接錯誤。C++這個特性被稱為類型安全連接。它同樣暴露出另一種類型的錯誤:原型不一致。當在一個模塊中函數(shù)的定義和在另一個模塊使用的函數(shù)原型不一致時,就發(fā)生這種錯誤。在C中,這是一個非常難調(diào)試出來的問題。C并不能捕捉到這種錯誤。程序將被編譯和連接,但是將會有未定義的操作發(fā)生,就像調(diào)用的代碼會將和函數(shù)期望不一樣的類型壓入棧中一樣。在C++中,它將產(chǎn)生一個連接錯誤。


當C++編譯器語法分析一個函數(shù)調(diào)用時,它通過查看傳遞給函數(shù)的參數(shù) 的類型來尋找匹配的函數(shù)。如果它找到了一個匹配的函數(shù),那么通過使用 編譯器的名字改編規(guī)則,它將創(chuàng)建一個CALL來調(diào)用正確的函數(shù)。 因為不同的編譯器使用不同的名字改編規(guī)則,所以不同編譯器編譯 的C++代碼可能不可以連接到一起。當考慮使用一個預編譯的C++庫時, 這個事實是非常重要的!如果有人想寫出一個能在C++代碼中使用的匯編 程序,那么他必須知道要使用的C++編譯器使用的名字改編規(guī)則(或使用下 面將解釋的技術)。 機敏的學生可能會詢問在圖 7.10中的代碼到底能不能如預期般工作。 因為C++改編了所有函數(shù)的函數(shù)名,那么printf將被改編,而編譯器將不 會產(chǎn)生一個到標號 printf處的CALL調(diào)用。這是一個非常正確的擔憂!如 果printf的原型被簡單地放置在文件的開始部分,那么這就將發(fā)生。原型為:


int printf ( const char ?, ...);


DJGPP將會把它改編為_printf_ _FPCce。(F表示function ,函數(shù) ,P表示pointer, 指針 ,C表示const ,常量 ,c表示char而e表示省略號。)那么它將不會調(diào)用 正規(guī)C庫中的printf函數(shù)!當然,必須有一種方法讓C++代碼用來調(diào)用C代 碼。這是非常重要的,因為到處都有 許多 非常有用的舊的C代碼。除了允許 你調(diào)用遺留的C代碼外,C++同樣允許你調(diào)用使用了正規(guī)的C改編約定的匯 編代碼。 C++擴展了extern關鍵字,允許它用來指定它修飾的函數(shù)或全局變量 使用的是正規(guī)C約定。在C++術語中,稱這些函數(shù)或全局變量使用了C 鏈 接 。例如,為了聲明printf為C鏈接,需使用下面的原型: 


extern ”C” int printf ( const char ?, ... );


這就告訴編譯器不要在這個函數(shù)上使用C++的名字改編規(guī)則,而使用C規(guī) 則來替代。但是,如果這樣做了,那么printf將不可以重載。這就提供了 一個簡易的方法用在C++和匯編程序接口上:使用C鏈接定義一個函數(shù), 然后再使用C調(diào)用約定。 為了方便,C++同樣允許定義函數(shù)或全局變量塊的C鏈接。通常函數(shù)或 全局變量塊用卷曲花括號表示。 


extern ”C” { 

   /? C鏈接的全局變量和函數(shù)原型 ?/ 



如果你檢查了當今的C/C++編譯器中的ANSI C頭文件,你會發(fā)現(xiàn)在每 個頭文件上面都有下面這個東西:


#ifdef _ _cplusplus 

extern ”C” { 

#endif 


而且在底部有一個包含閉卷曲花括號的同樣的結構。C++編譯器定義了 宏 cplusplus(有 兩條 領頭的下劃線)。上面的代碼片斷如果用C++來編 譯,那么整個頭文件就被一個extern "C"塊圍起來了,但是如果使用C來 編譯,就不會執(zhí)行任何操作(因為對于extern "C",C編譯器將產(chǎn)生一個 語法錯誤)。程序員可以使用同樣的技術用來在匯編程序中創(chuàng)建一個能 被C或C++使用的頭文件。


引用

引用的例子

引用是C++的另一個新特性。它允許你傳遞參數(shù)給函數(shù),而不需要明確 使用指針。例如,考慮圖 7.11中的代碼。事實上,引用參數(shù)是非常簡單, 實際上它們就是指針。只是編譯器對程序員隱藏它而已(正如Pascal編譯器 把var參數(shù)當作指針來執(zhí)行)。當編譯器產(chǎn)生此函數(shù)調(diào)用的第7行代碼的匯編語句時,它將y的地址傳遞給函數(shù)。如果有人是用匯編語言書寫的f函數(shù), 那么他們操作的情況,就好像原型如下似的:

void f( int ? xp);

引用是非常方便的,特別是對于運算符重載來說是非常有用的。運算符 重載又是C++的另一個特性,它允許你在對結構體或類類型進行操作時賦 予普通運算符另一種功能。例如,一個普遍的使用是賦予加號(+)運算符能 將字符串對象連接起來的功能。因此,如果a和b是字符串,那么a + b將得 到a和b連接后的字符串。實際上,C++可以調(diào)用一個函數(shù)來做這件事(事實 上,上面的表達式可以用函數(shù)的表示法來重寫為:operator +(a,b))。為 了提高效率,有人可能會希望傳遞字符串的地址來代替?zhèn)鬟f他們的值。若 沒有引用,那么將需要這樣做:operator +(&a,&b),但是若要求你以運 算符的語法來書寫應為:&a + &b。這是非常笨拙而且混亂的。但是,通過 使用引用,你可以像這樣書寫:a + b,這樣就看起來非常自然。

內(nèi)聯(lián)函數(shù)

到目前為止,內(nèi)聯(lián)函數(shù) 又是C++的另一個特性。內(nèi)聯(lián)函數(shù)照道理應該 可以取代容易犯錯誤的,攜帶參數(shù)的,基于預處理程序的宏?;叵胍幌?在C中,書寫一個求數(shù)的平方的宏可以是這樣的: 

#de?ne SQR(x) ((x)?(x)) 

因為預處理程序不能理解C而采用簡單的替換操作,在大多數(shù)情況下, 圓括號里要求是能正確計算出來的值。但是,即使是這個版本也不能給 出SQR(x++)的正確答案。 宏之所以被使用是因為它除去了進行一個簡單函數(shù)的函數(shù)調(diào)用的額外 時間開支。就像子程序那一章描述的,執(zhí)行一個函數(shù)調(diào)用包括好幾步。

內(nèi)聯(lián)函數(shù)

對于一個非常簡單的函數(shù)來說,用來進行函數(shù)調(diào)用的時間可能比實際上 執(zhí)行函數(shù)里的操作的時間還要多!內(nèi)聯(lián)函數(shù)是一個更為友好的用來書寫代 碼的方法,讓代碼看起來象一個標準的函數(shù),但是它并 不是 CALL指令能調(diào) 用的普通代碼塊。出現(xiàn)內(nèi)聯(lián)函數(shù)的調(diào)用表達式的地方將被執(zhí)行函數(shù)的代碼 替換。C++允許通過在函數(shù)定義前加上inline關鍵字來使函數(shù)成為內(nèi)聯(lián)函 數(shù)。如果,考慮在圖 7.12中聲明的函數(shù)。第10行對f的調(diào)用將執(zhí)行一個標準的函數(shù)調(diào)用(在匯編語言中,假定x的地址為ebp-8而y地址為ebp-4):

示例

這種情況下,使用內(nèi)聯(lián)函數(shù)有兩個優(yōu)點。首先,內(nèi)聯(lián)函數(shù)更快。沒有 參數(shù)需要壓入棧中,也不需要創(chuàng)建和毀壞堆棧幀,也不需要進行分支。其 次,內(nèi)聯(lián)函數(shù)調(diào)用使用的代碼是非常少!后面一點對這個例子來說是正確的,但是并不是在所有情況下都是正確的。 

內(nèi)聯(lián)函數(shù)的主要優(yōu)點是內(nèi)聯(lián)代碼不需要連接,所以對于使用內(nèi)聯(lián)函數(shù)的 所有 源文件來說,內(nèi)聯(lián)函數(shù)的代碼都必須有效。前面的匯編代碼的例子展示了這一點。對于非內(nèi)聯(lián)函數(shù)的調(diào)用,只要求知道參數(shù),返回值類型,調(diào) 用約定和函數(shù)的函數(shù)名。所有的這些信息都可以從函數(shù)的原型中得到。但 是,使用內(nèi)聯(lián)函數(shù)調(diào)用,就必須知道這個函數(shù)的所有代碼。這就意味著如 果改變了一個內(nèi)聯(lián)函數(shù)中的任何部分,那么 所有 使用了這個函數(shù)的源文件 必須重新編譯?;叵胍幌聦τ诜莾?nèi)聯(lián)函數(shù),如果函數(shù)原型沒有改變,通常 使用這個函數(shù)的源文件就不需要重新編譯。由于所有的這些原因,內(nèi)聯(lián)函 數(shù)的代碼通常放置在頭文件中。這樣做違反了在C語言中標準的穩(wěn)定和快速 準則:執(zhí)行的代碼語句 決不能 放置在頭文件中。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號