参考:Andrew Koenig《C 陷阱与缺陷(第二版)》4.6节
目录
[书上的例子:file.h 和 file.c](#书上的例子:file.h 和 file.c)
[file.c 里其实有两个声明](#file.c 里其实有两个声明)
如果一个函数忘了声明,编译器把它当成返回 int 的函数照单全收,连接器对类型不匹配毫无反应。
类型错了没人管,谁来管?
书 4.6 开头给出答案:
每个外部对象只在一个地方声明。
这个地方一般就是一个头文件,所有用到它的模块都 include 这个头文件。
特别要指出的是,定义它的那个模块,也应该 include 这个头文件。

这条规则为什么能堵住这个洞?先看书是怎么用的
书上的例子:file.h 和 file.c
书里接着用 4.1 提过的 filename 例子:
一个多模块程序,每个模块都要知道同一个文件名,希望只改一处、所有模块同时更新。
第一步,建一个头文件 file.h,里面只有声明:
cpp
extern char filename[];
第二步,所有用到 filename 的源文件,都加上:
cpp
#include "file.h"
第三步,选一个源文件给出初值,不妨叫 file.c:
cpp
#include "file.h"
char filename[] = "/etc/passwd";

file.c 里其实有两个声明
file.c 里的 include 展开,会看到 filename 出现了两次:
cpp
extern char filename[];
char filename[] = "/etc/passwd";
一个声明,一个定义,堆在同一个文件里。这合法吗?
判定条件是两句:只要各声明是一致的,而且其中最多只有一个定义,这样写就合法。
extern char filename[]; 和 char filename[] = "/etc/passwd";
类型一致、定义只有一个------合法。
这样写的效果
书最后点出这样做为什么值:
头文件声明了 filename 的类型,于是每个 include 它的模块自动把类型声明对了;
file.c 定义了 filename,因为它也 include 了头文件,定义的类型自动和声明相符。
编译所有文件,filename 的类型就肯定是正确的。
声明的类型只有一份,谁也没机会抄错。
直观演示
故意让两边不一致------head.h 声明 n 是 int:
cpp
extern int n;
conf.c 定义 n 是字符数组,但它 include 了头文件:
cpp
#include "head.h"
char n[] = "xyz";
编译 conf.c,当场报错:
cpp
conf.c(4): error C2040: "n":"char [0]"与"int"的间接寻址级别不同
两个冲突的声明出现在同一个编译单元里,编译器看得见两个,一比对就报 C2040。
错在编译期,程序根本没机会运行。
没头文件:同样的漂移,没人管。
把冲突拆到两个文件。d1.c:
cpp
/* defines n as a char array */
char n[] = "xyz";
d2.c,一口咬定 n 是 int:
cpp
#include <stdio.h>
extern int n;
int main(void)
{
printf("n = %d\n", n);
return 0;
}
编译 d1.c d2.c:零警告,链接通过,运行退出码 0,打印:
cpp
n = 8026488
"xyz" 被当成了 8026488。这不是随机的垃圾数字------
"xyz\0" 在内存里是四个字节 78 79 7A 00,d2.c
按小端把前四个字节读成一个 int,就是 0x007A7978 = 8026488。每次运行都是这个数,地址会变,值不会。
这一组对照就是 4.6 的全部意义:
|--------------|------------------------------|
| | 声明漂移的结局 |
| 声明只有一份(头文件) | error C2040,编译期拦下 |
| 声明各抄一份(无头文件) | 编译零警告、链接通过,运行期读出 8026488 |