上一章:【C陷阱与缺陷】第3章 语义陷阱
下一章:【C陷阱与缺陷】第5章 库函数
一个c程序可能由多个分别编译的部分组成,连接不同部分的叫做连接器(连接编辑器,载入器)。
编译器一次只处理一个文件,所以不能检测出多源程序文件的错误。
许多连接器是独立于c语言实现的,前面的错误是c语言的错误,连接器也可能没办法检测出错误。
当c语言的实现提供了lint的程序,务必用它用它用它,因为它能够捕获到大量这种错误。
1. 连接器
c语言的重要思想是分别编译(Separate Compilation),若干个源程序不同时间单独编译,然后恰当的使用整合。连接器一般于c编译器分离,不可能了解到c语言的诸多细节。编译器责任是把c源程序翻译成对连接器有意义的形式。
典型的连接器把由编译器或者汇编器生成的若干个目标模块,整合成一个载入模块或可执行文件的实体,该实体可以被操作系统直接执行。其中目标模块有些事作为输入提供给连接器,有些是根据程序的需要,像printf
这些函数,是在库文件中获取。
graph LR
A[C源程序]-->|编译器/汇编器|B[目标模块]
B-->C[连接器]
D[库文件]-->C
C-->E[载入模块/可执行文件]
连接器把目标模块看成一组外部对象(external object)组成。每个外部对象为机器中的内存的某个部分,即外部变量或函数,并通过外部名字来识别。通常包含无static修饰的函数与变量,有static修饰的函数与变量需要经过名称修饰后才成为外部对象。
graph LR
A[外部对象]---B["【无static修饰】 函数"]
A---C["【无static修饰】 外部变量"]
A---|名称修饰|D["静态函数"]
A---|名称修饰|E["静态外部变量"]
连接器禁止外部对象有相同的名字,连接器就是需要处理这类命名冲突。连接器对每个目标模块的每个外部对象,都要检查载入模块中是否存在同名的外部对象,没有,就将目标模块添加到载入模块中;如果有,就开始处理命名冲突。
当目标模块包含其他模块的外部对象的引用,例如printf
,连接器需要在生成载入模块的同时记录这些外部对象的引用,当连接器读入一个目标模块时,必须解析出这个目标模块中定义的所有外部对象的引用,并说明这些外部对象不再是未定义。
2. 声明与定义
声明语句:
int a;
如果出现在所有函数体外,就是一个外部对象a的定义,说明a是个int
类型变量,并分配内存,初值为0(某些编译器可能不是)。如果在函数内部则只是声明。
声明语句:
int a = 7;
在声明a的同时给a赋值,是定义语句。
声明语句:
extern int a;
则不是定义语句,说明了a是一个外部int
变量,是对外部变量a的引用,是对外部对象的显示引用。例如time
库中的void srand(int)
函数在外部变量random_seed
中保存了n的一份拷贝。
void srand(int n) {
extern int random_seed;
random_seed = n;
}
当程序中包含extern int a;
语句时,a必须要在本程序或者其他源文件中定义a。
3. 命名冲突与static修饰符
当一个外部对象使用了static
修饰时,该外部对象的作用域就只在本源程序中,其他源程序不能看到该外部对象,减少命名冲突的发生。
4. 形参、实参、返回值
如果一个函数在被定义或者声明前被调用,则它的返回类型默认为int
类型。
ANSI C标准出来前的C编译器,可能是这样声明和定义函数的:
int isvowel(); //声明
//定义
int isvowel(c)
char c;
{
//TODO
}
形参类型要匹配,如例子:
#include <stdio.h>
int main() {
int i;
char c;
for (i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d ", i);
}
printf("n");
return 0;
}
如果编译器没有使用内存对齐,则在scanf
语句中,给1字节大小的char
类型的c赋值时,写入的是4字节大小的int
类型,这会覆盖变量i
的低位,使得在没输入EOF
或者输入数据太小时,该循环会一直执行下去。输出为
0 0 0 0 0 1 2 3 4
而不是目标的
0 1 2 3 4
5. 检查外部类型
一处源文件中声明n:
extern int n;
另一处源文件中定义n:
long n;
则会出现:
- 编译器足够聪明,可以参测出这一冲突。
- c语言实现对
int
类型与long
类型的内部表示相同,尤其在32位机器上,巧合下正常使用。 - 内存对齐,使得
int
是long
的低位,巧合下正常使用。 - 变量
n
两个实例共享储存空间,对一个赋值,对另一个赋完全不同的值,不能使用。
还有上一章讲的错误声明:
char filename[] = "/etc/passwd"; //代表一片连续的内存空间
extern char *filename; //一个变量,内容是一篇空间的地址。
是错误的,想要正确使用则需要这样改:
char filename[] = "/etc/passwd";
extern char filename[];
或者:
char *filename = "/etc/passwd";
extern char *filename;
如果使用一个未声明的函数,例如未声明就使用了sqrt
函数,则相当于在外部写上:
extern int sqrt();
默认返回int
类型。
6. 头文件
解决命名冲突的方法可以使用如下规则:
每个外部对象只在一个地方声明,一般在头文件中声明。需要用到该外部对象的模块都要include
该头文件,定义该外部对象的模块也要include
改头文件。例如前面的filename
例子。
创建一个文件file.h
,里面包含声明:
extern char filename[];
需要用到该外部对象的每个c源程序都要加上:
#include "file.h"
最后,选择一个适当的c源程序,给出filename
的初始值,假设文件名为file.c
,包含:
#include "file.h"
char filename[] = "/etc/passwd";
变量的多次声明是合法的。
上一章:
DeathWatch:【C陷阱与缺陷】第3章 语义陷阱zhuanlan.zhihu.com![15f8f3d28128bec8912b433f6588fce5.png](https://i-blog.csdnimg.cn/blog_migrate/2fe8821f2ded319455ece3bb41b0ee4e.jpeg)
下一章:
DeathWatch:【C陷阱与缺陷】第5章 库函数zhuanlan.zhihu.com![6fea16a8594a988dc695697a8b0d91dd.png](https://i-blog.csdnimg.cn/blog_migrate/c0c32baaec3a55522ddf904a86894497.jpeg)