定义
本节澄清三个容易混淆的术语:
- 声明 (Declaration)
- 定义 (Definition)
- 暂定定义 (Tentative definition)
1. 声明 vs 定义
1.1 声明
声明的作用是:把一个名字引入某个作用域,并为该名字绑定类型信息(以及可能的链接、存储期信息)。声明不一定分配存储。
例子:
c
extern int x; /* 声明:说明 x 在别处定义 */
int f(void); /* 声明:函数原型 */1
2
2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
1.2 定义
定义是一种特殊的声明:它会为实体提供“实际内容”,通常意味着:
- 对对象:分配存储(并可能初始化)。
- 对函数:提供函数体。
例子:
c
int x = 1; /* 定义:分配存储并初始化 */
int f(void) { /* 定义:提供函数体 */
return x;
}1
2
3
4
2
3
4
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
extern + 初始化器
extern int x = 1; 仍然是定义:初始化器会使它成为定义。
2. 暂定定义(文件作用域)
在文件作用域,下面这种“没有初始化器且不带 extern”的对象声明,称为暂定定义:
c
int g; /* 暂定定义:外部链接 */
static int s; /* 暂定定义:内部链接 */1
2
2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
一个翻译单元内可以有多个同名暂定定义;如果该翻译单元最终没有出现同名的“真正定义”(带初始化器的定义),那么这些暂定定义会合并为一个定义,并进行零初始化。
3. 常见陷阱
int x;在函数体内不是暂定定义:它是一个普通的定义(自动存储期)。- 把“带内部链接的定义”写进头文件会导致每个翻译单元各自拥有一份实体(尤其是
static对象),这往往不是你想要的。 - 不要在同一翻译单元里让同一标识符同时拥有内部链接与外部链接,否则是未定义行为。
4. 头文件与实现文件的分工
对象的“声明放头文件、唯一定义放实现文件”是一条非常关键的组织边界。头文件负责让所有调用方看到一致类型信息,实现文件负责给出真实实体。只要这条分工被破坏,就容易出现多重定义或声明漂移,问题通常会在链接阶段集中爆发。
5. 暂定定义的阅读方式
阅读代码时遇到文件作用域下的 int g;,不要急着把它当成“只是声明”。更稳妥的做法是继续向后看:若同名实体没有显式定义,它最终会在该翻译单元落为零初始化定义。先按这条规则建立心智模型,链接行为会更容易预测。
6. 习题
- 判断:下面每一行是“声明”“定义”还是“暂定定义”?并说明理由。
c
extern int x;
int x;
int x = 1;
static int y;
static int y = 2;
int f(void);
int f(void) { return 0; }1
2
3
4
5
6
7
2
3
4
5
6
7
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
- 写一个最小示例(两个
.c文件):在a.c写int x;,在b.c写int x;,然后链接。解释会发生什么,为什么。 - 在一个翻译单元中,写出一个“多次暂定定义 + 最终无显式初始化器定义”的例子,并说明最终对象初值是什么。