本文为译文,原文点击这里
C有一个关键字extern如:
#include <stdio.h>
int main() {
extern int x;
printf("x = %d\n", x);
}
主义x
前面的extern
,这有什么用?让我们来编译一下:
$ clang main.c
/tmp/main-9b5b3f.o: In function `main':
main.c:(.text+0x15): undefined reference to `x'
clang-6.0: error: linker command failed with exit code 1 (use -v to see invocation)
为了理解这个错误,我们需要理解extern
,extern
是一个可以应用于声明(declaration)的关键字。如:
extern int x;
extern char * errstr;
为了理解extern
,我们必须首先理解声明(declaration)、定义(definition)和初始化(initialization)之间的区别。看下这个例子:
int x = 5;
上面这行代码做了三件事:
- 说明
x
存在并且类型是int; - 它为x分配内存(足够一个整型数);
- 为该内存分配初值5。
这三部分称为声明(declaration)、定义(definition)和初始化(initialization):
- 变量声明(declaration):声明具有给定类型的变量的存在性;
- 变量定义(definition):为该变量分配内存;
- 变量初始化(initialization):为该内存提供一个初始值。
我们经常将声明、定义和初始化变量一起完成,如上边的例子。然而,我们不必同时做这三件事!
声明 | 定义 | 初始化 | |
---|---|---|---|
int x = 5; | √ | √ | √ |
int x; | √ | √ | × |
extern int x; | √ | × | × |
对一个C变量来说,extern
声明了该变量而没有定义它。也就是说,在程序的那个点上没有为它分配内存,必须在其他地方定义变量。
其他地方留给链接器去寻找。注意,上面clang
的错误消息不是编译器错误,而是链接器错误。运行clang main.c
时,先运行编译器,然后是链接器。编译成功,但是链接失败了,因为它没有找到x。我们可以使用-c标志来看编译阶段:
$ clang -c main.c
$ ls
main.c main.o
然后,我们可以通过执行下面命令看到,链接阶段失败:
$ clang main.o
main.o: In function `main':
main.c:(.text+0x15): undefined reference to `x'
clang-6.0: error: linker command failed with exit code 1 (use -v to see invocation)
对象文件,比如main.o
,有一个符号表(symbol table)。这个表描述了对象文件中定义的内容,以及对象文件期望从其他地方获得的内容。我们可以用nm
工具来看这个符号表:
$ nm main.o
0000000000000000 T main
U printf
U x
T mian
说在main.o
中定义了一个符号(symbol)main
。U printf
和U x
是说main.o
中声明但是没有定义符号printf
和x
。main.o
期望连接器去寻找printf
和x
,clang链接器通过链接C标准库成功地找到了printf
。但是,链接器在任何地方都找不到x
,因此它会报错。
为了修正这个错误,我们需要在某处定义x
。我们可以在一个单独的目标文件,像这样:
// x.c
int x = 5; // define and initialize x
$ clang -c x.c
$ nm x.o
0000000000000000 D _x
$ clang main.o x.o
$ ./a.out
x = 5