天天看點

GNU C++的符号改編機制介紹(函數的名稱粉碎格式解析)

衆所周知,強大的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. ......      

繼續閱讀