“声明”仅仅是告诉编译器某个标识符是:变量(什么类型)还是函数(参数和返回值是什么)。要是在后面的代码中出现该标识符,编译器就知道如何处理。记住最重要的一点:声明变量不会导致编译器为这个变量分配存储空间。
C语言专门有一个关键字(keyword)用于声明变量或函数:extern。带有extern的语句出现时,编译器将只是认为你要告诉它某个标识符是什么,除此之外什么也不会做(直接变量初始化除外)。
先来试一下:
/*Example C code*/
extern int a;
int main(void)
{
extern int b;
a = 1;
b = 2;
return 0;
}
$gcc test.c
马上你会收到两条出错消息:
undefined reference to “a”
undefined reference to “b”
因为两条extern语句仅仅是声明变量,编译器虽然知道a和b是什么类型的变量,但在链接的时候却找不到它们的地址(因为变量没有被定义,所以编译器没有为它们分配存储空间),于是就出错了。
不妨看看汇编代码:
$gcc –S test.c
$cat test.s
.globl main
.type main, @function
main:
pushl %ebp
movl %esp, %ebp
subl $8, %esp
andl $-16, %esp
movl $0, %eax
subl %eax, %esp
movl $1, a
movl $2, b
movl $0, %eax
leave
ret
可见,编译器根据变量的声明已经知道如何处理外部变量a和b,但由于没有定义变量,所以汇编代码中没有由“.comm”开头的语句为外部变量分配存储空间,导致最后链接程序找不到有效的符号而报错。
说完“声明”,现在说“定义”。
有了对比就很容易明白,定义变量意味着不仅告诉编译器变量的类型,而且编译器同时必须为变量分配空间。定义变量的同时还可以初始化变量,例如:
int a = 9;
char c = ‘A’;
在函数里面定义、初始化内部变量已经在前面的文章中讨论过,现在探讨一下外部变量的定义和初始化。
首先要搞清楚编译器在什么情况下将语句认为是定义,什么情况下认为是声明。这里给出若干原则:
#1 带有初始化的语句是定义
例如:
int a = 1; //定义
#2 带有extern的语句是声明(除非对变量进行初始化)
例如:
extern int a; //声明
extern int b = 2; //定义
#3 既没有初始化又没有extern的语句是“暂时定义”(tentative definition)
例如:
int a; //暂时定义
C语言中,外部变量只能被(正式)定义一次:
int a = 0;
int a = 0; //错误!重复定义
又或者:
int a = 0;
double a = 0.1; //错误!标识符a已经被使用
暂时定义有点特殊,因为它是暂时的,我们不妨这样看:
暂时定义可以出现无数次,如果在链接时系统全局空间没有相同名字的变量定义,则暂时定义“自动升级”为(正式的)定义,这时系统会为暂时定义的变量分配存储空间,此后,这些相同的暂时定义(加起来)仍然只算作是一个(正式)定义。
例如:
/*Example C code*/
int a; //暂时定义
int a; //暂时定义
int main(void)
{
a = 1;
return 0;
}
int a; //暂时定义
让我们看一下汇编代码:
$gcc –S test.c
$cat test.s
.globl main
.type main, @function
main:
...
movl $1, a
...
.comm a, 4, 4
程序显示编译器只给变量“a”分配空间一次,尽管C程序中有3个暂时定义语句。
刚才讲到如果没有相同名字的外部变量定义,则暂时定义会自动变成定义,那么,如果有相同名字的外部变量定义呢?很简单,这时暂时定义的作用相当于声明。
例如:
/*Example C code*/
int a; //暂时定义
int a; //暂时定义
int main(void)
{
a = 1;
return 0;
}
int a = 0; //定义
这里因为定义了外部变量,所以最上面的两个暂时定义就相当于仅仅声明a是int变量。看看汇编代码。
.globl main
.type main, @function
main:
...
movl $1, a
...
.globl a //a是一个全局可见的符号
.data //表示在数据段分配空间(函数的代码会放在代码段)
.align 4 //指示为a分配的地址必须是“4字节对齐”
.type a, @object //告诉编译器a所代表的空间存放的是数据
.size a, 4 //表示分配给a的空间大小是4个字节
a: //正式给出符号a的位置
.long 0 //用数值“0”初始化符号a代表的那块存储空间
这里注意,即使不对a进行初始化(.long句),a:这句代码也仍然要出现,因为它告诉编译器分配空间,没有这句话编译器还是不会分配空间。
回忆一下暂时定义对应的汇编语句:
.comm a, 4, 4
然后和上面的代码作一对比,相信很容易区分暂时定义和(正式)定义在汇编代码中的表示。