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

2018-12-04 17:43 更新

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


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


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


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


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


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


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

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


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


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


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


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


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


extern ”C” { 

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



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


#ifdef _ _cplusplus 

extern ”C” { 

#endif 


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


引用

引用的例子

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

void f( int ? xp);

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

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

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

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

因?yàn)轭A(yù)處理程序不能理解C而采用簡(jiǎn)單的替換操作,在大多數(shù)情況下, 圓括號(hào)里要求是能正確計(jì)算出來的值。但是,即使是這個(gè)版本也不能給 出SQR(x++)的正確答案。 宏之所以被使用是因?yàn)樗チ诉M(jìn)行一個(gè)簡(jiǎn)單函數(shù)的函數(shù)調(diào)用的額外 時(shí)間開支。就像子程序那一章描述的,執(zhí)行一個(gè)函數(shù)調(diào)用包括好幾步。

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

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

示例

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)