变量的存储模型

变量可以超出函数作用域范围存活,也可以跨文件或限定文件范围内访问,这些特性称为变量的存储模型或存储类。与存储类相关的特性,可以从变量所属作用域、链接特性以及存储时期三个角度去探讨。

存储时期

存储时期就是变量在内存中保留的时间,分为静态存储时期和自动存储时期。

// 变量global_bar具有静态存储时间特性,在程序运行期间一直存活。
int global_var = 0;

// 变量x、p、b具有自动存储时间特性
void foo(double x)
{
    // 变量x、p 在进入函数时分配内存,退出时释放内存。
    int *p = 0x0;
    {
        // 进入块时为b分配内存,退出时收回内存。
        struct Bar b;
    }
}

链接

作用域用于限定程序可以访问一个标识符的一个或多个区域;变量的作用域可以是代码块作用域、函数原型作用域、或文件作用域。变量的作用域和链接一起表明程序的哪些部分可以使用指定变量。一个变量具有下列链接之一:外部链接、内部链接、空链接。

全局变量具有外部链接特性,也就是说它具有跨文件访问的潜力;当需要在另一个.c文件中访问该全局变量时,我们不能再次去定义它,这会引发重定义错误,这称为单定义规则,即:变量只能有一次定义,在每个使用外部变量的文件中,都必须声明它。为此,C语言提供了两种变量声明方式:

/*==== [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;
}

glimix.com

示例中在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;
}

glimix.com

函数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中保留着它们的声明。

glimix.com

另外,被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;
}

glimix.com

现在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也可用于声明内部变量,此时变量是某个特定函数的局部变量,只能在该函数中使用。

#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;
}

glimix.com

局部变量ai随着函数的每次调用被初始化,而静态变量si仅在第一次进入函数时初始化一次, 之后随着函数的再次进入,始终维持着最后的状态,也就是其状态并没有随着函数的退出而消失。变量sk用类似的手法验证了这一特性。依赖静态局部对象仅初始化一次的特性,很适合在函数中实现仅执行一次的操作。

void initialize()
{
    static int init_once = 1;

    if (init_once)
    {
        // 仅执行一次的操作
        init_once = 0;
    }
}

陕ICP备2025078817号-1 陕公网安备61011202001108号