这一章在干嘛?
C 的高明之处在于让你精细控制程序的”细枝末节”——哪个函数能看见哪个变量(作用域)、变量活多久(存储期限)、内存什么时候分配什么时候回收。这一章先建立作用域、链接、存储期限三个概念,再讲五种存储类,然后是随机数函数的实战,最后登场的是手动管理内存的
malloc()和free()。这是 C 内存世界的地图。
12.1 存储类:作用域、链接与存储期限
C 为变量提供了五种存储类(storage class)模型(本章末尾还会加第六种——动态分配)。描述一个变量要看三件事:存储期限(storage duration)——它在内存里待多久;作用域(scope)和链接(linkage)——共同决定程序的哪些部分能按名字访问它。
作用域
- 块作用域(block scope):定义在一对花括号内,从定义处可见到块结束;函数形参也算。C99 放宽了”必须写在块开头”的老规矩,还把 for/while/if 的控制部分也算作块:
for (int i = 0; i < 10; i++)里的 i 只在循环里可见。 - 函数原型作用域:原型参数名从声明处到原型结束有效,编译器只关心类型(变长数组参数除外)。
- 文件作用域(file scope):定义在所有函数之外,从定义处到文件末尾可见。能被多个函数使用的文件作用域变量也叫全局变量(global variable)。
- 还有个函数作用域,只用于 goto 的标签。
链接
int giants = 5; // 文件作用域,外部链接
static int dodgers = 3; // 文件作用域,内部链接- 外部链接(external linkage):多文件程序的任何地方都能用;
- 内部链接(internal linkage):只在本文件可用,加
static就是它; - 块作用域和原型作用域的变量无链接,是所在块的私有财产。
存储期限
- 静态存储期限:整个程序运行期间都存在。文件作用域变量都属此类(注意:文件作用域加
static改变的是链接,不是期限)。 - 自动存储期限:块作用域变量的默认选项——进入块时分配内存,离开块就释放,内存像可以反复涂写的工作台。
五种存储类一览表
| 存储类 | 期限 | 作用域 | 链接 | 声明方式 |
|---|---|---|---|---|
| 自动(automatic) | 自动 | 块 | 无 | 块内 |
| 寄存器(register) | 自动 | 块 | 无 | 块内,带关键字 register |
| 静态、外部链接 | 静态 | 文件 | 外部 | 所有函数之外 |
| 静态、内部链接 | 静态 | 文件 | 内部 | 所有函数之外,带关键字 static |
| 静态、无链接 | 静态 | 块 | 无 | 块内,带关键字 static |
12.2 自动变量与寄存器变量
块内(或函数头)声明的变量默认就是自动存储类,也可显式写 auto int plox;——主要用于表达”我就是要覆盖外层定义”的意图。auto 是**存储类说明符(storage class specifier)**之一。
嵌套块的可见性:内层变量只在所在块可见;内层同名变量会”隐藏”外层定义,块结束外层变量重新可见。
程序清单 12.1 hiding.c
/* hiding.c -- variables in blocks */
#include <stdio.h>
int main()
{
int x = 30; /* 最初的 x */
printf("x in outer block: %d\n", x);
{
int x = 77; /* 新的 x,隐藏了第一个 x */
printf("x in inner block: %d\n", x);
}
printf("x in outer block: %d\n", x);
while (x++ < 33) /* 最初的 x */
{
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 是 30;内层块里 77;循环条件用外层 x(负责终止,30 加到 34),循环体里打印的是每次新建的内层 x(永远从 100 加到 101,输出三行 101),最后外层 x 是 34。这个例子不是鼓励你这么写,而是演示”块内定义变量”会发生什么。C99 还规定循环体本身就是子块:清单 12.2 forc99.c 里 for (int n = 1; ...) 的循环索引 n 隐藏外层 n(10),循环体里再定义的 n(30)又隐藏循环索引(需 gcc -std=c99 激活)。
自动变量的初始化
未初始化的自动变量是垃圾,它的地址更是雷区
自动变量不初始化时,值是这块内存上一次残留的任意内容,每次运行都可能不同。更进一步:千万不要返回自动变量的地址——函数一结束,局部变量随块一起消亡,返回的指针指向已经”人去楼空”的内存,用它就是未定义行为。要跨函数共享数据,用参数、返回值或静态/动态分配的内存。
自动变量可以用非常量表达式初始化,只要用到的变量已定义在前:int rance = 5 * ruth;。
寄存器变量
register int quick; 是请求编译器把变量放进寄存器(register)或最快的内存以加速访问。它更像”请求”而非”命令”——寄存器不够编译器可以拒绝,变量退回普通自动变量。因为变量可能不在内存里,寄存器变量不能取地址(不能对它用 &)。形参也可以请求 register:void macho(register int n)。
12.3 块作用域的静态变量
static 用在块内声明,变量获得静态存储期限:块结束后不消失,下次调用还能记着上次的值——但作用域和”无链接”不变,仍是这个块的私有财产。
程序清单 12.3 loc_stat.c
/* loc_stat.c -- using a local static variable */
#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++);
}输出:fade 永远是 1(每次调用重新初始化),stay 依次是 1、2、3(记得上一次的自增)。两个声明长得一样,本质天差地别:
int fade = 1;是运行时动作,每次调用都执行;static int stay = 1;不是运行时语句——静态变量在程序加载时就位,初始化只发生一次(用调试器单步会”跳过”这行)。不显式初始化则自动置 0。
注意 static 不能修饰形参:int wontwork(static int flu); 非法。
静态变量 = 会记事的局部变量
想让函数”记住”自己被调过几次、上轮算到哪了,不必动用全局变量——块内 static 就够了:作用域仍锁在函数内部,只把”寿命”延长到整个程序。封装与持久性两全。
12.4 外部变量与链接
外部变量(静态、外部链接)
定义写在所有函数之外就是外部变量(external variable):文件作用域 + 外部链接 + 静态期限。函数内可用 extern 再声明一次(可省略,写了是文档说明)。
程序清单 12.4 global.c
/* global.c -- uses an external variable */
#include <stdio.h>
int units = 0; /* 一个外部变量 */
void critic(void);
int main(void)
{
extern int units; /* 可选的重复声明 */
printf("How many pounds 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);
}critic() 读入的新值,main() 的 while 循环立刻知道——两个函数操作的是同一个 units。陷阱:若在 main() 里写 int units;(不带 extern),会创建一个新的自动变量把外部 units 隐藏掉。
定义声明 vs 引用声明
int tern = 1; // 定义声明:分配存储空间
extern int tern; // 引用声明:指向别处已定义的 ternextern的意思是”去别处找定义”,本身不分配空间;- 初始化只能伴随定义声明:
extern char permis = 'Y';是错误; - 外部变量只能初始化一次,且只能用常量表达式(
int x2 = 2 * x;不行;size_t z = sizeof(int);可以)。

静态变量与内部链接
文件作用域的定义前加 static,变量就限定为本文件私有:
int traveler = 1; // 外部链接,其他文件可用
static int stayhome = 1; // 内部链接,仅本文件可用函数内的 extern int stayhome; 只是文档说明,不改变其内部链接。
多文件程序与链接
多文件共享一个外部变量的 ANSI 标准做法:一个文件放定义声明(可初始化),其他文件放引用声明(带 extern)。背后的功臣是链接器(linker)——它把你的目标代码、启动代码和库代码拼成可执行文件:

存储类说明符与函数
五个存储类说明符:auto、register、static、extern、typedef(typedef 纯为语法凑数;一个声明最多用其中一个)。函数也有存储类:默认是外部函数(其他文件可调用);加 static 则是本文件私有,可避免与其他文件的同名函数冲突:
double gamma(); // 默认外部
static double beta(); // 本文件私有
extern double delta(); // 显式外部慎用外部变量
“把所有变量都设成外部的,函数间就不用传参了”——很有诱惑力,但后患无穷:任何函数都可能偷偷改坏其他函数正在用的变量。几十年的集体经验证明这个风险远大于便利。保护性编程的金科玉律是”need to know”原则:每个函数的内部变量尽量私有,只共享必须共享的。用其他存储类前先问自己:真的需要吗?
12.5 随机数函数与静态变量
看一个把静态变量用出花样的实战:伪随机数(pseudorandom number)生成器。方案是”种子(seed)→ 生成新数 → 新数当新种子”——函数必须记住上次的种子,这正是静态变量的主场。
程序清单 12.7 rand0.c
/* rand0.c -- produces random numbers */
/* uses ANSI C portable algorithm */
static unsigned long int next = 1; /* the seed */
int rand0(void)
{
/* magic formula to generate pseudorandom number */
next = next * 1103515245 + 12345;
return (unsigned int) (next/65536) % 32768;
}next 是文件作用域的静态变量(内部链接),每次调用被”魔法公式”改写,返回 0~32767 的数。驱动程序连跑两次输出一模一样——种子固定为 1,序列自然重演,这就是”伪”随机。对策:加一个重置种子的函数,让同文件的两个函数共享 next:
程序清单 12.9 s_and_r.c
/* s_and_r.c -- file for rand1() and srand1() */
static unsigned long int next = 1; /* the seed */
int rand1(void)
{
/* magic formula to generate pseudorandom number */
next = next * 1103515245 + 12345;
return (unsigned int) (next/65536) % 32768;
}
void srand1(unsigned int seed)
{
next = seed;
}不同种子产出不同序列。实际使用时用系统时间自动播种最方便:
#include <time.h> /* time() 的原型 */
srand1((unsigned int) time(0)); /* 用时钟初始化种子 */time(0) 返回系统时间(time_t 类型),值随时间变化,强转成 unsigned int 当种子。标准库同款是 rand() 和 srand()(stdlib.h)。
掷骰子:roll_n_dice()
把 rand() 包装成任意面数的骰子:rand() % sides + 1 得到 1~sides 的随机数。清单 12.11 diceroll.c 把这个逻辑封装成完整模块:
/* diceroll.c(节选) */
#include "diceroll.h"
#include <stdlib.h>
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, 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;
}配套头文件 diceroll.h(双引号包含让编译器在本地目录找):extern int roll_count; 和 int roll_n_dice(int dice, int sides);。这里用上了全套招式:rollem() 声明为 static 成为私有助手;roll_count 是外部变量,头文件的 extern 让使用方也能看到——定义声明只有一份,引用声明随便多。主程序用 srand((unsigned int) time(0)) 播种后每次运行结果都不同。
12.6 分配内存:malloc() 与 free()
五种存储类的规矩都是打包好的。想更灵活?用库函数手动分配和释放内存。
malloc() 的机制:传入需要的字节数,它找一块合适的空闲内存——这块内存是匿名的,malloc 不给它名字,只返回首字节的地址(ANSI 下是 pointer-to-void 通用指针)。把地址存进指针变量,就能像数组名一样用:
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));从此 C 创建数组有三条路:常量尺寸的普通数组;C99 变长数组(VLA);动态数组(dynamic array)——malloc 在运行时按需分配,尺寸运行时说了算。malloc 失败返回空指针,必须检查。看完整例子:
程序清单 12.14 dyn_arr.c
/* dyn_arr.c -- dynamically allocated array */
#include <stdio.h>
#include <stdlib.h> /* for 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;
}逐段看:malloc(max * sizeof(double)) 按用户输入分配;ptd == NULL 时用 exit(EXIT_FAILURE) 终止(stdlib.h 定义了 EXIT_SUCCESS/EXIT_FAILURE 两个标准返回值);之后 ptd[i] 用法和数组名一模一样;结尾 free(ptd) 把内存还给”内存池”。把 malloc/free 想象成内存池管理:malloc 取出,free 归还,归还的可以复用。free 的参数必须是 malloc/calloc 返回的地址,不能拿它释放声明出来的数组。
malloc 与 free 必须成对出现
malloc 的内存不会自动回收,生命周期从 malloc 开始到 free 为止——程序员说了算。好习惯:每次 malloc,脑子里立刻登记一个对应的 free,就像开闸放水后记得关闸。
内存泄漏:malloc 的头号事故
只借不还,内存就会枯竭:
void gobble(double ar[], int n)
{
double * temp = (double *) malloc( n * sizeof(double));
/* ... */
/* free(temp); // 忘了 free! */
}gobble() 被循环调用 1000 次:第一次 malloc 拿走 16000 字节,函数结束时指针 temp(自动变量)消失了,但那 16000 字节既访问不到也没归还;第二次又拿走 16000 字节……循环跑完,16,000,000 字节被白白锁死——这就是内存泄漏(memory leak),程序很可能中途耗光内存。在函数末尾补上 free(temp); 就能防住。
calloc() 与变长数组
calloc() 是 malloc 的孪生兄弟:接受两个参数(单元个数、每单元字节数),并把内存的所有位清零:
long * newmem;
newmem = (long *) calloc(100, sizeof(long));同样用 free() 释放。VLA 与 malloc 的取舍:VLA 是自动存储,离开定义块自动释放、无需 free,但数组不能离开所在函数;malloc 的数组可以在函数间传递,用完在任意位置 free(free 的指针变量和 malloc 的不必是同一个,地址一致即可)。多维数组用 VLA 语法更舒服,malloc 则要写 int (* p2)[6]; 这类”指向数组的指针”声明。
三种内存模型
把程序的内存想象成三个区:静态区——编译时定好大小,程序启动即就位,运行全程存在(静态变量、字符串常量住这里);栈(stack)——自动变量的家,进块分配出块回收,全自动;动态分配区(堆)——malloc/free 手动控制,可以在 A 函数创建、B 函数释放——自由,但容易碎片化,速度通常比栈慢。
12.7 ANSI C 类型限定词
C90 加入了 const 和 volatile 两个类型限定词(type qualifier),C99 再加 restrict。C99 还规定限定词幂等——重复声明同一个限定词不算错。
const 限定词
简单变量:const int nochange = 12; 初始化后不可赋值、不可自增自减。也用于数组:const int days1[12] = {31, 28, ...};。指针的 const 就讲究了——看 const 在 * 的哪边:
const float * pf; /* pf 指向的值是常量,pf 本身可变 */
float * const pt; /* pt 是常量指针,指向的值可变 */
const float * const ptr; /* 两者都不可变 */
float const * pfc; /* 等价于 const float * pfc */口诀:const 在 * 左边修饰数据,在 * 右边修饰指针。最常见的用途是函数形参——告诉调用者”我只读不改”:void display(const int array[], int limit);。ANSI C 库就是这么标注的:char *strcat(char *, const char *); 第一个串被改(目标),第二个串只读(源)。
const 数据做全局变量是安全的(反正改不了)。跨文件共享 const 数据两个策略:一是老规矩——一个文件定义、其他文件 extern 引用;二是放进头文件并加 static,每个文件各得一份拷贝——数据是常量且一致,复制无妨:
/* constant.h */
static const double PI = 3.14159;volatile 限定词
volatile 告诉编译器:这个变量的值可能被程序之外的力量改变(硬件时钟、并发程序共享的数据),别做激进优化。为什么需要它?
val1 = x;
/* 一段不使用 x 的代码 */
val2 = x;聪明的编译器会把 x 缓存(caching)进寄存器,第二次直接读寄存器省一次访存——但如果 x 在这期间被硬件改了,val2 就拿到旧值。声明 volatile int loc1; 后,编译器对它的每次使用都老实访问内存。一个值可以既是 const 又是 volatile(如硬件时钟:程序不该改它 = const,硬件会改它 = volatile)。
restrict 限定词
C99 新增,只用于指针,含义是”这个指针是访问该数据块的唯一初始入口”——给编译器发优化许可证:
int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;restar 是 malloc 块的唯一入口,可以加 restrict;par 与 ar 共享数据,不行。于是 restar[n] += 5; restar[n] += 3; 可被合并成 restar[n] += 8;(编译器保证没人中途动数据);对 par 同样合并就会算错,因为 ar 中途改了数据。restrict 有两个受众:编译器(放心优化)和程序员(必须守约,编译器查不出违约)。C99 库里 memcpy() 用 restrict 标注(要求源和目标不重叠),memmove() 不用(允许重叠、小心拷贝)。C99 还允许把限定词和 static 写进形参的方括号:double stick(double ar[static 20]); 表示实参指向至少 20 个元素的数组——又是优化提示兼用户约定。
本章要点回顾
- 三要素:作用域(块/原型/文件)+ 链接(无/内部/外部)+ 存储期限(自动/静态)
- 一句话记五种存储类:默认自动;
static在块内 = 记事的局部变量,在文件级 = 本文件私有;extern= 引用别处的定义 - 外部变量慎用;函数间通信优先用参数和返回值
- 随机数生成器是静态变量的教科书案例:种子必须跨调用存活
malloc()/free()手动管理堆内存,必须成对出现防内存泄漏;calloc()额外清零const锁数据或锁指针(看它在*哪边);volatile防优化;restrict发优化许可证
自测题(点击展开)
1. 哪些存储类创建的变量对包含它们的函数来说是局部的?
答案:自动、寄存器、静态无链接三种——它们的变量都有块作用域且无链接,只在所在块(通常是函数)内可见。
2. 哪些存储类的变量在整个程序运行期间都存在?
答案:三种静态存储类(外部链接、内部链接、无链接的静态变量),以及 malloc/calloc 分配的动态内存——后者从 malloc() 开始到 free() 为止,控制权在程序员手里。
3.
int *p1 = (int *) malloc(100 * sizeof(int));与int *p1 = (int *) calloc(100, sizeof(int));最终结果有何不同?答案:都是分配能放 100 个 int 的空间并让 p1 指向它。区别有二:calloc 接受两个参数(个数与单元大小),且 calloc 会把内存所有位清零,malloc 不初始化(内容是垃圾值)。
4.
extern char permis = 'Y';错在哪?答案:extern 表示引用声明——“去别处找定义”,它不分配存储空间,因此不能初始化。初始化只能出现在定义声明里。
5. 为什么块内
static int stay = 1;的初始化只执行一次,而int fade = 1;每次调用都执行?答案:fade 是自动变量,初始化属于函数运行时动作,每次进入块都执行;stay 是静态变量,程序加载时就已就位并初始化,块内的声明只是告诉编译器”只有这个块能用它”,并不是运行时语句。