C++對象模型筆記: Name Mangling與重載

上一篇筆記裏面說到,如果c++的成員函數都是全局的,怎麼區分兩個類中的同名的成員函數調用,例如:下面定義了2個類Point1Point2的對象p1p2;其中Point1Point2都有成員函數print

 

 

 

編譯器怎麼區分呢?

 

其實,類中的成員函數print在編譯器轉換成全局函數的時候就已經不叫print了,舉個例子:

比如你現在在301教室,但是出了你現在所在的這棟樓,就不能籠統的叫301了,要叫2301……如果出了中國,剛纔那個301教室要叫全局名稱:中華人民共和國******….301教室,是不是?

 

所以,在兩個不同類裏面的同名函數print,化成全局函數的時候就不叫print了,轉換的技術就叫做:Name Mangling ! 查英文詞典,mangling居然是亂砍的意思,夠親切的,就是亂砍,呵呵。

 

Name mangling的目的就是避免重複,原理就是:找到一種編碼方法,使得

1)、每一個名稱經過轉換後,要有唯一的名字;

2)、這種編碼必須簡單,而且要可逆,就是能由全局名稱很快的知道局部的名稱~

 

具體的編碼方法每個編譯器(g++vs2005VCL…)都不相同,而且比較煩瑣,這裏不再詳細闡述,想知道的可以googleName Mangling

 

如果你想偷懶,又想知道經過編譯器轉換後的全局名稱,可以也很容易,就是不要去寫函數的定義,僅僅是寫個聲明,然後在主函數中調用它,生成工程的時候就會出現連接錯誤,具體例子如下:

 

 

 

 

上面的例子能通過編譯,但是鏈接的時候會出錯,下面給出vs2005中的錯誤提示:

 

1>------ 已啓動生成: 項目: funobject, 配置: Debug Win32 ------

1>正在編譯...

1>main.cpp

1>正在鏈接...

1>main.obj : error LNK2019: 無法解析的外部符號"public: void __thiscall Point2::print(void)" (?print@Point2@@QAEXXZ),該符號在函數_main 中被引用

1>main.obj : error LNK2019: 無法解析的外部符號"public: void __thiscall Point1::print(int,int)" (?print@Point1@@QAEXHH@Z),該符號在函數_main 中被引用

1>main.obj : error LNK2019: 無法解析的外部符號"public: void __thiscall Point1::print(float)" (?print@Point1@@QAEXM@Z),該符號在函數_main 中被引用

1>main.obj : error LNK2019: 無法解析的外部符號"public: void __thiscall Point1::print(void)" (?print@Point1@@QAEXXZ),該符號在函數_main 中被引用

1>D:/MYY/program/VC/funobject/Debug/funobject.exe : fatal error LNK1120: 4 個無法解析的外部命令

1>生成日誌保存在“file://d:/MYY/program/VC/funobject/Debug/BuildLog.htm

1>funobject - 5 個錯誤,個警告

========== 生成: 0 已成功, 1 已失敗, 0 最新, 0 已跳過==========

 

從上面的提示裏面的括號我們就可以知道那些函數的全局命名,從而也可以可以見一斑了(管中窺豹嘛):

 

?print@Point1@@QAEXHH@Z   ?print@Point1@@QAEXM@Z   ?print@Point1@@QAEXXZ

我們來看一下在Point1中重載的那三個函數,從@@QAEX後面的就不同了,分別是:

帶兩個int參數的:HH@Z;    帶一個float參數的:M@Z    不帶參數的: XZ

 

我們可以猜想,VS2005中,帶參數的在最後要用@Z作後綴,floatM來表示,intH來表示,帶幾個參數就用幾個簡化的字母……(我也沒仔細看過vs2005name mangling的說明文檔啊,只是猜想,不足爲訓);至於是不是真的,要靠各位去看文檔和多做幾個例子了,呵呵。

 

g++中,又是另外一番風景:

(注意:在g++裏面,你不能從連接錯誤的提示裏面得到上面的這些信息,所以要用另外的辦法,如下:假設以上的代碼在test.cpp文件中,先編譯它,g++ -c test.cpp 得到test.o文件,然後用nm命令來查看test.o文件裏面的符號即可:nm test.o)

_ZN6Point15printEf   _ZN6Point15printEii  _ZN6Point15printEv  _ZN6Point25printEv

 

其中,我們也可以作如是猜測,並通過閱讀文檔去驗證它:ii表示兩個intv代表voidf代表float;等等(再次強調,我沒有讀過相關規格文檔去驗證,想知道的就自己上google去看看,呵呵。)

 

下面說相關的幾個問題:

 

1、   有了name mangling之後,下面的語句:pt.print(); 編譯器就會先看pt在符號表裏面(至於什麼是符號表,可以看編譯原理,我已經忘記了,也寫不出來,呵呵)的相關類型,查到是Point,然後,就用name mangling去轉換爲全局函數名稱調用。這就叫做“編譯期綁定”,也叫做“靜態綁定”(static binding)。

 

2、   關於函數重載,上面也說了,重載的函數經過name mangling之後,全局名稱後面的部分會隨着參數的個數,類型,和類型的順序而有所不同。正是因爲這種機制能區分出不同的函數,所以函數纔可以用這三點作爲重載的依據,至於返回值信息?很遺憾,至少我在轉換過後的全局名稱裏面看不到(技術文檔裏面的說明我就不清楚了)。現在有個問題,爲什麼返回值類型不能作爲重載的依據?下面看例子吧,假設允許返回值類型不同,則Point類裏面有兩個print成員函數,參數都爲空,一個返回int,另一個返回值爲void,那麼下面的調用語句:Point pt; pt.print(); 該調用哪一個print?(注意,返回值可以不用接收的,爲了靈活的緣故)。爲了避免出現二義性,所以返回值類型不作爲函數重載的依據。

 

3、   運算符重載:運算符重載也是一種函數重載。這裏要注意無論是C還是C++,標識符的名稱都只能由

數字,字母或下劃線組成,但是運算符重載好像是例外?(注意:我是說好像)下面看看vs2005下對於下面代碼的鏈接錯誤輸出:

 

 

 

 

1>main.obj : error LNK2019: 無法解析的外部符號"public: int __thiscall Point::operator+(int)" (??HPoint@@QAEHH@Z),該符號在函數_main 中被引用

1>      D:/MYY/program/VC/funobject/Debug/funobject.exe : fatal error LNK1120: 1 個無法解析的外部命令

 

看到了嘛,是??Hpoint@@QAEHH@Z,沒有加號。所以得出以下兩點推論:

第一點,爲什麼標識符只能以數字,字母和下劃線組成:因爲其他字符在編譯器裏面有別的重要的用途(看上面的全局名稱可知一二);還有就是因爲如果允許其它字符,那看一下下面的語句:a=b=c;標識符是abc呢,還是a=b顯然增加了編譯器的複雜度。

 

第二點,爲什麼運算符重載要求只能重載原有的運算符:因爲編譯器內部的符號表已經把這些運算符與某個符號對應起來了,比如+,編譯器裏面可能在符號表裏面有個符號對應這個+,假設是PLUS(這只是我的假設啊);如果現在允許你用新的符號重載,新的符號在符號表裏面沒有對應的選項,那麼用name mangling的時候,怎麼轉換那些符號?(再說一遍,C++標準的標識符是隻有數字,字母和下劃線,從這個角度來說,VS2005也不是100%遵循標準的,可能吧)

 

以上的是我的推測,沒有經過驗證,也沒有經過任何C++大師的肯定;所以可能是錯誤的,究竟如何,還請大家一起討論吧。

 

還有一個問題,就是C++C的混合編程,因爲C++比較複雜的原因,所以它的name mangling 技術也比較複雜,而C語言的name mangling技術就相對簡單(也許有人驚訝:“C也有這個啊”,當然有了,現在不是告訴你了嘛,呵呵),所以在C++中調用C的函數往往會在鏈接期間出錯,這個C的函數是指已經做成庫的形式提供的函數。

 

下面給個例子。(下面的例子在vs2005上進行說明)

 

先成一個在test.c中寫個C函數func

 

 

 

 

 

編譯它(確保後綴是.c,這樣vs2005會以C的方式編譯這個文件,注意是編譯,不是生成,快捷鍵是ctrl + F7

 

然後再創建個main.cpp中寫個調用語句:

 

  

 

現在可以生成解決方案了(快捷鍵是F7),看到連接錯誤了吧:

 

無法解析的外部符號"void __cdecl func(void)" (?func@@YAXXZ),該符號在函數_main 中被引用

 

那麼,這個問題怎麼解決呢?在main.c最上面聲明這個語句的時候在其前加上extern C就可以了,它會告訴編譯器,我這個函數名在轉換的時候用C式的name mangling~,這樣就能匹配,並而通過鏈接了,不信你試試?

 

 

 

 

 

 

至於CC++混合編程時更一般的解決方案,參考別的文章。

 

好了,C++程序在編譯期間把類的成員函數通過name mangling轉換成了唯一的全局函數,再通過符號表等技術把每條調用語句都轉換成爲調用相應的函數,好像問題都OK了,現在看一下以下的代碼:

 

 

 

 

 

對於上面程序的最後一行,我們想要的結果是調用Point2D中的print函數(這是多態的要求),但是,如果按照上面的討論,編譯器先在符號表裏面找到p的類型是Point,然後進行name mangling轉換最後的結果卻是Point裏面的print函數!!,那麼對於這種情況,我們聰明的編譯器又怎麼辦呢?它又會爲我們提供了怎樣的機制來解決這類問題,以正確的提供多態的語義呢?

 

請看下篇,動態綁定(dynamic binding),謝謝。

 

PS.

1、上面說的符號表可能相當於一種數據結構,把名稱,類型,和相關的屬性綁在一起的結構體,至於詳細的說明,請參考其他資料。

2、以上知識點在《深度探索c++對象模型》第4.1節中的:Nonstatic member function裏面

主要知識點如下:

 

c++的設計準則之一是非靜態成員函數必須和一般的全局函數有相同的效率。

 

也就是:

 

float Point::print();  和 float Point_print(Point *p); 在執行上要達到一樣的效率;

 

而達到一樣的效率的唯一方法就是將前者通過編譯器轉換成後者。這一節裏面就說明了相關的東西,相信會令你大開眼界的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章