4.1.2 有害的猜测

在考虑C库创建中的一般问题之前还应当了解一个更重要的问题。注意头文件CLib.h必须包含在所有涉及CStash的文件中,因为编译器不能正确地猜测这个结构像什么。然而,它能猜测一个函数像什么。这看上去像是一个特征,但实际上是C的一个主要缺陷。

虽然总是应当通过包含头文件声明函数,但是函数声明在C中不是基本的。调用没有声明的函数在C中是可以的(但是在C++中不可以)。一个好的编译器会告诫程序员应当首先声明函数,但是,按照C语言的标准,并不强迫这样。这是危险习惯,因为C编译器可能会假设,带有一个int参数的函数有包含int的参数表,尽管它实际上可能包含了一个float。正如我们将看到的,这会产生非常难发现的bug。

每个独立的C文件(带有扩展名.c的文件)是一个翻译单元(translation unit)。这就是说,编译器在每个翻译单元上单独运行,这时它只知道这个单元。这样,由包含文件提供的任何信息都是相当重要的,因为它决定了编译器对程序的其他部分的理解。在头文件中的声明是特别重要的,因为在包含头文件的任何地方,编译器准确地知道做什么。例如,如果在头文件中有一个声明是void func(float),编译器就知道,如果用一个整型参数调用这个函数,应当把这个int转换为float,作为传递参数[这被称为提升(promotion)]。如果没有声明,C编译器简单地假设有一个func(int)存在,它就不做提升,错误数据就悄悄地传给了func()。

对于每个翻译单元,编译器创造一个目标文件,用.o或者.obj,或者其他类似的符号作为扩展名。这些目标文件,连同必要的启动代码,由连接器连接为可执行程序。在连接过程中,应当确定所有的外部引用。例如,在CLibTest.cpp中,声明和使用了initialize()和fetch()这样的函数(这就是,告诉编译器它们像什么),但在其中未定义。它们在别处定义,即在CLib.cpp中。这样,在CLib.cpp中的调用是外部引用。当连接器将所有的对象文件放在一起时,它必须取未确定的外部引用,找出它们实际访问的地址。在可执行程序中用这些地址替换这些外部引用。

在C中,连接器所要查找的外部引用是一些简单的函数名字,通常在它们的前面加下划线。因此,所有的连接器都必须匹配调用处的函数名和在对象文件中的函数体。如果在某处我们调用一个函数func(int),而在某一目标文件中有func(float)的函数体,连接器将认为有_func在一处而且有_func在另一处,它认为这都对,在调用func()的地方,把int置入栈中,而func()函数体处认为float在栈中。如果这个函数只读这个值而不写,它不会破坏这个栈。事实上,如果它读取的这个float值可能刚好有某种意思,这是最坏的情况,因为这个bug很难找出。