一、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



 

   以上算是对自己的交代。