变量的存储模型
变量可以超出函数作用域范围存活,也可以跨文件或限定文件范围内访问,这些特性称为变量的存储模型或存储类。与存储类相关的特性,可以从变量所属作用域、链接特性以及存储时期三个角度去探讨。
存储时期
存储时期就是变量在内存中保留的时间,分为静态存储时期和自动存储时期。
- 具有静态存储时期的变量,它在程序执行期间一直存在;拥有文件作用域的变量具有静态存储时期,如全局变量。
- 具有代码块作用域的变量通常具有自动存储时期;程序进入变量所在代码块时为这些变量分配内存,退出代码块时相应的内存被释放,函数的局部变量具有这种特性。
// 变量global_bar具有静态存储时间特性,在程序运行期间一直存活。
int global_var = 0;
// 变量x、p、b具有自动存储时间特性
void foo(double x)
{
// 变量x、p 在进入函数时分配内存,退出时释放内存。
int *p = 0x0;
{
// 进入块时为b分配内存,退出时收回内存。
struct Bar b;
}
}
链接
作用域用于限定程序可以访问一个标识符的一个或多个区域;变量的作用域可以是代码块作用域、函数原型作用域、或文件作用域。变量的作用域和链接一起表明程序的哪些部分可以使用指定变量。一个变量具有下列链接之一:外部链接、内部链接、空链接。
- 具有外部链接的变量可以在一个多文件程序的任何地方使用。
- 具有内部链接的变量可以在一个文件的任何地方使用。
- 具有文件作用域的变量可能有内部链或者是外部链接。
- 具有代码块作用域或者函数原型作用域的变量有空链接,意味着它们是由其定义所在的代码块或函数原型私有的。
全局变量具有外部链接特性,也就是说它具有跨文件访问的潜力;当需要在另一个.c文件中访问该全局变量时,我们不能再次去定义它,这会引发重定义错误,这称为单定义规则,即:变量只能有一次定义,在每个使用外部变量的文件中,都必须声明它。为此,C语言提供了两种变量声明方式:
- 第一种是定义声明,或简称定义(即定义一个变量),它给变量分配存储空间。
- 另一种是引用声明,或简称声明,它不给变量分配存储空间,仅是引用已有的变量,使用
extern关键字实现。
/*==== [va.c] ====*/
int var_a = 2048;
/*==== [vb.c] ====*/
double var_b = 3.1415926;
/*==== [main.c] ====*/
#include <stdio.h>
extern int var_a;
extern double var_b;
int main()
{
printf("%s ===> ", __FUNCTION__);
printf("var_a: %d ", var_a);
printf("var_b: %f\n", var_b);
return 0;
}

示例中在va.c与vb.c两个不同的编译单元定义了各自的全局变量,我们使用extern向main.c编译单元引入这些已定义变量的名称,因此main()函数可以读写变量的值。
全局变量同时满足针对链接特性的第二条,但它拥有的是外部链接特性。规则中在一个文件的任何地方可访问,但这与定义变量的位置有密切的相关性。下例中全局变量var_b在vb.c中可以被其定义位置之下的函数等访问;而在其上定义的函数由于名称的不可见性,故而不能访问它。
/*==== [vb.c] ====*/
void cannot_rw_vb()
{
// 错误:var_b 未声明的标识符
var_b = 6;
}
double var_b = 3.1415926;
void can_rw_vb()
{
// 正确:这里可以访问var_b
var_b = 9;
}
函数声明与定义中的所有变量,拥有空链接特性,这些变量属于函数或代码块自身,如m、arr、n, d, pd、by2。
void drop(int m, double arr[]);
void drop(int n, double d[])
{
double *pd;
{ struct bar *by2; }
}
函数默认具有外部链接特性,在其它地方使用时,我们只需要提供函数声明即可(通常是由头文件提供,这些函数通常称为接口。),这一点不像全局变量那样需要显式的附加extern关键字。
/*==== [fun.h] ====*/
void dolink(const char *l);
extern void drop(int n, double d[]);
/*==== [fun.c] ====*/
#include <stdio.h>
#include "fun.h"
int fun_c_var = 12;
void dolink(const char *l)
{
printf("<fun.c>: " __FUNCTION__"\n");
}
void drop(int n, double d[])
{
printf("<fun.c>: " __FUNCTION__"\n");
}
int compare(int *p, int *d)
{
printf("<fun.c>: " __FUNCTION__"\n");
return -1;
}
/*==== [main.c] ====*/
#include <stdio.h>
// 使用包含头文件的方式,为函数 dolink、drop 提供声明。
#include "fun.h"
// 使用显式声明的方式为函数compare提供声明
int compare(int *p, int *d);
// 使用extern引入fun_c_var的声明
extern int fun_c_var;
int main()
{
dolink(NULL);
drop(2, NULL);
compare(NULL, NULL);
printf("<main.c>: fun_c_var: %d\n", fun_c_var);
return 0;
}

函数drop的声明中显式的加上了extern关键字,表示函数名可在其它地方使用,因为函数默认具有外部链接特性,所以这不是必须的。为了在main函数中使用fun.c实现的三个函数,首先我们包含了fun.h,这样就引入了dolink与drop两个名称;而compare则是我们通过在main.c中声明原型而引入其名称的。
从fun.h中提供的接口看,fun模块(由fun.h/fun.c构造)的设计者并不希望compare、fun_c_var之类的函数或数据被外界访问,它们仅是模块内部实现某些逻辑细节,为此我们可以使用static声明限定外部变量或函数,将这些对象的作用域限定为当前编译单元内(被编译文件内)。
/*==== [fun.c] ====*/
#include <stdio.h>
static int fun_c_var = 12; // 静态存储 + 内部链接
static int compare(int *p, int *d) // 内部链接
{
printf("<fun.c>: " __FUNCTION__"\n");
return -1;
}
注意,对于具有文件作用域的变量(fun_c_var),关键字static表明的是其拥有内部链接类型而非存储时期。现在,这两个名称被限定只能在fun.c中访问,当再次构建程序时,链接期间将出现无法解析符号的错误,即使我们依旧在main.c中保留着它们的声明。

另外,被static修饰的外部变量,作用域在本编译单元内,也具有隐藏外部同名称的变量的效果。
/*==== [fun.c] ====*/
extern int var_a;
static int var_a = 12; // 新增与main.c中同名的变量var_a
void dolink(const char *l)
{
printf("<fun.c>: var_a: %d\n", var_a);
}
/*==== [main.c] ====*/
#include <stdio.h>
#include "fun.h"
// 这里不可能访问fun.c中的 static int var_a
// 这里的extern表示存在一个外部变量var_a,而它就是main.c中的var_a
extern int var_a;
int var_a = 999;
int main()
{
dolink(NULL);
printf("<main.c>: var_a: %d\n", var_a);
return 0;
}

现在main.c中定义着一个与fun.c中同名的变量var_a,但两者互不影响,各自作用在自己的文件范围内。而当去掉foo.c中var_a的static修饰后,就违背了单一定义原则。从foo.c的角度看,用static限定的变量var_a隐藏了main.c中全局变量var_a,在foo.c中只能看到自己定义的var_a,而不能看到main.c中的var_a;反之也成立。
局部静态
static也可用于声明内部变量,此时变量是某个特定函数的局部变量,只能在该函数中使用。
- 静态局部变量不管所在函数是否被调用,它都一直存在。
- 静态局部变量在函数第一次被调用时仅初始化一次。
- 未初始的静态局部变量会默认获得一个初始0值,int是0,float为0.0f, 指针设为NULL。
#include <stdio.h>
void foo()
{
int ai = 5;
static int si = 5;
printf("ai = %d, si = %d\n", ai++, si++);
}
void bar()
{
static float sk;
printf("sk = %.2f\n", sk);
sk = 6;
printf("sk = %.2f\n", sk++);
}
int main()
{
foo();
foo();
foo();
printf("\n");
bar();
bar();
bar();
return 0;
}

局部变量ai随着函数的每次调用被初始化,而静态变量si仅在第一次进入函数时初始化一次, 之后随着函数的再次进入,始终维持着最后的状态,也就是其状态并没有随着函数的退出而消失。变量sk用类似的手法验证了这一特性。依赖静态局部对象仅初始化一次的特性,很适合在函数中实现仅执行一次的操作。
void initialize()
{
static int init_once = 1;
if (init_once)
{
// 仅执行一次的操作
init_once = 0;
}
}
陕公网安备61011202001108号