【C-Primer-Plus读书笔记】第12章:存储类别、链接和内存管理
存储类别
C提供了多种不同的模型或存储类别在内存中存储数据:
- 从硬件方面看,被存储的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象。
- 从软件方面看,程序需要一种方法访问对象。这可以通过声明变量来完成,变量名就是标识符,标识符可以用来指定特定对象的内容。标识符即是软件(即C程序)指定硬件内存的对象的方式。
- 变量名不是指定对象的唯一途径,例如指针也同样可以指定对象(这里是说解引用的指针,指针本身指定的是一个存储地址的对象)。
- 一般而言,那些指定对象的表达式被称为左值。所有示例表明,如果可以使用左值改变对象中的值,该左值就是一个可修改的左值。
- 数组是对象,数组的每个元素也是对象。
- 可以用存储期描述对象,所谓存储期是指对象在内存中保留了多长时间。
- 标识符用于访问对象,可以用作用域和链接描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。
- 不同的存储类别具有不同的存储期、作用域和链接。
作用域
作用域描述程序中可访问标识符的区域。一个C变量的作用域可以说块作用域、函数作用域、函数原型作用域或文件作用域。
- 块作用域的块是用一对花括号括起来的代码区域。定义在块中的变量具有块作用域,块作用域变量的可见范围是从定义处到包含该定义的块的末尾。
整个函数体是一个块,函数中的任何复合语句也是一个块,虽然函数的形参声明在函数的左花括号之前,但是它们也具有块作用域,属于函数体这个块。
C99把块的概念扩展到包括for循环、while循环、do while循环和if语句所控制的代码,即使这些代码没有用花括号括起来,也算是块的一部分,包括圆括号内(测试条件)的变量。
- 函数作用域仅用于
goto
标签。 - 函数原型作用域用于函数原型中的形参名(变量名)。其范围是从形参定义处到原型声明结束。
形参名通常无关紧要,编译器只关心类型,且即使有形参名也不必与函数定义中的一致。只有在变长数组中形参名才有用。
- 文件作用域是指变量定义在函数外面,具有文件作用域的变量,从它定义处到该定义所在文件的末尾均可见。文件作用域变量成为全局变量。
你认为的多个文件在编译器中可能以一个文件出现,通常在源代码中include一个或多个头文件,C预处理器实际上是用包含的文件替换
#include
指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件,这个文件被称为翻译单元。
链接
C变量有3种链接属性:外部链接、内部链接或无链接。
- 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。
- 具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用(用
static
关键字修饰声明的文件作用域变量可以创建内部链接变量)。
存储期
作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
- 具有静态存储期的对象,它在程序的执行期间一致存在。文件作用域变量具有静态存储期。注意对于文件作用域变量,关键字
static
表明了其链接属性,而非存储期。 - 线程存储期用于并发程序设计,程序执行可被分为多个线程。其从被声明时到线程结束一直存在。以关键字
_Thread_local
声明。 - 具有自动存储期的变量通常是块作用域,它在程序进入定义这些变量的块时存在,退出块时即释放为变量分配的内存。变长数组稍有不同,它们的存储期从声明处(而不是块的开始处)到块的末尾。块作用域也能具有静态存储期,在声明前加上关键字
static
。
C使用作用域、链接和存储期为变量定义了多种存储方案,我们先详细介绍以下5种存储类别:自动、寄存器、静态块作用域、静态外部链接、静态内部链接。
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字 register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字 static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字 static |
自动变量
- 属于自动存储类别的变量具有自动存储期、块作用域且无链接。关键字
auto
是存储类别说明符。可显式指定(一般为了强调某种意图)也可省略。 - 如果内层块中声明的变量与外层块中的变量同名,内层块会隐藏外层块的定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。
- 前面提到C99的一个特性:作为循环或if语句的一部分,即使不用花括号也是一个块。完整地说,整个循环(包括圆括号内的测试条件内容和花括号的内容)是它所在块地子块,循环体(花括号内的内容)是整个循环块(圆括号内的测试条件内容)的子块。这些规则会影响到声明的变量和这些变量的作用域。
- 自动变量不会初始化,除非显式初始化它。对于未初始化的变量,其值是之前该内存区域所存储的任意值,别指望这个值是0。
寄存器变量
- 变量通常是存储在计算机内存中。如果幸运的话,寄存器变量存储在CPU的寄存器中,或者概括地说,存储在最快的可用内存中。
- 由于寄存器变量存储在寄存器而非内存中,所以无法获取寄存器变量的地址,即不能进行取地址操作。
- 绝大多数方面,寄存器变量和自动变量都一样。都是块作用域、无链接和自动存储期。
- 使用存储类别说明符
register
便可声明寄存器变量。 - 声明变量为
register
类别与直接命令相比更像是一种请求,编译器会衡量你的请求后或许会直接忽略,所以可能不会如你所愿,如此一来寄存器变量就会变成普通的自动变量。即使如此,仍然不能对其进行取地址操作。 - 可声明为
register
的数据类型有限。如处理器中的寄存器可能没有足够大的空间来存储double
类型的值。
块作用域的静态变量
- 静态变量中静态的意思是该变量在内存中原地不动,并不是说它的值不变。
- 具有文件作用域的变量自动具有(也必须是)静态存储期。
- 具有块作用域的变量声明为
static
(静态存储期)后,唯一的区别就是当程序离开它所在块后,这些变量不会消失(被释放)。也就是说这种变量具有块作用域、无链接,但是具有静态存储期。 - 静态变量只在编译它所在块时被初始化一次。如果未显式初始化静态变量,它们会被初始化为0。
- 静态变量和外部变量在程序被载入内存时已执行完毕。把静态变量声明在指定块中是为了告诉编译器只有该块才能看到该变量。这条声明并未在运行时执行。
- 不能在函数形参中使用
static
。 - “局部静态变量”是描述具有块作用域的静态变量的另一个术语。同时一些老的C文献称之为内部静态存储类别,这里的内部是指函数内部而非内部链接。
外部链接的静态变量
- 外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类别有时被称为外部存储类别,属于该类别的变量称为外部变量。
- 把变量的定义性声明放在所有函数的外面便创建了外部变量。为了指出该函数使用了外部变量,可以在函数中用关键字
extern
再次声明(可选的重复声明,如果声明时未使用extern
则创建了一个自动变量)。如果是跨文件使用外部变量(一个源代码文件使用定义在另一个源代码文件中的外部变量),则必须用extern
在该文件中声明该变量。 - 初始化外部变量时与自动变量不同的是,如果未初始化外部变量会被自动初始化为0。另一个不同是只能用常量表达式初始化文件作用域变量。
- 在函数外并且是第一次声明变量,该声明构成了变量的定义,在函数内用
extern
关键字再次声明只告诉编译器使用之前已创建好的变量,所以这不是定义。前者被称为定义式声明,后者被称为引用式声明(该声明并不会引起分配存储空间)。 - 外部变量只能被初始化一次,且必须在定义该变量时进行。
内部链接的静态变量
- 内部链接的静态变量具有静态存储期、文件作用域和内部链接。在所有函数外部(同外部变量)用存储类别说明符
static
定义该变量。 - 过去称其为外部静态变量,但这个术语有点自相矛盾。普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。
多文件
只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。
- C通过在一个文件中进行定义式声明,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用
extern
关键字。而且只有定义式声明才能初始化变量。
注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用
extern
关键字)。
存储类别说明符
C语言有6个关键字作为存储类别说明符:auto
、register
、static
、extern
、_Thread_local
和typedef
。
auto
说明符表明变量时自动存储期,只能用于块作用域的变量中。register
说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量。static
说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果用于文件作用域声明,作用域受限于该文件;如果用于块作用域声明,作用域则受限于该块。因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问。块作用域的静态变量无链接;文件作用域的静态变量具有内部链接。extern
说明符表明声明的变量定义在别处。如果该声明具有文件作用域,则引用的变量必须具有外部链接;如果该声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
存储类别和函数
函数也有存储类别,可以是外部函数(默认)或静态函数。C99新增了第3中类别——内联函数。
- 外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。
- 同变量一样,通常使用extern关键字声明定义在其他文件中的函数。表明当前文件中使用的函数定义在别处。
存储类别的选择
- 优先选择自动存储类别,即默认的存储类别。
- 随意使用外部存储类别的变量导致的后果远远超过了它所带来的便利。
- 唯一例外的是const数据,因为它在初始化后就不能被修改,所以可以依具体情况声明为外部存储类别。
- 保护性程序设计的黄金法则是:“按需知道”原则。尽量在函数内部解决该函数的任务,只共享那些需要共享的变量。
随机数函数和静态变量
- ANSI C库提供了
rand()
函数生成随机数。 - 实际上,
rand()
是伪随机数生成器,意思是可预测生成数字的实际序列。但是,数字在其取值范围内均匀分布。 - 我们用可移植的ANSI C版本,而不是编译器内置的
rand()
函数,可移植版本的方案开始于一个种子数字。该函数使用该种子生成新的数,这个新数又成为新的种子。然后新种子可用于生成更新的种子,以此类推。如此则需要一个静态变量记录上一次使用的种子。 - 如果C实现允许访问一些可变的量(如,时钟系统),可以用这些值(可能会被截断)初始化种子值。
分配内存:malloc()
和free()
(原型在stdlib.h
头文件中)
- 存储类别有一个共同之处:在确定用哪种存储类别后,根据已制定好的内存管理规则,将自动选择其作用域和存储期。
- 静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。
- 除此之外,运用
malloc()
函数可以在程序运行时分配更多内存,该函数接受一个参数:所需的字节数。 malloc()
函数会找到合适的空闲内存块,这样的内存是匿名的。即malloc()
分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此可以把该地址赋给一个指针变量,并使用指针访问这块内存。malloc()
函数的返回类型通常被定义为指向char的指针。然而,从ANSI C标准开始,C使用一个新的类型:指向void
的指针。该类型相当于一个通用指针。通常该函数的返回值会被强制转换为匹配的类型。然而,把指向void
的指针赋给任意类型的指针完全不用考虑类型匹配的问题。如果malloc()
分配内存失败,将返回空指针。
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
回到创建数组上,现在我们有3种创建数组的方法:
- 声明普通数组,用常量表达式表示数组维度
- 声明变长数组,用变量表达式表示数组维度
- 调用
malloc()
函数申请数组所需存储空间
使用第2种和第3种方法可以创建动态数组。和普通数组不同,可以在程序运行时选择数组的大小和分配内存。
- 通常,
malloc()
要与free()
配套使用。free()
函数的参数就是之前malloc()
返回的地址,该函数释放之前malloc()
分配的内存。因此,动态内存分配的存储期从调用malloc()
分配内存到调用free()
释放内存为止。 - 不能用
free()
函数释放通过其他方式(如声明一个数组)分配的内存。 exit()
函数可用于结束程序。标准提供了两个返回值以保证在所有操作系统中都能正常工作(该返回值最终返回给操作系统):EXIT_SUCCESS(或者,相当于0)表示普通程序的返回,EXIT_FAILURE表示程序异常中止,使用时以参数传递给exit()
函数即可。- 为保险起见,请对每一个
malloc()
函数严格配对使用free()
函数,不要依赖操作系统来清理。
free()
的重要性:
- 静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存数量只会增加,除非使用
free()
进行释放。 - 如果没有正确释放内存,有内存泄漏的风险。
calloc()
函数
分配内存也可以使用calloc()
。
double * ptd;
ptd = (double *) calloc(30, sizeof(double));
calloc()
函数接受两个无符号整数作为参数(ANSI C规定是size_t
类型),第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。calloc()
函数还有一个特性:它把块中的所有位都设置为0。free()
函数也可用于释放calloc()
分配的内存。
动态内存分配和变长数组
变长数组和调用malloc()
在功能上有些重合,不同的是:
- 变长数组是自动存储类型,所以内存空间会在其离开所在块时自动释放;
- 另一方面用
malloc()
创建的数组不必局限在一个函数内访问; free()
所用的指针变量不必与malloc()
的指针变量相同(得是同一个地址,重点是地址);- 且不能用
free()
释放同一块内存两次。
存储类别和动态内存分配
- 可以认为程序把它可用的内存分为3部分:一部分具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配。
- 总而言之,程序把静态对象、自动对象和动态分配的对象存储在不同的区域。
ANSI C类型限定符
const
限定符(C90)
以const
关键字声明的对象,其值不能通过赋值或递增、递减来修改。
- 在指针和形参声明中使用
const
- 用
const
声明指针时需要注意区分是限定指针本身还是限定指针所指向的值:const
放在*
左侧任意位置,限定了指针指向的数据不能改变;const
放在*
的右侧,限定了指针本身不能改变。 - 在函数形参中,如果一个指针仅用于给函数访问值,应将其声明为一个
const
限定类型的指针。
- 对全局数据使用
const
使用const
限定符声明全局数据很合理,可以避免程序任何部分都能更改数据。
volatile
类型限定符(C90)
volatile
限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。
restrict
类型限定符(C99)
restrict
关键字允许编译器优化某部分代码以更好的支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
_Atomic
类型限定符(C11)
_Atomic
限定符用于并发程序设计时声明原子类型对象(当一个线程对一个原子类型对象执行原子操作时,其他线程不能访问该对象),并发程序设计把程序执行分成可以同时执行的多个线程。
旧关键字的新位置
C99允许把类型限定符和存储类别说明符static
放在函数原型和函数头的形式参数的初始方括号中。
void ofmouth(int * const a1, int * restrict a2, int n); //以前的风格
void ofmouth(int a1[const], int a2[restrict], int n); //C99允许