C Primer Plus--C存储类、链接和内存管理之存储类(storage class)

存储类


C为变量提供了5种不同的存储类型:

  • 自动
  • 寄存器
  • 具有代码块作用域的静态
  • 具有外部链接的静态
  • 具有内部链接的静态

不同角度描述变量:

  • 存储时期 变量在内存中保留的时间
  • 变量作用域(Scope)以及它的链接(Linkage) 变量的作用域和链接一起表明程序的哪些部分可以通过变量名来访问该变量

不同的存储类提供了变量的作用域、链接以及存储时期的不同组合。

作用域

作用域分为:

  • 代码块作用域(block scope)
    代码块在C中指的是一对花括号之间。定义在代码块之间的变量其可见性仅存在于其定义处和闭花括号之间。函数的形式参量、for、while、if-else等中定义的变量都是属于代码块作用域。
  • 函数原型作用域(function prototype scope)
    函数原型作用域从变量定义处一直到函数原型结尾,这解释了为什么我们平时定义函数原型的时候的形式参量名字可以与函数定义中形参名字不同,甚至根本没有名字:编译器只关心原型形参的数据类型,因为函数原型形参变量作用域极短,其名称并不重要。
    int function(int arg1, int arg2);
    int function1(int, int);
    
    但是有些情况下函数原型里的形参名称有作用:存在变长数组参量时。
    void function2(int n, int m, int array[m][n]);
    
    变长数组array中使用的变量m n是之前已经声明的。
  • 文件作用域(file scope)
    一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量其可见性存在于其定义处到文件结尾处。这样的变量也被成为全局变量(global variable)
    int function(int arg1, int arg2);
    int file_scope = 0;
    int main() {
        return 0;
    }
    int function(int a, int b){
    
    }
    
    这里面file_scope对于mainfunction两个函数都是可见的。

链接

C变量具有下列三种链接之一:

  • 外部链接 external linkage
  • 内部链接 internal linkage
  • 空链接 no linkage

具有函数原型作用域或者代码块作用域的变量具有空链接,他们是由其定义所在的代码块或者函数原型私有的。具有文件作用域的变量可能有内部或者外部链接。有外部链接的变量可以在一个多文件程序任何地方进行访问。一个具有内部链接的变量可以在其所在的单个文件里任何地方访问。
区分一个具有文件作用域是外部链接还是内部链接可以看static关键字。

static int full_global = 0;//内部链接 只在本文件中全局可见
int file_global = 1;//外部链接 full_global可以被同一程序多个文件访问

存储时期

C变量可以有以下两种存储时期之一:

  • 静态存储时期 static storage duration
    具有静态存储时期的变量会在程序执行期间一直存在。具有文件作用域的变量是具有静态存储时期的。
  • 自动存储时期 automatic storage duration
    具有代码块作用域的变量一般情况下具有自动存储时期。这样的变量在程序进入代码块时被分配内存,退出代码快时其内存被释放。

由以上作用域、链接、存储时期得到了五种存储类:

存储类 存储时期 作用域 链接 声明方式
自动 自动 代码块 代码块内
寄存器 自动 代码块 代码块内,使用关键字register
具有外部链接的静态 静态 文件 外部 所有函数之外
具有内部链接的静态 静态 文件 内部 所有函数之外,使用修饰符static
空链接的静态 静态 代码块 代码块内,使用修饰符static

自动变量

默认情况下,在代码块或函数的头部定义的任意变量都属于自动存储类型。也可以使用关键字auto来显示的表明此变量为自动变量:auto int auto_var = 0;,这样做的目的可以是显式覆盖一个外部函数定义的同名变量或者强调该变量的存储类型不可以改变为其他存储方式。auto称为存储类说明符(storage class specifier)。
自动变量具有代码块作用域和空链接,这样只有在变量定义的代码块里才可以通过变量名访问该变量。C也允许通过向函数传递参数地址来访问变量,但这样属于间接访问,不是通过变量名直接访问的。
覆盖(hide)指的是不同作用域的变量名称相同的情况下,会根据程序所在环境按名称访问相应的变量值。举个栗子:

int x = 0;
while (x++ < 3){
     int x = 10;
     x++;
     printf("%d\n",x);
 }
printf("%d",x);

输出:

11
11
11
4

while循环每次判断用的是外层x的值,进入代码块后,x被重新定义并初始化,代码块里面使用的是暂时的x,每次循环退出时x会被销毁,因此也就有了上面的输出。写程序千万不要写这种代码!!

自动变量不会被自动初始化,必须得显式初始化!但是全局变量会存在默认初始化!

寄存器变量

变量一般存储在计算机的内存中,可以通过关键字register来显式得申请将变量存储在CPU寄存器中或者存储在速度最快的可用内存来达到更快的访问和操作速度。但是,显式得声明变量为寄存器变量并不会导致该变量一定是寄存器变量,这需要编译器考虑到寄存器数目或者高速可用内存数量,如果不行,那么该变量就会变为自动变量。寄存器变量与自动变量有相同的代码块作用域、空链接、自动存储时期,但是无法使用&操作符获取寄存器变量的地址,即使没有申请成功,该变量为自动变量,也还是无法获取它的地址。
register关键字能够申请的类型是有限的,像C primer plus里提到了处理器可能没有足够大的寄存器来容纳double类型的数据。

具有代码块作用域的静态变量

静态变量并不是指变量不可变,而是指变量的位置固定不动。在代码块内部使用修饰符static声明变量会产生具有代码块作用域、空链接但静态的变量。
栗子:

#include<stdio.h>

void block_static(void);

int main(int argc,char *argv[]) {
    for (int i = 0; i < 4; ++i) {
        printf("loop %d\n:",i);
        block_static();
    }
    
    return 0;
}

void block_static(void){
    int test = 10;
    static int foo = 100;
    printf("test = %d, foo = %d\n",test++,foo++);
}

输出结果:

loop 0
:test = 10, foo = 100
loop 1
:test = 10, foo = 101
loop 2
:test = 10, foo = 102
loop 3
:test = 10, foo = 103

静态变量foo的内存位置固定不变,每次block_static函数执行时都会访问它的值,它每次增加的值并没有丢失,static int foo = 100;这个语句既在block_static第一次执行时声明了静态变量foo,之后在block_static每次执行时告诉这个函数foo对其是可见的,它知道这个变量的地址,它可以去访问。而test变量就不同,它是自动变量,每次blocl_static执行时,test先被分配内存并初始化,执行结束时内存被回收,丢失这个值,下一次函数执行时已经丢失了对原先test内存地址访问权,会重新创建test并分配内存然后销毁,周而复始。
形式参量无能使用static修饰。

具有外部链接的静态变量

具有外部链接的静态变量具有文件作用域、外部链接和静态存储时期,也被称为外部变量。
当想要创建一个外部变量时,把变量定义在所有函数之外即可,也可以显式地使用 extern关键字来声明。当变量是在别的文件定义的时,必须使用extern声明变量。

#include<stdio.h>

int Global = 100;


int main() {

    extern int Global;//这一行其实可以省略

    return 0;
}

上面的例子中main函数要访问Global变量无须声明,这样做只是为了表明main函数需要用它而已。需要注意的是如果这样写:extern int Global = 10;是会出错的,因为外部变量不允许在函数内部进行定义。变量的声明与定义是不同的。这里Global既然是外部变量,就不允许函数内部对其进行修改(具体见此处)。另外,如果把extern去掉会产生一个自动变量Global将外部变量Global覆盖掉。
外部变量可以显式地初始化,但是若是没有显式初始化,那么默认会被赋值为0。而且,只可以用常量表达式来对外部变量进行初始化

int Global = 100;
int y = 3 + 10;//合法,3+10是一个常量表达式
size_t z = sizeof(int);//合法,sizeof是编译时运算符,当其操作数不为变长数组时,返回值为一个常量
int x = y; //不合法,y是一个变量

int main() {

    extern int Global;

    return 0;
}

extern关键字

int tern = 1;//外部变量tern定义,初始化

int main(void){
	extern int tern;
	return 0;
}

在上面的代码段中tern有两次声明,第一次是对外部变量的定义声明为变量分配了内存空间,第二处因为有extern存在,表明是对外部变量extern的引用,为引用声明,并不涉及内存分配。在C Primer Plus中这样说:关键字extern表明该声明不是一个定义,因为它指示编译器参考其他地方。
如果这样做:

extern int tern;
int main(void){
}

那么编译器会假定tern是在其他文件中定义的外部变量,而不会引起空间分配。因此,不要用extern来进行外部定义,只用它来引用一个已经存在的外部定义
一个外部变量只允许一次初始化,必须在外部变量被定义声明的同时进行初始化。
extern int tern = 1000;不合法,因为此时编译器假定tern已经存在,这是一个引用声明,不是定义声明。不能对tern进行二次初始化。

具有内部链接的静态变量

具有内部链接的静态变量具有静态存储时期、文件作用域以及内部链接。它通过static关键字在函数外部进行定义。
这种变量不同于外部变量,它只能被同文件中的函数进行访问,在函数中同样可以使用extern关键字来进行引用声明,但不会改变它的内部链接。

int global_full = 100;//外部链接、文件作用域、静态
static int global_not_full = 10;//内部链接、文件作用域、静态

int main() {

    extern int global_full;
    extern int global_not_full;//global_not_full仍是内部链接
    //这两种其实都多余,main中可以直接访问这两个变量
    return 0;
}

多文件

当一个C程序包含多个独立代码文件时,若需要共享外部变量,这时候外部链接、内部链接才显得重要。通过在一个文件中定义外部变量,在其他文件中引用声明这个变量实现共享。除了这个本身的定义声明之外,其他所有生命都必须使用关键字extern来进行引用,并且只能在定义的时候初始化一次。注意,其他文件要想使用这个变量,必须显示的使用extern声明,否则该变量不能用于其他文件。

这是我对《C Primer Plus》第十二章存储类、链接和内存管理所写的一部分笔记,未完待续。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章