存储类、链接和内存管理

C的强大功能之一在于允许您控制程序的细节, C的内存管理正是这种控制能力的例子, 它通过让您决定知道哪些变量以及一个变量在程序中存在多长时间来实现控制。 简而言之, 就是变量的作用域( 多大范围内可知) 和生存期( 存在多长时间 )。

存储类、链接

  C为变量提供了5种不同的存储模型,或称存储类。可以按照一个变量(更一般地,一个数据对象)的存储时期(storage duration)描述它,也可以按照它的作用域(scope)以及它的链接(linkage)来描述它。存储时期就是变量在内存中保留的时间,变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量。
  不同的存储类提供了变量的作用域、链接和存储时期不停的组合。如下:

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

作用域

  作用域可以是代码作用域、函数原型作用域,或者文件作用域。
代码作用域:可以理解为一个代码块的范围,就是花括号包围的范围。

1
2
3
4
5
6
int test(int a)
{
int b;
...
return b;
}

代码中的a、b都有直到结束花括号的代码块作用域。在C99中把代码块的概念扩大了,如for循环、while循环或者if语句中即使没有使用花括号,这些语句中的变量也属于代码块作用域。

1
2
3
4
5
6
7
8
9
10
11
12
13
int test(int a)
{
int b;
int i;
for(i = 0; i < 10; i++>)
{
double q; //q的开始作用域
...
q += i; // q的结束作用域
}
...
return b;
}

  函数原型作用域,变量的作用域一直到声明结尾

1
2
int test(int a, double b);
void vla_test(int n, int m, ar[n][m]); // 可变长数组声明

  文件作用域,变量在整个文件中都是可见的。

1
2
3
4
5
6
#include <stdio.h>
int count = 0; // 具有文件作用域
int main()
{
...
}

链接

  一个C变量具有下列链接:外部链接(external linkage), 内部链接(internal linkage),或空链接(no linkage)。
  具有代码块作用域或者函数原型作用域的变量有空链接,意味着由其定义在定义所在的代码块或函数原型所私有的。具有文件作用域的变量有内部链接或外部内接。一个具有外部链接的变量可以在多文件程序中使用;一个具有内部链接的变量可以在一个文件中的任何地方使用(存储类说明符static)。

1
2
3
4
5
6
int a = 10;           //文件作用域,外部链接
static int b = 20 //文件作用域,内部链接
int main()
{
...
}

存储时期

  一个C变量具有静态存储时期(static storage duration)和自动存储时期(automatic storage duration)。一个变量具有静态存储时期,它在程序执行期间一直存在。具有文件作用域的变量具有静态存储时期。注:对于具有文件作用域的变量,关键词static表明链接,并非存储时期。所有的文件作用域变量都具有静态存储时期
  具有代码块作用域的变量一般情况下具有自动存储时期。在程序定义这些变量的代码中,将为这些变量分配内存,退出该代码块,分配的内存就会被释放。当然也具有静态存储时期、代码块作用域的变量,只要在声明前添加static关键字。

五种存储类

自动

  在一个代码块内(或一个函数头部作为参量)声明的变量属于自动存储类,这些变量具有自动存储时期、代码块作用域和空连接。默认情况下,代码块或函数头部的变量属于自动存储类,当然也可以用关键字auto显式表明。未经初始化值不固定。

1
2
3
int main()
{
auto int a;

寄存器

  在一个代码块内(或一个函数头部作为参量)使用存储类说明符register声明的变量属于寄存器存储类。这些变量具有代码块作用域、空连接以及自动存储时期,同时被访问的速度最快,因为存储在寄存器中,但不能使用地址运算符获取地址。未经初始化值不固定。

1
2
3
int main(void)
{
register int a;

静态、空连接

  在一个代码块内使用存储类修饰符static声明的变量属于静态空链接存储类。该类具有静态存储时期、代码块作用域和空链接。仅在编译时初始化一次。未经初始化值为0。

1
2
3
void test()
{
static int a;

静态、外部链接

  在所有函数外、未使用存储类修饰符static声明的变量属于静态外部链接存储类。该类具有静态存储时期、文件作用域和外部链接。仅在编译时初始化一次。未经初始化值为0。

静态、内部链接

  在所有函数外、使用了存储类修饰符static声明的变量属于静态外部链接存储类。该类具有静态存储时期、文件作用域和内部链接。仅在编译时初始化一次。未经初始化值为0。

参考《C Primer Plus》
  包含五种存储类的小程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// parta.c -- 各种存储类
#include <stdio.h>
void report_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 execute %d times\n", count);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// partb.c -- 程序的其余部分
#include <stdio.h>
extern int count; //引用声明, 外部链接

static int total = 0; // 静态定义, 内部链接
void accumulate(int k); // 原型
void accumulate(int k)
{
static int subtotal = 0; //静态、空链接

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

程序运行结果:

1
2
3
4
5
6
7
8
9
10
11
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 execute 3 times

注:extern 有两个作用,1.在声明一个外部变量后,为使程序逻辑清晰,可以通过extern再次声明;2.当其他文件定义一个外部变量需要使用时,要通过extern来进行引用声明

1
2
3
4
5
6
7
8
// temp是其他文件的变量(多文件编译)
extern int temp; /* 必须声明 */
int arr[100];
int local;
int main(void)
{
extern int local; /* 可选声明 */
extern int arr[]; /* 可选声明 */

内存管理

  当约定使用哪种存储,就自动决定了作用域与存储时期。这是预先的内存管理机制,初次之外可以通过库函数分配和管理内存,提供程序灵活性。
一些内存分配都是自动完成的。

1
2
3
double x;
char arr[] = "Hello, world!";
int num[10];

malloc() & free()

C的功能远不止这些,这里用到的malloc()函数
maclloc函数接收一个参数——内存字节数。它会在内存中找到合适的内存块,内存是匿名的,所以它会返回那块内存的第一个字节的地址。因此,它可以赋值给指针。

1
2
3
4
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
...
free(ptd);

这里声明了一个指针ptd,(double *)是返回的类型,30 * sizeof(double)是30个double字节大小的内存。返回的地址赋给指针ptd,如果内存未分配成功,就返回空指针(NULL)。当该指针指向的内存不在使用,可以用free()释放内存。

程序示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/* dyn_arr.c 为数组动态分配空间 */
#include <stdio.h>
#include <stdlib.h> // 为malloc()和free()函数提供原型
int main(void)
{
double * ptd;
int max;
int number;
int i = 0;

puts("What is the maxium 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)
{
puts("\n");
}
puts("Done.");
free(ptd);
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
What is the maxium number of type double entries?
5
Enter the values(q to quit):
20 30 40 50 55 80
Here are your 5 entries:
20.00 30.00 40.00 50.00 55.00

Done.

calloc()

内存分配还可以使用calloc(),如下

1
2
long * ptd;
ptd = (long *) calloc(100, sizeof(long))

calloc()接收两个参数,第一个是内存单元数,第二个是每个单元以字节计的大小,与malloc()类似。

ANSI C 类型限定词

  一个变量是以它的类型和存储类表征的。C90增加两个属性:不变性和易变性——关键字const和volatile。这样就创建了受限类型(qualified type)。C99又添加了一个限定词restrict,用以方便编译器优化。

const

示例:

1
2
3
4
5
6
const int a = 10// 限定为常量
a = 20; // 编译器报错,不可更改
~~~
数组
~~~C
const int arr[5] = {1, 2, 3, 4, 5};

指针

1
2
3
const float * pf;   /* pf指向一个常量浮点数 */
float const * pf; /* 同上 */
float * const pf; /* pf是一个常量指针 */

函数参量

1
2
void test(const int arr[], int n);
char * strcat(char *, const char *);

volatile

示例:

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

  volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。例如,

1
2
3
val1 = x;
... // 其他代码,并未明确告诉编译器,对 x进行过操作
val2 = x;

  编译器会发现您对x值进行两次操作,val1赋值后,它会把x临时存放到一个寄存器,到val2时,直接把寄存器中的x值赋给val2,这种过程叫做“缓存”。如果中间嵌入汇编代码对x进行修改,编译器就无法及时得知x改变。volatile就可以提醒编译器x是一个随时可能变化的值。

restrict

  restrict只用于指针,表明指针是访问一个数据对象的唯一的且初始的方式,也可用于编译器优化。
举书上两个例子

1
2
3
4
5
6
7
8
9
10
11
int ar[10];
int * restrict restar = (int *)malloc(10 * sizeof(int));
int * par = ar;
for(n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}

通过restrict修饰后,编译器就会进行优化,用同样效果的语句来代替restar的两个语句,如:

1
2
3
4
5
6
7
8
9
10
int ar[10];
int * restrict restar = (int *)malloc(10 * sizeof(int));
int * par = ar;
for(n = 0; n < 10; n++)
{
par[n] += 5;
restar[n] += 8; // 优化后
ar[n] *= 2;
par[n] += 3;
}

restrict在函数参量中使用可以防止内存重叠, C99中的两个库函数原型:

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