第12章 存储类、链接和内存管理
目录
- 12.1 存储类
- 12.1.1 作用域
- 12.1.2 链接
- 12.1.3 存储时期
- 12.1.4 自动变量
- 12.1.5 寄存器变量
- 12.1.6 具有代码块作用域的静态变量
- 12.1.7 具有外部链接的静态变量
- 12.1.8 具有内部链接的静态变量
- 12.1.9 多文件
- 12.2 存储类说明符
- 12.3 存储类和函数
- 12.4 随机函数和静态变量
- 12.5 掷骰子
- 12.6 分配内存:malloc()和free()
- 12.6.1 free()的重要性
- 12.6.2 函数calloc()
- 12.6.3 动态内存分配与变长数组
- 12.6.4 存储类与动态内存分配
- 12.7 ANSI C的类型限定词
- 12.7.1 类型限定词const
- 12.7.2 类型限定词volatile
- 12.7.3 类型限定词restrict
- 12.7.4 旧关键字的新位置
- 12.11 编程练习
- 习题7
12.1 存储类
- C为变量提供了5种不同的存储模型,或称存储类。还有基于指针的第6种存储模型。可以按照一个变量(更一般地,一个数据对象)的存储时期描述它,也可以按照它的作用域以及它的链接来描述它。不同的存储类提供了变量的作用域、链接以及存储时期的不同组合。
- (1)
存储时期就是变量在内层中的保留时间
。您可以拥有在整个程序运行期间都存在的变量,或者只有在包含该变量的函数执行时才存在的变量。您也可以使用函数调用为数据的存储显示地分配和释放内层。 - (2)
变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量
。您可以拥有供多个不同的源代码文件共享的变量、某个特定文件中的所有函数都可以使用的变量、只有在某个特定函数中才可以使用的变量、甚至只有某个函数的一个小部分内可以使用的变量。
12.1.1 作用域
- 作用域描述了程序中可以访问一个标识符的一个或多个区域。一个C变量的作用域可以是
代码块作用域、函数原型作用域,或者文件作用域
。- 代码块是包含在开始花括号和对应的结束花括号之内的一段代码。
在代码块内定义的变量具有代码块作用域
,从该变量被定义的地方到包含该定义的代码块的末尾该变量均可见。- 整个函数体是一个代码块。
函数的形式参量尽管在函数的开始花括号前进行定义,同样也具有代码块作用域,隶属于包含函数体的代码块
。 一个函数内的任意复合语句也是一个代码块
。- 传统上,具有代码块作用域的变量都必须在代码块的开始处进行声明。C99放宽了这一规则,允许在一个代码块中任何位置声明变量。
- C99把代码块的概念扩大到包括由for循环、while循环、do while循环或者if语句所控制的代码。
即使这些代码没有用花括号括起来
。
- 整个函数体是一个代码块。
- 函数原型作用域从变量定义处一直到原型声明的末尾。这意味着编译器在处理一个函数原型的参数时,它所关心的只是该参数的类型:您使用什么名字(如果使用了的话)通常是无关紧要的,不需要使它们和在函数定义中使用的变量名保持一致。
- 一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量从它定义处到包含该定义的文件结尾处都是可见的。文件作用域变量也被称为全局变量。
还有一种被称为函数作用域的作用域,但它只适用于goto语句使用的标签。函数作用域意味着一个特定函数中的goto标签对该函数中任何地方的代码都是可见的,无论该标签出现在哪个代码块中
。
- 代码块是包含在开始花括号和对应的结束花括号之内的一段代码。
12.1.2 链接
- 一个C变量具有下列链接之一:外部链接、内部链接或空链接。
- 具有代码块作用域或者函数原型作用域的变量有空连接。意味着它们由其定义所在的代码块或函数原型私有。
- 具有文件作用域的变量可能有内部或外部链接。
- 用存储类说明符static修饰的文件作用域变量有内部链接。内部链接的变量可以在一个文件的任何地方使用。
- 没有用存储类说明符static修饰的文件作用域变量有外部链接。外部链接的变量可以在一个多文件程序的任何地方使用。
12.1.3 存储时期
-
一个C变量有以下两种存储时期之一:静态存储时期和自动存储时期。
- 如果一个变量具有静态存储时期,它在程序执行期间将一直存在。具有文件作用域的变量具有静态存储时期。
注意对于具有文件作用域的变量,关键字static表明链接类型,并非存储时期
。- 具有代码块作用域的变量一般情况下具有自动存储时期。自动存储时期的变量,在程序进入定义这些变量的代码块时,将为这些变量分配内存;当退出这个代码块时,分配的内存将被释放。该思想把自动变量使用的内存视为一个可以重复使用的工作区或者暂存内存。
- C使用作用域、链接和存储时期来定义5种存储类:自动、寄存器、具有代码块作用域的静态、具有外部链接的静态,以及具有内部链接的静态。
存储类 时期 作用域 链接 声明方式 自动 自动 代码块 空 代码块内 寄存器 自动 代码块 空 代码块内,使用关键字register 具有外部链接的静态 静态 文件 外部 所有函数之外 具有内部链接的静态 静态 文件 内部 所有函数之外,使用关键字static 空链接的静态 静态 代码块 空 代码块内,使用关键字static
12.1.4 自动变量
- 属于自动存储类的变量具有自动存储时期、代码块作用域和空链接。默认情况下,在代码块或函数的头部定义的任意变量都属于自动存储类。
- 为了表明有意覆盖一个外部函数定义时,或者为了表明不能把变量改变为其他存储类这一点很重要时,可以显示的用关键字auto来声明自动变量。
代码块作用域和空链接意味着只有变量定义所在的代码块才可以通过名字访问该变量。当然,可以用参数向其他函数传送该变量的值和地址,但那是以间接的方式知道的
。- 如果在内层代码块定义了一个具有和外层代码块变量同一名字的变量,那么在内层代码块中,内层定义的同名变量覆盖了外层的同名变量,但当运行离开内层代码块时,外层的变量又重新恢复作用。
- 不带{}的代码块
- C99的一个特性,语句若为循环或者if语句的一部分,即使没有使用{},也认为是一个代码块。更完整的说,
整个循环是该循环所在代码块的子代码块,而循环体是整个循环代码块的子代码块
。
- C99的一个特性,语句若为循环或者if语句的一部分,即使没有使用{},也认为是一个代码块。更完整的说,
- 自动变量的初始化
- 除非您显示地初始化自动变量,否则它不会被自动初始化。
若一个非常量表达式中所用到的变量先前都定义过的话,可将自动变量初始化为该表达式
。例如:int ruth=1;int rance=5*ruth;
12.1.5 寄存器变量
- 使用存储类说明符可以声明寄存器变量。例如:register int quick;
- 通常,变量存储在计算机内存中。如果幸运,寄存器变量可以被存储在CPU的寄存器中,或更一般地,存储在速度更快的可用内存中,从而可以比普通变量更快地被访问和操作。
- 寄存器变量多是存储在一个寄存器而不是内层中,所以无法获得寄存器变量的地址。但在其它方面,寄存器变量与自动变量是一样的。
我们说“如果幸运”是因为声明一个寄存器变量仅是一个请求,而非一条直接的命令。即使你声明的寄存器变量没有幸运的存放在寄存器中,而存放在内层中成为一个普通的自动变量,你也不能获得这个实际存放在内层中的寄存器变量的地址
。- 可以使用register声明的类型是有限的。例如,处理器可能没有足够大的寄存器来容纳double类型。
12.1.6 具有代码块作用域的静态变量
静态变量这一名称听起来很矛盾,像是一个不可变的变量。实际上是指变量的位置固定不动
。- 可以创建具有代码块作用域,兼具静态存储的局部变量。这些变量和自动变量具有相同的作用域,但是当包含这些变量的函数完成工作时,它们并不消失。也就是说,这些变量具有代码块作用域、空链接,却有静态存储时期。这样的变量通过存储类说明符static在代码块内声明创建。
- 如果不显示地对静态变量进行初始化,它们将被初始化为0。
- 静态变量在程序调入内存时就已经就位了。之所以在函数中通过static声明一个静态变量,是告诉编译器,虽然这个静态变量在程序运行过程中一直存在,但只有包含该静态变量的函数可以看见它。
- 对函数参量不能用static。
12.1.7 具有外部链接的静态变量
- 具有外部链接的静态变量具有文件作用域、外部链接和静态存储时期。这一类型有时被称为外部存储类,这一类型的变量被称为外部变量。把变量的定义放在所有函数之外,即创建了一个外部变量。
- 为了使程序更加清晰,可以在使用外部变量的函数中通过使用extern关键字来再次声明它。
如果变量是在别的文件中定义的,使用extern来声明该变量就是必须的
。 - 如果在代码块内声明了一个跟外部变量同名的变量,那么代码块作用域的变量就会覆盖具有文件作用域的同名的外部变量。
- 变量的作用域和链接决定了在程序的什么位置可以通过变量名访问变量,变量的存储时期绝对了变量在内存中的保留时间。即使一个变量的在程序的运行过程中一直存在,如果你在这个变量的作用域之外,你还是不能访问到它。例如,在函数之外声明的外部变量,这个声明之前的函数是看不到这个外部变量的,这个声明之后的函数是能看到这个变量的,因为变量的作用域是从变量的声明之处开始的。
- 外部变量的初始化
- 和自动变量一样,外部变量可以被显示地初始化。不同于自动变量的是,如果你不对外部变量初始化,它们将自动被赋初值0。这一原则也适用于外部定义的数组元素。
不同于自动变量,只可以用常量表达式来初始化静态变量
。- 只要类型不是一个变长数组,sizeof表达式就被认为是常量表达式。
- 外部名字
- C99标准要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。
- 定义和声明
int tern=1; //第一次声明称为定义声明,为变量留出了存储空间,此时可以显示初始化。
main() //不显示初始化,静态存储时期的变量会默认初始化为0;自动存储时期的变量会得到一个不确定的原存储空间的值
。
{
extern int tern; //第二次声明称为引用声明,告诉编译器要使用先前定义的变量tern关键字extern表明该声明不是一个定义,因为它指示编译器参考其他地方,
因此不要用关键字extern来进行外部定义,只用它来引用一个已存在的外部定义,不会引起空间分配。
一个外部变量只可进行一次初始化,而且一定是在变量被定义时进行。
12.1.8 具有内部链接的静态变量
- 在所有函数外部,通过存储类说明符static来创建具有静态存储时期、文件作用域以及内部链接的变量。
- 在所有函数外部,创建的普通外部变量,可以被多文件程序的任一文件中的函数访问;具有内部链接的静态变量只可以被与它在同一文件中的函数使用。
- 可以在函数中使用存储类说明符extern来再次声明任何具有文件作用域的变量,但是这样的声明不改变链接。
extern的作用是告诉编译器后面的变量是引用的其他地方定义的变量
,这个其他地方可以是同一个程序的其他文件中的外部变量,也可以是同一个程序同一个文件中的具有内部链接的静态变量,不是专门用来引用外部变量的
。
12.1.9 多文件
多文件程序中,除了一个声明(定义声明)外,其他所有声明都必须使用关键字extern,并且只有在定义声明中才可以对该变量进行初始化
。- 注意:除非第二个文件中也声明了该变量(通过使用extern),否则在一个文件中定义的外部变量不可以用于第二个文件。
一个外部变量声明本身只是使一个变量可能对其他文件可用
。
12.2 存储类说明符
- C语言中有5个作为存储类说明符的关键字,它们是auto、register、static、extern以及typedef。
- 关键字typedef与内存无关,由于语法原因被归入此类。不可以在一个声明中使用一个以上存储类说明符,这意味着不能将其他任一存储类说明符作为typedef的一部分。
- 说明符auto表明一个变量具有自动存储时期。该说明符只能用在具有代码块作用域的变量声明中,而这样的变量已经拥有自动存储时期,因此它主要用来明确指出意图,使程序更易读。
- 说明符register也只能用于具有代码块作用域的变量。它的使用使您不能获得变量地址。
- 说明符static用于具有代码块作用域的变量声明时,使该变量具有静态存储时期。变量仍具有代码块作用域和空链接。
- static用于文件作用域的变量声明时,表明该变量具有内部链接。
- 说明符extern表明您在声明一个已经在别处定义的变量。
如果包含extern的声明具有文件作用域,所指向的变量必然具有外部链接。如果包含extern的声明具有代码块作用域,所指向的变量可能具有外部链接也可能具有内部链接,这取决于该变量的定义声明
。
12.3 存储类和函数
- 函数也具有存储类。函数可以时外部的(默认情况下)或者静态的(C99增加了第三种可能性,即在16章中将讨论的内联函数)。外部函数可被其他文件中的函数调用,而静态函数只可以在定义它的文件中使用。
- 使用static创建一个特定模块私有的函数,可以避免名字冲突,在其他文件中使用相同名称的不同函数。
- 使用关键字extern来声明在其他文件中定义的函数。
保护性程序设计中一个非常重要的规则就是“需要知道”原则。尽可能保持每个函数的内部工作对函数的私有性,只共享哪些需要共享的变量
。
12.4 随机函数和静态变量
该小节举了个例子关于具有文件作用域、内部链接的静态变量的运用。感觉比较重要的部分是关于ANSI C对于随机数的支持。书中介绍了两种,一种:ANSI C标准允许C实现使用针对特定机器的最佳算法。还有一种ANSI C提供的可移植的标准算法。书中用的rand()函数也不知道是哪种算法。关于随机函数这点以后找机会再好好研究。
还有书中完善求随机数的建议中提到的time()函数,也要好好研究以下。总感觉这两个函数再解决一些特殊问题上会很实用。
12.5 掷骰子
- 通过指令include包含头文件时,将文件名置于双引号而非尖括号中,是为了指示编译器在本地寻找文件,而不是到编译器存放标准头文件的标准位置去寻找文件。在“本地寻找”的意义取决于具体的C实现。一些常见的解释是将头文件与源代码文件放在同一个目录或文件中,或者与工程文件(如果编译器使用它们)放在同一个目录或文件夹中。
- 头文件的用法之一:在本地创建一个头文件,然后在头文件中声明一个变量或声明函数原型,其他文件通过在开头包含此头文件降低重复代码的书写。
最重要的还是对求随机数函数的应用,感觉计算机求的随机数不是真随机的,最多可能就是通过数学算法模拟接近随机数而已,以后再研究,设计到数学了。关于存储类型的应用都能看懂,抓住保护性程序设计的原则就行。
12.6 分配内存:malloc()和free()
- 5种存储类有一个共同之处:在决定了使用哪一个存储类之后,就自动决定了作用域和存储时期,您的选择服从预先指定的内存管理规则。
- 所有的程序都必须留出足够内存来存储它们使用的数据。
- malloc()函数,它接受一个参数:所需内存字节数。然后找到可用内存中一个大小合适的块。malloc()分配了内存,但没有为它指定名字。返回分配内存的第一个字节的地址。因此可以把地址赋给一个指针变量,并使用该指针来访问那块内存。
- 因为char代表一个字节,所以传统上曾将malloc()定义为指向char的指针类型。然后,ANSI C标准使用了一个新类型:指向void的指针。这一类型被用作“通用指针”。
- 函数malloc()可用来返回数组指针、结构指针等。因此一般需要把返回值的类型指派为适当的类型。
- 现在,创建一个数组有三种方法:
- (1)声明一个数组,声明时用常量表达式指定数组维数,然后可以用数组名访问数组元素。
- (2)声明一个变长数组,声明时用变量表达式指定数组维数,然后用数组名来访问数组元素。
- (3)声明一个指针,调用malloc(),然后使用该指针来访问数组元素。
一般对应于每个malloc()调用,应该调用一次free()。函数free()的参数是先前malloc()返回的地址,它释放先前分配的内存。这样所分配的内存的持续时间从调用malloc()分配内存开始,到调用free()释放内存以供再次使用为止
。不能通过free()来释放通过其他方式(例如声明一个数组)分配的内存。- 在C中,类型指派是可选的,而在C++中必须有,因此使用类型指派将使把C程序移植到C++更容易。
- 函数free()只释放它的参数所指向的内存块。在书中一些简单例子中,使用free()不是必须的,因为程序结束时所以已分配的内存将被自动释放。在大型复杂的程序中,能够释放并再利用内存将是重要的。
12.6.1 free()的重要性
- 在编译程序时,静态变量的数量是固定的,在程序运行时也不改变。自动变量使用的内存数量在程序执行时自动增加或者减少。被分配的内存所使用内存数量只会增加,除非你记得使用free()。
12.6.2 函数calloc()
- 内存分配还可以使用calloc(),该函数接受两个参数,第一个参数是所需内存单元个数,第二个参数是每个单元以字节统计的大小。
- calloc()有个特性:它将块中的全部位都置为0(然而要注意,在某些硬件系统中,浮点值0不是用全部位为0来表示的)。
12.6.3 动态内存分配与变长数组
- 变长数组与malloc()对比:
- 功能上一致:都可以用来创建一个大小在运行时决定的数组。
- 区别:变长数组的存储时期是自动存储时期,运行完定义变长数组的代码块,变长数组的内存会自动释放;malloc()分配的内存即使在一个代码块中创建,使用也不必局限于一个函数中,释放时要手动通过free()来释放。
创建多维数组时,变长数组要比通过malloc()方便的多
。
12.6.4 存储类与动态内存分配
- 程序将它的可用内存分成了三个独立部分:
- (1)在编译时就已经知道了静态存储时期存储类变量所需的内存数量,存储在这一部分的数据在整个程序运行期间都可用。这一类型的变量在程序开始时就存在,到程序结束时终止。使用这类变量从定义处开始,通常定义都放在开头,部分合法但不常规的声明在程序中间,特此说明。
- (2)一个自动变量在程序进入包含该变量定义的代码块时产生,在退出这一代码块时终止。伴随着程序对函数的调用和终止,自动变量使用的内存数量也在增加和减少。
- (3)动态分配的内存在调用malloc()或相关函数时产生,在调用free()时释放。由程序员而不是一系列固定规则控制内存持续时间,因此内存块可在一个函数中创建,而在另一个函数中释放。由于这点动态内存分配所用的的内存部分可能变长碎片状,也就是说在活动的内存块之间散布着未使用的字节片。
一个具有外部链接的、具有内部链接的以及具有空链接的静态变量的;一个自动变量的;另一个是动态分配的内存的
。12.7 ANSI C的类型限定词
C90增加了两个属性:不变性和易变性。这些属性通过const和volatile声明的。C99标准增加了第三个限定词restrict,用以方便优化。C99授予类型限定词一个新属性:它们是幂相等的。其实只意味着在同一个声明中可以不只一次的使用同一个类型限定词,多余的被忽略掉。例如:const const cosnt int n=6;相当于const int n=6。
12.7.1 类型限定词const
声明带有关键字const的变量,可以在声明时初始化,但不能通过赋值、增量或减量运算来修改该变量的值
。- 如果指针只是用来让函数访问值,将它声明为const受限指针。如果指针被用来改变调用函数中的数据,则不使用关键字const。ANSI C库也遵循这一惯例。
- 在文件之间共享const数据时,有两个策略:
- (1)遵循外部变量的惯用规则:在一个文件中进行定义声明,在其他文件中进行引用声明(使用关键字extern)。
- (2)使用static关键字将const常量定义在头文件中,使之具有内部链接,然后在需要用到该变量的文件中使用include指令包含该头文件,使每个文件有一个独立的拷贝。因为每个文件的拷贝都是独立的,所以不能用该变量在文件之间通话。使用头文件的好处是不必惦记着在一个文件中定义声明,在下一个文件中引用声明。
缺点是全部文件都包含同一个头文件,复制了数据。如果数据少还好,如果单个数据就多,每个文件都复制数据,最后组合起来的文件浪费内存
。
12.7.2 类型限定词volatile
- 限定词volatile告诉编译器该变量除了可被程序改变以外还可被其他代理改变。典型地,它被用于硬件地址和与其他并行运行的程序共享的数据。
- ANSI C支持volatile关键字的一个原因就是可以方便编译器优化。在没有volatile关键字之前,程序中的变量都假设它还可能被其他代理商改变,所以一些优化编译器不能使用。现在通过volatile关键字明确了哪些变量还会被其他代理商改变,没有被volatile限定的变量编译器就可以优化了。
- volatile 不保证原子性,多线程中仍需结合锁或原子操作
12.7.3 类型限定词restrict
关键字restrict通过允许编译器优化某几种代码增强了计算支持。它只能用于指针,并表明指针是访问一个数据对象的唯一且初始的方式
。- 没有restrict关键字,编译器将不得不设想比较糟糕的情形,可能两次使用指针之间,其他标识符可能改变了数据的值。使用了restrict关键字以后,编译器可以放心的寻找计算的捷径。
- 可以将关键字restrict作为指针型函数参量的限定词使用。这意味着编译器可以假定在函数体内没有其他标识符修改指针指向的数据,因而可以试着优化代码,反之则不然。
- 关键字restrict有两个读者:一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个是用户,它告诉用户仅使用满足restrict要求的参数。一般,编译器无法检查您是否遵循了这一限制,如果你不遵循也是在让自己冒险。
12.7.4 旧关键字的新位置
C99允许将类型限定词和存储类限定词static放在函数原型和函数头部的形式参量所属的初始方括号内
。- 对于类型限定词的情形,这样做为已有功能提供了一个可选语法。
- static的情形不同,它引发了一些新问题。例如:double stick(double ar[static 20]);表明函数调用中,实际参数将是一个指向数组首元素的指针,该数组至少具有20个元素。这样做的目的是允许编译器使用这个信息来优化函数的代码。
- 与restrict相同,关键字static有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个是用户,它告诉用户仅使用满足static要求的参数。
12.11 编程练习
这章主要讨论的就是存储类说明符,内存管理,认真看肯定能看懂。练习感觉有价值的就是练习7,对随机数不太了解,花了点时间研究,还有就是我采用的是多文件编写的这题,不是把所有函数放在一个文件中,所以编译时也花了点心思研究编译器命令。整体来说都挺简单的,就是要理解的内容多了点,
习题7
rolldice.c
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>static int rollem(int sides) //通过static修饰函数,使得只有本文件中函数才能调用此函数
{int r;r=rand()%sides+1; //rand()函数求随机数return r;
}int roll_n_dice(int dice,int sides) //此函数作为掷骰子功能的唯一对外接口
{int i;int total=0;if (dice<1){printf("至少要有一个骰子!!!\n");return -1;}if (sides<2){printf("骰子至少要有两个面!!!\n");return -2;}for (i=0;i<dice ;i++ ){total+=rollem(sides); //调用本文件中static修饰的掷骰子函数}return total;
}
pra12_7.c
#include <stdio.h>
#include <windows.h>
#include <stdlib.h>
#include <time.h>extern int roll_n_dice(int,int);int main(void)
{SetConsoleOutputCP(65001); // 设置为UTF-8 我用的Editplus写代码,需要调用此函数让控制台能支持中文int * p;int temp,set,dice,sides;int i=0;printf("输入要掷骰子的次数:\n");scanf("%d",&set);printf("输入骰子的个数:\n");scanf("%d",&dice);printf("输入骰子的面数:\n");scanf("%d",&sides);p=(int *)malloc(set*sizeof(int)); //强行使用了malloc()函数,练习一下,用其他方法更方便srand(time(0)); //srand()和rand()配合使用,srand()函数改变种子,不改变的话每次调用rand()都得到一样的伪随机数while ((temp=roll_n_dice(dice,sides))>=0&&i<set){p[i]=temp;printf("%5d",p[i]);i++;}printf("\n程序结束!!!\n");return 0;
}