文章目录
存储类别、链接和内存管理
12.1存储类别
从硬件方面来看,被储存的每个值都占用一定的物理内存,c语言把这样的一块内存称为对象。对象可以储存一个或多个值。一个对象可能并未储存实际的值,但是它在储存适当的值时一定具有相应的大小。
可以用存储期描述对象,所谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可以用作用域和链接描述标识符,标识符的作用域和链接表明了程序的哪些部分可以使用它。不同的存储类别具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用。对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期。对于并发编程,对象可以在特定线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。
12.1.1作用域
变量的定义在函数的外面,具有文件作用域。具有文件作用域的变量,从它的定义处到该定义所在文件的末尾均可见。由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量。
需要注意的是,多个文件在编译器中可能以一个文件出现。例如,通常在源代码(
.c
扩展名)中包含一个或多个头文件(.h
扩展名)。头文件会依次包含其他头文件,所以会包含多个单独的物理文件。但是,c预处理实际上是用包含的头文件内容替换#include
指令。所以,编译器源代码文件和所有的头文件都看成是一个包含信息的单独文件。这个文件被称为翻译单元。描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元。如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成。每个翻译单元均对应一个源代码文件和它所包含的文件。
12.1.2链接
C变量有3种链接属性:外部链接、内部链接或无链接。具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。这意味着这些变量属于定义它们的块、函数或原型私有。具有文件作用域的变量可以是外部链接或内部链接。外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用。
区分文件作用域的变量是内部链接还是外部链接,可以查看外部定义中是否使用了存储类别说明符
static
:
int giants = 5; // 文件作用域,外部链接。
static int dodgers = 3; // 文件作用域,内部链接。
int main(void) {
// ...
return 0;
}
12.1.3存储期
C对象有4中存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。需要注意的是,变长数组的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。
另一方面,块作用域变量也能具有静态存储期。为了创建这样的变量,要把变量声明在块中,且在声明前面加上关键字static
:
void more(int number) {
int index;
// 只有在执行该函数时,程序才能使用ct访问它所指定的对象。
static int ct = 0;
// ...
return 0;
}
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
12.1.4自动变量
默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。为了更清楚地表达意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类别),可以显式使用关键字
auto
:
int main(void) {
auto int plox;
// ...
return 0;
}
关键字
auto
是存储类别说明符,但是其在c++中的用法完全不同,如果编写c/c++兼容的程序,最好不要使用auto
作为存储类别说明符。
需要注意的是,自动变量不会初始化,除非显式初始化它:
int main(void) {
// repid变量的值是之前占用分配给repid的空间中的任意值(如果有的话),别指望这个值是0。
int repid;
int tents = 5;
return 0;
}
12.1.5寄存器变量
寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。绝大多数方面,寄存器变量和自动变量一样,都是块作用域、无链接和自动存储期:
int main(void) {
// 更像是一种请求,编译器可以直接忽略,这种情况下,寄存器变量就变成普通的自动变量。
// 即使是这样,仍然不能对该变量使用地址运算符。
register int quick;
return 0;
}
可以声明为
register
的数据类型有限。例如,处理器中的寄存器可能没有足够大的空间来储存double
类型的值。
12.1.7外部链接的静态变量
为了指出函数使用了外部变量,可以在函数中使用关键字
extern
再次声明。如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须使用extern
在该文件中声明该变量:
int Errupt; /* 外部定义的变量 */
double Up[100]; /* 外部定义的数组 */
extern char Coal; /* 如果Coal被定义在另一个文件,则必须这样声明 */
void next(void);
int main(void) {
extern int Errupt; /* 可选的声明 */
extern double Up[]; /* 可选的声明 */
// ...
return 0;
}
void next(void) {
// ...
}
关键字
extern
表明该声明不是定义,因为它指示编译器去别处查询其定义。该声明并不会引起分配存储空间。因此,不要用关键字extern
创建外部定义,只用它来引用现有的外部定义。
另一方面,外部变量只能初始化一次,且必须在定义该变量时进行。
12.1.8内部链接的静态变量
该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部,用存储类别说明符
static
定义的变量具有这种存储类别。
普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。
12.1.9多文件
如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用
extern
关键字)。
12.1.10存储类别说明符
extern
说明符表明声明的变量定义在别处。如果包含extern
的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern
的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。
12.1.11存储类别和函数
函数也有存储类别,可以是外部函数(默认)或静态函数。外部函数可以被其他文件的函数访问,但是静态函数只能用于其定义所在的文件。
12.4分配内存:malloc()和free()
静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。
另一方面,可以在程序运行时分配更多的内存。主要的工具是malloc()
函数。该函数接受一个参数:所需的内存字节数。malloc()
函数会找到合适的空闲内存块,这样的内存是匿名的。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char
表示1字节,malloc()
的返回类型通常被定义为指向char
的指针。然而,从ANSI C标准开始,c使用一个新的类型:指向void
的指针。该类型相当于一个通用指针,随后使用强制类型转换为相应的指针类型即可。如果malloc()
分配内存失败,将返回空指针。
// 为30个double类型的值请求内存空间,并设置ptd指向该位置,
// 可以像使用数组名一样使用它。
double *ptd;
ptd = (double *) malloc(30 * sizeof(double));
通常,
malloc()
要与free()
配套使用。free()
函数的参数是之前malloc()
返回的地址,该函数释放之前malloc()
分配的内存。因此,动态分配内存的存储期从调用malloc()
分配内存到调用free()
释放内存为止。
不能用free()
释放通过其他方式(如,声明一个数组)分配的内存。
#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?");
if (scanf("%d", &max) != 1) {
puts("Number not correctly entered -- bye.");
exit(EXIT_FAILURE);
}
// 在c中,不一定要使用强制类型转换,但是在c++中必须使用。
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]) == i) {
++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;
}
12.4.2calloc()函数
分配内存还可以使用
calloc()
:
long *newmem;
newmem = (long *) calloc(100, sizeof(long));
和
malloc()
类似,在ANSI之前,calloc()
也返回指向char
的指针;在ANSI之后,返回指向void
的指针。
calloc()
函数接受两个无符号整数作为参数(ANSI规定是size_t
类型)。第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)。
calloc()
函数还有一个特性:它把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。
12.5ANSI C类型限定符
C99为类型限定符增加了一个新属性:它们现在是幂等的,其实意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略:
const const const int n = 6; // 与const int n = 6相同
typedef const int zip;
const zip q = 8;
12.5.2volatile类型限定符
volatile
限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算机传入的信息。
volatile
的语法和const
一样:
volatile int locl; /* locl是一个易变的位置 */
volatile int *ploc; /* ploc是一个指向易变位置的指针 */
volatile
涉及编译器的优化:
val1 = x;
/* 一些不使用x的代码 */
val2 = x;
智能的(进行优化的)编译器会注意到以上代码使用了两次
x
,但并未改变它的值。于是编译器把x
的值临时储存在寄存器中,然后在val2
需要使用x
时,才从寄存器中(而不是原始内存位置上)读取x
的值,以节约时间。这个过程被称为高速缓存。通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x
的值,就不能这样优化了。如果没有volatile
关键字,编译器就不知道这种事情是否会发生。因此,为了安全起见,编译器不会进行高速缓存。这是在ANSI之前的情况。现在,如果声明中没有volatile
关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。
可以同时使用const
和volatile
限定一个值。例如,通常用const
把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile
。只能在声明中同时使用这两个限定符,它们的顺序不重要:
volatile const int loc;
const volatile int *ploc;
12.5.3restrict类型限定符
restrict
关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
int ar[10];
// 指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。
int *restrict restar = (int *) malloc(10 * sizeof(int));
// 指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不能把它设置为restrict。
int *par = ar;
for (int 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
限定符还可用于函数形参中的指针。这意味着编译器可以假设在函数体内的其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。
例如,c库有两个函数用于把一个位置上的字节拷贝到另一个位置:
void *memcpy(void *restrict s1, const void *restrict s2, size_t n);
void *memmove(void *s1, const void *s1, size_t n);
这两个函数都从位置
s2
把n
字节拷贝到位置s1
。memcpy()
函数要求两个位置不重叠,但是memmove()
没有这样的要求。声明s1
和s2
为restrict
说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足了memcpy()
无重叠的要求。memmove()
函数允许重叠,它在拷贝数据时不得不更小心,以防在使用数据之前就先覆盖了数据。
restrict
关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict
要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。
12.5.4_Atomic类型限定符(C11)
并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件
stdatomic.h
和threads.h
,提供了一些可选的管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象:
int hogs; // 普通声明
hogs = 12; // 普通赋值
// 可以替换成:
_Atomic int hogs; // hogs是一个原子类型的常量
atomic_store(&hogs, 12); // stdatomic.h中的宏