首先我表示很悲劇,在看《程序員的自我修養--鏈接、裝載與庫》之前我竟不知道C有強符號、弱符號、強引用和弱引用。在看到3.5.5節弱符號和強符號時,我感覺有些困惑,所以寫下此篇,希望能和同樣感覺的朋友交流也希望高人指點。
首先我們看一下書中關於它們的定義。
引入場景:(1)文件A中定義並初始化變量i(int i = 1), 文件B中定義並初始化變量i(int i = 2)。編譯鏈接A、B時會報錯b.o:(.data+0x0): multiple definition of `i';a.o:(.data+0x0): multiple definition of `i'。(2)在文件C中定義並初始化兩個變量i(int i = 1; int i = 2), 編譯鏈接時會報錯c.c:2:5: error: redefinition of ‘i’; c.c:1:5: note: previous definition of ‘i’ was here。
強符號:像場景中這樣的符號定義被稱為強符號,對於C/C++來說,編譯器默認函數和初始化的全局變量為強符號。
弱符號:接上文,為初始化的全局變量為弱符號。
編譯器關於強弱符號的規則有:(1)強符號不允許多次定義,但強弱可以共存;(2)強弱共存時,強覆蓋弱;(3)都是弱符號時,選擇占用空間最大的,如選擇 double類型的而不選擇int類型的。
由以上定義所以有我之前沒有想到的場景:
代碼a.c:
1 int i = 2;
代碼b.c:
#include<stdio.h> int i; int main(int argc, char** argv) { printf("i = %d\n", i); return 0; }
編譯文件a和b並鏈接,結果輸出i為2而不是0。
並且在同一個文件中定義但未初始化兩個相同的變量不會報錯,只有在使用變量時才會報錯。
對於GCC編譯器來說,還允許使用__attribute__((weak))來將強符號定義為弱符號,所已有
代碼c.c
1 #include<stdio.h> 2 3 __attribute__((weak)) int i = 1; 4 5 int main(int argc, char** argv) 6 { 7 printf("i = %d\n", i); 8 return 0; 9 }
結果i的輸出仍未2而不是1。
那麼對於函數而言是不是也這樣呢?先不看函數,而是先看由強弱符號而進一步引入的強弱引用。書中關於強弱引用的概述是對於強引用若未定義則鏈接時肯定會報錯,而對於弱引用則不會報錯,鏈接器默認其為0(這一點對於函數好理解,即函數符號所代表入口地址為0;對於變量就要注意了,既然是引用那自然就是地址了,所以同函數一樣變量的地址為0而不是變量的值為0)。此時對於強弱引用是不是還沒有什麼明確的概念呢?到底什麼是引用?引用和符號又是什麼關系?這裡我說一下我的理解(歡迎指正),在定義和聲明處指定的函數名、變量名即為對應的符號,而在代碼其他處調用函數或使用變量時,則把函說明和變量名看作引用,這樣一來符號和引用在代碼層面上其實就是一個東西,只是根據環境而叫法不同而已。那麼強符號對應強引用,弱符號對應弱引用。
有上面的強弱引用的特點可看出,當一個函數為弱引用時,不管這個函數有沒有定義,鏈接時都不會報錯,而且我們可以根據判斷函數名是否為0來決定是否執行這個函數。這樣一來,包含這些函數的庫就可以以模塊、插件的形式和我們的引用組合一起,方便使用和卸載,並且由於強符號可以覆蓋弱符號和強弱符號與強弱引用的關系可知,我們自己定義函數可以覆蓋庫中的函數,多麼美妙。
先看根據條件判斷是否執行函數:
代碼d.c
1 #include<stdio.h> 2 3 void func() 4 { 5 printf("func()#1\n"); 6 }
代碼e.c
1 #include<stdio.h> 2 3 __attribute__((weak)) void func(); 4 5 int main(int argc, char** argv) 6 { 7 if (func) 8 func(); 9 return 0; 10 }
編譯d.c,cc -c d.c 輸出d.o;編譯e.c並鏈接d.o,cc d.o e.c -o e輸出可執行文件e,運行e正常執行函數func。編譯e.c但不鏈接d.o,此時並不會報錯,只不過func不會執行,因為沒有它的定義所以if(func)為假。
再看函數覆蓋:
代碼f.c
1 #include<stdio.h> 2 3 __attribute__((weak)) void func() 4 { 5 printf("func()#1\n"); 6 }
代碼g.c
1 #include<stdio.h> 2 3 void func() 4 { 5 printf("func()#2\n"); 6 } 7 8 int main(int argc, char** argv) 9 { 10 func(); 11 return 0; 12 } 13 ~
編譯鏈接,結構輸出"func()#2"。
以上可以說明函數和變量是保持一致的,其實對應變量也可以像使用函數那樣先判斷再使用,只不過不是判斷變量的值而是變量的地址,如
代碼v1.c
int i = 2;
代碼v2.c
1 #include<stdio.h> 2 3 __attribute__((weak)) extern int i; 4 5 int main(int argc, char** argv) 6 { 7 if (&i) 8 printf("i = %d\n", i); 9 return 0; 10 } 11 ~
編譯並鏈接v1時,輸出2;編譯但不鏈接v1時無輸出。這樣做時要分清定義和聲明的區別,__attribute__((weak)) int i 是定義變量並轉換為弱符號,這樣i是分配了空間的,而__attribute__((weak)) extern int i 則將原來定義的變量i由強符號轉換為弱符號,導致使用i時不是強引用而是弱引用。不過雖然變量可以這麼做但沒有函數那樣有意義。
上面關於強弱引用仍舊使用的是GCC提供的__attribute__((weak)),而書中還提到了__attribute__((weakref)),後者貌似更能體現“引用”這一關鍵詞。而我之所以使用前者來介紹強弱引用,是因為我對關於強弱符號與強弱引用對應關系的理解。關於__attribute__((weakref))的使用方法,這裡介紹一種(兩者都有不同的使用方法)。
代碼a.c
1 #include<stdio.h> 2 3 void bar() 4 { 5 printf("foo()\n"); 6 }
代碼b.c
1 #include<stdio.h> 2 3 static void foo() __attribute__((weakref("bar"))); 4 5 int main(int argc, char** argv) 6 { 7 if (foo) 8 foo(); 9 10 return 0; 11 }
注意函數foo的static修飾符,沒有的話會報錯,這樣將函數foo限制在只有本文件內可使用。
好了,夜已深,寫的有點凌亂,我也凌亂了。
qq(int* q)中的(int* q)是qq這個函數的形參表,int* q表示的是q是一個指向int對象(或數據類型)的指針,即該函數接受一個int型指針為參數;
至於qq(int& q),這個貌似只有在C++中才見得到吧,該句的意思是函數qq接受一個int類型的引用(引用只有在C++等語言中才有)q。
而qq(int q),表示的是qq這個函數接受一個整形的數據q。
默認情況下,C/C++是按值傳遞(對於C則是任何時候都是按值傳遞),也就是說當你調用一個接受參數的函數的時候,該函數的形參是原參數的一份拷貝,舉例:
void qq(int a) // 形參是a局部變量
{
a++; // 只改變形參,不會影響b
}// a在此時被自動銷毀
int main()
{
int b = 1;
qq(b);
printf("%d", b); // 打印出1
}
可以看出在傳值給函數qq的時候,a僅僅作為b的一份拷貝,他們所駐扎的存儲空間是不一樣的,所以改變a不會影響b。
當然在C中傳指針時也是按值傳遞,
void qq(int* a)
{
*a = 1989; // a存儲著b的地址,*a用於獲得駐扎在該地址的b的值
} // 指針a在此時也被自動銷毀,
int main()
{
int b = 1;
qq(&b); // 按值傳遞,傳遞的是b的地址
printf("%d", b); // 打印出1989
}
這一次qq的形參a是一個指針,所以接受的是b的地址的拷貝(地址是一個長整形數據)而不是b的拷貝,當在函數qq中使用*a = 1989時,*a把b的地址所存儲的值改變為1989,所以外部世界的b的值(這裡指的是main中的b,外部是相對於函數qq而言)也隨之改變。
而對於函數qq(int& a),這是C++中引入的一個新類型:引用,所帶來的新的函數傳值方式,即按引用傳值。舉例:
void qq(int& a) // a可以看作是b的別名,a其實和b擁有相同的內存地址
{
a = 1989;
} // a被銷毀了嗎?
int main()
{
int b = 1;
qq(b); // 看起來像按值傳遞
printf("%d", b); // 結果和上面的一樣,也是1989
}
引用的意義是指代原對象本身,也可以理解為別名,比如某人被別人起了多個外號,小a,大q,傻x,而他本人可能叫w,但不管是小a,大q,傻x還是w,其他人都知道他們是同一個人。因此對傳入qq的參數a可以理解為main中b的別名,即等價於main中的b,所以任何對a的操作其實就是對b的操作,因此main中b的值最後也發生了改變。
簡單地加以概括,其實函數傳值就那麼2種類型:
1、改變實參的值。
如傳指針或引用時改變他們所引用對象的值。
2、不改變實參的值。
一般的按值傳遞。
這下應該很容易理解了吧。...余下全文>>
強forte(f), 弱piano(p),
中強mezzo-forte(mf), 中弱mezzo-piano(mp),
漸強crescendo(cresc)(<),漸弱diminuendo(dim)(>)
括號裡是縮寫