Andrew Koenig《C 陷阱与缺陷(第二版)》4.5节
目录
先看两个文件。
cpp
/* a.c ------ 声明 n 是 int */
#include <stdio.h>
extern int n;
int main(void)
{
printf("n = %d\n", n);
return 0;
}
cpp
/* b.c ------ 定义 n 是 long */
long n = 305419896;
一个变量叫 n,a.c 说它是 int,b.c 说它是 long。
这是一个无效的 C 程序。 但编译器不拦你,链接器不管,程序照样生成、照样能跑。

为什么没人拦你
书里说得很清楚:
这是一个无效的 C 程序,因为同一个外部变量名在两个不同的文件中被声明为不同的类型。然而,大多数 C 语言实现却不能检测出这种错误。 编译器对这两个不同的文件分别进行处理,这两个文件的编译时间甚至可以相差好几个月。因此,编译器在编译一个文件时,并不知道另一个文件的内容。连接器可能对 C 语言一无所知,因此它也不知道如何比较两个 n 的定义中的类型。
拆成两句:
- 编译器一次只看一个文件。 编译 a.c 的时候,b.c 还没被打开过------它不会知道
n在那边是什么类型? - 链接器只看符号名。 它在两个目标文件里各找到一个叫
n的符号,把它俩接上,收工。类型?那是编译器的事,链接器不认得。
四种可能
书说运行时"存在很多的可能情况",列了四条:
1.编译器足够"聪明",检测到冲突,给你一条诊断消息。
2.int 和 long 的内部表示正好一样(书说尤其在 32 位机上)------程序碰巧正常工作,"本来错误的程序因为某种巧合却能够工作"。
| 类型 | 32 位系统 | 64 位系统 | |
|---|---|---|---|
int |
4 字节 | 4 字节 | |
long |
4 字节 | 8 字节 |
3.两者要求的存储大小不同,但共享存储空间的方式恰好对得上------也能工作。
也就指的是在int不溢出的情况下,int和long是生效的
4.共享方式使得给其中一个赋值,等于给另一个赋了个完全不同的值------程序不能正常工作。
这就是溢出的情况了,也是最坏的情况。
结论

因此,保证一个特定名称的所有外部定义在每个目标模块中都有相同的类型,一般来说是程序员的责任。而且,"相同的类型"应该是严格意义上的相同。
注意后半句:"严格意义上的相同"。书马上给了一个容易踩的例子:
cpp
/* 文件 1 */
char filename[] = "/etc/passwd";
/* 文件 2 */
extern char* filename;
书说:
尽管在某些上下文环境中,数组与指针非常类似,但它们毕竟不同。......这两个对 filename 的声明使用存储空间的方式是不同的;它们无法以一种合乎情理的方式共存。

两张图一摆就明白了:
图 4.1 里 filename 就是那 12 个格子本身;
图 4.2 里 filename 是一个盒子,盒子里装着一个箭头。
名字一样,布局完全不是一回事。
详见 :char p\[\]= "hello";q = p; 复制的是字符串吗?C 的"举隅法"陷阱-CSDN博客
改法书上给了两条,任选一条,两边一致就行:
cpp
char filename[] = "/etc/passwd"; /* 文件 1 */
extern char filename[]; /* 文件 2 */
或者:
cpp
char* filename = "/etc/passwd"; /* 文件 1 */
extern char* filename; /* 文件 2 */
还有另一种"外部类型"
书在结尾提了第二种:
有关外部类型方面,另一种容易带来麻烦的方式是忽略了声明函数的返回类型,或者声明了错误的返回类型。
这就是 4.4 节那个 sqrt 的例子。书补了一句规则:
C 语言中的规则是,如果一个未声明的标识符后跟一个开括号,那么它将被视为一个返回整型的函数。因此,这个程序完全等同于
extern int sqrt();。
详见:调用函数时老是有莫名其妙地错误?函数的形参实参与返回值-CSDN博客
到这里书讲完了。总结就是:
声明和定义必须严格同类型,这件事标准没派给编译器,派给了你。