C语言中存储类别、链接与内存管理
第12章 存储类别、链接和内存管理
通过内存管理系统指定变量的作用域和生命周期,实现对程序的控制。合理使用内存是程序设计的一个要点。
12.1 存储类别
C提供了多种不同的模型和存储类别,在内存中储存数据。
被储存的每一个值都占用一定的物理内存;C语言把这样一块内存称为对象(object)。
对象可以储存一个或多个值。或者并未存储实际的值。
C不是面向对象的语言,面向对象编程中的对象指的是类对象。
程序需要一种方法访问对象 ---> 通过声明变量来完成
int entity =3;
该声明创建了一个名为entity的标识符(identifier)。标识符是一个名称。
标识符可以用来指定特定对象的内容。标识符遵循变量的命名规则。
标识符是软件指定硬件内存中的对象的方式。
变量名(标识符)不是唯一指定对象的途径。
int * pt = &entity;
*pt不是一个标识符,它不是一个名称。它确实指定了一个对象。
左值:指定对象的表达式;*pt 和entity都是左值。
再举一个例子:const char * pc = “Behold a string literal!”
const限定符保证被pc指向的内容不被修改。但是不能保证pc不指向别的字符串;
存储期(storage duration)描述对象。存储期是指对象在内存中保留了多长时间。
标识符(identifier)用于访问对象。
可用作用域(scope)和链接(linkage)描述标识符。
标识符的作用域和链接表明了程序的哪些部分可以使用它。
不同的存储类别具有不同的存储期、作用域和链接。
标识符可以再源代码的多文件中共享,可用于特定文件的任意函数中,可仅限于特定函数中使用。
对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以再特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。
12.1.1 作用域
块:是用花括号括起来的代码区域。整个函数体是一个块,函数中的任意复合语句也是一个块。定义在块中的变量具有块作用域。
1、块作用域:(block scope)
块作用域中的变量可见范围是从定义处到包含该定义的块的末尾。注意:形式参数声明在函数花括号之前,但是它们也具有块作用域,也是属于函数这个块。C99把块的概念扩展到了for循环、while循环、do while循环和if语句所控制的代码,即是这些代码没有被花括号括起来,也算是块的一部分。一旦离开块,就不能再被访问。
2、函数作用域:(function scope)
3、函数原型作用域:(function prototype scope)
函数原型作用域的范围是从形参定义处到原型声明结束。编译器在处理该函数原型时不关心形参名,只关心它的类型。即使有形参名,也不必和函数定义中匹配。
只有在变长数组中,形参名才有用:void use_a_VLA(int n,int m, ar[n][m]);
方括号中必须使用在函数原型中已声明的名称。
4、文件作用域:(file scope)
变量定义在函数的外面,具有文件作用域。具有文件作用域的变量,从它的定义到该定义所在文件的末尾均可见。文件作用域变量也称为全局变量。
注意:翻译单元和文件
每个翻译单元均对应一个源代码文件和它所包含的文件。
源代码(.c)中包含一个或多个头文件(.h扩展名)。
C预处理实际上是用包含的头文件的内容替换#include指令。
编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元(translation unit)。描述一个具有文件作用域的变量时,它的实际可见范围是翻译单元。
12.1.2 链接
C变量具有3种链接属性:外部链接、内部链接或无链接。
具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。意味着这些变量只属于定义它们的块、函数或原型私有。
具有文件作用域的变量可以是外部链接或内部链接。
外部链接变量可以再多文件程序中使用,内部链接变量智能在一个翻译单元中使用。
非正式术语:
“内部链接的文件作用域”描述仅限于一个翻译单元。->简化称呼,叫作“文件作用域”;
“外部链接的文件作用域”描述可延伸至其他翻译单元的作用域。->简化称呼,叫作“程序作用域”。
如何知道文件作用域变量是外部链接还是内部链接?可以查看外部定义中是否使用了存储类别说明符。
12.1.3 存储期
作用域和链接描述了标识符的可见性。
存储期描述了通过这些标识符访问的对象的生存期。
C对象有4个存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
如果对象具有静态存储期,那么它在程序的执行期间一直存在。
文件作用域变量:默认具有静态存储期。
对于文件作用域变量,关键字static表明了其内部链接属性,而非存储期。
以static声明的文件作用域变量具有内部链接。
所有的文件作用域变量都具有静态存储期。
线程存储期用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。
块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存。当退出这些块时,释放刚才为变量分配的内存。这种做法相当于把自动变量占用的内存视为一个可重复使用的工作区或暂存区。自动变量离开函数时会被销毁。
变长数组稍有不同,它们的存储期从声明处到块的末尾。而不是块的开始和块的末尾。
块作用域的变量也可以声明为静态变量。这样可以方便其他函数通过间接的方式访问该变量,例如通过指针形参或返回值。
12.1.4 自动变量
默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
如果为了更清楚表达你的意图,可以显式使用auto关键字。
但是注意C++中auto关键字的用法完全不同。如果写C/C++兼容的程序,最好不要用auto作为存储类别说明符。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(参数用于传递变量的值和地址给另一个函数,这是间接的方法)。另一个函数可以使用同名变量,但是该变量是储存在不同内存位置上的另一个变量。
如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用又回到了原来的作用域。
1、没有花括号的块
作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个个循环是它所在块的子块。循环体是整个循环块的子块。与此类似,if语句是一个块,与之相关联的子语句是if语句的子块。
2、自动变量的初始化
自动变量不会初始化,除非显式初始化它。
如果没有初始化,别指望整个值是0;
可以用非常量表达式,初始化自动变量,前提是所用的变量已在前面定义过。
例如:
int main(void)
{
int ruth = 1;
int rance = 5* ruth;
}
12.1.5 寄存器变量
变量储存在计算机内存中。
如果幸运的话,寄存器变量储存在CPU的寄存器中。或者概括地说,储存在最快的可用内存中。
与普通变量相比,访问和处理这些变量的速度更快。由于寄存器变量储存在寄存器而非内存中,所以无法得到寄存器变量的地址。绝大多数方面,寄存器变量和自动变量都一样,也就是说它们都是块作用域、无链接和自动存储期。
使用存储类别说明符:register便可声明寄存器变量;
int main(void)
{
register int quick;
}
“如果幸运”是因为声明变量register类别与直接命令相比更像是一种请求。请求成功就成了寄存器变量,不成功就变成普通的自动变量。编译器会根据寄存器或最快可用内存的数量衡量这个请求。即便是这样,仍然不能对该变量使用求址运算符。
可声明为register的数据类型有限,例如处理器中的寄存器可能没有足够大的空间来储存double类型的值。
12.1.6 块作用域的静态变量
静态变量(static variable):意思是该变量在内存中原地不动,并不是说它的值不变。静态可以是指它的生存期很长,跟程序一样。
如果是创建静态存储期、块作用域的局部变量,这些变量与自动变量一样,具有相同的作用域。但是程序离开它们所在的函数后,这些变量不会消失。
这种变量具有块作用域、无链接,但是具有静态存储期。计算机在多次调用之间会记录它们的值。
注意不能在函数的形参中使用static;
注意 static int stay =1; ---> 函数中有这么一条语句的话,只在编译时被初始化一次。下次再调用函数时,不会再执行这条语句。
块作用域的静态变量和文件作用域的静态变量不一样;static关键字表达的含义不同;
12.1.7 外部链接的静态变量
外部链接的静态变量具有文件作用域、外部链接和静态存储期。
该类别有时被称为外部存储类别(external storage class)。
属于该类别的变量称为外部变量(external variable)。
如果一个文件使用的外部变量定义在另一个源文件中。则必须用extern在该文件中声明该变量。
extern char Coal;
如果省略了extern关键字,相当于创建了自动变量:char Coal;
1、初始化外部变量
外部变量和自动变量类似,也可以被显式初始化。
但是如果未初始化外部变量,它们会被自动初始化为0。
这点与自动变量不同,自动变量如果没有初始化,其值是随机的。
而且注意:只能使用常量表达式初始化外部变量,这点与自动变量不同。
2、使用外部变量
int main()
{
extern int units; //这么做是为了指出该函数要使用这个外部变量。这是一个引用式声明。
}
3、外部名称
4、定义和声明
定义式声明和引用式声明是不一样的。关键字extern表明该声明不是定义。因为它指示编译器去别处查询其定义。所以定义式声明会引发内存分配存储空间。而引用式声明会指示编译器去寻找定义。
12.1.8 内部链接的静态变量
该存储类别的变量具有:静态存储期、文件作用域、内部链接;
在所有函数外部(这点与外部变量相同);
用于存储类别说明符(static)定义的变量具有这种存储类别。
例如:
static int svil = 1; //静态变量,内部链接
int main(void)
{
…
}
12.1.9 多文件
只有文件由多个翻译单元组成时,才能体现内部链接和外部链接的重要性。
复杂的C语言程序通常由多个单独的源代码文件组成。有些时候,这些文件需要共享一个外部变量。C语言通过在一个文件进行定义式声明。然后在其他文件通过引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。
外部变量定义在一个文件中,其他变量在使用它之前必须先声明它。也就是说,在某文件中对外部变量进行定义式声明知识单方面允许其他文件使用该变量,其他文件在用extern关键字声明之前不能直接使用它。
12.1.10 存储类别说明符
关键字extern和static的含义取决于上下文;
C语言中有6个关键字:auto、register、static、extern、_Thread_local、typedef
auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由于块中声明的变量本身就默认具有自动存储期。所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。
register说明符也只用于块作用域的变量。把变量归为寄存器的存储类别。请求最快速度访问该变量。同时,还保护了该变量的地址不被获取。
static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用域声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块。
extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
12.1.11 存储类别和函数
函数也有存储类别;
可以是外部函数(默认)或静态函数;
C99还新增了第3种类别,内联函数;
外部函数可以被其他文件的函数访问,静态函数只能用于其定义所在的文件。
static说明符所创建的函数,属于特定模块私有。
用extern关键字声明定义在其他文件中的函数。
除非使用static关键字,否则一般函数声明都默认为extern;
12.1.12 存储类别的选择
随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。
const数据,它们在初始化之后就不会被修改,所以不用担心它们被意外修改。
保护性程序设计的黄金法则是:“按需知道”原则。
尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。
除自动存储类别外,其他存储类别也很有用。不过,在使用其他存储类别之前要考虑一下是否有必要这样做。