连接
4.1 什么是连接器
连接器的输入是一组目标模块和库文件。连接器的输出是一个载入模块。连接器读入目标模块和库文件,同时生成载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否已有同名的外部对象。如果没有,连接器将这个外部对象添加到载入模块中;如果有,连接器要处理命名冲突。
除了外部对象,目标模块中可能还包括对其他模块中的外部对象的引用。在连接器生成载入模块的过程中,它必须同时记录这些外部对象的引用。当连接器读入一个目标模块时,它必须解析这个目标模块中定义的所有外部对象的引用,并做出标记说明这些外部对象不再是未定义的。
尽量使用lint程序检测错误。
4.2 声明与定义
下面的声明语句:
int a;
如果其位置位于所有的函数体之外,那么它就被称为外部对象a的定义。声明外部对象的同时最好进行初始化。
下面的声明语句:
extern int a;
表示对外部变量a的引用,而不是对a的定义。
每个外部对象都必须在程序的某个地方进行定义。
如果一个程序对同一个外部变量的定义不只一次,系统一般会报错。最好的解决办法是每个外部变量只定义一次。
4.3 命名冲突与static修饰符
static修饰符是一个能够减少命名冲突的工具。
static int a;
此时,a的作用域限制在一个源文件里,对于其他源文件a是不可见的。同理,如果若干个函数需要共享一组外部对象,可以将这些函数放在一个源文件中,把它们需要用到的对象也都在同一个源文件中以static修饰符声明。
static修饰符不仅适用于变量,同样也适用于函数。如果一个函数仅仅被同一源文件中的其他函数调用,就应该声明为static。
4.4 形参,实参与返回值
任何C函数都有一个形参列表,列表中的每个参数都是一个变量,该变量在函数调用的时候初始化。形参列表也可以为空。
函数调用时,调用方将实参列表传递给被调函数。如果一个函数形参列表为空,在被调用时实参列表也为空。
任何一个C函数都有返回类型,要么是void要么是函数生成的结果类型。
如果一个函数在被定义或者声明之前被调用,那么它的返回值就默认为整型。
下面例子:
#include <stdio.h>
int main(void)
{
double s;
s=sqrt(2);
printf("%g
",s);
return 0;
}
程序不能运行,原因第一个是sqrt需要接受一个双精度值为实参,而实际上传递了一个整型参数。第二个是因为sqrt函数的返回值类型是双精度类型,但却没有声明。
一种更正方式为:
#include <stdio.h>
double sqrt(double);
int main(void)
{
double s;
s=sqrt(2);
printf("%g
",s);
return 0;
}
另一种更正方法:
#include <stdio.h>
double sqrt();
int main(void)
{
double s;
s=sqrt(2.0);
printf("%g
",s);
return 0;
}
最好的更正方式:
//从系统头文件math.h获取sqrt函数的详细定义
#include <math.h>
int main(void)
{
double s;
s=sqrt(2.0);
printf("%g
",s);
return 0;
}
另外一个例子:
#include <stdio.h>
int main(void)
{
int i;
char c;
for (i =0 ;i<5;i++)
{
scanf("%d",&c);
printf("%d",i);
}
printf("
");
return 0;
}
输入 : 0 1 2 3 4
输出: 00000
分析:
由于c是char类型,而不是int类型。而程序要求scanf读入一个整数,应该传递给它一个指向整数的指针。而实际上却得到一个指向字符的指针,scanf不能分辨这种情况。因此只是将这个指向字符指针作为指向整数的指针来接受,并且在指针指向的地方存入一个整数,由于整数所占的存储空间要大于字符所占的,所以字符c附件的内存被覆盖。
由于整数类型的高端部分为0,而每次字符c读入时,都会将i的低端部分覆盖为0.所以最后输出的数值都为0。
4.5 检查外部类型
保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型是每个程序员的责任。
不要忽略了声明函数的返回类型,或者声明错误的返回类型。
4.6 头文件
要解决外部对象问题,可以规定:每个外部对象只在一个地方声明。这个地方就是头文件,需要用到的该外部对象的所有模块都应该包括这个头文件。定义该外部对象的模块也应该包括在头文件中。