最近在微博上向兩位大神問了個“關於頭文件中定義全局變量”的問題,然后得到了好多回答,有以前就知道的,有以前不知道的,現就個人所獲總結一下,把對C語言的理解理一下。
一、C語言的編譯過程
我們知道我們寫的C文件是一堆ASCII字符,而計算機實際運行程序是一對二進制數,他們之間必須有一個轉換,才能正常的運行。這個轉換就是編譯過程,C語言的編譯過程包括以下幾步:1)預處理;2)編譯;3)鏈接;
以編譯b.h,b.c和a.c文件為例說明以上步驟:
1.預處理
預處理就是對那些源代碼中前面加#號的代碼的處理,它主要做一些替換處理工作,比如:”頭文件包含“,”宏定義替換“等,經過預處理器(cpp)預處理后,把a.c和a.h生成ASCII碼的中間文件a.i文件。
在預處理中,頭文件包含就是指,在a.c文件中將#include“b.h”刪除,而將a.h中的所有內容通通拷貝到a.c文件中原本屬於#include“b.h”的位置。
2.編譯
將預處理后的單個文件編譯成.o文件,編譯還可以細分為以下兩個步驟:
a)C編譯器(ccl)將a.i文件翻譯成一個ASCII匯編語言文件a.s;
b)匯編器(as)將a.s文件翻譯成可重定位目標文件a.o。
3.鏈接
編譯器用同樣的步驟將b.c和b.h編譯成可重定向目標文件b.o。然后連接器(ld)將a.o和b.o鏈接生成可執行文件a(在windows中即生成我們熟悉的a.exe)。
二、“頭文件中定義全局變量”的本質(1)
通過前面,我們知道編譯器是不認所謂頭文件的,在預處理的時候,頭文件就已經被拷貝到源文件中了。所以在頭文件中定義全局變量實際就是在每個包含此頭文件的源文件中定義了一個全局變量,然而由於在C語言中所有的源文件都是單獨編譯的,所以在編譯階段,文件之間不知道他們擁有一個與別人沖突的全局變量,編譯順利通過。在接下來的敘述中將不再關注頭文件,而直接關注各源文件中相互沖突的全局變量。
然后到了鏈接階段,鏈接器鏈接時,發現竟然有多個.o文件擁有名字相同的全局變量,這怎么搞,不可能放任他們同時存在的,不然這么多重定向文件生成一個可執行文件后,都不知道該調哪個。
舉個例子,就像每個小班都有一個叫張三的人,在每個班單獨上課時,老師喊張三,所有人都知道叫的是自己班的張三。有一天上大課,老師發現每個班都有一個張三,這時候他再喊張三時,就沒有人知道他喊的是哪個張三,於是就亂了。
既然同時有多個同名全局變量不行,那面對這種情況鏈接器會怎么處理呢?
三、鏈接器的處理方式
面對相同的名字鏈接器是分情況來處理的。
首先,所有的全局符號,在鏈接器這當做兩類看待:a)強符號;b)弱符號。
強符號包括:已經初始化的全局變量(初始化和賦值是不同的,注意區分)、函數名;
弱符號包括:沒有初始化的全局變量。
接着,鏈接器根據不同的符號組合,有不同的處理方式:
a)強符號之間沖突,直接報錯,鏈接失敗。
b)強符號與弱符號之間沖突,強符號覆蓋弱符號。
c)弱符號之間沖突,鏈接器會自己選一個來覆蓋其他符號,選擇方式各編譯器不同。
四、“頭文件中定義全局變量”的本質(2)
我們知道了鏈接器的處理方式,那么我們將得出以下結論:
a)兩個沖突的全局變量在定義時都做了初始化,則直接報錯,編譯不成功;
b)兩個沖突的全局變量在定義時,只有一個做了初始化,則鏈接時,其他同名全局變量被此變量替換;
c)兩個沖突的全局變量在定義時,都沒有做初始化,則在鏈接時,根據鏈接器的喜好,從中選一個作為最終的全局變量替換其他變量;
d)若未初始化的全局變量與函數名沖突,則函數名替換掉全局變量;(A:非常變態,都不知道怎么死的! B:呵呵,還好,只要調用全局變量,一般都會報類型錯誤的)
e)若初始化后的全局變量和函數名沖突,則直接報錯。
五、良好的編程習慣--繞開這個坑
1.繞開這個坑
既然知道了這個坑長得什么樣,那就接着來總結一下避免掉進去的方法:
a)不要在頭文件中定義(definition)變量;
b)各源文件中的不准備共用的全局變量都搞成靜態變量,前面加個static;
c)全局變量在定義時最好初始化;
d)函數名和變量名用不同的命名方式,最大限度的避免沖突。
e)不要定義同樣的變量名,我個人覺得可以將變量名和文件名關聯。
2.一些解釋
其他幾條都比較好理解,再來解釋一下第二條吧。
編譯器在編譯單個文件時,為靜態變量和全局變量在.data區中分配空間,然后將其名字寫在.systab中,但在編譯器傳給匯編器時,把靜態變量的名字做了一些處理,使其具有唯一的名字(具體怎么做的我不知道,我猜可能是和文件名關聯了一下),而全局變量沒有做這個處理。所以在鏈接時,全局變量會產生沖突,而靜態變量由於名字唯一,無沖突,每個文件中使用的變量都是自己定義的那個變量。
六、一些實驗
為進一步搞清楚不同編譯器對全局變量的處理,分別在VS2010和gcc上做了如下試驗:
1.多個.c文件中定義相同名稱的全局變量,而沒有聲明:
1 //a.c 2 3 #include "a.h" 4 5 int a; 6 int main() 7 { 8 fun(); 9 10 printf("a 在a中的地址為%d \n",&a); 11 return 0; 12 }
1 //b.c 2 3 #include "a.h" 4 5 int a; 6 void fun() 7 { 8 9 printf("a 在b中的地址為%d \n",&a); 10 }
1 //a.h 2 3 #include<stdio.h> 4 5 void fun();
以上代碼在gcc中的編譯結果為通過且在兩個文件中a為相同的地址,即在兩個文件中使用同一個a,在a.c中改變a的值,b.c中a的值同樣發生變化。在VS2010中的表現與gcc一致。運行結果:
a 在b中的地址為619860
a 在a中的地址為619860
2.多個.c文件中使用相同名稱的靜態變量
1 //a.c 2 3 #include "a.h" 4 static int a; 5 6 int main() 7 { 8 fun(); 9 10 printf("a 在a中的地址為%d \n",&a); 11 return 0; 12 }
1 //b.c 2 3 #include "a.h" 4 static int a; 5 6 void fun() 7 { 8 9 printf("a 在b中的地址為%d \n",&a); 10 }
1 //a.h 2 3 #include<stdio.h> 4 5 void fun();
gcc對上述代碼編譯通過,顯示兩個不同文件中的a的地址不一樣,即兩個a是相互獨立無關的;vs2010的表現與gcc一致。運行結果如下:
a在b中的地址為4210696
a在a中的地址為4210700
3.全局變量與靜態變量名沖突
1 //a.c 2 3 #include "a.h" 4 5 int a; 6 int main() 7 { 8 fun(); 9 10 printf("a 在a中的地址為%d \n",&a); 11 return 0; 12 }
1 //b.c 2 3 #include "a.h" 4 static int a; 5 6 void fun() 7 { 8 9 printf("a 在b中的地址為%d \n",&a); 10 }
1 //a.h 2 3 #include<stdio.h> 4 5 void fun(); 6 7 extern int a;
在gcc下上述代碼編譯不通過,報error: static declaration of 'a' follows non-static declaration
在vs2010下,上述代碼編譯通過,兩個a相互獨立,互不相關,運行結果如下:
a在b中的地址為17461560
a在a中的地址為17462620
4.在頭文件中做了聲明,但在所有.c文件中全部定義為靜態變量
1 //a.c 2 3 #include "a.h" 4 static int a; 5 6 int main() 7 { 8 fun(); 9 10 printf("a 在a中的地址為%d \n",&a); 11 return 0; 12 }
1 //b.c 2 3 #include "a.h" 4 static int a; 5 6 void fun() 7 { 8 9 printf("a 在b中的地址為%d \n",&a); 10 }
1 //a.h 2 3 #include<stdio.h> 4 5 6 void fun(); 7 8 extern int a;
在gcc中依然是編譯不通過。
在vs2010中link時有警告,但依然編譯成功,運行結果如下:
a在b中的地址為18641212
a在a中的地址為18641208
以上算是對自己的交代。