全局变量、局部变量和作用域
我们把函数中定义的变量称为局部变量(Local Variable),由于形参
相当于函数中定义的变量,所以形参也是一种局部变量
在这里“局部”有两层含义:
1、一个函数中定义的变量不能被另一个函数使用。例如 print_time 中
的 hour 和 minute 在 main 函数中没有定义,不能使用,同样 main 函数中
的局部变量也不能被 print_time 函数使用。如果这样定义:void print_time(int hour, int minute)
{
printf("%d:%d\n", hour, minute);
}
int main(void)
{
int hour = 23, minute = 59;
print_time(hour, minute);
return 0;
}
main 函数中定义了局部变量 hour,print_time 函数中也有参数 hour,虽
然它们名称相同,但仍然是两个不同的变量,代表不同的存储单元。main
函数的局部变量 minute 和 print_time 函数的参数 minute 也是如此。
2、每次调用函数时局部变量都表示不同的存储空间。局部变量在每次
函数调用时分配存储空间,在每次函数返回时释放存储空间,例如调用
print_time(23, 59)时分配 hour 和 minute 两个变量的存储空间,在里面
分别存上 23 和 59,函数返回时释放它们的存储空间,下次再调用
print_time(12, 20)时又分配 hour 和 minute 的存储空间,在里面分别存
上 12 和 20。与局部变量的概念相对的是全局变量(Global Variable),全局变量定
义在所有的函数体之外,它们在程序开始运行时分配存储空间,在程序
结束时释放存储空间,在任何函数中都可以访问全局变量,例如:
例 . 全局变量
#include <stdio.h>
int hour = 23, minute = 59;
void print_time(void)
{
printf("%d:%d in print_time\n", hour, minute);
}
int main(void)
{
print_time();
printf("%d:%d in main\n", hour, minute);
return 0;
}
正因为全局变量在任何函数中都可以访问,所以在程序运行过程中全局
变量被读写的顺序从源代码中是看不出来的,源代码的书写顺序并不能
反映函数的调用顺序。程序出现了 Bug 往往就是因为在某个不起眼的地方对全局变量的读写顺序不正确,如果代码规模很大,这种错误是很
难找到的。而对局部变量的访问不仅局限在一个函数内部,而且局限在
一次函数调用之中,从函数的源代码很容易看出访问的先后顺序是怎样
的,所以比较容易找到 Bug。因此,虽然全局变量用起来很方便,但一
定要慎用,能用函数传参代替的就不要用全局变量。
如果全局变量和局部变量重名了会怎么样呢?如果上面的例子改为:
例 . 作用域
局部变量可以用类型相符的任意表达式来初始化,而全局变量只能用常
量表达式(Constant Expression)初始化。例如,全局变量 pi 这样
初始化是合法的:
double pi = 3.14 + 0.0016;但这样初始化是不合法的:
double pi = acos(-1.0);
然而局部变量这样初始化却是可以的。程序开始运行时要用适当的值来
初始化全局变量,所以初始值必须保存在编译生成的可执行文件中,因
此初始值在编译时就要计算出来,然而上面第二种 Initializer 的值必须
在程序运行时调用 acos 函数才能得到,所以不能用来初始化全局变量。
请注意区分编译时和运行时这两个概念。为了简化编译器的实现,C 语
言从语法上规定全局变量只能用常量表达式来初始化,因此下面这种全
局变量初始化是不合法的:
int minute = 360;
int hour = minute / 60;
虽然在编译时计算出 hour 的初始值是可能的,但是 minute / 60 不是常
量表达式,不符合语法规定,所以编译器不必想办法去算这个初始值。
如果全局变量在定义时不初始化则初始值是 0,如果局部变量在定义时
不初始化则初始值是不确定的。所以,局部变量在使用之前一定要先赋
值,如果基于一个不确定的值做后续计算肯定会引入 Bug。
如何证明“局部变量的存储空间在每次函数调用时分配,在函数返回时
释放”?
例 . 验证局部变量存储空间的分配和释放#include <stdio.h>
void foo(void)
{
int i;
printf("%d\n", i);
i = 777;
}
int main(void)
{
foo();
foo();
return 0;
}
第一次调用 foo 函数,分配变量 i 的存储空间,然后打印 i 的值,由于
i 未初始化,打印的应该是一个不确定的值,然后把 i 赋值为 777,函
数返回,释放 i 的存储空间。第二次调用 foo 函数,分配变量 i 的存储
空间,然后打印 i 的值,由于 i 未初始化,如果打印的又是一个不确定
的值,就证明了“局部变量的存储空间在每次函数调用时分配,在函数
返回时释放”。分析完了,我们运行程序看看是不是像我们分析的这样:
134518128777
结果出乎意料,第二次调用打印的 i 值正是第一次调用末尾赋给 i 的值
777。
是我记错了?
是教材写错了?
我们再做一次实验,在两次 foo 函数调用之间插一个别的函数调用,结
果就大不相同了:
int main(void)
{
foo();
printf("hello\n");
foo();
return 0;
}
结果是:
134518200
hello
0
这一回,第二次调用 foo 打印的 i 值又不是 777 了而是 0,“局部变量的
存储空间在每次函数调用时分配,在函数返回时释放”这个结论似乎对了,但另一个结论又不对了:全局变量不初始化才是 0 啊,不是说“局
部变量不初始化则初值不确定”吗?
关键的一点是,我说“初值不确定”,有没有说这个不确定值不能是 0?
有没有说这个不确定值不能是上次调用赋的值?
不要把必要条件(Necessary Condition)当充分条件(Sufficient
Condition),这一点在 Debug 时尤其重要,看到错误现象不要轻易断
定原因是什么,一定要考虑再三,找出它的真正原因。
研究“函数的调用过程”就能解释这些现象了。
从 “自定义函数”介绍的语法规则可以看出,非定义的函数声明也可以写
在局部作用域中,例如:
int main(void)
{
void print_time(int, int);
print_time(23, 59);
return 0;
}
这样声明的标识符 print_time 具有局部作域,只在 main 函数中是有效的
函数名,出了 main 函数就不存在 print_time 这个标识符了。写非定义的函数声明时参数可以只写类型而不起名,例如上面代码中的
void print_time(int, int);,只要告诉编译器参数类型是什么,编译器
就能为 print_time(23, 59)函数调用生成正确的指令。另外注意,虽然在
一个函数体中可以声明另一个函数,但不能定义另一个函数,C 语言不
允许嵌套定义函数