C 存储类别

C 存储类别

C提供了多种不同的模型或存储类别(storage class)在内存中储存数据。要理解这些存储类别,先要复习一些概念和术语。标识符是C语言中用于标识唯一对象的符号,包括变量名、函数名、命令名称或常量名称等。

  • 作用域(scope)
  • 链接(linkage)
  • 存储期(storage duration)

作用域(scope)

作用域描述程序中可访问标识符的区域,也就是说在程序的哪些部分可以使用该标识符

在C语言中,共有三种作用域

  • 块作用域(block scope):块是用一对花括号括起来的代码区域。定义在块中的变量具有块作用域,块作用域变量的可见范围是从定义处到包含该定义的块的末尾。另外,函数的型材也具有块作用域。
  • 函数原型作用域(function prototype scope:用于函数原型中的形参名(变量名),函数原型作用域的范围是从形参定义处到原型声明结束。
  • 文件作用域(file scope):定义在函数外面的变量具有文件作用域,具有文件作用域的变量从它的定义处到该定义所在文件的末尾均可见。

链接(linkage)

恩链接的主要作用是定义在程序具有多个文件时,是否可以跨文件访问。作用域和链接描述了标识符的可见性。

C语言共有三种链接属性

  • 外部链接(external linkage):外部链接变量可以在多文件程序中使用
  • 内部链接(internal linkage):内部链接变量只能在一个翻译单元中使用
  • 无链接(no linkage):无链接变量只能被定义块所私有,不能被程序其他部分引用。

具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量,意味着这些变量属于定义它们的块、函数或原型私有。

具有文件作用域的变量可以是外部链接或内部链接,通过关键字static定义

int giants = 5;  // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接
int main()
{
    ...
}
...

使用static修饰的变量,具有内部链接属性。具有文件作用域的变量默认是外部链接。

对于变量dodgers和变量giants, 该文件中的任意函数都可使用它。变量dodgers属文件私有,对于其他文件不可见,而对于变量giants, 该文件和同一程序的其他文件都可以使用,只需要在引用这个变量的外部文件中声明

extern int giants;

上述代码中的extern关键字告诉编译器,这不是一个定义,只是一个声明,表示这个变量位于其他文件中。不要用extern进行外部定义,它只是用来引用一个已经存在的外部定义。

存储期(storage duration)

存储期描述了通过这些标识符访问的对象的生存期,即变量在内存中的生存时间。

C对象共有四种存储期

  • 静态存储期(static storage duration):在程序执行期间一直存在,文件作用域变量具有静态存储期。注意,对于文件作用域变量,关键字static表明了其链接属性,而非存储期。以static声明的文件作用域具有内部链接。但无论内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。另外关键字static还可以使得块作用域变量具有静态存储期。
  • 线程存储期(thread storage duration):用于并发设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
  • 自动存储期(automatic storage duration):块作用域的变量通常具有自动存储期,当程序进入定义变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。变长数组稍有不同,他们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾
  • 动态分配存储期(allocated storage duration):用动态内存分配函数分配和解分配。比如malloc函数,从声明到free释放内存为止。

C语言中,变量的属性信息不仅仅是其类型,还包括了作用域、链接和存储期。这些不同属性的组合方式呈现了变量的不同表现,下面开始讨论存储类别。

存储类别(Storage Class)

变量的存储类别取决于作用域链接存储期。存储类别由声明变量的位置和与之关联的关键字决定,定义在所有函数外部的变量具有文件作用域、外部链接、静态存储期。声明在函数中的变量是自动变量,除非该变量前面使用了其他关键字。他们具有块作用域、无链接、自动存储期。以static关键字声明在函数中的变量具有块作用域、无链接、静态存储期。以static关键字声明在函数外部的变量具有文件作用域、内部链接、静态存储期。

C11新增了一个存储类别说明符:_Thread_local,以该关键字声明的对象具有线程存储期,意思是线程中声明的对象在该线程运行期间一直存在,且在线程开始时被初始化,因此这种对象属于线程私有。

存储类别 存储期 作用域 链接 如何声明
自动 自动 在块中
寄存器 自动 在块中,用关键字register
静态、外部链接 静态 文件 外部 在所有函数外部
静态、内部链接 静态 文件 内部 在所有函数外部,使用关键字static
静态、无链接 静态 在块中,使用关键字static
线程、外部链接 线程 文件 外部 在所有块外部,使用关键字_Thread_local
线程、内部链接 线程 文件 内部 在所有块的外部,使用关键字static和_Thread_local
线程、无链接 线程 在快中,使用关键字static和_Thread_local

自动变量

属于自动存储类别的变量具有自动存储期块作用域无链接。默认情况下,声明在块和函数头中的任何变量都属于自动存储类别,当然也可以显示使用关键字auto

自动存储期的变量意味着程序在进入该变量声明所在的块时变量存在,程序退出该块时变量消失。分配的空间被回收,可做他用。

块作用域和无链接意味着只有在该变量定义的块中才能通过变量名访问该变量(当然,参数用于传递变量的值和地址给另一个函数,这是间接的方法),并且内层块会隐藏外层块的定义。

关于自动存储类变量,需要注意其不会初始化,除非显示初始化,否则变量的初始值可能为任意值。

注意:关键字auto是存储类别说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类别说明符。

寄存器变量

寄存器变量的属性和自动存储类几乎一样,都是块作用域无链接自动存储期。使用存储类别说明符register来声明寄存器变量。

int main (void)
{
    register int quick;

register类别声明了一个请求,编译器会根据寄存器或最快可用内存的数量衡量你的请求,从而加快访问速度;亦可能或略你的请求,使得寄存器变量变成普通的自动变量。但仍不能对该变量使用地址运算符。

块作用域的静态变量

块作用域的静态变量具有静态存储期块作用域无链接,静态变量中的静态表示该变量在内存中保持不变,并不是值不变。之前提过,可以创建具有静态存储期块作用域 的局部变量,这些变量和自动变量一样,具有相同的作用域,但是程序离开其所在函数后,变量不会消失。即具有静态存储期。在块中(提供块作用域无链接)以存储类别说明符static声明这种变量。

此处static是第二种用法。对一个具有块作用域的变量用static修饰,改变了变量的存储期。

void trystat(void)
{
    int fade = 1;
    static int stay = 1;
    printf("fade = %d and stay = %d\n", fade++, stay++);
}

上述代码中,静态变量stay保存了它被递增1后的值,但是fade变量每次都是1。这表明初始化的不同,每次调用trystat()都会初始化fade,但是stay只在编译trystat()时被初始化一次,之后每一次运行trystat函数都不会为stay分配新的内存空间,因为它一直存在着,所占用的空间从没被回收。

fade声明每次调用函数时都会执行,这是运行时行为。而stay声明实际上并不是trystat函数的一部分,因为静态变量和外部变量在程序被载入内存时已执行完毕,把声明放在函数中只是告诉编译器只有在trystat函数中才能看到该变量,这条声明未在运行时执行。

即静态存储期保证其在整个程序运行期间都存在,而块作用域确定只有在函数块内访问该变量。

外部链接的静态变量

外部链接的静态变量具有文件作用域外部链接静态存储期

该类别有时被称为外部存储类别(external storage class), 属于该类别的变量成为外部变量(external variable)。把变量的定义性声明放在所有函数的外面便创建了外部变量。为了表明函数使用了外部变量,可以再函数中用关键字external再次声明。如果一个源文件使用的外部变量定义在另一个源文件中,则必须用extern在该文件中声明该变量。如下所示

int Errupt;  // 外部存储类变量
double Up[100];  // 外部存储类变量
extern char Coal; // 必须声明,说明Coal被定义在其他文件中

void next(void);
int main(void)
{
    extern int Errupt;  // 可选声明
    extern double Up[];  // 可选声明
    ...
}
void next(void)
{
    ...
}

若在main函数中,有以下代码

int Errupt;

则该变量为自动变量,是一个独立的局部变量,与外部变量Errupt不同。该局部变量Erruptmain函数可见,而且在执行块中的语句时,会隐藏外部同名变量Errput.而对于该文件的其他函数来说,外部变量Errupt仍可见。

需要注意的是,若外部变量没有进行显示初始化,会自动初始化为0,而且如果要对外部变量初始化,只能使用常量进行初始化。

int x = 10;                // 没问题,10为常量
int y = 3 + 20;            // 没问题,用于初始化的为常量表达式
size_t z = sizeof(int);    // 没问题,用于初始化的为常量表达式
int x2 = 2 * x;            // 不行,x为变量

内部链接的静态变量

该存储类别的变量具有静态存储期文件作用域内部链接。在所有函数内部(这点与外部变量相同),用存储类别说明符static定义的变量具有这种存储类别。此处的static改变了变量的链接属性

static int svil = 1; // 静态变量,内部链接
int main(void)
{

普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量,这样的声明并不会改变其链接属性。

存储类别说明符

C中共有6个关键字作为存储类别说明符

  • auto:表明变量是自动存储期,只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存 储期,故使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图
  • register:也只用于块作用域的变量,它把变量归为寄存器存储类别,请求以最快速度访问该变量。同事,还保护了该变量的地址不被获取
  • static:该说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件;如果static用于块作用域声明,作用域受限于该块。因此,只要程序在运行对象就存在并保留其值,但只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接。文件作用域的静态变量具有内部链接。
  • extern:表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
  • _Thread_local
  • typedeftypedef关键字与任何内存存储无关,只是语法的原因被归于此类。

注意:绝大多数情况下,不能在声明中使用多个存储类别说明符,所以这意味着不能使用多个存储类别说明符作为typedef的一部分。唯一例外的是_Thread_local,它可以和staticextern一起使用。

参考

  • C Primer Plus
  • C语言中的内存管理那些事0-基本概念
  • C语言中的内存管理那些事1-存储类
  • C语言存储类别(Storage Class)

你可能感兴趣的:(C)