C Primer Plus(第五版)12

第 12 章 存储类, 链接和内存管理

在本章中你将学习下列内容

. 关键字: auto, extern, static, register, const, volatile, restricted.

. 函数: rand(), srand(), time(), malloc(), calloc(), free()

. 在 C 中如何确定变量的作用域 ( 它在多大范围内可知) 以及变量的生存期 (它存在多长时间).

. 设计更复杂的程序.

C 的强大功能之一在于它允许你控制程序的细节. C 的内存管理系统正是这种控制能力的例子, 它通过让你决定哪些函数知道哪些变量以及一个变量在程序中存在多长时间来实现这种控制. 使用内存存储是程序设计的又一元素.

'12.1 存储类'

C 为变量提供了 5 种不同的存储模型, 或称存储类. 还有基于指针的第 6 种存储模型, 本章稍后 (分配内存 malloc() 和 free() 小节) 将会提到. 可以按照一个变量 (更一般地, 一个数据对象) 的存储时期 (storage duration) 描述它, 也可以按照它的作用域 (scope) 以及它的链接 (linkage) 来描述它. 存储时期就是变量在内存中保留的时间, 变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量. 不同的存储类提供了变量的作用域, 链接以及存储时期的不同组合. 你可以拥有供多个不同的源代码文件共享的变量, 某个特定文件中的所有函数都可以使用的变量, 只有在某个特定函数中才可以使用的变量, 甚至只有某个函数的一个小部分内可以使用的变量.

你可以拥有在整个程序运行期间都存在的变量, 或者只有在包含该变量的函数执行时才存在的变量. 你也可以使用函数调用为数据的存储显式地分配和释放内存.

在分析这 5 种存储类之前, 我们需要研究这些术语的意义: 作用域, 链接以及存储时期. 然后, 我们再介绍具体的存储类.

'12.1.1 作用域'

作用域描述了程序中可以访问一个标识符的一个或多个区域. 一个 C 变量的作用域可以是代码块作用哉, 函数原型作用域, 或者文件作用域. 到目前为止的程序实例中使用的都是代码作用域变量. 回忆一下, 一个代码块是包含在花括号和对应的结束括号之内的一段代码. 例如, 整个函数体是一个代码块. 一个函数内的任一复合语句也是一个代码块. 在代码块中定义的变量具有代码块作用域 (block scope), 从该变量被定义的地方到包含该定义的代码块的末尾该变量均可见. 另外, 函数的形式参量尽管在函数的开始花括号前进行定义, 同样也具有代码块作用域, 隶属于包含函数体的代码块. 所以迄今为止使用的局部变量, 包括函数形式参量, 都具有代码块作用域. 因此, 下面的代码中的变量 cleo 和 patrick 都有直到结束花括号的代码块作用域.

double blocky (double cleo)
{
double patrick = 0.0;
...
return patrick;
}

在一个内部代码块中声明的变量, 其作用域只局限于该代码块:

double blocky (double cleo)
{
double patrick - 0.0;
int i;
for (i = 0; i < 10; i++)
{
double q = cleo * i; // q 作用域的开始
...
patrick *= q;
}
... // q 作用型号的结束
return patrick;
}

在这个例子中, q 的作用域被在内部代码块内, 只有该代码块内的代码可以访问 q .

传统上, 具有代码块作用域的变量都必须在代码块的开始处进行声明. C99 放宽了这一规则, 允许在一个代码块中任何位置声明变量. 一个新的可能是变量声明可以出现在 for 循环的控制部分, 也就是说, 现在可以这样做:

for (int i = 0; i <10; i++)
printf ("A C99 feature : i = %d",i);

做为这一新功能的一部分, C99 反代码块的概念扩大到包括由 for 循环, while 循环, do while 循环或者 if语句所控制的代码 -- 即使这些代码没有用花括号括起来. 因此在前述 for 循环中, 变量 i 被认为是 for 循环代码块的一部分. 这样它的作用域就局限于这个 for 循环, 程序的执行离开该 for 循环后就不再能看到变量 i 了.

函数原型作用域 (function prototype scope) 适用于函数原型中使用的变量名, 如下所示:

int migty (int mouse, double large);

函数原型作用域从变量定义处一直到原型声明的末尾. 这意味着编译器在处理一个函数原型的参数时, 它所关心的只是该参数的类型; 你使用什么名字 (如果使用了的话) 通常是无关紧要的, 不需要使它们和在函数定义中使用的变量名保持一致. 名字起作用的一种情形是变长数组参量:

void use_a_VLA (int n, int m, ar[n][m]);

如果在方括号中使用了变量名, 则该变量名必须是在原型中已经声明了的.

一个在所有函数之外定义的变量具有文件作用域 (file scope). 具有文件作用域的变量从它定义处到包含该定义的文件结尾处都是可见的. 看看下面的例子:

#include <stdio.h>
int units = 0; // 具有文件作用域的变量
void critic (void);
int main (void)
{
...
}
void critic (void)
{
...
}

这里, 变量 units 具有文件作用域, 在 main(0 和 critic() 中都可以使用它. 因为它们可以在不止一个函数中使用, 文件作用域变量也被称为全局变量 (global variable).

另外还有一种被称为函数作用域 (function scope) 的作用域, 但它只适用于 goto 语句使用的标签. 函数作用域意味着一个特定函数中的 goto 标签对该函数中任何地方的代码都是可见的, 无论该标签出现有哪一个代码块中.

' 12.1.2 链接 '

接下来, 让我们看看链接. 一个 C 变量具有下列链接之一: 外部链接 (external linkage), 内部链接
(internal linkage), 或空链接 (no linkage). 具有代码作用域或者函数原型作用域的变量有空链接, 意味着它们由其定义所在的代码块或函数原型所私有的. 具有文件作用域的变量可能有内部或外部链接. 一个具有链接的变量可以在一个多文件程序的任何地方使用. 一个具有内部链接的变量可以在一个文件的任何地方使用.

那么怎样知道一个文件作用域变量具有内部链接不是外部链接? 你可以看看在外部定义中是否使用了存储类说明符 static :

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

和该文件属于同一程序的其他文件可以使用变量 giants. 变量 dodgers 是该文件私有的, 但是可以被该文件中的任一函数使用.

-------------------------------------------------------------------------------------

" 12.1.3 存储时期 "

一个 C 变量有以下两种存储时期之一: 静态存储时期 (static storage duration) 和自动存储时期 (automatic storage duration0. 如果一个变量具有静态存储时期, 它在程序执行期间将一直存在. 具有文件作用域的变量具有静态存储时期. 注意对于具有文件作用域的变量, 关键词 static 表明链接类型, 并非存储时期.
一个使用 static 声明了的文件作用域变量具有内部链接, 而所有的文件作用域变量, 无论它具有内部链接, 还是具有外部链接, 都具有静态存储时期.

具有代码块作用域的变量一般情况下具有自动存储时期. 在程序进入定义这些变量的代码块时, 将为这些变量分配内存; 当退出这个代码块时, 分配的内存将被释放. 该思想把自动变量使用的内存视为一个可以重复使用的工作区或者暂存内存. 例如, 在一个函数调用结束后, 它的变量所占用的内存可被用来存储下一个被调用函数的变量.

迄今为止我们使用的局部变量都属于自动类型. 例如, 在下列代码中, 变量 number 和 index 在每次开始调用函数 bore() 时生成, 在每次结束函数调用时消失:

void bore (int number)
{
int index;
for ( index = 0; index < number; index++)
puts ("They don't make them the way they used to \n");
return 0;
}

C 使用作用域, 链接和存储时期来定义 5 种存储类: 自动, 寄存器, 具有代码作用域的静态, 具有外部链接的静态, 以及具有内部链接的静态. 表 12.1 列出了这些组合. 现在已经介绍了作用域, 链接和存储时期, 我们可以详细地讨论这些存储类了.

表 12.1 5 种存储类
--------------------------------------------------------------------------------------------
存储类 时期 作用域 链接 声明方式
--------------------------------------------------------------------------------------------
自动 自动 代码块 空 代码块内
--------------------------------------------------------------------------------------------
寄存器 自动 代码块 空 代码内, 使用关键字 register
--------------------------------------------------------------------------------------------
具有外部链接的静态 静态 文件 外部 所有函数之外
--------------------------------------------------------------------------------------------
具有内部链接的静态 静态 文件 内部 所有函数之外, 使用关键字 static
---------------------------------------------------------------------------------------------
空链接的静态 静态 代码块 空 代码块内, 使用关键字 static
--------------------------------------------------------------------------------------------

" 12.1.4 自动变量 "

属于自动存储类的变量具有自动存储时期, 代码块作用域和空链接. 默认情况下, 在代码块或函数的头部定义的任意变量都属于自动存储类. 然而, 也可以如下面所示的那样显式地使用关键字 auto 使你的这个意图更清晰:

int main (void)
{
auto int plox;

例如, 为了表明有意覆盖一个外部函数定义时, 或者为了表明不能把变量改变为其他存储类这一点很重要时, 可以这样做. 关键字 auto 称为存储类说明符 (storage class specifier).

代码块作用域和空链接意味着只有变量定义所在的代码块才可以通过名字访问该变量 (当然, 可以用参数向其他函数传送该谈到的值和地址, 但那是以间接的方式知道的). 另一个函数可以使用具有同样名字的变量, 但那将是存储在不同内存位置中的一个独立变量.

回忆一下, 自动存储时期意味着程序进入包含变量声明的代码块, 变量开始存在. 当程序离开这个代码块时, 自动变量消失了. 它所占用的内存可用来做别的事情.

再来仔细看一下嵌套代码块. 只有定义变量的代码块及其内部的任何代码块可以访问这个变量:

int loop (int n)
{
int m; // m 的作用域
scanf ("%d", &m);
{
int i; // m 和 i 的作用域
for (i = m; i < n; i++)
puts ("i is local to a sub-block \n");
}
return m; // m 作用域, i 已经消失
}

在这段代码中, 变量 i 仅在内层花括号中是可见的. 如果试图在内层代码块之前或之后使用该变量, 将得到一个编译错误, 通常, 在设计程序时不使用这一特性. 然而有些时候, 如果其他地方用不到这个变量的话, 在子代码块中定义一个变量是有用的. 通过这种方式, 你可以在使用变量的位置附近说明变量的含义. 而且, 变量只会在需要它时才占用内存. 变量 n 和 m 在函数头部和外层代码块中定义, 在整个函数中可用, 并一直存在到函数终止.

如果在内层代码定义了一个具有和外层代码块变量同一名字的变量, 将发生什么? 那么在内层代码块定义的名字是内层代码块所使用的变量. 我们称之为内层定义覆盖 (hide)了外部定义, 但当运行离开内层代码块时, 外部变量重新恢复使用. 程序清单 12.1 对此进行了示例说明.

程序清单 12.1 hiding.c 程序
---------------------------------------------------------------------
/* hiding.c -- 代码块内的变量 */
#include <stdio.h>
int main ()
{
int x = 30; // 初始化 x
printf ("x in outer block: %d \n",x);
{
int x = 77; // 新的 x 覆盖第一个
printf ("x in inner block: %d \n",x);
}
printf ("x in outer block: %d \n",x);
while (x++ < 33) // 初始化
{
int x = 100; // 新的 x, 覆盖第一个 x
x++;
printf ("x in while loop: %d \n",x);
}
printf ("x in outer block: %d \n",x);
return 0;
}

输出如下:
x in outer block: 30
x in inner block: 77
x in outer block: 30
x in while loop: 101
x in while loop: 101
x in while loop: 101
x in outer block: 34

首先, 程序创建了一个变量 x 并为其赋值 30, 如第一个 printf() 语句所示. 接着定义了一个新的值为 77 的变量 x, 如第二个 printf()语句所示. 第三个 printf() 语句显示出是一个新的变量覆盖了初始的变量 x . 该语句位于第一个内层代码块, 显示出起始的 x 值, 表明起始的变量 x 既没有消失也不曾改变.

该程序最令人迷惑的部分也许是 while 循环. 这个 while 循环的判断使用了起始的 x :

while (x++ < 33)

然而, 在循环内部, 程序看到了第三个 x 变量, 即在 while 循环代码块内定义的一个变量. 因此, 当循环体中的代码使用 x++ 时, 是新的 x 被递增到了 101, 接着被显示. 每次循环结束以后, 新的 x 就消失了. 然后循环条件判断语句使用并递增起始的 x , 又进入循环代码块. 再次创建新的 x . 在本例中, 新的 x 创建和消亡了 3 次. 注意, 该循环必须在条件判断语句中递增 x , 因为若在循环体内递增 x 的话, 递增的将是另一个 x 而非判断所用的那个 x .

这个例子并不是要鼓励你写类似的代码, 而是举例说明在一个代码中定义变量时将会发生什么.

-------------------------------------------------------------------------------

一. 不带 {} 的代码块

先前曾提到 C99 有一个特性, 语句若为循环或者 if 语句的一部分, 即使没有使用 {}, 也认为是一个代码块. 更完整地说, 整个循环是该循环所在代码块的子代码块, 而循环体是整个循环代码块的子代码块. 与之类似, if 语句是一个代码块, 其相关子语句也是 if 语句的子代码块. 这一规则影响到你能够在何处定义变量以及变量的作用域. 程序清单 12.2 显示了在一个 for 循环中该特性是如何作用的.

程序清单 12.2 forc99.c 程序
------------------------------------------------------------------
// forc99.c -- C99 关于代码块的新规则
#include <stdio.h>
int main ()
{
int n = 10;

printf ("Initially, n = %d \n", n);
for (int n = 1; n < 3; n++)
printf ("loop 1: n = %d \n",n);
printf ("After loop 1, n = %d \n", n);
for (int n = 1; n < 3; n++)
{
printf ("loop 2 index n = %d \n", n);
int n = 30;
printf ("loop 2: n = %d \n",n);
n++;
}
printf ("After loop 2, n = %d \n",n);
return 0;
}

注:... 没办法 VC 有 DEV 都不支持这个特性

-------------------------------------------------------------------
PS : 对 C99 的支持

有些编译器可能不支持这些新的 C99 作用域规则. 其他的编译器可能提供一个激活这些规则的选项. 例如, 在编写本书的时候, gcc 默认地支持很多 C99 的特性, 但是需要使用 - std = c99 选项来激活这些规则

---------------------------------------------------------------

在第一个 for 循环的控制部分中声明的 n 到该循环末尾一直起作用, 覆盖了初始的 n .但在运行完该循环后, 初始的 n 恢复作用.

在第二个 for 循环中, n 声明为一个循环索引, 覆盖了初始的 n, 接着, 在循环体内声明的 n 覆盖了循环索引 n . 当程序执行完循环体后, 在循环体内的 n 消失 ,循环判断使用索引 n .整个循环终止时, 初始的 n 又恢复作用.

-----------------------------------------------------------------------

二. 自动变量的初始化

除非你显式地初始化自动变量, 否则它不会被自动初始化. 考虑下列声明:

int main (void)
{
int repid;
int tents = 5;

变量 tents 初始化为 5, 而变量 repid 的初值则是先前占有用分配给它的空间的任意值. 不要指望这个值是 0 .倘若一个非常量表达式中所用到的变量先前都定义过的话, 可将自动变量初始化为该表达式:

int main (void)
{
int ruth = 1;
int rance = 5 * ruth; // 使用先前定义过的变量

12.1.5 寄存器变量

通常, 变量存储在计算机内存中. 如果幸运, 寄存器变量可以被存储在 CPU 寄存器中, 或更一般地, 存储在速度最快的可用内存中, 从而可以比普通变量更快地被访问和操作. 因为寄存器变量多是存放在一个寄存器而非内存中, 所以无法获得寄存器变量的地址. 但在其他的许多方面, 寄存器变量与自动变量是一样的. 也就是说, 它们都有代码块作用域, 空链接以及自动存储时期. 通过使用存储类说明符 register 可以声明寄存器变量:

int main (void)
{
register int quick;

我们说 "如果幸运" 是因为声明一个寄存器变量仅是一个请求, 而非一条直接的命令. 编译器必须在你的请求与可用寄存器的个数或可用高速内存的数量之间做权衡, 所以你可能达不成了自己的愿望. 这种情况下, 变量成为一个普通的自动变量; 然而, 你依然不能对它使用地址运算符.

可以把一个形式参量请求为寄存器变量. 只需在函数头部使用 register 关键字:

void macho (register int n)

可以使用 register 声明的类型是有限的. 例如, 处理器可能没有足够大的寄存器来容纳 double 类型.

12.1.6 具有代码块作用域的静态变量

静态变量 (static variable) 这一名称听起来很矛盾, 像是一个不可变的变量. 实际上, "静态" 是指变量的位置固定不动. 具有文件作用域的变量自动 (也是必须的) 具有静态存储时期. 也可以创建具有代码块作用域, 兼具静态存储的局部变量. 这些变量和自动变量具有相同的作用域, 但当包含这些变量的函数完成工作时, 它们并不消失. 也就是说, 这些变量具有代码块作用域, 空链接, 却有静态存储时期. 从一次函数调用到下一次调用, 计算机都记录着它们的值. 这样的变量通过使用存储类说明符 static (这提供了静态存储时期) 在代码块内声明 (这提供了代码块作用域和空链接) 创建. 程序清单 12.3 中的例子说明了这一技术.

程序清单 12.3 loc_stat.c 程序
--------------------------------------------------------------
/* loc_stat.c --- 使用一个局部静态变量 */
#include <stdio.h>
void trystat (void);
int main (void)
{
int count;

for (count = 1; count <= 3; count++) {
printf ("Here comes iteration %d : \n",count);
trystat();
}
return 0;
}

void trystat (void)
{
int fade = 1;
static int stay = 1;

printf ("fade = %d and stay = %d \n", fade++, stay++);
}

注意, trystat() 在打印每个变量的值后递增变量. 运行程序将返回下列结果:

Here comes iteration 1 :
fade = 1 and stay = 1
Here comes iteration 2 :
fade = 1 and stay = 2
Here comes iteration 3 :
fade = 1 and stay = 3

静态变量 stay 记得它的值曾被加 1, 而变量 fade 每次都重新开始. 这表明了初始化的不同: 在每次调用 trystat() 时 fade 都被初始化, 而 stay 只在编译 trystat() 时被初始化一次. 如果不显式地对静态变量进行初始化, 它们将被初始化为 0 .

下面两个声明看起来很相似:

int fade = 1;
static int stay = 1;

然而, 第一个语句确实是 函数 trystat() 的一部分, 每次调用该函数时都会执行它. 它是个运行时的动作. 而第二个语句实际上并不是函数 trystat() 的一部分. 如果调试程序逐步运行该程序, 你会发现程序看起来跳过了那一步. 那是因为静态变量在外部程序调入内存时就已经就位了. 把这个语句放在 trystat() 函数中是为了告诉器只有函数 trrstat() 可以看到该变量. 它不是在运行时执行的语句.

对函数参量不能使用 static:

int wontwork (static int flu); /* 不允许 */

阅读一些老的 C 文献时, 你会发现该存储类被归为内部静态存储类. 然而, 这里的内部一词被用来表明在函数内部, 而不是内部链接 .

12.1.7 具有外部链接的静态变量

具有外部链接的静态变量具有文件作用域, 链接和静态存储时期. 这一类型有时被称为外部存储类 (external storage class), 这一类型的变量被称为外部变量 (external variable). 把变量的定义声明放在所有函数之外, 即创建了一个外部变量. 为了使程序更加清晰, 可以在使用外部变量的函数中通过使用 extern 关键字来再次声明它. 如果变量是在别的文件中定义的, 使用 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)
{
...
}

Errupt 的两次声明是个链接的例子, 因为它们都指向同一个变量. 外部变量具有外部链接, 稍后我们将再提到这一点.

请注意不必在 double Up 的可选声明中指明数组大小. 第一次声明已提供了这一信息. 因为外部变量具有文件作用域, 它们从被声明处到文件结尾都是可见的, 所以 main() 中的一组 extern 声明完全可以省略掉. 而它们出现在那里, 作用只不过是表示 main() 函数使用这些变量.

如果函数中的声明漏掉了 extern ,就会建立一个独立的自动变量. 也就是说, 如果在 main() 中用:

extern int Errupt;

替换

int Errupt;

将使编译器创建一个名为 Errupt 的自动变量. 它将是一个独立的局部变量, 而不同于初始的 Errupt. 在程序执行 main() 时该局部变量会引作用; 但在像 next() 这种同一文件内的其他函数中, 外部的 Errupt 将起作用. 简言之, 在程序执行代码块内语句时, 代码块作用域的变量覆盖了具有文件作用域的同名变量.

外部变量具有静态存储时期. 因此, 数组 Up 一直存在并保持其值, 不管程序是否在执行 main(), next() 还是其他函数.

下列 3 个例子展示了外部变量和自动变量的 4 种可能组合. 例 1 中有一个外部变量: Hocus. 它对 main() 和 magic() 都是可见的.

-------------------------------------------------------------
/* 例 1 */
int Hocus;
int magic();
int main (void)
{
extern int Hocus; /* 声明 Hocus 为外部变量 */
...
}
int magic()
{
extern int Hocus; /* 与上面的 Hocus 是同一变量 */
...
}
-------------------------------------------------------------

例 2 中有一个外部变量 Hocus, 对两个函数都是可见的. 这次, magic() 通过默认方式获外部变量.

--------------------------------------------------------------
/* 例 2 */
int Hocus;
int magic();
int main (void)
{
extern int Hocus; /* 声明 Hocus 为外部变量 */
...
}
int magic(0
{
... /* 末声明 Hocus, 但知道该变量 */
}

---------------------------------------------------------------

在例 3 中, 创建了 4 个独立的变量. main() 中的 Hocus 默认为自动变量, 而且是 main() 的局部变量. magic() 中的 Hocus 被显式地声明为自动变量, 只对 magic() 可见. 外部变量 Hocus 对 main() 或 magic() 不可见, 但对文件中其他不单独拥有局部 Hocus 的函数都可见. 最后, Pocus 是一个外部变量, 对 magic() 可见而对 main() 不可见, 因为 Pocus 的声明在 main() 之后.

-----------------------------------------------------------------
/* 例 3 */
int Hocus;
int magic();
int main (void)
{
int Hocus; // 声明 Hocus, 默认为自动变量
...
}
int pocus;
int magic()
{
auto int Hocus; // 把局部变量 Hoceus 显式地声明为自动变量
...
}

这些例子说明了外部变量的作用域: 从声明的位置开始到文件结尾为止. 它们也说明了外部变量的生存期. 外部变量 Hocus 和 Pocus 存在的时间与程序运行时间一样. 并且它们不局限于任一函数, 在一个特定函数返回时并不消失.

-------------------------------------------------------------------------------

一 外部变量初始化

和自动变量一样, 外部变量可以被显式地初始化. 不同于自动变量的是, 如果你不对外部变量进行初始化, 它们将自动被赋初值 0 . 这一原则也适用于外部定义的数组元素. 不同于自动变量, 只可以用常量表达式来初始化文件作用域变量:

int x = 10; /* 可以, 10 是常量 */
int y = 3 + 20; /* 可以, 一个常量表达式 */
size_t z = sizeof(int); /* 可以, 一个常量表达式 */
int x2 = 2 * x; /* 不可以, x 是一个变量 */

(只要类型不是一个变长数组, sizeof 表达式就被认为是常量表达式 )

-------------------------------------------------------------------------------

二. 外部变量的使用

我们来看一个包含有外部变量的简单例子. 特别地, 假设需要两个分别叫作 main() 和 critic() 的函数来访问变量 units. 可以如程序清单 12.4 所示, 在这两个函数之外的开始处声明变量 units.

程序清单 12.4 global.c 程序
------------------------------------------------------------
/* global.c -- 使用外部变量 */
#include <stdio.h>
int units = 0; /* 一个外部变量 */
void critic (void);
int main (void)
{
extern int units; /* 可选的二次声明 */

printf ("How many poubds to a firkin of butter ?\n");
scanf ("%d", &units);
while (units != 56)
critic();
printf ("You must have looked it up! \n");
return 0;
}

void critic (void)
{
/* 这里省略了可选的声明 */
printf ("No luck, chummy, Try again. \n");
scanf ("%d", &units);
}

下面是一个输出示例:

How many poubds to a firkin of butter ?
14
No luck, chummy, Try again.
56
You must have looked it up!

注意函数 critic() 是怎样读取 units 的第二个值的; 当 main() 结束 while 循环时, 也知道了新值. 因此, 两个函数 main() 和 critic() 都用标识符 units 来访问同一个变量. 在 C 的术语中, 称 units 具有 文件作用域, 外部链接以及静态存储时期.

通过在所有函数定义的外面 (外部) 定义变量 units, 它成为一个外部变量. 要使 units 对文件中随后的全部函数都可用, 只需像前面这样做即可.

来看一些细节. 首先, units 声明所在的位置使得它对后面的函数可用, 而不需采取任何其他操作. 这样, ctitics() 就可以使用变量 units.

与之类似地, 也不需要做任何事来允许 mian() 访问 units. 然而, main() 确实有如下声明:

extern int units;

在这个例子中, 声明主要是使程序的可读性更好. 存储类说明符 extern 告诉编译器在该函数中用到的 units 都是指同一个在函数外部 (甚至在文件之外) 定义的变量. 再次, main() 和 critic() 都使用了外部定义的 units.

-----------------------------------------------------------------------

三. 外部名字

C99 标准要求编译器识别局部标识符的前 63 个字符和外部标识符的前 31 个字符. 这修订了以前的要求: 识别局部标识符的前 31 个字符和外部标识符的前 6 个字符. 因为 C99 标准相对新一些, 很可能你还是依照旧规则工作. 对外部变量名字规定比对局部变量名字规定更严格, 是因为外部名字需要遵守局部环境的规则, 而该规则可能是有更多的限制的.

-----------------------------------------------------------------------

四 定义和声明

我们来更仔细地看一下变量定义与变量声明的区别. 考虑下面的例子;

int tern = 1; /* 定义 tern */
main()
{
external int tern; /* 使用在其他地方定义的 tern 变量 */

这里, tern 声明了两次. 第一次声明为变量留出了存储空间. 它构成了变量的定义. 第二次声明只是告诉编译器要使用先前定义的变量 tern, 因此不是一个定义. 第一次声明称为定义声明 (defining declaration), 第二次声明称为引用声明 (referencing declaration). 关键字 extern 表明该声明不是一个定义, 因为它指示编译器参考其他地方.

如果这样做:

extern int tern;
int main (void)
{

那么编译器假定 tern 的真正定义是在程序中其他某个地方, 也许是在另一个文件中. 这样的声明不会引起空间分配. 因此, 不要用关键字 extern 来进行外部定义; 只用它来引用一个已经存在的外部定义.

一个外部变量只可进行一次初始化, 而且一定是在变量被定义时进行. 下面的语句是错的:

extern char permis = 'Y'; /* 错误 */

因为关键字 extern 的存在标志着这是一个引用声明, 而非定义声明.

12.1.8 具有内部链接的静态变量

这种存储类的变量具有静态存储时期, 文件作用域以及内部链接. 通过使用存储类说明符 static 在所有的函数外部进行定义 (正如定义外部变量那样) 来创建一个这样的变量:

static int svil = 1; /* 具有内部链接的静态变量 */
int main (void)
{
以前称这类变量为外部静态 (external static) 变量, 但因为它们具有内部链接, 因此有点让人困惑. 很不幸, 没有新的简称来代替外部静态一词, 只能使用 "具有内部链接的静态变量" (static variable with internal linkage). 普通的外部变量可以被程序的任一文件中所包含的函数使用, 而具有内部链接的静态变量只可以被与它在同一个文件中的函数使用. 可以在函数中使用存储类说明符 extern 来再次声明任何具有文件作用域的. 这样的声明并不改变链接. 考虑如下代码;

int traveler = 1; /* 外部链接 */
static int stayhome = 1; /* 内部链接 */

int main ()
{
extern int traveler; /* 使用全局变量 traveler */
extern int stayhome; /* 使用全局变量 stayhome */

对这个文件来说 traveler 和 stayhome 都是全局的, 但只有 traveler 可以被其他文件中的代码使用. 使用 extern 的两个声明表明 main() 在使用两个全局变量, 但 stayhome 仍具有内部链接.

12.1.9 多文件

只有在使用一个由多文件构成的程序时, 内部链接和外部链接的区别才显得重要, 因此我们简要地谈一下这个问题.

复杂的 C 程序往往使用多个独立的代码文件. 有些时候, 这些文件可能需要共享一个外部变量. ANSI C 通过在一个文件中定义变量, 在其他文件中引用声明这个变量来实现共享. 也就是说, 除了一个声明 (定义声明) 外, 其他所有声明都必须使用关键字 extern, 并且只有在定义声明中才可以对该变量进行初始化.

注意: 除非在第二个文件中也声明了该变量 (通过使用 extern), 否则在一个文件中定义的外部变量不可以用于第二个文件. 一个外部变量声明本身只是使用一个变量可能对其他文件可用.

然而历史上, 许多编译器对这一问题遵循了不同的规则. 例如在许多 UNIX 系统中, 如果包含初始化的外部变量声明不超过一个的话, 允许在多个文件中来声明该变量而不使用 extern 关键字. 如果有一个包含初始化声明, 该声明就被当作变量的定义.

12.2 存储类说明符

你可能已经注意到关键字 static 和 extern 的意义随上下文而不同. C 语言中有 5 个作为存储类说明符的关键字, 它们是 auto, register, static, extern 以及 typedef . 关键字 typedef 与内存存储无关, 由于语法原因被归入此类. 特别地, 不可以在一个声明中使用一个以上存储类说明符, 这意味着不能将其他倾向于一存储类说明符作为 typedef 的一部分.

说明符 auto 表示一个变量具有自动存储时期. 该说明符只能用在具有代码块作用域的变量声明中, 而这样的变量已经拥有自动存储时期, 因此它主要用来明确指出意图, 使程序更易读.

说明符 register 也只能用于具有代码块作用域的变量. 它将一个变量归入寄存器存储类, 这相当于请求将该变量存储在一个寄存器内, 以更快地存取. 它的使用也使你不能获得变量的地址.

说明符 static 在用于具有代码块作用域的变量的声明时, 使该变量具有静态存储时期, 从而得以在程序运行期间 (即使在包含该变量的代码块并没有运行时) 存在并保留其值. 变量仍具有代码块作用域和空链接. static 用于具有文件作用域的变量的声明时, 表明该变量具有内部链接.

说明符 extern 表明你在声明一个已经在别处定义了的变量. 如果包含 extern 的声明具有文件作用域, 所指向变量必然具有外部链接. 如果包含 extern 的声明具有代码块作用域, 所指向的变量可能具有外部链接也可能具有内部链接, 这取决于该变量的定义声明.

-----------------------------------------------------------------------------------

PS: 总结: 存储类

自动变量具有代码块作用域, 空链接和自动存储时期. 它们是局部的, 为定义它们的代码块 (通常是一个函数) 所私有. 寄存器变量与自动变量具有相同的属性, 但编译器可能使用速度更快的内存或寄存器来存储它们. 无法获取一个寄存器变量的地址.

具有静态存储时期的变量可能具有外部链接, 内部链接或空链接. 当变量在文件的所有函数之外声明时, 它是一个具有文件作用域的外部变量, 具有外部链接和静态存储时期. 如果在这样的声明中再加上关键字 static, 将获得一个具有静态存储时期, 文件作用域和内部链接的变量. 如果在一个函数内使用关键字 static 声明变量, 变量将具有静态存储时期, 代码块作用域和空链接.

当程序执行到包含变量声明的代码块时, 给具有自动存储时期的变量分配内存, 并在代码块结束时释放这部分内存. 如果没有初始化, 这样的变量具有一个无效值. 在程序编译时给具有静态存储时期的变量分配内存, 并且在程序运行时一直保持. 如果没有初始化, 这样的变量被设置为 0. 具有代码块作用域的变量局部于包含变量声明的代码块.

具有文件作用域的变量对文件中在它声明之后的所有函数可见. 如果一个文件作用域变量具有外部链接, 则它可被程序中的其他文件使用. 如果一个文件作用域变量具有内部链接, 它只能在声明它的文件中使用.

下面给出了一个使用全部 5 种存储类的小程序. 它由两个文件 (程序清单 12.5 和程序清单 12.6 ) 组成, 因此你需要进行多文件编译 (请参见第 9 章 "函数", 或你的编译器指导手册). 程序的主要目的是使用全部 5 种存储类, 并非提供一个设计范例; 更好的设计将不需要文件作用域变量.

程序清单 12.5 parta.c 文件
---------------------------------------------------------------
/* parta.c -- 各种存储类 */
#include <stdio.h>
void repotr_count();
void accumulate (int k);
int count = 0; /* 文件作用域, 外部链接 */
int main (void)
{
int value; /* 自动变量 */
register int i; /* 寄存器变量 */

printf ("Enter a positive integer (0 to quit) :");
while (scanf ("%d", &value) == 1 && value > 0){
++count; /* 使用文件作用域变量 */
for ( i = value; i >= 0; i--)
accumulate(i);
printf ("Enter a positive integer (0 to quit): ");
}
report_count();
return 0;
}

void report_count()
{
printf (" Loop executed %d times \n",count);
}

--------------------------------------------------------------
程序清单 12.6 partb.c 文件
----------------------------------------------------------------
/* partb.c -- 程序的其余部分 */
#include <stdio.h>

extern int count; // 引用声明, 外部链接

static int total = 0; // 静态定义, 内部链接
void accumulate (int k); // 原型
void accumulate (int k) // k 具有代码块作用域, 空链接
{
static int sutotal = 0; // 静态 空链接

if ( k <= 0){
printf ("loop cycle : %d \n", count);
printf ("subtotal: %d ; total : %d \n", sutotal,total);
sutotal = 0;
}else {
sutotal += k;
total += k;
}
}

下面是一个运行示例:

Enter a positive integer (0 to quit) :5
loop cycle : 1
subtotal: 15 ; total : 15
Enter a positive integer (0 to quit): 10
loop cycle : 2
subtotal: 55 ; total : 70
Enter a positive integer (0 to quit): 2
loop cycle : 3
subtotal: 3 ; total : 73
Enter a positive integer (0 to quit): 0
Loop executed 3 times

12.3 存储类和函数

函数也具有存储类. 函数可能是外部的 (默认情况下) 或者静态的 (C99 增加了第三种可能性, 即在 第 16 章 "C预处理器和 C 库" 中将讨论内联函数). 外部函数可被其他文件中的函数调用, 而静态函数只可以在定义它的文件中使用. 例如, 考虑一个包含如下函数声明的文件:

double gamma(); // 默认为外部的
static double beta();
extern double delta();

函数 gamma() 和 delta() 可被程序的其他文件中的函数使用, 而 beta() 则不可以. 因为 beta() 被限定在一个文件内, 故可在其他文件中使用具有相同名称的不同函数. 使用 static 存储类的原因之一就是创建为一个特定模块所私有的函数, 从而避免可能的名字冲突.

通常使用关键字 extern 来声明在其他文件中定义的函数. 这一习惯做法主要是为了使程序更清晰, 因为除非函数声明使用了关键字 static, 否则认为它是 extern 的.

使用那种存储类

对 "使用那种存储类?" 这个问题的回答多半是 "自动的". 否则为什么要选择自动类型作为默认类型? 是的, 我们知道乍看起来外部存储很有诱惑力. 把变量都设成外部变量, 就不用为使用参数和指针在函数之间传递数据而费心了. 然而, 这存在一种不十分明显的缺陷. 你将不得不为函数 A() 违背你的意图, 偷偷修改了函数 B()所用的变量而焦急. 多年来, 无数程序员的经验给出了无可置疑的证据, 证明随意使用外部变量带来的这一不十分明显的危险远比它所带来的表面吸引力重要.

保护性程序设计中一个非常重要的规则就是 "需要知道" 原则, 尽可能保持每个函数的内部工作对该函数的私有性, 只共享那些需要共享的变量. 除了自动类型以外, 其他类型也是有用的, 并且可用. 但请在使用一个类型前, 问问自己是否必须那样做.
12.4 随机数函数和静态变量

现在你已经对不同的存储类有了一定的了解, 我们来看几个使用这些存储类的程序. 首先, 来看一个随机数函数, 该函数使用了一个具有内部链接的静态变量. ANSI C 程序提供了 rand() 函数来产生随机数. 有多种产生随机数的算法, ANSI C 标准允许 C 实现使用针对特定机器的最佳算法. 不过, ANSI C 也提供了一个可移植的标准算法, 可以在不同的系统中产生相同的随机数. 事实上, rand() 是一个 "伪随机数发生器", 这意味着可以预测数字的实际顺序 (计算机不具有自发性), 但这些数字在可能的取值范围内均匀地分布.

为了看清楚程序内部发生了什么, 我们使用可移植的 ANSI 版本程序, 而不是编译器内置的 rand() 函数. 这一方案始于一个称为 "种子" 的数字. 函数使用这个种子来产生一个新数, 而这个新数又称为新的种子. 接着, 这个新的种子被用来产生一个更新的种子, 依此类推. 这种方案要想行之有效, 随机数函数必须记下上次被调用时所使用的种子. 对, 这需要一个静态变量. 程序清单 12.7 中的程序是版本 0 (很快你将看到版本 1 ).

程序清单 12.7 rand0.c 函数文件
--------------------------------------------------------------------------------
// rand0.c -- 产生随机数
// 使用 ANSI C 的可移植算法
static unsigned long int next = 1; /* 种子 */
int rand0 (void)
{
/* 产生伪随机数的魔术般的公式 */
next = next * 1103515245 + 12345;
return (unsigned int) (next / 65536) % 32768;
}

在程序清单 12.7 静态变量 next 的初始值为 1, 在每次调用函数时它的值被一个魔术般的公式修改. 结果是一个在 0 到 32767 范围内的返回值. 注意 next 是静态, 具有内部链接的, 而不只是静态, 空链接的. 这是为了稍后在将本例扩展时, 便于 next 为同一文件中的两个函数共享.

我们用程序清单 12.8 所示的简单驱动程序来测试一下 rand0() 函数

----------------------------------------------
程序清单 12.8 r_drive0.c 驱动程序
/* r_drive0.c -- 测试 rand0() 函数 */
/* 与 rand0.c 一起编译 */
#include <stdio.h>
extern int rand0 (void);

int main (void)
{
int count;

for (count = 0; count < 5; count++)
printf ("%hd\n",rand0());

return 0;
}

现在又有一个机会来练习使用多文件. 程序清单 12.7 和 程序清单 12.8 分别使用一个文件. 关键字 extern 提醒你 rand0() 在一个单独的文件中定义. 输出如下:

16838
5758
10113
17515
31051

输出看起来是随机的. 但让我们再来运行一次, 这次结果如下:

16838
5758
10113
17515
31051

唔, 看起来很像啊, 这就是 "伪" 的特征了. 每次运行主程序时, 都从同一个种子值 1 开始. 可以通过引入允许重置种子的第二个函数 srand1() 来解决这个问题. 关键是使 next 成为一个具有内部链接的静态变量, 并只对 rand1() 和 srand1() 可见 ( C 程序库中与 srand1() 等效的函数被称为 srand() ). 把 srand1() 添加到包含 rand1(0 的文件中. 程序清单 12.9 给出了修改后的程序.

程序清单 12.9 s_and_r.c 程序
----------------------------------------------------------------------------
/* s_and_r.c -- 包含 rand1() 和 srand1() 的文件 */
/* 使用 ANSI C 的可移植算法 */
static unsigned long int next = 1; /* 种子*/

int rand1 (void)
{
/* 产生伪随机数的魔术般的公式 */
next = next * 1103515245 + 12345;
return (unsigned int ) (next / 65536) % 32768;
}

void srand1 (unsigned int seed)
{
next = seed;
}

注意 next 是一个具有内部链接的文件作用域变量. 这意味着它可以同时被 rand1() 和 srand1() 使用, 但不可以被其他文件中的函数使用. 使用程序清单 12.10 中的驱动程序来测试这些函数.

程序清单 12.10 r_drive1.c 程序
-----------------------------------------------------
/* r_drive1.c -- 测试函数 rand1() 和 sand1() */
/* 与 s_and_r.c 一起编译 */
#include <stdio.h>
extern void srand1 (unsigned int x);
extern int rand1 (void);

int main (void)
{
int count;
unsigned seed;

printf ("Please enter your choice for seed .\n");
while (scanf("%u", &seed) == 1){
srand1(seed); /* 重置种子 */
for (count = 0; count < 5; count++)
printf ("%hd\n",rand1());
printf ("psease enter next seed (q to quit): \n");
}
printf ("Done \n");
return 0;
}

又使用了两个. 运行一次程序.

Please enter your choice for sedd .
1
16838
5758
10113
17515
31051
Please enter next seed (q to quit) :
513
20067
23475
8955
20841
15324
Please enter next seed (q to quit) :
q
Dne

将 1 作为 seed 的值, 产生了与前面相同的结果. 现在来试试将 513 作为 seed 的值:

---------------------------------------------------------------------------

PS: 自动重置种子

如果你的 C 实现允许你访问系统时钟这样不断变化的量, 可以用它们的值 (可能需要截断) 来初始化种子值. 例如, ANSI C 有一个函数 time() 可以返回系统时间. 时间单位由系统决定, 但有用的一眯是返回值为数值类型, 并且随着时间变化. 其确切类型与系统有关, 名称为 time_t , 但你可以对它进行类型指派. 下面是基本思路:

#inlcude <time.h> /* 为 time() 函数提供 ANSI 原型 */
srand1 ((unsigned int) time(0)); /* 初始化种子 */

通常, time() 的参数是一个 time_t 类型对象的地址. 那种情形下, 时间值也存储在那个地址中. 然而, 你也可以传送空指针 (0) 作为参数. 此时, 时间仅仅通过返回值机制提供. 可以对 标准的 ANSI C 函数 srand() 和 rand() 使用同样的技术. 使用这些函数时, 要包含 stdlib.h 头文件. 实际上, 既然已经知道 sand1() 和 rand1() 如何使用一个具有内部链接的静态变量, 你同样也可以使用你的编译器提供的版本. 我们将在下个例子中这样做

12.5 掷骰子

我们准备模仿一种非常流行的随机性行为: 掷骰子. 掷骰子是普遍的形式是用两个 6 面的骰子, 但也有其他可能. 在一些奇特的冒险游戏中, 使用全部 5 种几何上可行的骰子: 4,6,8,12,和 20面. 聪明的古希腊人证明了仅有 5 种规则立方体的所有面的形状和大小都相同, 这些立方体成为各种骰子的基础. 骰子也可以做成其他面数的, 但将不会是所有面都相同, 因而它们各面朝上的几率也就不会相同.

计算机不受这些几何上的限制, 因而可以设计一种具有任意面数的电子骰子. 先从 6 面开始, 再进行扩展.

我们想得到从 1 到 6 之间的一个随机数. 然而, rand() 产生的是从 0 到 RAND_MAX 范围内的整数;RAND_MAX 在 stdlib.h 中定义, 它通常是 INT_MAX. 因此, 需要做一些调整. 下面是一种方法:

1. 把随机数对 6 取模, 将产生从 0 到 5 的整数.

2. 加 1, 新数将为从 1 到 6 范围内的整数.

3. 为了方便扩展, 将步骤 1 中的数字 6 用骰子面数来代替.

下面的代码实现了这些想法:

#include <stdlib.h> /* 为 rand() 函数提供原型 */
int rollem (int sides)
{
int roll;

roll = rand() % sides + 1;
return roll;
}

进一步, 我们想实现这样的功能: 它允许掷任意个骰子, 并且返回点数总和. 程序清单 12.11 实现了这样的功能.

程序清单 12.11 diceroll.c 文件
---------------------------------------------------------------------
/* diceroll.c -- 掷骰子的模拟程序 */
#include "diceroll.h"
#include <stdio.h>
#include <stdlib.h> /* 为 rand() 函数提供类库 */

int roll_count = 0; /* 外部链接 */

static int rollem (int sides) /* 这个文件的私有函数 */
{
int roll;

roll = rand() % sides + 1;
++roll_count; /* 计数函数调用 */
return roll;
}

int roll_n_dice (int dice, int sides)
{
int d;
int total = 0;
if (sides < 2)
{
printf ("Need at least 2 sides. \n");
return -2;
}
if (dice < 1)
{
printf ("Need at least 1 die \n");
return -1;
}
for (d = 0; d < dice; d++)
total += rollem (sides);
return total;
}

这个文件中加入了一些新东西. 首先, 它把 rollem() 变成由该文件私有的函数, 这个函数用于辅助 roll_n_dice(); 其次, 为了举例说明外部链接如何工作, 文件声明了一个外部变量 roll_count, 这个变量跟踪记录函数 rollem() 的调用次数. 例子本身有一点不妥, 但它显示了外部变量是如何工作的.

再次, 文件包含下面的语句:

#include "diceroll.h"

如果使用诸如 rand() 的标准库函数, 你需要在程序中包含标准头文件 (对 rand() 来说是 stdlib.h), 而不是声明函数, 因为头文件中已经包含了正确的声明. 我们将仿效这一做法, 提供一个头文件 diceroll.h 以供函数 roll_n_dice () 使用. 将文件名置于双引号而非尖括号中, 是为了指示编译器在本地寻找文件, 而不是到编译器存放标准头文件的标准位置去寻找文件. "在本地寻找" 的意义取决于具体的 C 实现. 一些常见解释是将头文件与源代码文件放在同一个目录或文件夹中, 或者与工程文件 (如果编译器使用它们) 放在同一个目录或文件夹中, 程序清单 12.12 显示了该头文件的内容.

程序清单 12.12 diceroll.h 文件
-------------------------------------------------------------
// diceroll.h

extern int roll_count;

int roll_n_dice (int dice, int sides);

这个头文件中包含函数原型声明和一个 extern 声明. 因为文件 diceroll.c 包含了这一头文件, 它也就实际上包含了 roll_count 的两个声明:

extern int roll_count; // 来自头文件
int roll_count = 0; // 来自源代码文件

这是可以的. 一个变量只可以有一个定义声明, 但使用 extern 的声明是一个引用声明, 这样的声明想用多少就可以用多少.

使用 roll_n_dice() 的程序也应该包含这一头文件. 这样做不仅仅提供 roll_n_dice() 原型, 还使得 roll_count 对程序可用. 程序清单 12.13 证明了这些.

程序清单 12.13 manydice.c 文件
----------------------------------------------------------------
/* manydice.c -- 多次掷骰子的模拟程序 */
/* 与 diceroll.c 一起编译 */
#include <stdio.h>
#include <stdlib.h> /* 为 srand() 函数提供原型 */
#include <time.h> /* 为 time() 函数提供原型 */
#include "diceroll.h" /* 为 roll_n_dice() 和 roll_count 函数提供原型 */

int main (void)
{
int dice, roll;
int sides;

srand ((unsigned int) time(0)); // 随机种子
printf ("Enter the number of sides per die, 0 to stop \n");
while (scanf ("%d",&sides) == 1 && sides > 0)
{
printf ("How many dice? \n");
scanf("%d", &dice);
roll = roll_n_dice(dice,sides);
printf ("You have rlled a %d using %d-sided dice .\n",roll,dice,sides);
printf ("How many sides? Enter 0 to stop \n");
}
printf ("The rollem() function waw called %d times \n",roll_count); /* 使用外部变量 */
printf (" GOOD FORTUNE TO YOU! \n");
return 0;
}

将程序清单 12.13 与包含程序清单 12.11 的文件一起编译. 为了简化问题, 把程序清单 12.11, 12.12 和 12.13 放在同一文件中或同一目录下, 运行最后得到的程序, 输出应该像下面这样:

Enter the number of sides per die, 0 to stop
6
How many dice?
2
You have rlled a 3 using 2 6-sided dice .
How many sides? Enter 0 to stop
6
How many dice?
2
You have rlled a 8 using 2 6-sided dice .
How many sides? Enter 0 to stop
6
How many dice?
2
You have rlled a 8 using 2 6-sided dice .
How many sides? Enter 0 to stop
0
The rollem() function waw called 6 times
GOOD FORTUNE TO YOU!

因为程序使用 srand() 来随机确定随机数种子, 所以大多数情况下, 即使有相同的输入也不可能得到相同的输出. 注意, manydice.c 中的 main() 确实可以访问 diceroll.c 中定义的变量 roll_count.

可通过多种方式使用 roll_n_dice(). 对于 sides 为 2 的情形, 程序模仿掷硬币, "面朝上" 为 2, "背朝上" 为 1 (反之亦然, 你可以随意选择). 你可以很容易地修改程序来像显示总体结果那样显示个别结果, 或者建一个掷双骰子赌博模拟器. 如果需要掷多次骰子, 像在一些角色扮演游戏中一样, 很容易修改程序来产生下列输出:

Enter the number of sides per die, 0 to stop
18
How many dice?
6 3
Here are 18 sets of 3 6-sided throw.
12 10 6 9 8 14 8 15 9 14 12 17 11 7 10
13 8 14

How many sides? Enter 0 to stop
q

rand1() 或 rand() (但不是 rollem() ) 的另一个用处是创建一个猜数程序: 计算机选数, 你来猜. 自己试着做一下.

12.6 分配内存: malloc() 和 free()

这 5 种存储类有一个共同之处: 在决定了使用哪一存储类之后, 就自动决定了作用域和存储时期. 你的选择服从预先的内存管理规则. 然而, 还有另一个选择给你更多灵活性. 这一选择就是使用库函数来分配和管理内存.

首先, 回顾一些有关内存分配的事实. 所有的程序都必须留出足够内存来存储它们使用的数据. 一些内存分配是自动完成的. 例如, 可以这样声明:

float x;
char place[] = "Dancing Oxen Creek";

于是, 系统将留出存储 float 或字符串的足够内存空间, 你也可以更明确的请求确切数量的内存:

int plates[100];

这一声明留出 100 个内存位置, 每位置可存储一个 int 值. 在所有这些情形中, 声明同时给出了内存的标识符, 因此你可以使用 x 或 place 来标识数据.

C 的功能还不止这些. 可以在程序运行时分配更多的内存. 主要工具是函数 malloc(), 它接受一个参数: 所需内存字节数. 然后 malloc() 找到可用内存中一个大小适合的块. 内存是匿名的; 也就是说, malloc()分配了内存, 但没有为它指定名字, 然而, 它却可以返回那块内存第一个字节的地址. 因此, 你可以把那个地址赋值给一个指针变量, 并使用该指针来访问那块内存. 因为 char 代表一个字节, 所以传统上曾将 malloc() 定义为指向 char 的指针类型.

然而, ANSI C 标准使用了一个新类型; 指向 void 的指针. 这一类型被用作 "通用指针". 函数 malloc() 可用来返回数组指针, 结构指针等等, 因此一般需要把返回值的类型指派为适当的类型. 在 ANSI C 中, 为了程序清晰应对指针进行类型指派, 但将 void 指针值赋值给其他类型的指针并不构成类型冲突. 如果 malloc() 找不到所需的空间, 它将空指针.

我们使用 malloc() 来创建一个数组. 可以在程序运行时使用 malloc() 请求一个存储块, 另外还需要一个指针来存放该块在内存中的益. 例如, 考虑如下代码:

double *ptd;
ptd = (double *) malloc(30 * sizeof (double));

这段代码请求 30 个 double 类型值的空间, 并且把 ptd 指向该空间的所在位置. 注意 ptd 是作为指向一个 double 类型值的指针声明的, 而不是指向 30 个 double 类型值的数据块的指针. 记住: 数组的名字是它第一个元素的地址. 因此, 如果你令 ptd 指向一个内存块的第一个元素, 就可以像使用数组名一样使用它. 也就是说, 可以使用表达式 ptd[0] 来访问内存块的第一个元素, ptd[1] 来访问第二个元素, 依此类推. 正如前面所学, 可以在指针符号中使用数组名, 也可以在数组符号中使用指针.

现在, 创建一个数组有三种方法:

1. 声明一个数组, 声明时用常量表达式指定数组维数, 然后可以用数组名访问数组元素.

2. 声明变长数组, 声明时用变量表达式指定数组维数, 然后用数组名来访问数组元素 (回忆一下, 这是 C99 的一
特性).

3. 声明一个指针, 调用 malloc(), 然后使用该指针来访问数组元素.

使用第二种或第三种方法可以做一些用普通的数组声明做不到的事: 创建一个动态数组 (dynamicarray), 即一个在程序运行时才分配内存并可在程序运行时选择大小的数组. 例如, 假定 n 是一个整数变量. 在 C99 之前, 不能这样做:

double item[n]; // 如果 n 是一个变量, c99 之前不允许这样做

然而, 即使在 c99 之前的编译器中, 也可以这样做:

ptd = (double *) malloc (n * sizeof(double)); // 可以

这行得通, 而且正如你将看到的那样, 这样做比使用一个变长数组更灵活.

一般地, 对应每个 malloc() 调用, 应该调用一次 free(). 函数 free() 的参数是先前 malloc() 返回的地址, 它释放先前分配的内存. 这样, 所分配内存的持续时间从调用 malloc() 分配内存开始, 到调用 free() 释放内存以供再使用为止. 设想 malloc() 和 free() 管理的一个内存池. 每次调用 malloc() 分配内存给程序使用, 每次调用 free() 将内存归还池中, 使内存可被再次使用. free() 的参数应是一指针, 指向由 malloc() 分配的内存块; 不能使用 free() 来释放通过其他方式 (例如声明一个数组) 分配的内存. 在头文件 stdlib.h 中有 malloc() 和 free() 的原型.

通过使用 malloc(), 程序可以在运行时决定需要多大的数组并创建它. 程序清单 12.14 举例证明了这一可能. 它把内存块地址赋给指针 ptd, 接着以使用数组名的方式使用 ptd. 程序还调用了 exit() 函数. 该函数的原型在 stdlib.h 中, 用来在内存分配失败时结束程序. 值 EXIT_FAILURE 也在这个头文件中定义. 标准库提供了两个保证能够在所有操作系统下工作的返回值: EXIT_SUCCESS (或者, 等同于 0)指示程序正常终止, EXIT_FAILURE 指示程序异常终止. 有些操作系统, 包括 UNIC, linux 和 Windows, 能够接受其他的整数值.

程序清单 12.14 dyn_arr.c 程序
----------------------------------------------------------------
// dyn_arr -- 为数组分配存储空间
#include <stdio.h>
#include <stdlib.h> // 为 malloc() 和 free()函数提供原型
int main (void)
{
double *ptd;
int max;
int number;
int i = 0;

puts ("What is the maximum number of type double entries?");
scanf ("%d", &max);
ptd = (double *) malloc (max * sizeof (double));
if (ptd == NULL)
{
puts ("Memory allocation failed, Goodbye");
exit (EXIT_FAILURE);
}

// ptd 现在指向有 max 个元素的数组
puts ("Enter the values (q to quit) :");
while (i < max && scanf("%lf", &ptd[i]) == 1)
++i;
printf ("Here are your %d entries: \n", number = i);
for ( i = 0; i < number; i++)
{
printf ("%7.2f", ptd[i]);
if ( i % 7 == 6)
putchar ('\n');
}
if ( i % 7 != 0)
putchar ('\n');
puts ("Done");
free(ptd);

return 0;
}

下面是一个运行示例. 该例中输入了 6 个数, 但程序只处理了 5 个, 因为我们将数组大小限定为 5.

What is the maximum number of type double entries?
5
Enter the values (q to quit) :
20 30 35 25 40 80
Here are your 5 enteries:
20.00 30.00 35.00 25.00 40.00
Done

来看一下代码. 程序通过下列几行获取所需的数组大小:

puts ("What is the maximum numeber of type double entries ?");
scanf ("%d", &max);

接着, 下面的行分配对于存放所请求数目的项来说足够大的内存, 并将该内存块的地址赋给指针 ptd:

ptd = (double *) malloc (max * sizeof (double));

在 C 中, 类型指派 (double *) 是可选的, 而在 C++ 中必须有, 因此使用类型指派将使把 C 程序移植到 C++ 更容易.

malloc() 可能无法获得所需数量的内存. 在那种情形下, 函数返回空指针, 程序终止.

if (ptd == NULL)
{
puts ("Memory allocation failed. Goodbye .");
exit (EXIT_FAILURE);
}

如果成功地分配了内存, 程序将把 ptd 视为一个具有 max 个元素的数组的名字.

注意在程序末尾附近的函数 free(). 它释放 malloc() 分配的内存. 函数 free() 只释放它的参数所指向的内存块. 在这个特定例子中, 使用 free() 不是必须的, 因为在程序终止后所有已分配的内存都将被自动释放. 然而在一个更复杂的程序中, 能够释放并再利用内存是重要的.

使用动态数组将获得什么? 主要是获得了程序灵活性. 假定知道一个程序在大多数情况下需要的数组元素不超过 100 个; 而在某些情况下, 却需要 10000 个元素. 在声明数组时, 不得不考虑到最坏情形并声明一个具有 10000 个元素的数组. 在多数情况下, 程序将浪费内存. 如果有一次需要 10001 个元素, 程序就会出错. 你可以使用动态数组来使程序适应不同的情形.

12.6.1 free() 的重要性

在编译程序时, 静态变量的数量是固定的; 在程序运行时也不改变. 自动变量使用的内存数量在程序执行时自动增加或者减少. 但被分配的内存所使用的内存数量只会增加, 除非你记得使用 free(). 例如, 假定有一个如下代码勾勒出的函数, 它创建一个数组的临时拷贝:

...
int main ()
{
double glad[2000];
int i
...
for ( i = 0; i < 1000; i++)
gobble (glad,2000);
...
}

void gobble (double ar[], int n)
{
double * temp = (double *) malloc (n * sizeof (double));
...
/* free (temp); 忘记使用 free() */
}

第一次调用 gobble()时, 它创建了指针 temp, 并使用 malloc() 为之分配 16000 字节的内存 (设 double 是 8 个字节). 假定我们如暗示的那样没有使用 free(). 当函数终止时, 指针 temp 作为一个自动变量消失了. 但它所指向的 16000 个字节的内存仍旧存在. 我们无法访问这些内存, 因为地址不见了. 由于没有调用 free(), 不可以再使用它了.

第二次调用 gobble(), 它又创建了一个 temp, 再次使用 malloc() 分配 16000 个字节的内存. 第一个 16000 字节的块已不可用, 因此 malloc() 不得不再找一个 1600 万字节的内存从内存池中移走. 事实上, 在到达这一步前, 程序很可能已经内存溢出了. 这类问题被称为内存泄漏 (memory leak), 可以通过在函数末尾处调用 free() 防止该问题出现.

12.6.2 函数 calloc()

内存分配还可以使用 calloc(). 典型的应用如下:

long *newmem;
newmem = (long *) calloc (100, sizeof (long));

与 malloc() 类似, calloc() 在 ANSI 以前的版本中返回一个 char 指针, 在 ANSI 中返回一个 void 指针. 如果要存储不同类型, 应该使用类型指派运算符. 这个新函数接受两个参数, 都应是无符号的整数 (在 ANSI 中是 size_t 类型). 第一个参数是所需内存单元的数量, 第二个参数是每单元以字节计的大小. 在这里, long 使用 4 个字节, 因此这一指令建立了 100 个 4 字节单元, 总共使用 400 个字节来存储.

使用 sizeof(long) 而不是 4 使代码更易移植. 它可在其他系统中运行, 这些系统中 long 不是 4 字节而是别的大小.

函数 calloc() 还有一个特性: 它将块中的全部位都置为 0 (然而要注意, 在某些硬件系统中, 浮点值 0 不是用全部位为 0 来表示的).

函数 free() 也可以用来释放由 calloc() 分配的内存.

动态内存分配是很多高级编程技巧的关键. 在 17 章 "高级数据表示" 中我们将研究一些. 你自己的 C 库可能提供了其他内存管理函数, 有些可移植, 有些不可以. 你可能应该抽时间看一下.

12.6.3 动态内存分配与变长数组

变长数组 ( Variable-Length Array, VLA) 与 malloc() 在功能上有些一致. 例如, 它们都可以用创建一个大小在运行时决定的数组:

int vlamal()
{
int n;
int *pi;
scanf ("%d", &n);
pi = (int *) malloc (n * sizeof (int));
int ar[n]; // 变长数组
pi[2] = ar[2] = -5;
...
}

一个区别在于 VLA 是自动存储的. 自动存储的结果之一就是 VLA 所用内存空间在运行完定义部分之后会自动释放. 在本例中, 就是函数 vlamal() 终止的时候. 因此不必使用 free(). 另一方面, 使用由 malloc() 创建的数组不必局限在一个函数中. 例如, 函数可以创建一个数组并返回指针, 供调用该函数的函数访问. 接着, 后者可以在它结束时调用 free(). free() 可以使用不同于 malloc() 指针的指针变量; 必须一致的是指针中存储的地址.

VLA 对多维数组来说更方便. 你可以使用 malloc() 来定义一个二维数组, 但语法很麻烦. 如果编译器不支持 VLA 特性, 必须回写一维的大小, 正如下面的函数调用:

int n = 5;
int m = 6;

int ar2[n][m]; /* nxm 的变长数组 */
int (* p2)[6]; /* 在 c99 之前可以使用 */

int (* p3)[m]; /* 要求变长数组支持 */

p2 = (int (*)[6]) malloc (n * 6 * sizeof(int)); // nx6 数组
p3 = (int (*)[m]) malloc (n * m * sizeof(int)); // nxm 数组

// 上面的表达式也要求变长数组支持
ar2[1][2] = p2[1][2] = 12;

有必要查看一下指针声明. 函数 malloc() 返回一个指针, 因此 p2 必须是适当类型的指针. 下面的声明:

int (* p2) [6]; // 在 C99 之前可以使用

表明 p2 指向一个包含 6 个 int 值的数组. 这意味着 p2[i] 被解释为一个由 6 个整数构成的元素, p2[i][j] 将是一个 int 值.

第二个指针声明使用变量来指定 p3 所指数组的大小. 这意味着 p3 将被看作一个指向 VLA 的指针, 这正是代码不能在 C90 标准中运行的原因.

12.6.4 存储类与动态内存分配

你可能正在为存储类和动态内存分配之间的联系感到疑惑. 我们来看一个理想模型. 可以认为程序将它的可用内存分成了三个独立的部分: 一个是具有外部链接的, 具有内部链接的以及具有空链接的静态变量的; 一个是自动变量的; 另一个是动态分配的内存的.

在编译时就已经知道了静态存储时期存储类变量所需的内存数量, 存储在这一部分的数据在整个程序运行期间都可用. 这一类型的每个变量在程序开始时就已存在, 到程序结束时终止.

然而, 一个自动变量在程序进入包含该变量定义的代码块时产生, 在退出这一代码块时终止. 因此, 伴随着程序对函数的调用和终止, 自动变量使用的内存数量也在增加和减少. 典型地, 将这一部分内存处理为一个堆栈. 这意味在内存中, 新变量使用的内存数量也在增加和减少. 典型地, 将这一部分内存处理为一个堆栈. 这意味着在内存中, 新变量在创建时按顺序加入, 在消亡时按相反顺序移除.

动态分配的内存在调用 malloc() 或相关函数时产生, 在调用 free() 时释放. 由程序员而不是一系列固定的规则控制内存持续时间, 因此内存块可在一个函数中创建, 而在另一个函数中释放. 由于这点, 动态内存分配所用的内存部分可能变成碎片状, 也就是说, 在活动的内存块之间散布着末使用的字节片. 不管怎样, 使用动态内存往往导致进程比使用堆栈内存慢.

12.7 ANSI C 的类型限定词

你已经知道一个变量是以它的类型和存储类表征的. C90 增加了两个属性: 不变性和易变性. 这些属性是通过关键字 const 和 volatile 声明的, 这样就创建了受限类型 (qualified type). C99 标准添加了第三个限定词 restrict, 用以方便编译器优化.

C99 授予类型限定词一个新属性: 它们现在是幂等的 (idempotent)! 这听起来像一个强大的功能, 其实只意味着可以在一个声明中不止一次地使用同一限定词, 多余的将被忽略掉:

const const const int n = 6; /* 相当于 const int n = 6 */

例如, 这使下列序列可以被接受:

typedef const int zip;
const zip q = 8;

-------------------------------------------------------------------------------------

12.7.1 类型限定词 const

第 4 章 "字符串和格式化输入/输出" 和第 10 章 "数组和指针" 已经介绍过 const. 回顾一下, 如果变量声明中带有关键字 const , 则不能通过赋值, 增量或减量运算来修改该变量的值. 在与 ANSI 兼容的编译器中, 下面的代码将产生一个错误信息:

const int nochange; /* 把 nochange 限定为常量 */
nochange = 12; /* 不允许 */

然而, 可以初始化一个 const 变量. 因此, 下面的代码是对的:

const int nochange = 12; /* 可以 */

上面的声明使 nochange 成为一个只读变量. 在初始化以后, 不可以再改变它. 例如, 可以用关键字 const 创建一组程序不可以改变的数据:

例如, 可以用关键字 const 创建一组不可以改变的数据:

const int days1[12] = {31,28,31,30,31,30,31,31,30,31,30,31};

一. 在指针和参量声明中使用 const

在声明一个简单变量和数组时使用关键字 const 很简单. 指针则要复杂一些, 因为不得不把让指针本身成为 const 与让指针指向的值成为 const 区分开来. 下面的声明表明 pf 指向的值必须是不变的:

const float *pf; /* pf 指向一个常量浮点数值 */

但 pf 本身的值可以改变. 例如, 它可以指向另一个 const 值. 相反, 下面的声明表明指针 pt 本身的值不可以改变:

float *const pt; /* pt 是一个常量指针 */

它必须总是指向同一个地址, 但所指向的值可以改变. 最后, 下面的声明:

const float * const ptr;

意味着 ptr 必须总是指向同一个位置, 并且它所指位置存储的值也不能改变.

还有第三种放置 const 关键字的方法:

float const * prc; /* 等同于: const float * pfc; */

正如注释所表示的那样, 把 const 放在类型名的后边和 * 的前边, 意味着指针不能够用来改变它所指向的值. 总而言之, 一个位于 * 左边任意位置的 const 使得数据成为常量, 而一个位于 * 右边的 const 使得指针自身成为常量.

这个新关键字的一个常见用法是声明作为函数形式参量的指针. 例如, 假定一个名为 display() 的函数显示一个数组的内容. 为了使用它, 你可能会把数组名作为实际参数传送, 但数组名是一个地址, 这样做将允许函数改变调用函数中的数据. 下面的原型防止了这样的情况发生:

void display (const int array[], int limit);

在函数原型和函数头部, 参量声明 const int array[] 与 const int * array 相同, 因此该表明 array 指向的数据是不可变的.

ANSI C 库遵循这一惯例. 如果指针只是用来让函数访问值, 将把它声明为 const 受限指针. 如果指针被用来改变调用函数中的数据, 则不使用关键字 const. 例如, ANSI C 中 strcat() 声明如下:

char *strcat (char *, const char *);

回忆一下, 函数 strcat() 在第一个字符串的末尾处添加第二个字符串的一份拷贝. 这改变了第一字符串, 但不改变第二个字符. 该声明也体现了这一点.

------------------------------------------------------------------------------------

二. 对全局数据使用 const

回忆一下, 使用全局变量被认为是一个冒险的方法, 因为它暴露了数据, 使程序的任何部分都可以错误地修改数据. 如果数据是 const 的, 这种危险就不存在了, 因此对全局数据使用 const 限定词是很合理的. 可以有 const 变量, const 数组和 const 结构 (结构是将在第 14 章中讨论的复合数据类型).

然而, 在文件之间共享 const 数据时要小心. 可以使用两个策略. 首先是遵循外部变量的惯用规则: 在一个文件中进行定义声明, 在其他文件中进行引用声明 (使得关键字 extern ):

/* file1.c -- 定义一些全局常量 */
const double PI = 3.14159;
const char * MONTHS[12] =
{"January", "February","March","April","May","June","July",
"August","September","October","November","December");

/* file2.c -- 使用在其他文件中定义的全局常量 */
extern const double PI;
extern const * MONTHS[];

其次是将常量放在一个 include 文件中. 这时还必须使用静态外部存储类:

/* constant.h - 定义一些全局常量 */
static const double PI = 3.14159;
static const char * MONTHS[12] =
{"January", "February","March","April","May","June","July",
"August","September","October","November","December");

/* file1.c -- 使用在其他文件中定义的全局常量 */
#include "constant.h"

/* file2.c -- 使用在其他文件中定义的全局常量 */
#include "constant.h"

如果不使用关键字 static, 在文件 file1.c 和 file2.c 中包含 constant.h 将导致每个文件都有同一标识符的定义声明, ANSI 标准不支持这样做 (然而, 一些编译的确支持这样做). 通过使每个标识符成为静态外部的, 实际上给了每个文件一个独立的数据拷贝. 如果文件想使用该数据来与另一个文件通话, 这样做就不行了, 因为个文件都只能看见它自己的拷贝. 然而, 由于数据是不变的 (通过使用关键字 const) 和 相同的 (通过使两个文件都包含同样的头文件), 这就不是问题了.

使用头文件的好处是不必惦记着在一个中进行定义声明, 在下一个文件中进行引用声明; 全部文件都包含同一个头文件. 缺点在于复制了数据. 在前述的例子中, 这不构成一个真正的问题; 但如果常量数据包含着巨大的数组, 它可能就是一个问题了.

-----------------------------------------------------------------------------------

12.7.2 类型限定词 volatile

限定词 volatile 告诉编译器该变量除了可被程序改变以外还可被其他代理改变. 典型地, 它被用于硬件地址和与其他并运行的程序共享的数据. 例如, 一个地址中可能保存着当前的时钟时间. 不管程序做些什么, 该地址的值都会随着时间而改变. 另一种情况是一个地址被用来接收来自其他计算机的信息.

语法同 const:

volatile int loc1; /* loc1 是一个易变的位置 */
volatile int * ploc; /* ploc 指向一个易变的位置 */

这些语句声明 loc1 是一个 volatile 值, 并且 ploc 指向一个 volatile 值.

你可能以为 volatile 是一个有趣的概念, 但你也可能奇怪为什么 ANSI 觉得有必要把 volatile 作为一个关键字, 原因是它可以方便编译器优化. 例如, 假定有如下代码:

vall = x;
/* 一些不使用 x 的代码 x */
val2 = x;

一个聪明的 (优化的) 编译器可能注意到你两次使用了 x, 而没有改变它的值. 它将把 x 临时存储在一个寄存器中, 接着, 当 val2 需要 x 时, 可以通过从寄存器而非初始的内存位置中读取该值以节省时间. 这个过程被称为缓存 (caching). 通常, 缓存是一个好的优化方式, 但如果在两个语句间其他代理改变了 x 的话就不是这样了. 如果没有规定 volatile 关键字, 编译器将无从得知这种改变是否可能发生. 因此, 为了安全起见, 编译器不使用缓存. 那是在 ANSI 以前的情形. 然而现在, 如果在声明中没有使用关键字 volatile, 编译器就可以假定一个值在使用过程中没有被修改, 它就可以试着优化代码.

一个值可以同时是 const 和 volatile, 例如, 硬件时钟一般设定为不能由程序改变, 这一点使它成为 const; 但它被程序以外的代理改变, 这使它成为 volatile 的. 只需在声明中同时使用这两个限定词, 如下所示; 顺序并不重要:

volatile const int loc;
const volatile int * ploc;

---------------------------------------------------------------------------------------

12.7.3 类型限定词 restrict

关键字 restrict 通过允许编译器优化某几种代码增强了计算支持. 它只可用于指针, 并表明指针是访问一个数据对象的唯一且初始的方式. 为了清楚为何这样做有用, 我们需要看一些例子. 考虑下面的例子:

int ar[10];
int * restrict restar = (int *) malloc (10 * sizeof (int);
int * par = ar;

这里, 指针 restar 是访问由 malloc() 分配的内存的唯一且初始的方式. 因此, 它可以由关键 restrict 限定. 然而, par 指针既不是初始的, 也不是访问数组 ar 中数据的唯一的方式, 因此不可以把它限定为 restrict.

现在考虑下面这个更加复杂的例子, 其中 n 是一个 int:

for (n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;

知道了 restar 是访问它所指向数据块的唯一初始方式, 编译器就可以用具有同样效果的一条语句来代替包含 restar 的两个语句:

restar[n] += 8; /* 可以进行替换 */

然而, 将两个包含 par 的语句精简为一个语句将导致计算错误:

par[n] += 8; /* 给出错误的结果 */

出现错误结果的原因是循环在 par 两次访问同一个数据之间, 使用 ar 改变了该数据的值.

没有关键字 restrict , 编译器将不得不设想比较糟的那种情形, 也就是在两次使用指针之间, 其他标识符可能改变了数据的值. 使用了关键字 restrict 以后, 编译器可以放心地寻找计算的捷径.

可以将关键字 restrict 作为指针型函数参量的限定词使用. 这意味着编译器可以假定在函数体内没有其他标识符修改指针指向的数据, 因而是可以试着优化代码, 反之则不然. 例如, C 库中有两个函数可以从一个位置把字节复制到另一个位置. 在 C99 标准下, 它们的原型如下:

void * memcpy (void * restrict s1, const void * restrict s2, size_t n);
void * memcpy (void * s1, const void * s2, size_t n);

每一个函数都从位置 s2 把 n 个字节复制到位置 s1. 函数 memcpy() 允许重叠, 它不得不在复制数据时更小心, 以防在使用数据前就覆盖了数据.

关键字 restrict 有两个读者. 一个是编译器, 它告诉编译器可以*地做一些有关优化的假定. 另一个读者是用户, 它告诉用户仅使用满足 restrict 要求的参数. 一般, 编译器无法检查你是否遵循了这一限制, 如果你蔑视它也就是在让自己冒险.

----------------------------------------------------------------------------

12.7.4 旧关键字的新位置

C99 允许将类型限定词和存储类限定词 static 放在函数原型头部的形式参量所属的方括号内. 对于类型限定词的情形, 这样做为已有功能提供了一个可选语法. 例如, 下面是一个使用旧语法的声明:

void ofmouth (int * const a1, int * restrict a2, int n); // 以前的

它表明 a1 是一个指向 int 的 const 指针. 回忆一下, 这意味着该指针是不变的, 而不是它所指向的数据不变. 还表明 a2 是一个受限指针, 如上一节所述. 等价的新语法如下:

void ofmouth (int a1[const], int a2 [restrict]. int n); // C99 允许

static 的情形是不同的, 因为它引发了一些新的问题. 例如考虑如下原型:

double stick (double ar[static 20]);

使用 static 表明在函数调用中, 实际参数将是一个指向数组首元素的指针, 该数组至少具有 20 个元素. 这样做的目的是允许编译器使用这个信息来优化函数的代码.

与 restrict 相同, 关键字 static 有两个读者. 一个是编译器, 它告诉编译器可以*地做一些有关优化的假定. 另一个是用户, 它告诉用户仅使用满足 static 要求的参数.

12.8 关键概念

C 提供了一些管理内存的模型. 你应该熟悉这些不同的选项. 还需要培养什么时候选用什么类型的判断力. 大多数情况下, 自动变量是最佳的选择. 如果决定使用另一个类型, 应该有一个充足的理由. 通常, 用自动变量, 函数参量和返回值在函数间传递数据比使用全局变量更好一些. 另一方面, 全局变量对保持不变的数据非常有用.

应该尽力理解静态内存, 自动内存和分配内存的特性. 具体地, 要知道所需静态内存的数量在编译时就决定了, 静态数据在程序载入内存时就被载入了内存. 在程序运行时为自动变量分配和释放内存, 因此在程序运行时, 自动变量使用的内存数量会不断变化. 可以把自动内存认为是一个可重写的工作区. 分配的内存也会增加和减少, 但这个过程是由函数调用控制, 而不是自动发生的.

12.9 总结

用于存储程序数据的内存可用存储时期, 作用域和链接来表征. 存储时期可以是静态的, 自动的或者分配的. 如果是静态的, 内存在程序开始执行时被分配, 并在程序运行时一直存在. 如果是自动的, 变量所用内存在程序执行到该变量定义所在代码块时开始分配, 在代码块时释放. 如果是分配的内存, 内存通过调用 malloc() (或其他相关函数) 分配, 通过调用函数 free() 释放.

作用域决定了哪一部分程序可以访问某个数据. 在所有函数之外定义的变量具有文件作用域, 并对该变量声明之后定义的全部函数可见. 在代码块内定义或者作为函数参量定义的变量具有代码块作用域, 并只在该代码块及其子代码块中可见.

链接描述了程序的某个单元定义的变量可被链接到其他哪些地方. 具有代码块作用域的变量作为局部变量, 具有空链接. 具有文件作用域的变量可有内部链接或外部链接. 内部链接意味着变量只可在包含变量定义的文件内部使用. 外部链接意味着变量也可在其他文件中使用.

下面是 C 的 5 种存储类:

1. 自动 --- 在一个代码块内 (或在一个函数头部作为参量) 声明挤爆一, 无论有没有存储类修饰符 auto , 都
属于自动存储类. 该类具有自动存储时期, 代码块作用域和空链接. 如末经初始化, 它的值是不定的.

2. 寄存器 --- 在一个代码块内 (或在一个函数头部作为参量) 使用存储类修饰符 register 声明的变量属于寄
存器存储类. 该类具有自动存储时期, 代码块作用域和空链接, 并且你无法获得其地址. 把一个变量声明为寄
存器变量可以指示编译器提供可用的最快访问. 如末经初始化, 它的值是不定的.

3. 静态 空链接 --- 在一个代码块内使用存储类修饰符 static 声明的变量属于静态空链接存储类. 该类具有静
态存储时期, 文件作用域和空链接, 仅在编译时初始化一次. 如末明确初始化, 它的字节都被设定为 0 .

4. 静态 外部链接 --- 在所有函数外部定义, 末使用存储类修饰符 static 的变量属于静态, 外部链接存储类,
该类具有静态存储时期, 文件作用域和外部链接, 仅在编译时初始化一次. 如末明确初始化, 它的字节都被
设定为 0 .

5. 静态, 内部链接 --- 在所有函数外部定义, 使用存储类修饰符 static 的变量属于静态, 内部链接存储类,
该类具有静态存储时期, 文件作用域和内部链接, 仅在编译时初始化一次, 如末明确初始化, 它的字节都
被设定为 0.

分配内存是使用函数 malloc() (或相关的函数) 提供的内存, 该函数返回一个指向具有所请求字节数的内存块的指针. 将这一内存块的地址作为参数来调用函数 free(), 可以使该内存块重新可用.

类型限定词说明符有 const, volatile 和 restrict . 说明符 const 将数据限定为不变的. 在使用指针时,
const 可以表明指针本身不变或指针指向的数据不变, 这取决于 const 在声明中的位置. 说明符 volatile 表明数据除了可被程序修改外还可通过其他方式修改, 其目的是警示编译器在优化时不要做出相反的假设. 说明符 restrict 也是为了优化而设置. 由 restrict 限定的指针被认为是提供了对其所指向的数据块的唯一访问途径.

12.10 复习题

1. 哪一存储类生成的变量对于包含他们的函数来说是局部变量?

答: 自动存储类, 寄存器存储类 和 静态空链接存储类

----------------------------------------------------------

2. 哪一存储类的变量在包含它们的程序运行时期内一直存在?

答: 静态空链接存储类, 静态内部链接存储类 和 静态外部链接存储类

--------------------------------------------------------------

3. 哪一存储类的变量可以在多个文件中使用? 哪一存储类的变量只限于在一个文件中使用?

答: 静态外部链接存储类可以在多个文件中使用, 静态内部链接存储类只限于在一个文件中使用

-----------------------------------------------------------------

4. 代码块作用域变量具有哪种链接?

答: 空链接

--------------------------------------------------------------------

5. 关键字 extern 的用处是什么?

答: 在声明中使用关键字 extern 表明一个变量或函数已经在其他地方被定义了.

----------------------------------------------------------------------

6. 考虑如下代码段:

int * p1 = (int *) malloc (100 * sizeof (int));

考虑到最终的结果, 下面语句有何不同?

int * p1 = (int *) calloc (100, sizeof (int));

答: 都分配一个具有 100 个 int 值的数组. 使用 calloc() 的语句还把每个元素设置为 0 .

-----------------------------------------------------------------------

7. 下列每个变量对哪些函数是可见的? 程序有什么错误吗?

答:

/* 文件1 */
int daisy;
int main (void)
{
int lily;
...;
}
int petal()
{
extern int daisy, lily;
...
}

/* 文件2 */
extern int daisy;
static int lily;
int rose;
int stem()
{
int roes;
...;
}
void root ()
{
...
}

答: daisy 对 main() 是默认可见的, 对 petal(), stem() 和 root() 是通过 extern 声明可见的. 文件 2 中的声明 extern int daisy; 使得 daisy 对该文件中的所有函数可见. 第一个 lily 是 main() 的局部变量. petal() 中对 lily 的引用是错误的, 因为两个文件中都没有 lily 的外部声明. 有一个外部的静态 lily, 但是它只对第二个文件中的函数可见. 第一个外部 rose 对 root() 可见, 但是 strm() 使用它自己的局部 rose 覆盖了外部的 rose.

----------------------------------------------------------------------

8. 下面程序会打印出什么?

#include <stdio.h>
char color = 'B';
void first (void);
void second (void);

int main (void)
{
extern char color;

printf (" color int main() is %c \n",color);
first();
printf ("color int main() is %c \n", color);
second();
printf (" color int main() is %c \n", color);
return 0;
}

void first (void)
{
char color;

color = 'R'; /* 此处 color 为 first 函数的局部变量, 赋值不改变全局变量 color 的值 */
printf ("color in first() is %c \n", color);
}

void second (void)
{
color = 'G'; /* 此处是对 color 全局变量的赋值进行操作 */
printf ("color in second() is %c \n",color);
}

答:

color int main() is B
color in first() is R
color int main() is B
color in second() is G
color int main() in G

------------------------------------------------------------------------------------

9. 文件开始处做了下列声明:

static int plink;
int value_ct (const int arr[], int value, int n);

a. 这些声明表明了程序员的什么意图?

答: 它告诉我们程序将使用一个变量 plink, 该变量局部于包含该函数的文件. value_ct() 第一个参数是一个指向整数的指针, 并假定它指向具有 n 个元素的数组的第一个元素. 这里最重要的一点是不允许程序使用指针 ar
来修改原始数组中的值.

b. 用 const int value 和 const int n 代替 int value 和 int n 会增强对调用程序中的值的保护吗?

答: 不会, value 和 n 已经是原始数据的拷贝, 所以函数不能改变调用程序中的对应值. 这样声明直到的作用只是防止在函数中改变 value 和 n 的值. 例如, 如果用 const 限定 n , 那么函数就不能使用 n++ 表达式.

12.11 编程练习

---------------------------------------------------------------------------

1. 不使用全局变量, 重写程序清单 12.4 中的程序.

解:
#include "stdafx.h"
void critic (int *pi);
int main (void)
{
int units;

printf ("How many poubds to a firkin of butter ? \n");
scanf ("%d", &units);
while (units != 56)
critic(&units);
printf ("You must have looked it up! \n");
return 0;
}

void critic (int *pi)
{
printf ("No luck, chummy, try again \n");
scanf ("%d", pi); /* 这道题主是是此处的使用形式 */
}

----------------------------------------------------------------------------------

2. 在美国通常是以英里每加仓来计算油耗, 在欧洲是以每百公里来计算. 下面是某程序的一部分, 该程序让用户选择一个模式 (公制的或美制的), 然后收集数据来计算油耗.

// pe12 - 2b.c
#include <stdio.h>
#include "pe12-2a.h"
int main (void)
{
int mode;

printf ("Enter 0 for metric mode, 1 for US mode :");
scanf ("%d", &mode);
while (mode >= 0)
{
set_mode (mode);
get_info();
show_info();
printf ("Enter 0 for metric mode, 1 for US mode");
printf (" (-1 to quit): ");
scanf ("%d", &mode);
}
printf ("Done \n");
return 0;
}

下面是一些输出示例:
Enter 0 for metric mode, 1 for US mode: 0
Enter distance traveled in kilometers: 600
Enter fuel consumed in liters: 78.8
Fuel consumption is 13.13 liters per 100 km.

Enter 0 for metric mode, 1 for US mode (-1 to quit): 1
Enter distance traveled in miles: 434
Enter fuel consumed in gallons: 12.7
Fuel consumption is 34.2 miles per gallon.

Enter 0 for metric mode, 1 for US mode (-1 to quit): 3
invalid mode specified. Mode 1 (US) used.
Enter distance traveled in miles: 388
Enter fuel consumed in gallons: 15.3
Fuel consumption is 25.4 miles per gallon.

Enter 0 for metric mode, 1 for US mode (-1 to quit): -1
Done.

如果用户键入了不正确的模式, 程序向用户给出提示信息并选取最接近的模式. 请提供一个头文件 pel2-2a.h 和源代码文件 pel2-2a.c , 来使程序可以运行. 源代码文件应定义 3 个具有文件作用域, 内部链接的变量. 一个代表模式, 一个代表距离, 还有一个代表消耗的燃料. 函数 get_info() 根据模式设置提示输入相应的数据, 并将用户的回答存入文件作用域变量. 函数 show_info() 根据所选的模式计算并显示燃料消耗值.

解:
-------------------------------------------------
/* pel2 - 2a .h 头文件 定义了变量和全程函数 */
#include "stdafx.h"

/* 定义了三个静态内部链接的变量 */
static int walemode = 1;
static float len = 0.0;
static float fuel = 0.0;

/* 程序所需要到的函数 */
void set_mode (int i);
void get_info (void);
void show_info (void);

-----------------------------------------

/* pel2 - 2a.c 须和 pel2 - 2a.h 头文件一起编译的 源代码 */
#include "stdafx.h"
#include "pel2-2a.h"
int main (void)
{
int mode;

printf("Enter 0 for metric mdoe, 1 for US mode :");
scanf ("%d", &mode);
while (mode >= 0) {
set_mode (mode);
get_info();
show_info();
printf ("Enetr 0 for metric mode, 1 for US mode \n");
printf ("(-1 to quit) :");
scanf ("%d", &mode);
}
printf ("Done \n");
return 0;
}

/* set_mode 函数: 获取用户输入并赋值给全局变量 */
void set_mode(int i)
{
walemode = i; /* 将用户选择的模式赋值给变量 */

if ((i != 0) && (i != 1)){
printf ("invalid mode specified. Mode 1 (US) used.");
walemode = 1;
}
}

/* get_info 函数: 接受用户输入并赋值给相应的全局变量 */
void get_info (void)
{
if (walemode == 0){
printf ("Enter distance traveled in kilometers :");
scanf("%f",&len);
printf ("Enter fuel consumed in liters :");
scanf ("%f",&fuel);
}else {
printf ("Enter distance traveled in miles :");
scanf ("%f",&len);
printf ("Enter fuel consumed in gallons: ");
scanf("%f",&fuel);
}
}

/* shoe_info 函数: 根据全局变量的数值, 显示结果给用户 */
void show_info (void)
{
if (walemode == 0)
printf ("Fuel consumption is %.2f liters per 100 km \n", fuel/len * 100);
else
printf ("Fuel consumption is %.2lf miles per gallon \n",len/fuel);
}

------------------------------------------------------------------------------------------

3. 重新设计练习 2 中的程序, 使它仅使用自动变量. 程序提供相同的用户界面, 也就是说, 要提示用户模式等等. 然而, 你还必须给出一组不同的函数调用.

解:

-------------------------
/* pel2 - 2a .h 头文件 定义了变量和全程函数 */
#include "stdafx.h"
#define METRIC 0
#define US 1

/* 程序所需要到的函数 */
void set_mode (int *pm);
void get_info (int mode, double * pd, double * pf);
void show_info (int mode, double len, double fuel);

------------------------

/* pel2 - 2a.c 须和 pel2 - 2a.h 头文件一起编译的 源代码 */
#include "stdafx.h"
#include "pel2-3a.h"

/* set_mode 函数: 获取用户输入并赋值给全局变量 */
void set_mode(int *pm)
{

if ((*pm != METRIC) && (*pm != US)){
printf ("invalid mode specified. Mode 1 (US) used.");
*pm = US;
}
}

/* get_info 函数: 接受用户输入并赋值给相应的全局变量 */
void get_info (int mode, double * pd, double * pf)
{
if (mode == 0){
printf ("Enter distance traveled in kilometers :");
scanf("%lf",pd);
printf ("Enter fuel consumed in liters :");
scanf ("%lf",pf);
}else {
printf ("Enter distance traveled in miles :");
scanf ("%lf",pd);
printf ("Enter fuel consumed in gallons: ");
scanf("%lf",pf);
}
}

/* shoe_info 函数: 根据全局变量的数值, 显示结果给用户 */
void show_info (int mode, double len, double fuel)
{
if (mode == 0)
printf ("Fuel consumption is %.2f liters per 100 km \n", fuel/len * 100);
else
printf ("Fuel consumption is %.2lf miles per gallon \n",len/fuel);
}

--------------------------------------------------------

/* pel2 - 3b.c 与 pel2 - 3a (函数体) 与 pel2 - 3a.h 头文件 */
#include "stdafx.h"
#include "pel2-3a.h"

int main (void)
{
int mode;
double len, fuel;

printf("Enter 0 for metric mdoe, 1 for US mode :");
scanf ("%d", &mode);
while (mode >= 0) {
set_mode (&mode);
get_info(mode,&len,&fuel);
show_info(mode,len,fuel);
printf ("Enetr 0 for metric mode, 1 for US mode ");
printf ("(-1 to quit) :");
scanf ("%d", &mode);
}
printf ("Done \n");
return 0;
}

-----------------------------------------------------------------

4. 编写一个函数, 它返回函数自身被调用的次数, 并在一个循环中测试之.

解:

/* fun.c 返回函数自身被调用的次数 */
#include "stdafx.h"
static int count = 0; /* 定义一个全局变量给函数使用 */
void funreturn (void);
int main (void)
{
int i = 0;
int n;

puts("输入你想函数调用的次数 ( q to quit ): ");
while ((scanf("%d",&i) == 1)){
for ( n = 0; n < i; n++)
funreturn();
printf ("函数一共调用了 %d", count);
count = 0;
puts("输入新的函数调用的次数 ( q to quit ): ");
}
puts ("Done \n");
}

/* funreturn 函数: 用于测试的函数 */
void funreturn (void)
{
count++;
printf ("本次是 %d 调用 \n",count);
}

--------------------------------------------------------------------

5. 编写一个产生 100 个 1 到 10 范围内的随机数的程序, 并且以降序排序 (可以将 11 章的排序算法稍加改动来对整数进行排序. 这里, 对数字本身排序即可).

解:

/* rand.c -- 产生 100 个随机数并排序 */
#include "stdafx.h"
void show_arr (const int ar[], int limit );
void sortarr (int ar[], int limit);
#define LEN 100

int main (void)
{
int i;
int arr[LEN];

for (i = 0; i < LEN; i++)
arr[i] = rand() % 10 + 1; /* 用 rand函数给数组元素赋值 */
puts("初始化的数组元素");
show_arr (arr,LEN);
sortarr (arr,LEN);
puts ("排序后的数组元素");
show_arr (arr,LEN);

return 0;
}

/* show_arr 函数: 历遍数组元素 */
void show_arr (const int ar[], int limit )
{
int i = 0;

for (i = 0; i < limit; i++){
printf("%4d", ar[i]);
if (i % 10 == 9) /* 如果显示元素到达10个 换行 */
putchar('\n');
}
if (i % 10 != 0)
putchar('\n');
}

/* sortarr 函数: 将数组元素进行排序 */
void sortarr(int ar[], int limit)
{
int top, now, temp;

for (top = 0; top < limit-1; top++)
for (now = top+1; now < limit; now++)
if (ar[now] > ar[top]){
temp = ar[now];
ar[now] = ar[top];
ar[top] = temp;
}
}

-----------------------------------------------------------------------------

6. 编写一个产生 1000 个 1 到 10 范围内的随机数的程序. 不必保存或打印数字, 仅打印每个数被产生了多少次. 让程序对 10 个不同的程序值进行计算. 数字出现的次数相同吗? 可以使用本章中的函数或 ANSI C 中的函数 rand() 和 srand(), 它们与我们的函数具有相同的形式. 这是一个游戏特定随机数发生器的随机性方法.

解:
/* seed.c 产生 1000个 1 到 10 的随机数 */
#include "stdafx.h"
#define LEN 1000
#define TIMES 10
void show_arr (int ar[], int len);

int main (void)
{
int i,j;
int arr[LEN]; /* 定义容纳 随机数的数组 */

for (i = 0; i < TIMES; i++){
srand(i); /* 设置种子 seed */
for (j = 0; j < LEN; j++)
arr[j] = rand() % 10 + 1;
printf ("第 %d 次 :\n",i+1);
show_arr(arr,LEN);
putchar('\n');
}
return 0;
}

/* show_arr 函数: 历遍数组元素 */
void show_arr (int ar[], int len)
{
int i, j;
int count = 0; /*统计计数*/

for (i = 1; i < TIMES+1; i++){
for (j = 0; j < len; j++)
if (i == ar[j]) /* 统计随机数的使用次数 */
count++;

printf ("数字 %2d 使用了 %2d 次 \n",i, count);
count = 0;
}
}

--------------------------------------------------------------------------

7. 编写一个程序, 该程序与我们在显示程序清单 12.13 的输出之后所讨论的修改版程序具有相同表现. 也就是说, 输出应像下面这样:

Enter the number of sets; enter q to stop
18
How many sides and how many dice?
6 3
Here are 18 sets of 3 6 - stided throws.
12 10 6 9 8 15 9 14 12 17 11 7 10
13 8 14
How many sets? Enter q to stop.
q

解: 对这个没兴趣 这是官方源码

#include "stdafx.h"
int rollem(int);

int main(void)
{
int dice, count, roll;
int sides;
int set, sets;

srand((unsigned int) time(0)); /* randomize rand() */

printf("Enter the number of sets; enter q to stop.\n");
while ( scanf("%d", &sets) == 1)
{
printf("How many sides and how many dice?\n");
scanf("%d %d", &sides, &dice);
printf("Here are %d sets of %d %d-sided throws.\n", sets, dice,
sides);
for (set = 0; set < sets; set++)
{
for (roll = 0, count = 0; count < dice; count++)
roll += rollem(sides);
/* running total of dice pips */
printf("%4d ", roll);
if (set % 15 == 14)
putchar('\n');
}
if (set % 15 != 0)
putchar('\n');
printf("How many sets? Enter q to stop.\n");
}
printf("GOOD FORTUNE TO YOU!\n");
return 0;
}

int rollem(int sides)
{
int roll;

roll = rand() % sides + 1;
return roll;
}

---------------------------------------------------------------------------------

8. 下面是某程序的一部分:

/* pe12 - 8.c*/

#include <stdio.h>
int * make_array (int elem, int val);
void show_array (const int ar[], int n);
int main (void)
{
int * pa;
int size;
int value;

printf ("Enter the number of elements :");
scanf ("%d", &size);
while (size > 0) {
printf ("Enter the initialization value :");
scanf ("%d", &value);
pa = make_array (size, value);
if (pa){
show_array(pa, size);
free(pa);
}
printf ("Enter the number of elements (< 1 to quit ): ");
scanf ("%d", &size);
}
printf ("Done \n");
return 0;
}

给出函数 make_array() 和 show_array() 的定义以使程序完整. 函数 make_array() 接受两个参数. 第一个是 int 数组的元素个数, 第二个是要赋给每个元素的值. 函数使用 malloc() 来创建一个适当大小的数组, 把每个元素设定为指定的值, 并返回一个数组指针. 函数 show_array() 以 8 个数一行格式显示数组内容.

解:

#include "stdafx.h"
int * make_array (int elem, int val);
void show_array (const int ar[], int n);
int main (void)
{
int * pa;
int size;
int value;

printf ("Enter the number of elements :");
scanf ("%d", &size);
while (size > 0) {
printf ("Enter the initialization value :");
scanf ("%d", &value);
pa = make_array (size, value);
if (pa){
show_array(pa, size);
free(pa);
}
printf ("Enter the number of elements ( < 1 to quit ): ");
scanf ("%d", &size);
}
printf ("Done \n");
return 0;
}

/* show_array 函数: 显示数组元素 */
void show_array (const int ar[], int n)
{
int i = 0;

while (i < n){
if ( i % 8 == 0)
putchar('\n');
printf("%5d", ar[i]);
i++;
}
putchar ('\n');
}

/* * make_array 函数: 分配数组元素 而初始化 */
int * make_array (int elem, int val)
{

int *pi;
int i = 0;

pi = (int *) malloc (elem * sizeof (int)); /* 按输入的元素个数分配内存空间 */
if (pi == NULL){
puts ("Memry allocation failed, Goodbye");
exit (EXIT_FAILURE);
}
while (i < elem){
*pi = val; /* 此处 pi 必需为 *pi 因为赋给的是值 */
pi++;
i++;
}
for (; i > 0; i--) /* 利用指针给数组元素递增赋值后, 必须将指针指向回数组首元素*/
pi--; /* 否则此时利用指针历遍来显示数组元素时将会发生越界 */

return pi;
}

上一篇:angular4 使用window事件


下一篇:MVC扩展控制器工厂,通过继承DefaultControllerFactory来决定使用哪个接口实现,使用Ninject