由於平時很少用到__attribute__定義函數或者變量的符號屬性,所以很難想象C語言可以向C++一樣進行函數或者變量的重載。
首先,複習一下有關強符號與弱符號的概念和編譯器對強弱符號的處理規則:
1.不同變量與函數所在段
變量類型我們可以分爲1)未初始化的,已初始化的;2)全局,局部;3)靜態,非靜態。
變量類型 | 所在段 |
初始化非靜態全局變量 | .data |
初始化靜態全局變量 | .data |
初始化靜態局部變量 | .data |
初始化絕非靜態局部變量 | 運行時動態分配 |
未初始化非靜態全局變量 | .bss |
未初始化靜態全局變量 | .bss |
未初始化靜態局部變量 | .bss |
未初始化非靜態局部變量 | 運行時動態分配 |
假設有如下代碼:
int global_init_var = 1;
int global_uninit_var;
static int static_init_var = 2;
static int static_uninit_var;
int main(void){
int local_init_var = 3;
int local_uninit_var;
static int static_local_init_var = 4;
static int static_local_uninit_var;
global_uninit_var = global_init_var+10;
static_uninit_var = static_init_var+10;
local_uninit_var = local_init_var+10;
static_local_uninit_var = static_local_init_var+10;
printf("%d,%d,%d,%d\n", global_uninit_var, static_uninit_var, local_uninit_var, static_local_uninit_var);
return 0;
}
全局未初始化變量其實在彙編完成後被分配爲COMMON特殊段內,在鏈接時候根據程序是否調用此變量決定是否將其置於.bss段中。
2.強符號與弱符號
聲明爲全局變量和非靜態函數,編譯器都將其符號視爲強符號,而使用__attribute__((weak))可將符號指定爲弱符號。
編譯器在遇到強、弱符號時,遵循下面的規則進行處理:
1)遇到多個強符號,出錯並提示有符號重複定義;
2)遇到一個強符號,多個弱符號,則選用強符號,弱符號被忽略,此時其他文件對其原若符號的操作將產生誤操作(弱符號地址指向了強符號地址)
3)多個弱符號,與鏈接器有關,一般選用佔用空間最大的符號。
看一個有趣的代碼:
在main.c和foo.c中分別寫入foo_only函數
void foo_only(void){
printf("foo_only() is in %s", __FILE__);
}
此時,編譯gcc -o test foo.c main.c就會提示符號被重複定義,這是因爲foo_only在兩個文件中均爲強符號。當我在main.c和foo.c中的任意一個文件內加上void foo_only(void) __attribute__((weak))
那麼鏈接器就會自動鏈接默認的強符號。
3.C語言重載方法
如果在使用第三方的庫或者自己提供庫給別人使用時,對庫中某些模塊不滿意,那麼應該提供客戶一個自給自足的機會。此時,weak屬性就非常有用。
實驗代碼:
foo1.c
foo2.c
運行gcc -o symbol_weak_strong foo1.c foo2.c
1)外部有則引用,沒有就用自己的(外部文件函數爲強符號,本文件函數爲弱符號)
現在我不知道外部文件foo2.c是否有我所使用的函數,那麼我先自己在文件foo1.c中定義一個,請注意foo2函數。
在含main的文件將foo2聲明爲弱符號,而外部文件的函數在無顯示聲明爲弱符號時,均爲強符號,因此,
編譯器將選擇foo2.c中的foo2函數。
2)自己有的就用自己的,沒有就引用外部的(外部文件函數聲明爲弱函數)
如果我對自己的代碼水平有足夠信心,那麼只要我寫了,我就用自己的。那麼就要求外部文件將函數符號聲明爲weak,下面的聲明語句位於foo.c。
此時,在foo1.c中的函數foo1作爲強符號使用,外部文件的foo2.c中的foo1就顯式聲明爲弱符號。
還可以在foo1.c主文件內顯式聲明外部文件的函數爲弱符號:extern void foo1(void) __attribute__((weak)); 這個被稱爲弱引用。
3)如果不小心兩個函數都聲明爲弱符號,那麼編譯器怎麼選擇?
說法一:按照函數佔用空間最大的函數作爲引用對象;
說法二:按照編譯鏈接順序進行引用。
由於與編譯器有關,所以再次不作詳細討論。
4. C語言的“僞函數重載“與C++的重載區別
C++爲了避免C語言那樣,不同人開發不同模塊代碼中,使用了相同的函數或變量名,增加了名稱空間和符號修飾來避免多模塊之間的符號衝突問題。
(C程序員會盡量避免使用全局變量或者規範命名方法來避免符號衝突,但無法解決這一問題)
GCC對於C++名稱修飾方法如下(摘自《程序員的自我修養》):
所有的符號都以”_Z"開頭,對於嵌套的名字(在名稱空間或在類裏面的),後面緊跟“N",然後是各個名稱空間和類的名字,每個名字前是名字字符串的長度,再以”E"結尾,如果是函數,則E後面緊跟參數列表的類型簡寫。比如
namespace N{
class C{
int var;
void func(float);
}
}
作用域爲N::C::func的函數經過名稱修飾後就是_ZN1N1C4funcEf。
作用域爲N::C::var的函數經過名稱修飾後就是_ZN1N1C3varE。
使用c++filt可以解析被修飾過的名稱。
C++做符號修飾是在彙編階段完成的;而上述所說的C聲明弱符號是作用在鏈接階段。