約在20世紀70年代以前,編譯器編譯源**產生目標檔案時,符號名與相應的變數和函式的名字是一樣的。比如乙個彙編源**裡面包含了乙個函式foo,那麼彙編器將它編譯成目標檔案以後,foo在目標檔案中的相對應的符號名也是foo。當後來unix平台和c語言發明時,已經存在了相當多的使用彙編編寫的庫和目標檔案。這樣就產生了乙個問題,那就是如果乙個c程式要使用這些庫的話,c語言中不可以使用這些庫中定義的函式和變數的名字作為符號名,否則將會跟現有的目標檔案衝突。比如有個用彙編編寫的庫中定義了乙個函式叫做main,那麼我們在c語言裡面就不可以再定義乙個main函式或變數了。同樣的道理,如果乙個c語言的目標檔案要用到乙個使用fortran語言編寫的目標檔案,我們也必須防止它們的名稱衝突。
為了防止類似的符號名衝突,unix下的c語言就規定,c語言源**檔案中的所有全域性的變數和函式經過編譯以後,相對應的符號名前加上下劃線"_"。而fortran語言的源**經過編譯以後,所有的符號名前加上"_",後面也加上"_"。比如乙個c語言函式"foo",那麼它編譯後的符號名就是"_foo";如果是fortran語言,就是"_foo_"。
這種簡單而原始的方法的確能夠暫時減少多種語言目標檔案之間的符號衝突的概率,但還是沒有從根本上解決符號衝突的問題。比如同一種語言編寫的目標檔案還有可能會產生符號衝突,當程式很大時,不同的模組由多個部門(個人)開發,它們之間的命名規範如果不嚴格,則有可能導致衝突。於是像c++這樣的後來設計的語言開始考慮到了這個問題,增加了命名空間(namespace)的方法來解決多模組的符號衝突問題。
但是隨著時間的推移,很多作業系統和編譯器被完全重寫了好幾遍,比如unix也分化成了很多種,整個環境發生了很大的變化,上面所提到的跟fortran和古老的彙編庫的符號衝突問題已經不是那麼明顯了。在現在的linux下的gcc編譯器中,預設情況下已經去掉了在c語言符號前加"_"的這種方式;但是windows平台下的編譯器還保持的這樣的傳統,比如visual c++編譯器就會在c語言符號前加"_",gcc在windows平台下的版本(cygwin、mingw)也會加"_"。gcc編譯器也可以通過引數選項"-fleading-underscore"或"-fno-leading-underscore"來開啟和關閉是否在c語言符號前加上下劃線。
c++符號修飾
眾所周知,強大而又複雜的c++擁有類、繼承、虛機制、過載、命名空間等這些特性,它們使得符號管理更為複雜。最簡單的例子,兩個相同名字的函式func(int)和func(double),儘管函式名相同,但是引數列表不同,這是c++裡面函式過載的最簡單的一種情況,那麼編譯器和鏈結器在鏈結過程中如何區分這兩個函式呢?為了支援c++這些複雜的特性,人們發明了符號修飾(name decoration)或符號改編(name mangling)的機制,下面我們來看看c++的符號修飾機制。
首先出現的乙個問題是c++允許多個不同引數型別的函式擁有一樣的名字,就是所謂的函式過載;另外c++還在語言級別支援命名空間,即允許在不同的命名空間有多個同樣名字的符號。比如清單3-4這段**:
清單3-4 c++ 函式的名稱修飾
int func(int);
float func(float);
class c ;
};
namespace n ;
}
這段**中有6個同名函式叫func,只不過它們的返回型別和引數及所在的命名空間不同。我們引入乙個術語叫做函式簽名(function signature),函式簽名包含了乙個函式的資訊,包括函式名、它的引數型別、它所在的類和命名空間及其他資訊。函式簽名用於識別不同的函式,就像簽名用於識別不同的人一樣,函式的名字只是函式簽名的一部分。由於上面6個同名函式的引數型別及所處的類和命名空間不同,我們可以認為它們的函式簽名不同。在編譯器及鏈結器處理符號時,它們使用某種名稱修飾的方法,使得每個函式簽名對應乙個修飾後名稱(decorated name)。編譯器在將c++源**編譯成目標檔案時,會將函式和變數的名字進行修飾,形成符號名,也就是說,c++的源**編譯後的目標檔案中所使用的符號名是相應的函式和變數的修飾後名稱。c++編譯器和鏈結器都使用符號來識別和處理函式和變數,所以對於不同函式簽名的函式,即使函式名相同,編譯器和鏈結器都認為它們是不同的函式。上面的6個函式簽名在gcc編譯器下,相對應的修飾後名稱如表3-18所示。
表3-18
函式簽名
修飾後名稱(符號名)
int func(int)
_z4funci
float func(float)
_z4funcf
int c::func(int)
_zn1c4funcei
int c::c2::func(int)
_zn1c2c24funcei
int n::func(int)
_zn1n4funcei
int n::c::func(int)
_zn1n1c4funcei
gcc的基本c++名稱修飾方法如下:所有的符號都以"_z"開頭,對於巢狀的名字(在命名空間或在類裡面的),後面緊跟"n",然後是各個命名空間和類的名字,每個名字前是名字字串長度,再以"e"結尾。比如n::c::func經過名稱修飾以後就是_zn1n1c4funce。對於乙個函式來說,它的引數列表緊跟在"e"後面,對於int型別來說,就是字母"i"。所以整個n::c::func(int)函式簽名經過修飾為_zn1n1c4funcei。更為具體的修飾方法我們在這裡不詳細介紹,有興趣的讀者可以參考gcc的名稱修飾標準。幸好這種名稱修飾方法我們平時程式開發中也很少手工分析名稱修飾問題,所以無須很詳細地了解這個過程。binutils裡面提供了乙個叫"c++filt"的工具可以用來解析被修飾過的名稱,比如:
$ c++filt _zn1n1c4funcei
n::c::func(int)
簽名和名稱修飾機制不光被使用到函式上,c++中的全域性變數和靜態變數也有同樣的機制。對於全域性變數來說,它跟函式一樣都是乙個全域性可見的名稱,它也遵循上面的名稱修飾機制,比如乙個命名空間foo中的全域性變數bar,它修飾後的名字為:_zn3foo3bare。值得注意的是,變數的型別並沒有被加入到修飾後名稱中,所以不論這個變數是整形還是浮點型甚至是乙個全域性物件,它的名稱都是一樣的。
名稱修飾機制也被用來防止靜態變數的名字衝突。比如main()函式裡面有乙個靜態變數叫foo,而func()函式裡面也有乙個靜態變數叫foo。為了區分這兩個變數,gcc會將它們的符號名分別修飾成兩個不同的名字_zz4maine3foo和_zz4funcve3foo,這樣就區分了這兩個變數。
不同的編譯器廠商的名稱修飾方法可能不同,所以不同的編譯器對於同乙個函式簽名可能對應不同的修飾後名稱。比如上面的函式簽名中在visual c++編譯器下,它們的修飾後名稱如表3-19所示。
表3-19
函式簽名
修飾後名稱
int func(int)
?func@@yahh@z
float func(float)
?func@@yamm@z
int c::func(int)
?func@c@@aaehh@z
int c::c2::func(int)
?func@c2@c@@aaehh@z
int n::func(int)
?func@n@@yahh@z
int n::c::func(int)
?func@c@n@@aaehh@z
我們以int n::c::func(int)這個函式簽名來猜測visual c++的名稱修飾規則(當然,你只須大概了解這個修飾規則就可以了)。修飾後名字由"?"開頭,接著是函式名由"@"符號結尾的函式名;後面跟著由"@"結尾的類名"c"和命名空間"n",再乙個"@"表示函式的命名空間結束;第乙個"a"表示函式呼叫型別為"__cdecl"(函式呼叫型別我們將在第4章詳細介紹),接著是函式的引數型別及返回值,由"@"結束,最後由"z"結尾。可以看到函式名、引數的型別和命名空間都被加入了修飾後名稱,這樣編譯器和鏈結器就可以區別同名但不同引數型別或名字空間的函式,而不會導致link的時候函式多重定義。
visual c++的名稱修飾規則並沒有對外公開,當然,一般情況下我們也無須了解這套規則,但是有時候可能須要將乙個修飾後名字轉換成函式簽名,比如在鏈結、除錯程式的時候可能會用到。microsoft提供了乙個undecoratesymbolname()的api,可以將修飾後名稱轉換成函式簽名。下面這段**使用undecoratesymbolname()將修飾後名稱轉換成函式簽名:
/* 2-4.c
* compile: cl 2-4.c /link dbghelp.lib
* usage: 2-4.exe decroatedname
*/
#include
#include
int main( int argc, char* argv )
else
return 0;
}
由於不同的編譯器採用不同的名字修飾方法,必然會導致由不同編譯器編譯產生的目標檔案無法正常相互鏈結,這是導致不同編譯器之間不能互操作的主要原因之一。我們後面的關於c++ abi和com的這一節將會詳細討論這個問題。
符號修飾與函式簽名
p 87 linux下的gcc編譯器中,預設情況下已經去掉了在c語言符號前加 的方式,但是windows平台下的編譯器還保持著在符號前加 的習慣。函式簽名使得函式在目標檔案中的符號變成與其原始檔的函式名 函式引數 所在的類和命名空間及其它資訊關聯了起來。函式簽名經名稱修飾變成修飾後名稱目標檔案中的符...
符號修飾與函式簽名 extern 「C」
程式設計師的自我修養 3.5.3以及3.5.4小節。符號修飾的由來 20世紀70年代以前,編譯器編譯 時產生的目標檔案中,符號名與相應的變數和函式的名字是一樣的,隨著程式語言的發展,例如c語言,如果乙個c語言程式要使用這些庫的話,其自身就不能使用這些庫中已經宣告了的函式和變數的名字作為符號名,否則將...
Python基礎 函式修飾器 符號
def dec f n 3 return f args,kw n dec def foo n return n 2python解析器遇到 且後面跟著函式時,會把函式foo當做引數傳遞給dec函式並執行,即 dec foo n 本例中執行 dec n 2 預設引數一定要用不可變物件,如果是可變物件,執...