5. C语言变量的存储类别和生存期
我们知道,变量是有数据类型的,用以说明它占用多大的内存空间,可以进行什么样的操作。
除了数据类型,变量还有一个属性,称为“存储类别”。存储类别就是数据在内存中的存放区域。一个正在运行的C程序的内存空间可以分为五个区域:程序代码区、静态数据区、堆区、栈区和命令行参数区,其中静态数据区和栈区可以用来存放变量的值。
静态数据区的内存在程序启动时就已经由操作系统分配好,占用的空间固定,程序运行期间不再改变,程序运行结束后才由操作系统释放;它可以存放全局变量、静态变量、一般常量和字符串常量。
栈区的内存在程序运行期间由操作系统根据需要来分配(使用到变量才分配内存;如果定义了变量但没有执行到该代码,也不会分配内存),占用的空间实时改变,使用完毕后立即释放,不必等到程序运行结束;它可以存放局部变量、函数参数等。
可以通过C语言中的关键字来控制变量的存放区域;C语言共有 4 个关键字用来指明变量的存储类别:auto(自动的)、static(静态的)、register(寄存器的)、extern(外部的)。
知道了变量的存储类别,就可以知道变量的生存期。通俗地讲,生存期指的是在程序运行过程中,变量从创建到销毁的一段时间,生存期的长短取决于变量的存储类别,也就是它所在的内存区域。
auto 变量
auto 是自动或默认的意思,很少用到,因为所有的变量默认就是 auto 的。也就是说,定义变量时加不加 auto 都一样,所以一般把它省略,不必多此一举。
例如:
- int n = 10;
与
- auto int n = 10;
的效果完全一样。
6. C语言extern变量和函数
在所有的代码块(函数、if 块、switch 块等)之外定义的变量称为全局变量,它的作用范围默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。
如果你一直在编写单个 .c 文件的程序,那么请注意,全局变量的作用范围不是从变量定义处到该文件结束,在其他文件中也有效。
虽然全局变量的作用范围是整个程序,但是如果希望在 a.c 中使用 b.c 中的变量,也必须先进行声明。声明使用 extern 关键字,请看下面的代码。
a.c 源码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 extern int num; // 必须对 num 进行声明 4 int main () 5 { 6 printf("num = %d", num); 7 system("pause"); 8 return 0; 9 }
b.c 源码:
1 int num = 100; // 对 num 进行定义
运行结果:
num = 100
我们在 b.c 中定义了一个全局变量 num,在 a.c 中调用了它。extern int num; 的作用是告诉编译器 num 不在 a.c 中,请到其他文件中查找。如果没有 extern,编译器就会在当前文件中查找,发现没有就会报错。
提示:编译是针对单个源文件的,在编译 a.c 时编译器找不到 num,链接时才会在 b.c 的目标代码(.obj 文件)中找到 num。
与其他变量不同,extern 变量有声明和定义之分。
extern 变量的定义格式为:
- extern type name = value;
不过 extern 可以省略(我们通常就是这么做的),全局变量默认就是 extern 的,如 b.c 文件所示。
声明格式为:
- extern type name;
注意:
- 在定义 extern 变量时不能省略 value,否则就变成了变量声明。
- 声明 extern 变量时要指明数据类型(必须和定义时的数据类型一致)。
- 声明可以有多次,定义只能有一次。
在 a.c 中,我们在所有代码块外部对 num 进行了声明,这个时候 num 的作用范围是 a.c 整个文件(确切的说是从声明开始处到文件结束)。如果在代码块内部声明会怎样呢?
对 a.c 进行更改:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main () 4 { 5 { 6 extern int num; 7 printf("num = %d", num); 8 } 9 printf("num = %d", num); 10 system("pause"); 11 return 0; 12 }
编译时报错,第 9 行的 num 未声明。这说明 extern 变量的作用域跟它的声明位置有关,在代码块内声明的 extern 变量在代码块外无效。
extern 函数
从本质上讲,函数和变量是类似的,它们都指向内存中的一块区域:函数指向存放了函数体二进制代码的程序代码区,变量指向静态数据区、栈区或堆区。
extern 除了用于变量,也可以用于函数,请看下面的代码:
sum.c 源码:
1 int sum(int n1, int n2) 2 { 3 return n1 + n2; 4 }
main.c 源码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 extern int sum(int, int); 4 int main () 5 { 6 int num1 = 20, num2 = 110; 7 printf("%d + %d = %d", num1, num2, sum(num1, num2)); 8 system("pause"); 9 return 0; 10 }
运行结果:
20 + 110 = 130
我们在 sum.c 中定义了一个函数 sum() 用来计算两个数的和,在 main.c 中对函数进行了调用。extern 的作用是告诉编译器 sum() 函数不在 main.c 中,请到其他文件中去查找。
但是,函数和变量的声明有所不同,对于函数,你可以省略 extern。例如将 main.c 中的:
- extern int sum(int, int);
改为:
- int sum(int, int);
仍然能够编译通过并正确运行。
这是因为,函数的定义和声明区别很明显,有函数体就是定义,没有函数体就是声明,所以有没有 extern 都是函数声明。但是变量不一样,没有 extern 就是变量定义,重复定义是错误的。
7. C语言static变量和函数
上一节我们讲到,全局变量和函数的作用范围默认是整个程序,也就是所有的源文件。这给我们带来了很大的方便,让我们能够在 A 文件中调用 B 文件中定义的变量和函数,不必把所有的代码都集中到一个文件,有利于模块化的程序设计。
但是有时候这也会带来冲突,例如在 a.c 中定义了一个全局变量 n,在 b.c 中又定义了一次,编译时就会发生重复定义的错误,因为变量只能定义一次。
如果两个文件都是我们自己编写的或者其中一个是,遇到这样的情况还比较好处理,改变变量的名字就可以;但如果两个文件都是其他程序员编写的,或者是第三方的库,修改起来就颇费精力了。所以,实际开发中我们一般将不需要被其他文件调用的全局变量或函数的作用范围限制在当前文件中。
可以通过 static 关键字来限制,请看下面的代码。
a.c 源码:
1 #include <stdio.h> 2 static int n = 10; 3 void print_n_a() 4 { 5 printf("n(a.c) = %d\n", n); 6 }
b.c 源码:
1 #include <stdio.h> 2 static int n = 20; 3 void print_n_b() 4 { 5 printf("n(b.c) = %d\n", n); 6 }
main.c 源码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int n = 100; 4 int main () 5 { 6 print_n_a(); 7 print_n_b(); 8 printf("n(main.c) = %d\n", n); 9 system("pause"); 10 return 0; 11 }
运行结果:
n(a.c) = 10
n(b.c) = 20
n(main.c) = 100
我们在 a.c、b.c 和 main.c 中都定义了变量 n,a.c 和 b.c 中的变量 n 都只在各自的文件内有效,main.c 中的变量 n 在整个程序内有效。
由此可见,加了 static 的变量或函数的作用范围仅限于当前文件,对其他源文件隐藏,利用这一特性可以在不同的文件中定义同名的变量或函数,而不必担心命名冲突。
static 局部变量
static 声明的变量称为静态变量,不管是全局变量还是局部变量,都存储在静态数据区(全局变量本来就存储在静态数据区,即使不加 static)。
静态数据区的数据在程序启动时就会初始化,直到程序运行结束;对于代码块中的静态局部变量,即使代码块执行结束,也不会销毁。
注意:静态数据区的变量只能初始化(定义)一次,以后只能改变它的值,不能再被初始化,即使有这样的语句,也无效。
请看下面的代码:
1 #include <stdio.h> 2 #include <stdlib.h> 3 int main () 4 { 5 int result, i; 6 for(i = 1; i<=100; i++) 7 { 8 result = sum(i); 9 } 10 printf("1+2+3+...+99+100 = %d\n", result); 11 system("pause"); 12 return 0; 13 } 14 int sum(int n) 15 { 16 // 也可以不赋初值 0,静态数据区的变量默认初始化为 0 17 static int result = 0; 18 result += n; 19 return result; 20 }
运行结果:
1+2+3+...+99+100 = 5050
我们在 sum() 中定义了一个静态局部变量 result,它存储在静态数据区,sum() 函数执行结束也不会销毁,下次调用继续有效。静态数据区的变量只能初始化一次,第一次调用 sum() 时已经对 result 进行了初始化,所以再次调用时就不会初始化了,也就是说 static int result = 0; 语句无效。
静态局部变量虽然存储在静态数据区,但是它的作用域仅限于定义它的代码块,sum() 中的 result 在函数外无效,与 main() 中的 result 不冲突,除了变量名一样,没有任何关系。
总结起来,static 变量的主要作用有两个。
1) 隐藏
程序有多个源文件时,将全局变量或函数的作用范围限制在当前文件,对其他文件隐藏。
2) 保持变量内容的持久化
将局部变量存储到静态数据区。静态数据区的内存在程序启动时就已分配好(内存中所有的字节默认值都是0x00),直到程序运行结束。
8. C语言register变量
一般情况下,变量的值是存储在内存中的,CPU 每次使用数据都要从内存中读取。如果有一些变量使用非常频繁,从内存中读取就会消耗很多时间,例如 for 循环中的增量控制:
1 int i; 2 for(i=0; i<1000; i++) 3 { 4 // Some Code 5 }
执行这段代码,CPU 为了获得 i,会读取 1000 次内存。
为了解决这个问题,可以将使用频繁的变量放在CPU的通用寄存器中,这样使用该变量时就不必访问内存,直接从寄存器中读取,大大提高程序的运行效率。
寄存器、缓存、内存
为了加深对 register 变量的理解,这里有必要讲一下CPU寄存器。
按照与CPU的远近来分,离CPU最近的是寄存器,然后是缓存,最后是内存。
寄存器是最贴近CPU的,而且CPU只在寄存器中进行存取。寄存的意思是暂时存放数据,不用每次都从内存中取,它是一个临时的存放数据的空间。
而寄存器的数据又来源于内存,于是 CPU <-- 寄存器 <-- 内存,这就是它们之间的信息交换。
那么为什么还需要缓存呢?因为如果频繁地操作内存中同一地址上的数据会影响速度,于是就在寄存器和内存之间设置一个缓存,把使用频繁的数据暂时保存到缓存,如果寄存器需要读取内存中同一地址上的数据,就不用大老远地再去访问内存,直接从缓存中读取即可。
缓存的速度远高于内存,价格也是如此。
注意:缓存的容量是有限的,寄存器只能从缓存中读取到部分数据,对于使用不是很频繁的数据,会绕过缓存,直接到内存中读取。所以不是每次都能从缓存中得到数据,这就是缓存的命中率,能够从缓存中读取就命中,否则就没命中。
关于缓存的命中率又是一门学问,哪些数据保留在缓存,哪些数据不保留,都有复杂的算法。
注意:上面所说的CPU是指CPU核心,从市场上购买的CPU已是封装好的套件,附带了寄存器和缓存,插到主板上就可以用。
从经济和速度的综合考虑,缓存又被分为一级缓存、二级缓存和三级缓存,它们的存取速度和价格依次降低,容量依次增加。购买到的CPU一般会标出三级缓存的容量。
register 变量
寄存器的数量是有限的,通常是把使用最频繁的变量定义为 register 的。
来看一个计算 π 的近似值的例子,求解的一个近似公式如下:
为了提高精度,循环的次数越多越好,可以将循环的增量控制定义为寄存器变量,如下所示:
1 #include <stdio.h> 2 #include <conio.h> 3 int main() 4 { 5 register int i = 0; // 寄存器变量 6 double sign = 1.0, res = 0, ad = 1.0; 7 for(i=1; i<=100000000; i++) 8 { 9 res += ad; 10 sign=-sign; 11 ad=sign/(2*i+1); 12 } 13 res *= 4; 14 printf("pi is %f", res); 15 getch(); 16 return 0; 17 }
运行结果:
pi is 3.141593
关于寄存器变量有以下事项需要注意:
1) 为寄存器变量分配寄存器是动态完成的,因此,只有局部变量和形式参数才能定义为寄存器变量。
2) 局部静态变量不能定义为寄存器变量,因为一个变量只能声明为一种存储类别。
3) 寄存器的长度一般和机器的字长一致,所以,只有较短的类型如int、char、short等才适合定义为寄存器变量,诸如double等较大的类型,不推荐将其定义为寄存器类型。
4) CPU的寄存器数目有限,因此,即使定义了寄存器变量,编译器可能并不真正为其分配寄存器,而是将其当做普通的auto变量来对待,为其分配栈内存。当然,有些优秀的编译器,能自动识别使用频繁的变量,如循环控制变量等,在有可用的寄存器时,即使没有使用 register 关键字,也自动为其分配寄存器,无须由程序员来指定。