衆所周知,強大的C++相較于C增添了許多功能。這其中就包括類、命名空間和重載這些特性。
對于類來說,不同類中可以定義名字相同的函數和變量,彼此不會互相幹擾。命名空間可以保證在各個不同名字空間内的類、函數和變量名字不會互相影響。而重載可以保證即使在同一個命名空間内的同一個類中,函數名字也可以相同,隻要參數不一樣就可以。
這樣的設計友善了程式開發者,不用擔心不同開發者都定義相同名字的函數的問題。但是,這也使得符号管理變得更為複雜。
對于在不同類中的同名函數,或者在不同名字空間中的同名函數,或者在同一名字空間或類中的同名重載函數,在最終的編譯和連結過程中是怎麼将它們區分開來的呢?為了支援C++這些特性,人們發明了所謂的符号改編(Name Mangling)機制。
其原理其實很簡單,就是按照函數所在名字空間、類以及參數的不同,按照一定規則對函數進行重命名。不同的編譯器其命名規則都不盡相同,這裡我們主要介紹GNU C++編譯器所使用的規則。主要分為以下幾種情況:
1)全局變量:
即在命名空間和類之外的變量,改編後的符号名就是變量名,也就是不做任何修改。
2)全局函數:
以“_Z”開頭,然後是函數名字元的個數,接着是函數名,最後是函數參數的别名。
關于函數參數的别名,後面還會有詳細的介紹。
3)類或命名空間中的變量或函數:
以“_ZN”開頭,然後是變量或函數所在名字空間或類名字的字元長度,然後接着的是真正的名字空間或類名,然後是變量或函數名的長度和變量或函數名,後面緊跟字母“E”,最後如果是函數的話則跟參數别名,如果是變量則什麼都不用加。
4)構造函數和析構函數
以”_ZN”開頭,然後是構造函數所在名字空間和類名字的字元長度,然後接着的是真正的名字空間或類名,然後構造函數接“C1”或者“C2”,析構函數接“D1”或者“D2”,然後加上字母“E”,最後接函數參數别名結束。
介紹完命名規則,下面我們再具體介紹一下函數參數别名的規則。主要分為下面幾種情況:
1)函數參數是基本類型時
每個基本類型的别名如下表:
參數類型 | 參數别名 |
void | v |
wchar_t | w |
bool | b |
char | c |
signed char | a |
unsigned char | h |
short | s |
unsigned short | t |
int | i |
unsigned int | j |
long | l |
unsigned long | m |
long long或__int64 | x |
unsigned long long或unsigned __int64 | y |
__int128 | n |
unsigned __int128 | o |
float | f |
double | d |
long double或__float80 | e |
__float128 | g |
2)函數參數是類或結構體時
當函數的參數中含有類或結構體時,在類或者結構體名字前加上類或結構體名的字元長度。
例如,全局函數int structure_func(int i, struct test s, double d),其經過符号改編後,函數名變成了_Z14structure_funci4testd。
3)函數參數是指針(*)時
當函數參數中含有指針時,該參數的别名是“P”(大寫)加上該指針指向的參數類型的别名。當參數為指針的指針時,該參數的别名是“PP”加上所指向的參數類型的别名,以此類推。
4)函數參數是一維數組時
當函數參數中含有一維數組時,和參數是指針的處理方式一樣,也是“P”加上作為參數的數組其元素類型的别名。
5)函數參數是多元數組時
對于多元數組,第一維可以看做是指針,其它維則看做是數組。
當函數參數中含有多元數組時,以“P”(代表數組的第一維)開始,後面接“A”加上各維數組的長度,以“_”間隔,最後以下劃線加數組元素類型的别名結束。
例如,全局函數void multi_array_func(int a[10][10][20][30]),其經過符号改編後,函數名變成了_Z16multi_array_funcPA10_A20_A30_i。
6)函數參數含有const修飾符時
當函數參數中含有const修飾符時,以“K”(大寫)開始,後面接修飾參數類型的别名。
7)函數參數是引用(&)時
當函數參數中含有引用時,該參數的别名是“R”(大寫)加上該引用所引用的變量類型的别名。
例如,全局函數void ref_const_func(const int &i),其經過符号改編後,函數名變成了_Z14ref_const_funcRKi。
8)函數參數是别的命名空間中的類或結構體
當函數的參數含有别的命名空間中的類或結構體時,該參數的别名是“N”(大寫),加上空間名的長度,再加上空間名,接着是類或結構名的長度和類或結構的名字,最後以“E”(大寫)結束。
再舉一個複雜點的例子,假設代碼如下:
[cpp] view plain copy
1. namespace NS1
2. {
3. class Test1
4. {
5. };
6. }
7.
8. namespace NS2
9. {
10. class Test2
11. {
12. public:
13. void MyFunction(NS1:Test1 t) {}
14. };
15. }
那麼MyFunction經過符号改變後變成了什麼呢?答案是:_ZN3NS25Test210MyFunctionEN3NS15Test1E。
最後,稍微總結一下。其實所謂GNU C++的符号改編機制非常簡單,隻要記住下面幾點就可以了:
1)除了全局變量不用做改編之外,其它所需要改編符号的時候,都是以_Z開始;
2)若想表示某個符号是在命名空間或類中的,要以“N”開始,以“E”結束;
3)所有的名字空間名、類名、函數名或變量名,改編的時候都是名字所包含的字元數加上真正的名字;
4)所有的名字按照從外層到裡層的順序進行改編;
5)如果是函數的話,所有的參數按照前後出現的順序進行改編。
最後再提一句,這裡的符号改編機制都是暗地裡編譯器幫你做的。隻要你的程式使用GNU C++編譯器進行編譯,它都會用上文所述的規則對你的各種符号名進行改編(包括變量和函數)。
如果你的程式有一些用C語言編寫及編譯,而另外一些用C++語言編寫及編譯,并且這兩部分還會互相調用到,則需要進行特殊處理。
C++程式在編譯的時候會用符号改編,而C程式在調用的時候并不會用符号改編,而是還用原始的函數名作為符号名進行調用,這樣C程式就找不到那個對應的C++函數了。
或者,倒過來,C程式在編譯的時候不會進行符号重編,而C++程式在調用的時候也會将這個函數名進行重編,這樣C++程式同樣也找不到那個對應的C函數了。
解決的方法是把那些需要讓C程式用到的C++程式中的變量和函數,或者C++程式用到的C程式中的變量和函數,單獨抽出來,讓編譯器不對它們進行符号重編。
具體方法是将它們用extern "C"包起來:
[cpp] view plain copy
1. extern "C"
2. {
3. void func();
4. ......
5. }
[cpp] view plain copy
1. extern "C" void func();
2. ......