Name Mangling,直接翻譯過來爲名字改寫 。它是深刻理解 C++ 編譯連接模型的必由之路。
筆者近期進行數據庫開發工做時,涉及到MySQL客戶端的編譯連接的問題,經過從新釐清了以前理解只知其一;不知其二的Name Manging,解決了讓人抓狂的編譯連接問題。
接下來,和你們聊聊C++的Name Mangling。ios
在進行編程的過程之中,咱們經常碰見變量或函數重名的狀況。好比:函數的重載,或經過不一樣程序塊與命名空間變量與函數的重名。c++
而在出現變量或函數名相同的狀況下,編譯器進行代碼編譯時須要保證變量與函數的簽名的全局惟一性。若是沒法進行上述保證,在連接階段就會產生連接的二義性,會致使編譯器不知道應該如何取用正確的變量與函數符號的內存地址。數據庫
爲了解決上述問題,編譯器實現了一種叫作Name Mangling的方式:它經過一個固定的命名規則來從新組織源代碼之中咱們定義的變量名和函數名,來確保了可以將被連接的目標文件中的符號簽名的惟一性。(因爲在C++的標準之中,並未強制規定Name Mangling的實現機制,因此不一樣的編譯器在不一樣的平臺上實現是徹底不一樣的。筆者的後續關於Name Mangling的講解將基於Linux上的GCC展開。)編程
上述內容講明白了Name Mangling的意義,咱們來經過實際的代碼來瞅瞅它是如何生效的。api
首先看看以下代碼:app
#include <iostream> #include <string> #include <vector> namespace Happen { struct MyClass { std::vector<std::string> _str_vec; }; } int main() { Happen::MyClass myClass; return 0; }
接下來,咱們使用g++獲取它的彙編代碼編輯器
g++ -S main.cpp
使用編輯器打開生成的main.s
文件,咱們就能夠看到下面這些被Name Mangling以後的命名了。函數
call _ZN6Happen7MyClassC1Ev movl $0, %ebx leaq -48(%rbp), %rax movq %rax, %rdi call _ZN6Happen7MyClassD1Ev
這裏能夠看到,代碼調用了_ZN6Happen7MyClassC1Ev
與_ZN6Happen7MyClassD1Ev
這兩個函數。這其實就是代碼之中調用了咱們定義的MyClass的構造函數與析構函數。而這裏使人望而生畏的命名就是Name Mangling的功勞啦~~工具
既然有了Name Mangling了,天然就要有Name DeMangling。上面的_ZN6Happen7MyClassC1Ev還能大概齊猜想出意思,可是你肯定你能看懂下面的這一長串Name Mangling以後的結果:學習
MN6Happen7MyClassESt6vectorINSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEESaIS7_EE
在C++之中,咱們經常使用typeid
,來獲取類型的type_info
信息,而Name Mangling就包含在type_info
之中,咱們來看以下代碼:
#include <iostream> #include <string> #include <vector> namespace Happen { struct MyClass { std::vector<std::string> _str_vec; }; } int main() { std::cout << typeid(&Happen::MyClass::_str_vec).name() << "\n"; return 0; }
它的輸出正是上面那串讓人「抓狂」的命名,咱們如今嘗試經過GNU的API來脫掉它的馬甲,真正的看看它究竟是啥。
這裏使用了abi::__cxa_demangle
來獲取DeMangling時真正的結果。
#include <iostream> #include <string> #include <vector> #include <cxxabi.h> namespace Happen { struct MyClass { std::vector<std::string> _str_vec; }; } int main() { char* real_name = abi::__cxa_demangle(typeid(&Happen::MyClass::_str_vec).name(), \ nullptr, nullptr, nullptr); std::cout << real_name << "\n"; return 0; }
這是經過Name DeMangling實際輸出的結果。(囧rz,好像可讀性也並無太好,C++的類型系統實在是太複雜了,不過起碼能讓咱們看清楚真正的名字是啥了。)
std::vector<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::allocator<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > Happen::MyClass::*
經過代碼進行名字辨析確實會帶來諸多不便,因此Linux提供了兩個好用的工具:
nm與c++filt,它們能夠做用在二進制文件,函數連接庫等之上(nm其實就是name mangling的縮寫)
經過nm的-C
參數就能夠直接輸出name demangling以後的結果了。
nm -C bin/.so/.a
或者也能夠經過c++filt
來實現一樣的功能
nm bin/.so/.a | c++filt
C++可以支持調用C語言的函數,一樣也支持實現函數庫被C語言調用,這個過程之中就涉及到兩種語言交互的Name Mangling了。(這個問題會經常致使編譯時出現使人抓狂的undefined reference to 『xxx』, 不少時候會讓人丈二和尚摸不着頭腦)
因爲C語言不支持函數重載,命名空間,類等邏輯,因此C語言的Name Mangling比C++簡單不少。咱們來看看經過gcc和g++的編譯結果有和不一樣吧,首先咱們定義一個簡單的函數sum
:
int sum(int a, int b) { return a + b; }
_Z3sumii
sum
這裏能夠明顯看到兩者的不一樣,因爲C++支持函數重載。因此須要在Name Mangling時添加參數的信息,也就是後面的兩個ii
,指代兩個int
類型。
因此經過C++定義的函數須要被C語言調用時,須要經過keyword:extern C
來顯式的讓編譯器明白鬚要使用C語言的Name Mangling規則,以便編譯器連接時可以正確的識別函數簽名來定位到所需的函數。
extern "C" { int sum(int a, int b) { return a + b; } };
將上述函數改寫爲上面的方式以後,經過g++編譯的結果也變爲了咱們所期待的sum
了。
C++的編譯連接問題經常讓人抓狂,不少時候若是沒有深刻了解這個過程之中的邏輯,很容易陷入困境。本篇聊了聊筆者在遇到編譯問題時學習Name Mangling來最終解決問題的學習小結。
但願你們可以有所收穫,筆者水平有限。成文之處不免有理解謬誤之處,歡迎你們多多討論,指教。