7.9 变量的存储方式和生存期
7.9.1 动态存储方式与静态存储方式
从 7.8 节已经了解,从变量的作用域(即从空间)的角度来观察,变量可以分为全局变量和局部变量。还可以从另一个角度,即从变量值存在的时间(即生存期)来观察。有的变量在程序运行的整个过程都是存在的,而有的变量则是在调用其所在的函数时才临时分配存储单元,而在函数调用结束后该存储单元就马上释放了,变量也就不存在了。
也就是说,变量的存储有两种不同的方式:静态存储方式和动态存储方式。静态存储方式是指在程序运行期间由系统分配固定的存储空间的方式,而动态存储方式则是在程序运行期间根据需要进行动态的分配存储空间的方式。
先看一下内存中的供用户使用的存储空间的情况。这个存储空间可以分为三部分:
- 程序区;
- 静态存储区;
- 动态存储区。
见图 7.16。
数据分别存放在静态存储区和动态存储区中。全局变量全部存放在静态存储区中,在程序开始执行时给全局变量分配存储区,程序执行完毕就释放。在程序执行过程中它们占据固定的存储单元,而不是动态地进行分配和释放。
在动态存储区中存放以下数据:
- 函数形式参数:在调用函数时给形参分配存储空间。
- 函数中定义的没有用关键字
static
声明的变量,即自动变量(详见后面的介绍)。 - 函数调用时的现场保护和返回地址等。
对以上这些数据,在函数调用开始时分配动态存储空间,函数结束时释放这些空间。在程序执行过程中,这种分配和释放是动态的。如果在一个程序中两次调用同一函数,而在此函数中定义了局部变量,在两次调用时分配给这些局部变量的存储空间的地址可能是不相同的。
如果一个程序中包含若干个函数,每个函数中的局部变量的生存期并不等于整个程序的执行周期。它只是程序执行周期的一部分。在程序执行过程中,先后调用各个函数,此时会动态地分配和释放存储空间。
在 C 语言中,每一个变量和函数都有两个属性:数据类型和数据的存储类别。数据类型是指变量或函数返回值的数据类型(如整型、浮点型等)。存储类别指的是数据在内存中存储的方式(如静态存储和动态存储)。
在定义和声明变量和函数时,一般应同时指定其数据类型和存储类别,也可以采用默认方式指定(即如果用户不指定,系统会隐含地指定为某一种存储类别)。
C 的存储类别包括四种:自动的(auto
)、静态的(static
)、寄存器的(register
)、外部的(extern
)。根据变量的存储类别,可以知道变量的作用域和生存期。下面分别作介绍。
7.9.2 C语言的存储类别
自动变量 (auto
)
- 存储类别:自动
- 定义方式:在函数内定义,默认存储类别为自动变量。
- 作用范围:在定义它的函数内部。
- 生存期:在进入定义它的函数时分配存储单元,函数结束时释放存储单元。
示例:
void example() {
auto int x = 10; // x 是自动变量
}
静态变量 (static
)
- 存储类别:静态
- 定义方式:在函数内或函数外用
static
关键字声明。 - 作用范围:在定义它的文件内部或函数内部。
- 生存期:在程序执行期间始终存在。
示例:
void example() {
static int y = 5; // y 是静态变量
y++;
printf("%d\n", y); // 每次调用 example(),y 的值会递增
}
寄存器变量 (register
)
- 存储类别:寄存器
- 定义方式:用
register
关键字声明。 - 作用范围:在定义它的函数内部。
- 生存期:在进入定义它的函数时分配存储单元,函数结束时释放存储单元。
示例:
void example() {
register int z = 0; // z 是寄存器变量
}
外部变量 (extern
)
- 存储类别:外部
- 定义方式:在函数外用
extern
关键字声明。 - 作用范围:在定义它的文件内部及其他引用它的文件内部。
- 生存期:在程序执行期间始终存在。
示例:
extern int a; // 声明外部变量 a
int main() {
printf("%d\n", a); // 使用外部变量 a
return 0;
}
int a = 100; // 定义外部变量 a
总结
通过以上介绍,我们了解了变量的存储方式和生存期。不同的存储类别决定了变量的作用域和生存期,在编写程序时,合理地选择变量的存储类别可以提高程序的效率和可读性。
7.9.2 局部变量的存储类别
1. 自动变量 (auto 变量)
函数中的局部变量,如果不专门声明为 static
(静态)存储类别,都是动态地分配存储空间的,数据存储在动态存储区中。函数中的形参和在函数中定义的局部变量(包括在复合语句中定义的局部变量)都属于此类。在调用该函数时,系统会给这些变量分配存储空间,在函数调用结束时就自动释放这些存储空间。因此这类局部变量称为自动变量。自动变量用关键字 auto
作存储类别的声明。例如:
int f(int a) {
auto int b, c = 3;
// b 和 c 是自动变量,c 被赋初值 3
...
} // 执行完 f 函数后,自动释放 a, b, c 所占的存储单元
实际上,关键字 auto
可以省略,不写 auto
则隐含指定为“自动存储类别”,它属于动态存储方式。程序中大多数变量属于自动变量。前面几章中介绍的例子,在函数中定义的变量都没有声明为 auto
,其实都隐含指定为自动变量。例如,在函数体中:
int b, c = 3;
auto int b, c = 3;
等价。
2. 静态局部变量 (static 局部变量)
有时希望函数中的局部变量的值在函数调用结束后不消失而继续保留原值,即其占用的存储单元不释放,在下一次再调用该函数时,该变量已有值(就是上一次函数调用结束时的值)。这时就应该指定该局部变量为“静态局部变量”,用关键字 static
进行声明。通过下面简单的例子可以了解它的特点。
例子:考察静态局部变量的值
编写程序:
#include <stdio.h>
int main() {
int f(int a); // 函数声明
int a = 2, i;
for (i = 0; i < 3; i++) // 先后 3 次调用 f 函数
printf("%d\n", f(a)); // 输出 f(a) 的值
return 0;
}
int f(int a) {
auto int b = 0; // 自动局部变量
static int c = 3; // 静态局部变量
b = b + 1;
c = c + 1;
return (a + b + c);
}
运行结果:
7
8
9
程序分析:main
函数第 1 次调用 f
函数时,实参 a
的值为 2。它传递给形参 a
。f
函数中的局部变量 b
的初值为 0,c
的初值为 3。第 1 次调用结束时,b = 1
,c = 4
,a + b + c = 7
。由于 c
被定义为静态局部变量,在函数调用结束后,它并不释放,仍保留 c
的值为 4。在第 2 次调用 f
函数时,b
的初值为 0,而 c
的初值为 4(上次调用结束时的值)。先后 3 次调用 f
函数时,b
和 c
的值如表 7.1 所示。
表 7.1 静态变量与自动变量的值的比较分析
第几次调用 | 调用时初值 | 调用结束时的值 | a + b + c |
---|---|---|---|
b | c | b | |
第 1 次 | 0 | 3 | 1 |
第 2 次 | 0 | 4 | 1 |
第 3 次 | 0 | 5 | 1 |
说明:
- 静态局部变量属于静态存储类别,在静态存储区内分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,分配在动态存储区空间而不在静态存储区空间,函数调用结束后即释放。
- 对静态局部变量是在编译时赋初值的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。而对自动变量赋初值,不是在编译时进行的,而是在函数调用时进行的,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
- 如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值 0(对数值型变量)或空字符
\0
(对字符变量)。而对自动变量来说,它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的内容是不可知的。 - 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的。因为它是局部变量,只能被本函数引用,而不能被其他函数引用。
什么情况下需要用局部静态变量呢?
需要保留函数上一次调用结束时的值时,例如可以用下面方法求 n!。
例子:输出 1 到 5 的阶乘值
编写程序:
#include <stdio.h>
int main() {
int fac(int n);
int i;
for (i = 1; i <= 5; i++) // 先后 5 次调用 fac 函数,每次计算并输出 i 的阶乘值
printf("%d! = %d\n", i, fac(i));
return 0;
}
int fac(int n) {
static int f = 1; // f 保留了上次调用结束时的值
f = f * n; // 在上次的 f 值的基础上再乘以 n
return f; // 返回值 f 是 n! 的值
}
运行结果:
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
说明:
- 每次调用
fac(i)
,输出一个i!
,同时保留这个i!
的值以便下次再乘以(i+1)
。 - 如果函数中的变量只被引用而不改变值,则定义为静态局部变量(同时初始化)比较方便,避免每次调用时重新赋值。
但是应该看到,用静态存储要多占内存(长期占用不释放,而不能像动态存储那样一个存储单元可以先后为多个变量使用,节约内存),而且降低了程序的可读性,当调用次数多时往往弄不清静态局部变量的当前值。因此,若非必要,不要多用静态局部变量。
3. 寄存器变量 (register 变量)
一般情况下,变量(包括静态存储方式和动态存储方式)的值是存放在内存中的。当程序中用到某个变量的值时,由控制器发出指令将内存中该变量的值送到运算器中。经过运算器进行运算,如果需要存储,再从运算器将数据送到内存存放。
如果有一些变量使用频繁(例如,在一个函数中执行 10000 次循环,每次循环中都要引用某局部变量),则为存取变量的值要花费不少时间。为提高执行效率,允许将局部变量的值放在 CPU 中的寄存器中,需要用时直接从寄存器取出参加运算,不必再到内存中去存取。由于对寄存器的存取速度远高于对内存的存取速度,因此这样做可以提高执行效率。这种变量叫做寄存器变量,用关键字 register
作声明。如:
register int count; // 定义 count 为寄存器变量
由于现在的计算机的速度越来越快,性能越来越高,优化的编译系统能够识别使用频繁的变量,从而自动地将这些变量放在寄存器中,而不需要程序设计者指定。因此,现在实际上用 register
声明变量的必要性不大。在此不详细介绍它的使用方法和有关规定,读者只需要知道有这种变量即可,以便在阅读他人写的程序时遇到 register
时不会感到困惑。
注意: 三种局部变量的存储位置是不同的:自动变量存储在动态存储区;静态局部变量存储在静态存储区;寄存器变量存储在 CPU 中的寄存器中。
7.9.3 全局变量的存储类别
全局变量存放在静态存储区中,因此它们的生存期是固定的,存在于程序的整个运行过程。但是,对于全局变量来说,还有一个问题尚待解决:它的作用域究竟从什么位置起,到什么位置止。作用域是包括整个文件范围还是文件中的一部分范围?是在一个文件中有效还是在程序的所有文件中都有效?这就需要指定不同的存储类别。
1. 在一个文件内扩展外部变量的作用域
如果外部变量不在文件的开头定义,其有效的作用范围只限于定义处到文件结束。在定义点之前的函数不能引用该外部变量。如果由于某种考虑,在定义点之前的函数需要引用该外部变量,则应该在引用之前用关键字 extern
对该变量作“外部变量声明”,表示把该外部变量的作用域扩展到此位置。有了此声明,就可以从“声明”处起,合法地使用该外部变量。
例子:调用函数,求3个整数中的大者
解题思路: 用 extern
声明外部变量,扩展外部变量在程序文件中的作用域。
编写程序:
#include <stdio.h>
int A, B, C; // 定义外部变量
int max();
int main() {
extern int A, B, C; // 扩展外部变量 A, B, C 的作用域到从此处开始
printf("Please enter three integer numbers: ");
scanf("%d %d %d", &A, &B, &C); // 输入 3 个整数给 A, B, C
printf("Max is %d\n", max());
return 0;
}
int max() {
int m;
m = A > B ? A : B; // 比较 A 和 B 的值
if (C > m) m = C; // 比较 C 和 m 的值
return m; // 返回最大的值
}
运行结果:
Please enter three integer numbers: 34 67 12
Max is 67
这个例子很简单,主要用来说明使用外部变量的方法。由于定义外部变量 A, B, C 的位置在函数 main
之后,本来在 main
函数中是不能引用外部变量 A, B, C 的。现在,在 main
函数的开头用 extern
对 A, B, C 进行“外部变量声明”,把 A, B, C 的作用域扩展到该位置。这样在 main
函数中就可以合法地使用全局变量 A, B, C 了,用 scanf
函数给外部变量 A, B, C 输入数据。如果不作 extern
声明,编译 main
函数时就会出错,系统无从知道 A, B, C 是后来定义的外部变量。
由于 A, B, C 是外部变量,所以在调用 max
函数时用不到参数传递。在 max
函数中可直接使用外部变量 A, B, C 的值。
注意: 提倡将外部变量的定义放在引用它的所有函数之前,这样可以避免在函数中多加一个 extern
声明。
用 extern
声明外部变量时,类型名可以写也可以省略。例如,extern int A, B, C;
也可以写成 extern A, B, C;
。因为它不是定义变量,可以不指定类型,只须写出外部变量名即可。
2. 将外部变量的作用域扩展到其他文件
一个 C 程序可以由一个或多个源程序文件组成。如果程序只由一个源文件组成,使用外部变量的方法前面已经介绍。如果程序由多个源程序文件组成,那么在一个文件中想引用另一个文件中已定义的外部变量,有什么办法呢?
如果一个程序包含两个文件,在两个文件中都要用到同一个外部变量 Num
,不能分别在两个文件中各自定义一个外部变量 Num
,否则在进行程序的连接时会出现“重复定义”的错误。正确的做法是:在任一个文件中定义外部变量 Num
,而在另一文件中用 extern
对 Num
作“外部变量声明”,即 extern int Num;
。在编译和连接时,系统会由此知道 Num
有“外部链接”,可以从别处找到已定义的外部变量 Num
,并将在另一文件中定义的外部变量 Num
的作用域扩展到本文件,在本文件中可以合法地引用外部变量 Num
。
例子:给定 b 的值,输入 a 和 m,求 a*b 和 a 的幂次方的值。
解题思路: 分别编写两个文件模块,其中 file1
包含主函数,另一个文件 file2
包含求 a
的幂次方的函数。在 file1
文件中定义外部变量 A
,在 file2
中用 extern
声明外部变量 A
,把 A
的作用域扩展到 file2
文件。
编写程序:
文件 file1.c
#include <stdio.h>
int A; // 定义外部变量
int main() {
int power(int);
int b = 3, c, d, m;
printf("Enter the number a and its power m:\n");
scanf("%d %d", &A, &m);
c = A * b;
printf("%d * %d = %d\n", A, b, c);
d = power(m);
printf("%d^%d = %d\n", A, m, d);
return 0;
}
文件 file2.c
extern int A; // 扩展外部变量 A 的作用域到本文件
int power(int n) {
int i, y = 1;
for (i = 1; i <= n; i++)
y *= A;
return y;
}
运行结果:
Enter the number a and its power m:
13 3
13 * 3 = 39
13^3 = 2197
从键盘输入 a
的值为 13,m
的值为 3,程序输出:13 * 3 = 39,13^3 = 2197。由于计算机无法输出上角标,故以 ^
代表幂次,13^3 表示 13³。
程序分析: file2.c
文件的开头有一个 extern
声明,它声明在本文件中出现的变量 A
是一个“在其他文件中定义过的外部变量”。本来外部变量 A
的作用域是 file1.c
,但现在用 extern
声明将其作用域扩大到 file2.c
文件。假如某一程序包括了 5 个源文件模块,在一个文件中定义外部整型变量 A
,其他 4 个文件都可以引用 A
,但必须在每一个文件中都加上一个 extern A;
声明。在各文件经过编译后,将各目标文件连接成一个可执行的目标文件。
说明: 用这种方法扩展全局变量的作用域应十分慎重,因为在执行一个文件中的操作时,可能会改变该全局变量的值,这会影响到另一文件中全局变量的值,从而影响该文件中函数的执行结果。
有的读者可能会问:extern
既可以用来扩展外部变量在本文件中的作用域,又可以使外部变量的作用域从一个文件扩展到程序中的其他文件,那么系统怎么区别处理呢?实际上,在编译时遇到 extern
时,先在本文件中找外部变量的定义,如果找到,就在本文件中扩展作用域;如果找不到,就在连接时从其他文件中找外部变量的定义。如果从其他文件中找到了,就将作用域扩展到本文件;如果再找不到,就按出错处理。
3. 将外部变量的作用域限制在本文件中
有时在程序设计中希望某些外部变量只限于被本文件引用,而不能被其他文件引用。这时可以在定义外部变量时加一个 static
声明。例如:
文件 file1.c
static int A;
int main() {
// ...
}
文件 file2.c
extern int A;
void fun(int n) {
A = A * n; // 出错,因为 A 的作用域仅限于 file1.c
}
在 file1.c
中定义了一个全局变量 A
,但它用了 static
声明,把变量 A
的作用域限制在本文件范围内,虽然在 file2.c
中用了 extern int A;
,但仍然不能使用 file1.c
中的全局变量 A
。
这种加上 static
声明、只能用于本文件的外部变量称为静态外部变量。在程序设计中,常由若干人分别完成各个模块,各人可以独立地在其设计的文件中使用相同的外部变量名而互不相干。只须在每个文件中定义外部变量时加上 static
即可。这就为程序的模块化、通用性提供方便。如果已确认其他文件不需要引用本文件的外部变量,就可以对本文件中的外部变量都加上 static
,成为静态外部变量,以免被其他文件误用。这就相当于把本文件的外部变量对外界“屏蔽”起来,从其他文件的角度看,这个静态外部变量是“看不见,不能用”的。
至于在各文件中在函数内定义的局部变量,本来就不能被函数外引用,更不能被其他文件引用,因此是安全的。
说明: 不要误认为对外部变量加 static
声明后才采取静态存储方式(存放在静态存储区中),而不加 static
的是采取动态存储(存放在动态存储区)。声明局部变量的存储类型和声明全局变量的存储类型的含义是不同的。对于局部变量来说,声明存储类型的作用是指定变量存储的区域(静态存储区或动态存储区)以及由此产生的生存期的问题,而对于全局变量来说,由于都是在编译时分配内存的,都存放在静态存储区,声明存储类型的作用是变量作用域的扩展问题。
用 static
声明一个变量的作用是:
- 对局部变量用
static
声明,把它分配在静态存储区,该变量在整个程序执行期间不释放,其所分配的空间始终存在。 - 对全局变量用
static
声明,则该变量的作用域只限于本文件模块(即被声明的文件中)。
注意: 用 auto
、register
和 static
声明变量时,是在定义变量的基础上加上这些关键字,而不能单独使用。下面的用法不对:
int a;
static a; // 错误,编译时会被认为“重新定义”
// 先定义整型变量 a,企图再将变量 a 声明为静态变量
7.9.4 存储类别小结
从以上讨论中可以得知,对一个数据的定义,需要指定两种属性:数据类型和存储类别,分别使用两个关键字。例如:
static int a; // 静态局部整型变量或静态外部整型变量
auto char c; // 自动变量,在函数内定义
register int d; // 寄存器变量,在函数内定义
此外,可以用 extern
声明已定义的外部变量,例如:
extern int b; // 将已定义的外部变量 b 的作用域扩展至此
存储类别的归纳
(1) 从作用域角度分
有局部变量和全局变量。它们采用的存储类别如下:
-
局部变量
- 自动变量,即动态局部变量(离开函数,值就消失)
- 静态局部变量(离开函数,值仍保留)
- 寄存器变量(离开函数,值就消失)
-
全局变量
- 静态外部变量(只限本文件引用)
- 外部变量(即非静态的外部变量,允许其他文件引用)
(2) 从变量存在的时间(生存期)来区分
有动态存储和静态存储两种类型。静态存储是程序整个运行时间都存在,而动态存储则是在调用函数时临时分配单元。
-
动态存储
- 自动变量(本函数内有效)
- 寄存器变量(本函数内有效)
- 形式参数(本函数内有效)
-
静态存储
- 静态局部变量(函数内有效)
- 静态外部变量(本文件内有效)
- 外部变量(用
extern
声明后,其他文件可引用)
(3) 从变量值存放的位置来区分
-
静态存储区
- 静态局部变量
- 静态外部变量
-
动态存储区
- 自动变量和形式参数
-
寄存器
- 寄存器变量
(4) 关于作用域和生存期的概念
作用域是指变量在程序的哪些部分可以被引用,这称为变量的可见性。生存期是指变量在程序的执行过程中其值存在的时间。前者是从空间的角度,后者是从时间的角度。二者有联系但不是同一回事。以下示意图展示了作用域和生存期的概念。
作用域示意图:
文件 file1.c
int a;
int main() {
void f1();
void f2();
f1();
f2();
}
void f1() {
auto int b;
}
void f2() {
// ...
}
生存期示意图:
main——f1——f2——main
a 生存期
b 生存期
如果一个变量在某个文件或函数范围内是有效的,就称该范围为该变量的作用域,在此作用域内可以引用该变量。例如上图中变量 a
的作用域是整个文件,变量 b
的作用域是函数 f1
。如果一个变量值在某一时刻是存在的,则认为这一时刻属于该变量的生存期,或称该变量在此时刻“存在”。
表7.2 各种类型变量的作用域和存在性
变量存储类别 | 作用域 | 存在性 |
---|---|---|
自动变量和寄存器变量 | 函数内 | 是 |
静态局部变量 | 函数内 | 是 |
静态外部变量 | 本文件 | 是 |
外部变量 | 全局 | 是 |
static 对局部变量和全局变量的作用不同
- 对局部变量来说,它使变量由动态存储方式改变为静态存储方式。
- 对全局变量来说,它使变量局部化(局限于本文件),但仍为静态存储方式。
从作用域角度看,凡有 static
声明的,其作用域都是局限的,或者局限于本函数内(静态局部变量),或者局限于本文件内(静态外部变量)。