4.1什么是连接器
C语言的一个重要思想是分别编译(Separate Compilation),即 若干个源程序可以在不同的时候单独进行编译,然后在恰当的时候整合到一起。
尽管连接器不理解C语言,但理解机器语言和内存布局。编译器把C源程序“翻译”成对连接器有意义的形式,这样连接器才能够读懂C语言。
典型的连接器把由编译器或者汇编器生成的若干个目标模块,整合成一个被称为载入模块或者可执行文件的实体,该实体能够被操作系统直接执行。某些目标模块是直接作为输入提供给连接器,而另外一些目标模块是根据连接过程的需要,从库文件中取得。
连接器通常把目标模块看成是由一组外部对象(external object)组成的。每个外部对象代表机器内存中的某个部分,并通过一个外部名称识别。
大多是的连接器禁止同一个载入模块中两个不同的外部对象拥有相同的名称。
连接器的工作情形:连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器的输出输一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有可同名的外部对象。如果没有,连接器就将该外部对象添加到载入模块中;如果有,连接器就要开始处理命名冲突。
4.2声明与定义
声明语句 int a; 如果出现位置在所有的函数体之外,那么就称其为外部对象 a 的定义。该语句说明了 a 是一个外部整型变量,同时为 a 分配了存储空间。由于外部对象 a 未被明确的指定初始值,所以C语言中默认值 为 0.
声明语句 int a=7; 在定义 a 的同时也为 a 指定了初始值。则这个语句不仅为 a 分配内存,而且说明了该内存中应该存储的值。
声明语句 extern int a ; 并不是对对象 a 的定义。这个语句仍然声明了 a 是一个外部整型变量,但因为包含了 extern 关键字,这就显示的声明了 a 的存储空间是在程序的其他地方分配的。从连接器 的角度看,上述声明是一个对外部对象 a 的引用,而不是对 a 的定义。这种声明形式即使出现在函数内部仍然有同样的含义,表示某个外部对象的显示引用。
每个外部对象都必须在程序的某个地方进行定义。比如,如果程序中包含了语句 extern int a;则该程序就必须在别的某个地方包括语句 int a;,这两个语句既可以是在同一个源文件中,也可位于不同的源文件中。
如果程序对同一个外部变量的定义不止一次,即定义 int a; 出现在两个或者更多的不同的源文件中,大多数系统都会拒绝该程序。
4.3命名冲突和static 修饰符
两个具有相同名称的外部对象实际上代表的是同一个对象(无论编程的人怎么想,系统会如此处理)。
如果两个不同的源文件中都包括了定义 int a;,那么或者程序错误(若连接器禁止外部变量重复定义),或者在两个源文件中共享 a 的同一个实例(无论两个源文件中的外部变量 a 是否应该共享)
即使两个重复定义的外部变量中的一个出现在系统提供的库文件中,也仍然进行同样的处理。
static 修饰符是一个能够减少此类命名冲突的有用工具。比如,static int a;和 int a ;,两个语句的含义相同,只不过有static 修饰符的语句表示 a 的作用域限制在一个源文件中,对于其他的源文件, a 是不可见的。因此,如果若干个函数需要共享一组外部对象时,可以将这些函数放到一个源文件中,把他们需要用到的对象也放到同一个源文件中以static 修饰符声明。
static 修饰符不仅适用于变量,同样适用于函数。如果一个函数仅仅被同一个源文件中的其他函数进行调用,我们应该声明该函数为static。
4.4 形参、实参和返回值
任何一个C函数都有一个形参列表,列表中的每一个参数都是一个变量,该变量在函数调用的过程中被初始化(而对某些函数来说,形参列表为空),函数调用时,调用方将实参列表传给被调函数(若函数的形参列表为空,在被调用时实参列表也为空。)
任何一个C函数都有返回类型,要么是void,要么是函数生成的结果的类型。
如果任何一个函数在调用它的每一个文件中,都在第一次被调用之前进行声明或者定义,那么就不会有任何与返回类型有关的麻烦。
如果一个函数在被定义或者声明之前被调用,南无它的返回类型就默认为整型。
函数只能有一个定义,如果某函数的调用和定义分别位于不同的文件,那么必须在调用该函数的文件中声明该函数。
如果函数没用float、short或者char 类型的参数,在函数声明中完全可以省略参数类型的说明(函数定义中不能省略类型参数类型的声明)。这样做,依赖于调用者能够提供数目正确且类型恰当的实参(恰当不意味着等同,float类型参数会自动转换为double类型,short或char 类型的参数会自动转换为int 类型)。
比如调用sqrt函数,应该在调用的文件之前作如下声明:double sqrt(double);或者 double sqrt();,但最好的形式是#include <math.h> 这样的包含头文件形式。
由于函数printf和scanf在不同的情形下可以接受不同类型的参数,所以比较容易出错,比如:
#include <stdio.h>
main()
{
int i;
char c;
for(i=0; i<5; i++)
{
scanf("%d",&c);
printf("%d “,i);
}
printf(”\n");
}
表面上看,函数是从标准设备输入5个参数,从标准输出设备写5个参数 0 1 2 3 4,但实际上这个程序不一定能够得到上述的结果。比如,在某个编译器上,它的输出为 0 0 0 0 0 1 2 3 4 ,造成这样的结果的关键是:这里的 c 被声明为 char 类型,不是 int 类型。当程序要求scanf 读入一个整数,应该传递给他一个指向整数的指针。而程序中scanf函数得到的却是一个指向字符的指针,scanf不能够分辨这种情况,它只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。因为整数所占的存储空间要大于字符所占用的存储空间,所以字符 c 附近的内存将被覆盖。字符 c 附近的内存中存储的内容是由编译器决定的,在这里它存放的是整数 i 的低端部分。因此,每次读入一个数值到 c 时,都会将 i 的低端部分覆盖为0,而 i 的高端部分本来就是 0,相当于 i 每次都被重新设置为 0,循环将一直执行下去。当到达文件的结束位置后,scanf 函数不再试图读入新的数值到 c,这时,i才可以正常的递增,最后终止循环。
4.5 检查外部类型
假定一个由两个源文件组成的C程序,其中一个包含了外部变量 n 的声明:extern int n;另一个文件中包含了外部变量 n 的定义: long n; 这里假定两个语句都不在任何一个函数体中,故 n 是一个外部变量。 在这种情况下,该C程序是一个无效的C程序,因为同一个外部变量名在两个不同的文件中被声明为不同的类型。当这个程序运行时,可能发生的情况:
(1)C语言编译器足够“聪明”,能够检测到这一类型冲突。此时,编程者会得到一条诊断信息,报告变量 n 在两个不同的文件中被给定了不同的类型。
(2)所使用的C语言实现对int 类型的数值与对long 类型的数值在内部的表示完全一样。在这种情况下,程序可能正常工作,就像 n 在两个文件中的声明一样,都是long(或int )类型,错误的程序由于某种巧合能够正常工作。
(3)变量 n 的两个示例虽然要求的存储空间的大小不同,但是他们共享存储空间的方式恰好能够满足:赋值给其中的一个值,对另一个也是有效的。比如,如果连接器安排int 类型的 n 与long 类型的 n 的低端部分共享存储空间,这样每个long 类型的 n 的赋值,恰好相当于把其低端部分赋给int 类型的 n,也能恰好的工作。
(4)变量 n 的两个实例共享存储空间的方式,使得对其中的一个进行赋值,其效果相当于同时给另一个赋了完全不同的值,这种情况下,程序完全不能够工作。
故要保证一个特定名称的所有的外部定义在每个目标目标模块中都有相同的类型。
忽略了声明函数的返回类型,或者声明了错误单位返回类型,都会带来不同程度的麻烦。
4.6头文件
避免连接过程中的错误,可以使用头文件的方法:每一个外部对象只在一个地方声明。这个声明的而地方一般就在一个头文件中,需要用到该外部对象的所有模块都应该包括这个头文件。特别需要指出的是,定义该外部对象的模块也应该包括在这个头文件中。这样也能够做到只在一处改动特定的外部变量名,则所有模块中的外部变量名得到同时更新。
比如,创建一个文件,叫file.h,其中包含了声明:extern char filename[ ]; ,然后在需要用到外部对象filename 的每个 C源文件中都应该加上语句 : #include “file.h” ,最后,选择一个C源文件,在其中给出 filename 的初始值,比如称这个文件为 file.c: #include “file.h” char filename[ ]="/etc/passwd"; 。
注意,源文件file.c 实际包含了filenam的两个声明,这一点将 #include 语句展开可以看出为: extern char filename[ ]; char filename[ ]="/etc/passwd"; ,这里只要保证filename 的各个声明是一致的,而且这些声明中最多只有一个是 filename 的定义,这样就是合法的。