一个 C 程序可能有很多部分组成,它们被分别编译,并由一个通常称为链接器、链接编辑器或加载器的程序绑定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现的错误。 在这一节中,我们将看到一些这种类型的错误。有一些 C 实现,但不是所有的,带有一个称为 lint的程序来捕获这些错误。如果具有一个这样的程序,那么无论怎样地强调它的重要性都不过分。
4.1 检查外部类型
假设你有一个 C 程序,被划分为两个文件。其中一个包含如下声明:
int n;
而令一个包含如下声明:
long n;
这不是一个有效的 C 程序,因为一些外部名称在两个文件中被声明为不同的类型。然而,很多实现检测不到这个错误,因为编译器在编译其中一个文件时并不知道另一个文件的内容。因此,检查类型的工作只能由链接器(或一些工具程序例如lint)来完成;如果操作系统的链接器不能识别数据类型,C 编译器也没法过多地强制它。
那么,这个程序运行时实际会发生什么?这有很多可能性:
1. 实现足够聪明,能够检测到类型冲突。则我们会得到一个诊断消息,说明 n 在两个文件中具有不同的类型。
2. 你所使用的实现将 int 和 long 视为相同的类型。 典型的情况是机器可以自然地进行 32 位运算。
在这种情况下你的程序或许能够工作,好像你两次都将变量声明为 long(或 int)。但这种程序的工作纯属偶然。
3. n 的两个实例需要不同的存储,它们以某种方式共享存储区,即对其中一个的赋值对另一个也有效。这可能发生,例如,编译器可以将 int 安排在 long 的低位。不论这是基于系统的还是基于机器的,这种程序的运行同样是偶然。
4. n 的两个实例以另一种方式共享存储区,即对其中一个赋值的效果是对另一个赋以不同的值。在这种情况下,程序可能失败。
这种情况发生的里一个例子出奇地频繁。程序的某一个文件包含下面的声明:
char filename[] = "etc/passwd";
而另一个文件包含这样的声明:
char *filename;
尽管在某些环境中数组和指针的行为非常相似,但它们是不同的。在第一个声明中,filename 是一个字符数组的名字。尽管使用数组的名字可以产生数组第一个元素的指针,但这个指针只有在需要的时候才产生并且不会持续。
在第二个声明中,filename 是一个指针的名字。这个指针可以指向程序员让它指向的任何地方。如果程序员没有给它赋一个值,它将具有一个默认的 0 值(null)[译注:实际上,在 C中一个为初始化的指针通常具有一个随机的值,这是很危险的!