存储期
存储期描述了通过这些标识符访问的对象的生存期。 C对象有4种存储期: 静态存储期、 线程存储期、 自动存储期、 动态分配存储期。
如果对象具有静态存储期, 那么它在程序的执行期间一直存在。 文件作用域变量具有静态存储期。对于文件作用域变量, 关键字 static表明了其链接属性, 而非存储期。static声明的文件作用域变量具有内部链接。 但是无论是内部链接还是外部链接, 所有的文件作用域变量都具有静态存储期。
线程存储期用于并发程序设计, 程序执行可被分为多个线程。 具有线程存储期的对象, 从被声明时到线程结束一直存在。 以关键字_Thread_local声明一个对象时, 每个线程都获得该变量的私有备份。
块作用域的变量通常都具有自动存储期。 当程序进入定义这些变量的块时, 为这些变量分配内存; 当退出这个块时, 释放刚才为变量分配的内存。
变长数组稍有不同, 它们的存储期从声明处到块的末尾, 而不是从块的开始处到块的末尾。
然而, 块作用域变量也能具有静态存储期。 为了创建这样的变量, 要把变量声明在块中, 且在声明前面加上关键字static
void more(int number)
{
int index;
static int ct = 0;
...
return 0;
}
变量ct储存在静态内存中, 它从程序被载入到程序结束期间都存在。 但是, 它的作用域定义在more()函数块中。 只有在执行该函数时, 程序才能使用ct访问它所指定的对象(但是, 该函数可以给其他函数提供该存储区的地址以便间接访问该对象, 例如通过指针形参或返回值) 。
C 使用作用域、 链接和存储期为变量定义了多种存储方案。
剩下5种存储类别: 自动、 寄存器、 静态块作用域、 静态外部链接、 静态内部链接。

自动变量
属于自动存储类别的变量具有自动存储期、 块作用域且无链接。 默认情况下, 声明在块或函数头中的任何变量都属于自动存储类别。
块作用域和无链接意味着只有在变量定义所在的块中才能通过变量名访问该变量(当然, 参数用于传递变量的值和地址给另一个函数, 但是这是间接的方法) 。 另一个函数可以使用同名变量, 但是该变量是储存在不同内存位置上的另一个变量。
变量具有自动存储期意味着, 程序在进入该变量声明所在的块时变量存在, 程序在退出该块时变量消失。 原来该变量占用的内存位置现在可做他用。
内层块会隐藏外层块的定义。 但是离开内层块后, 外层块变量的作用域又回到了原来的作用域。


while循环的测试条件中使用的是原始的x:
while(x++ < 33)
程序创建了第3个x变量, 该变量只定义在while循环中。所以, 当执行到循环体中的x++时, 递增为101的是新的x, 然后printf()语句显示了该值。 每轮迭代结束, 新的x变量就消失。 然后循环的测试条件使用并递增原始的x, 再次进入循环体, 再次创建新的x。
我们使用的编译器在创建while循环体中的x时, 并未复用内层块中x占用的内存
没有花括号的块
前面提到一个C99特性: 作为循环或if语句的一部分, 即使不使用花括号({}) , 也是一个块。 更完整地说, 整个循环是它所在块的子块(subblock) , 循环体是整个循环块的子块。 与此类似, if 语句是一个块, 与其相关联的子语句是if语句的子块。 这些规则会影响到声明的变量和这些变量的作用域。


第1个for循环头中声明的n, 其作用域作用至循环末尾, 而且隐藏了原始的n。 但是, 离开循环后, 原始的n又起作用了。
第2个for循环头中声明的n作为循环的索引, 隐藏了原始的n。 然后, 在循环体中又声明了一个n, 隐藏了索引n。 结束一轮迭代后, 声明在循环体中的n消失, 循环头使用索引n进行测试。 当整个循环结束时, 原始的 n 又起作用了。
支持C99和C11
有些编译器并不支持C99/C11的这些作用域规则(Microsoft Visual Studio2012就是其中之一) 。 有些编译会提供激活这些规则的选项。
自动变量的初始化
自动变量不会初始化, 除非显式初始化它。
寄存器变量
变量通常储存在计算机内存中。寄存器变量储存在CPU的寄存器中, 或者概括地说, 储存在最快的可用内存中。 与普通变量相比,访问和处理这些变量的速度更快。 由于寄存器变量储存在寄存器而非内存中, 所以无法获取寄存器变量的地址。 绝大多数方面, 寄存器变量和自动变量都一样。 也就是说, 它们都是块作用域、 无链接和自动存储期。
使用存储类别说明符register便可声明寄存器变量。
声明变量为register类别与直接命令相比更像是一种请求。 编译器必须根据寄存器或最快可用内存的数量衡量你的请求, 或者直接忽略你的请求。
在这种情况下,寄存器变量就变成普通的自动变量。 即使是这样, 仍然不能对该变量使用地址运算符。
在函数头中使用关键字register, 便可请求形参是寄存器变量:
void macho(register int n)
可声明为register的数据类型有限。 例如, 处理器中的寄存器可能没有足够大的空间来储存double类型的值。
块作用域的静态变量
静态的意思是该变量在内存中原地不动, 并不是说它的值不变。 具有文件作用域的变量自动具有(也必须是) 静态存储期。创建具有静态存储期、 块作用域的局部变量。 这些变量和自动变量一样, 具有相同的作用域, 但是程序离开它们所在的函数后, 这些变量不会消失。 也就是说, 这种变量具有块作用域、 无链接, 但是具有静态存储期。
计算机在多次函数调用之间会记录它们的值。 在块中(提供块作用域和无链接) 以存储类别说明符static(提供静态存储期) 声明这种变量。


静态变量stay保存了它被递增1后的值, 但是fade变量每次都是1。 这表明了初始化的不同: 每次调用trystat()都会初始化fade, 但是stay只在编译strstat()时被初始化一次。 如果未显式初始化静态变量, 它们会被初始化为0。
不能在函数的形参中使用static:
int wontwork(static int flu); // 不允许
“局部静态变量”是描述具有块作用域的静态变量的另一个术语。这种存储类别被称为内部静态存储类别(internal static storage class) 。 这里的内部指的是函数内部, 而非内部链接。
外部链接的静态变量
外部链接的静态变量具有文件作用域、 外部链接和静态存储期。 该类别有时称为外部存储类别(external storage class) , 属于该类别的变量称为外部变量(external variable) 。 把变量的定义性声明(defining declaration) 放在在所有函数的外面便创建了外部变量。 当然, 为了指出该函数使用了外部变量, 可以在函数中用关键字extern再次声明。
外部变量Errupt对于该文件的其他函数(如 next()) 也可见。 简而言之, 在执行块中的语句时, 块作用域中的变量将“隐藏”文件作用域中的同名变量。 如果不得已要使用与外部变量同名的局部变量, 可以在局部变量的声明中使用 auto 存储类别说明符明确表达这种意图。
外部变量具有静态存储期。 因此, 无论程序执行到main()、 next()还是其他函数, 数组Up及其值都一直存在。
从声明处到文件结尾。 除此之外, 还说明了外部变量的生命期。 外部变量Hocus和Pocus在程序运行中一直存在, 因为它们不受限于任何函数, 不会在某个函数返回后就消失。
初始化外部变量
外部变量和自动变量类似, 也可以被显式初始化。 与自动变量不同的是, 如果未初始化外部变量, 它们会被自动初始化为 0。 这一原则也适用于外部定义的数组元素。 与自动变量的情况不同, 只能使用常量表达式初始化文件作用域变量。
使用外部变量
假设有两个函数main()和critic(),它们都要访问变量units。 可以把units声明在这两个函数的上面。


当while循环结束时,main()也知道units的新值。 所以main()函数和critic()都可以通过标识符units访问相同的变量。 用C的术语来描述是, units具有文件作用域、 外部链接和静态存储期。
把units定义在所有函数定义外面(即外部) , units便是一个外部变量,对units定义下面的所有函数均可见。 因此, critics()可以直接使用units变量。
存储类别说明符extern告诉编译器, 该函数中任何使用units的地方都引用同一个定义在函数外部的变量。 再次强调, main()和critic()使用的都是外部定义的units。
外部名称
C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。
定义和声明
int tern = 1; /* tern被定义 */
main()
{
extern int tern; /* 使用在别处定义的tern */
tern被声明了两次。 第1次声明为变量预留了存储空间, 该声明构成了变量的定义。 第2次声明只告诉编译器使用之前已创建的tern变量, 所以这不是定义。 第1次声明被称为定义式声明(defining declaration) , 第2次声明被称为引用式声明(referencing declaration) 。 关键字extern表明该声明不是定义, 因为它指示编译器去别处查询其定义。
extern int tern;
int main(void)
{
编译器会假设 tern 实际的定义在该程序的别处, 也许在别的文件中。 该声明并不会引起分配存储空间。 因此, 不要用关键字extern创建外部定义,只用它来引用现有的外部定义。
外部变量只能初始化一次, 且必须在定义该变量时进行。
内部链接的静态变量
普通的外部变量可用于同一程序中任意文件中的函数, 但是内部链接的静态变量只能用于同一个文件中的函数。 可以使用存储类别说明符 extern, 在函数中重复声明任何具有文件作用域的变量。 这样的声明并不会改变其链接属性。
int traveler = 1; // 外部链接
static int stayhome = 1; // 内部链接
int main()
{
extern int traveler; // 使用定义在别处的 traveler
extern int stayhome; // 使用定义在别处的 stayhome
...
trveler和stayhome都具有文件作用域, 但是只有traveler可用于其他翻译单元(因为它具有外部链接) 。 这两个声明都使用了extern关键字, 指明了main()中使用的这两个变量的定义都在别处,但是这并未改变stayhome的内部链接属性。
多文件
复杂的C程序通常由多个单独的源代码文件组成。 有时, 这些文件可能要共享一个外部变量。 C通过在一个文件中进行定义式声明, 然后在其他文件中进行引用式声明来实现共享。 也就是说, 除了一个定义式声明外, 其他声明都要使用extern关键字。 而且, 只有定义式声明才能初始化变量。
如果外部变量定义在一个文件中, 那么其他文件在使用该变量之前必须先声明它(用 extern关键字) 。 也就是说, 在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量, 其他文件在用extern声明之前不能直接使用它。
许多 UNIX系统允许在多个文件中不使用 extern 关键字声明变量, 前提是只有一个带初始化的声明。编译器会把文件中一个带初始化的声明视为该变量的定义。
存储类别说明符
关键字static和extern的含义取决于上下文。 C语言有6个关键字作为存储类别说明符: auto、 register、 static、 extern、_Thread_local和typedef。 typedef关键字与任何内存存储无关, 把它归于此类有一些语法上的原因。 尤其是, 在绝大多数情况下, 不能在声明中使用多个存储类别说明符, 所以这意味着不能使用多个存储类别说明符作为typedef的一部分。 唯一例外的是_Thread_local, 它可以和static或extern一起使用。
auto说明符表明变量是自动存储期, 只能用于块作用域的变量声明中。由于在块中声明的变量本身就具有自动存储期, 所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。
register 说明符也只用于块作用域的变量, 它把变量归为寄存器存储类别, 请求最快速度访问该变量。 同时, 还保护了该变量的地址不被获取。
用 static 说明符创建的对象具有静态存储期, 载入程序时创建对象, 当程序结束时对象消失。 如果static 用于文件作用域声明, 作用域受限于该文件。 如果 static 用于块作用域声明, 作用域则受限于该块。 因此, 只要程序在运行对象就存在并保留其值, 但是只有在执行块内的代码时, 才能通过标识符访问。 块作用域的静态变量无链接。 文件作用域的静态变量具有内部链接。
extern 说明符表明声明的变量定义在别处。 如果包含 extern 的声明具有文件作用域, 则引用的变量必须具有外部链接。 如果包含 extern 的声明具有块作用域, 则引用的变量可能具有外部链接或内部链接, 这接取决于该变量的定义式声明。
小结: 存储类别
自动变量具有块作用域、 无链接、 自动存储期。 它们是局部变量, 属于其定义所在块(通常指函数) 私有。 寄存器变量的属性和自动变量相同, 但是编译器会使用更快的内存或寄存器储存它们。 不能获取寄存器变量的地址。
具有静态存储期的变量可以具有外部链接、 内部链接或无链接。 在同一个文件所有函数的外部声明的变量是外部变量, 具有文件作用域、 外部链接和静态存储期。 如果在这种声明前面加上关键字static, 那么其声明的变量具有文件作用域、 内部链接和静态存储期。 如果在函数中用 static 声明一个变量, 则该变量具有块作用域、 无链接、 静态存储期。
具有自动存储期的变量, 程序在进入该变量的声明所在块时才为其分配内存, 在退出该块时释放之前分配的内存。 如果未初始化, 自动变量中是垃圾值。 程序在编译时为具有静态存储期的变量分配内存, 并在程序的运行过程中一直保留这块内存。 如果未初始化, 这样的变量会被设置为0。
具有块作用域的变量是局部的, 属于包含该声明的块私有。 具有文件作用域的变量对文件(或翻译单元) 中位于其声明后面的所有函数可见。 具有外部链接的文件作用域变量, 可用于该程序的其他翻译单元。 具有内部链接的文件作用域变量, 只能用于其声明所在的文件内。


程序中, 块作用域的静态变量subtotal统计每次while循环传入accumulate()函数的总数, 具有文件作用域、 内部链接的变量 total 统计所有传入 accumulate()函数的总数。 当传入负值时, accumulate()函数报告total和subtotal的值, 并在报告后重置subtotal为0。 由于parta.c调用了 accumulate()函数, 所以必须包含 accumulate()函数的原型。 而 partb.c 只包含了accumulate()函数的定义, 并未在文件中调用该函数, 所以其原型为可选(即省略原型也不影响使用) 。 该函数使用了外部变量count 统计main()中的while循环迭代的次数

存储类别和函数
函数也有存储类别, 可以是外部函数(默认) 或静态函数。外部函数可以被其他文件的函数访问, 但是静态函数只能用于其定义所在的文件。
通常的做法是: 用 extern 关键字声明定义在其他文件中的函数。 这样做是为了表明当前文件中使用的函数被定义在别处。 除非使用static关键字,否则一般函数声明都默认为extern。
存储类别的选择
唯一例外的是const数据。 因为它们在初始化后就不会被修改, 所以不用担心它们被意外篡改。
const int DAYS = 7;
const char * MSGS[3] = {"Yes", "No", Maybe"};
保护性程序设计的黄金法则是: “按需知道”原则。 尽量在函数内部解决该函数的任务, 只共享那些需要共享的变量。 除自动存储类别外, 其他存储类别也很有用。
随机数函数和静态变量
一个使用内部链接的静态变量的函数: 随机数函数。 ANSI C库提供了rand()函数生成随机数。 生成随机数有多种算法, ANSI C允许C实现针对特定机器使用最佳算法。 然而, ANSI C标准还提供了一个可移植的标准算法, 在不同系统中生成相同的随机数。 实际上, rand()是“伪随机数生成器”, 意思是可预测生成数字的实际序列。 但是, 数字在其取值范围内均匀分布。



关键是要让next成为只供rand1()和srand1()访问的内部链接静态变量(srand1()相当于C库中的srand()函数) 。 把srand1()加入rand1()所在的文件中。



自动重置种子
如果 C 实现允许访问一些可变的量(如, 时钟系统) , 可以用这些值(可能会被截断) 初始化种子值。ANSI C有一个time()函数返回系统时间。 虽然时间单元因系统而异, 但是重点是该返回值是一个可进行运算的类型, 而且其值随着时间变化而变化。 time()返回值的类型名是time_t, 具体类型与系统有关。
#include <time.h> /* 提供time()的ANSI原型*/
srand1((unsigned int) time(0)); /* 初始化种子 */
一般而言, time()接受的参数是一个 time_t 类型对象的地址, 而时间值就储存在传入的地址上。 当然, 也可以传入空指针(0) 作为参数, 这种情况下, 只能通过返回值机制来提供值。
掷骰子
用一个函数提示用户选择任意面数的骰子, 并返回点数总和。

第一, rollem()函数属于该文件私有, 它是roll_n_dice()的辅助函数。 第二, 为了演示外部链接的特性, 该文件声明了一个外部变量roll_count。 该变量统计调用rollem()函数的次数。 这样设计有点蹩脚, 仅为了演示外部变量的特性。 第三, 该文件包含以下预处理指令。
如果使用标准库函数, 如 rand(), 要在当前文件中包含标准头文件(对rand()而言要包含stdlib.h) , 而不是声明该函数。 因为头文件中已经包含了正确的函数原型。

使用 roll_n_dice()函数的程序都要包含 diceroll.c 头文件。 包含该头文件后, 程序便可使用roll_n_dice()函数和roll_count变量。

