chapter12 存储类别、链接和内存管理
0. 一些术语
对象: 存储了指定值的一块内存叫做对象
标识符: 标识符是一个名词,它指定了一个特定对象的内容;
-
左值: 指定对象的表达式。考虑下面的声明:
const char * pc = "Behold a string literal";
其中,pc是可以修改的左值, *pc和字符串字面量是不可修改的左值;
1.作用域:
描述变量可见性。作用域描述程序中可见标识符的区域。包括块作用域、函数作用域、函数原型作用域和文件作用域;
-
块作用域: 函数形参具有块作用域,也属于函数体这个块,此外定义在函数体内的局部变量也具有块作用域。
块作用域变量的可见范围是从定义处到包含该定义的块的末尾。 - 函数作用域: 仅用于goto语句的标签;这意味着即使一个标签首次出现在函数内层,其作用域也延伸至整个函数。
- 函数原型作用域: 用于函数原型中的形参名,其可见范围从形参定义处开始,到函数原型声明结束为止。
-
文件作用域: 变量定义在函数外面,具有文件作用域。其可见范围是从定义处开始,到整个文件的末尾。
具有文件作用域的变量又称"全局变量",全局变量在当前"翻译单元"内可见。
2.链接:
描述变量可见性。C变量有三种链接: 外部链接、内部链接(文件作用域的变量具有前两种链接属性之一)、无链接(块
作用域、函数作用域、函数原型作用域变量都是无链接变量)。
- 外部链接: 外部链接变量可在多文件程序中使用。
- 内部链接: 内部链接变量只能在当前"翻译单元"中使用。
int giants = 5; //文件作用域,外部链接,同一程序全部翻译单元可用。
static int godgers = 3; //文件作用域,内部链接,只能在当前翻译单元使用。
int main()
{
...
}
3.存储期:
描述变量生存期。C对象有四种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期。
- 静态存储期: 如果对象具有静态存储期,它在程序执行期间一直存在。所有文件作用域变量,不论是否用static声明,亦即具有内\外部链接,都具有静态存储期。
然而,块作用域变量只要它用static声明,也可以具有静态存储期。
静态存储期变量,在整个程序执行期间有效,在程序载入时创建内存,在程序结束时释放内存。 - 线程存储期: 用于并发程序设计,具有线程存储期的变量,从声明开始到线程结束一直存在。
以关键字_Thread_local声明一个对象时,每个线程都获得该变量的私有备份。 - 自动存储期: 没有被static修饰的局部变量,都具有自动存储期。
- 动态分配存储期:...
4.存储类别:
存储类别是存储期、作用域、链接三者的综合搭配。存储类别有以下几种:
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无 | 块内 |
寄存器 | 自动 | 块 | 无 | 块内,使用关键字register |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 内部 | 所有函数外,使用关键字static |
静态无链接 | 静态 | 块 | 无 | 块内,使用关键字static |
auto(自动)存储类别: 具有自动存储类别的变量具有自动存储期、块作用域、无链接。
通常,声明在块或函数头中的变量都属于自动存储类别变量。可以显式地用auto修饰一个变量,以表示覆盖一个同名外部变量。
int plox;
int main()
{
auto int plox; /覆盖同名外部变量/
int repid;
int tents = 5;
}
自动变量不会初始化,除非你显式地初始化,所以不要指望上面的repid是0,它可能是之前占用这段内存的变量的值。寄存器register存储类别:
和自动变量一样具有块作用域、无链接、自动存储期,但是寄存器变量放在CPU的寄存器中,而不是内存中,不能获取其地址,但其速度更快。静态无链接存储类别:属于此类别的变量称之为“块作用域的静态变量”,具有无链接、静态存储期、块作用域。它是static修饰的局部变量,注意静态变量如果未显式初始化,会被初始化为0。静态变量和外部变量只会在编译时初始化一次,不会重复初始化。
注意, 不能在函数形参中使用static。静态内链接存储类别:属于此类别的变量称之为“内部链接的静态变量”,具有内链接、静态存储期、文件作用域。在函数中使用存储类别说明符extern重复声明任何具有文件作用域的变量,不会改变它们的链接属性。
int traveler =1; // 外部链接
static int stayhome =1; //内部链接
int main()
{
extern int traveler; //引用的变量定义在别处,不会改变其外链接属性
extern int stayhome; //引用的变量定义在别处,不会改变其内链接属性
}
-
静态外链接存储类别:属于此类别的变量称之为“外部变量”,具有外链接、静态存储期、文件作用域。注意,如果一个源代码文件使用的外部变量定义在另一个源码文件中,则必须使用extern在该文件中声明该变量;在块中,可以但不必要用extern关键字再次声明一次该外部变量。注意,块中声明的同名变量会隐藏外部变量。
【外部变量的定义和说明】:
int tern = 1; /*第一次声明,定义式声明,为变量预留了存储空间*/
int main()
{
extern int tern; /*第二次声明,引用式声明,不是定义,extern关键字表明这不是定义,它指示编译器去别处查询其定义*/
如果这么写:
extern int tern; /*这只会告诉编译器,外部变量可能定义在该程序的别处,这里不会真正定义外部变量,只会引用现有的外部定义*/
int main()
{
int units = 0; /*外部变量,具有外部链接的静态变量,具有文件作用域、静态存储期;外部变量会自动初始化为0*/
int main(int argc, char * argv[])
{
extern int units; /*可选的重复申明,注意,如果只省去extern关键字,表示申明了一个auto变量,覆盖了原始变量*/
}
5. 多文件
C通过在一个文件进行定义式声明,在另外的文件进行引用式声明来实现共享。注意,要是用定义在其它文件中的外部变量,必须先用extern做引用式声明,否则不能使用;
6.存储类别说明符
- auto
为了说明变量是自动存储期,只能用于块作用域变量的声明中。使用auto的主要目的是为了局部变量覆盖同名的外部变量。 - register
- static
- extern
- Thread_Local
- typedef
7.存储类别和函数
函数也有存储类别,可以是外部函数(默认)或静态函数。C99中新增了第3种类别--内联函数。外部函数可以被其他文件的函数访问,但静态函数只能用于其定义所在的文件。
double gamma(double); /*该函数默认为外部函数*/
static double beta(int, int); //同一程序其他文件不能调用beta()
extern double delta(double, int); //同一程序的其他文件可调用delta()和gamma()
通常的做法:用extern声明定义在其他文件中的函数。
8.示例:多文件联合编译,使用多种存储类别
- storageclassa.c
//storageclassa.c
#include <stdio.h>
#include <stdlib.h> /*为库函数srand()提供原型*/
#include <time.h> /*为time()提供原型*/
#include "diceroll.h" /*双引号,提醒编译器在本地查找文件,而不是到存放标准头文件的地方查找*/
// 和storageclassb.c联合编译
int count =0; // 文件作用域,外部链接
int roll_count = 0; // 文件作用域,外部链接
int main(int argc, char* argv[])
{
int value; //自动变量
register int i; //寄存器变量
puts("***************** 例子:理解静态变量只在程序载入内存时初始化一次 ******************");
for (int count=1; count <=3; count++ ) // C99支持在块头定义变量
{
printf("Here comes the %d iteration\n", count);
trystat();
}
puts("***************** 例子:计算小计和总计 ******************");
printf("Enter a positive integer (0 to quit): \n");
while (scanf("%d", &value) == 1 && value >0 )
{
++count;
for(i=value; i>=0; i--)
{
accumulate(i);
}
printf("Enter a positive integer (0 to quit): \n");
}
report_count();
puts("***************** 例子:掷骰子和随机数 ******************");
int sides, dice, status;
int roll;
srand((unsigned int)time(0));
printf("Enter the number of sides per die, 0 to quit\n");
while(scanf("%d",&sides)==1 && sides >0)
{
printf("How many dices?\n");
if ((status = scanf("%d", &dice)) != 1)
{
if (status == EOF)
break;
else
{
printf("You should enter an integer\n");
printf("let's begin again\n");
while(getchar()!='\n')
continue; // 清除输入缓冲区无效的输入
printf("How many dices?\n");
continue;
}
}
roll = roll_n_dice(dice,sides);
printf("You have rolled a %d using %d %d-sides dices\n", roll, dice, sides);
printf("Enter the number of sides per die, 0 to quit\n");
}
report_roll_count();
puts("*****************malloc()和free(),动态内存分配******************");
/*
【知识点】
动态分配内存的存储期从调用malloc()开始到调用free()释放内存结束,必须配套使用;
free()的参数是一个指针,指向malloc()函数返回的地址;
不能用free()释放用其他方式(例如声明一个数组)分配的内存;
malloc()和free()的原型都在stdlib.h头文件中。
*/
int max;
int number;
double * ptd;
int j = 0;
printf("Enter the maximum number of type double entries:\n");
if (scanf("%d",&max) != 1)
{
puts("Number not correctly entered -- bye.");
exit(EXIT_FAILURE);
}
ptd = (double *)(malloc(max*sizeof(double)));
if (ptd ==NULL)
{
puts("Memory allocation failed. Goodbye.");
exit(EXIT_FAILURE);
}
/*ptd现在指向有max元素的数组*/
printf("Please enter the values(q to quit)\n");
while (j < max && scanf("%lf", &ptd[j]) == 1)
++j;
printf("Here are your %d entries:\n", number = j);
for (j = 0; j< number; j++)
{
printf("%7.2f ", ptd[j]);
if (j % 7 == 6)
putchar('\n');
}
if (j%7 !=0)
putchar('\n');
puts("Done");
free(ptd);
puts("*********************** calloc()函数 **************************");
/*
【知识点】:
函数malloc()和calloc()都可以用来动态分配内存空间,但两者稍有区别。
calloc(m, n) 本质上等价于:
p = malloc(m * n);
memset(p, 0, m * n);
malloc()函数有一个参数,即要分配的内存空间的大小:
void *malloc(size_t size);
calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小。
void *calloc(size_t numElements,size_t sizeOfElement);
如果调用成功,函数malloc()和函数calloc()都将返回所分配的内存空间的首地址。
函数malloc()和函数calloc()的主要区别是前者不能初始化所分配的内存空间,而后者能。如果由malloc()函数分配的内存空间原来没有被
使用过,则其中的每一位可能都是0;反之,如果这部分内存曾经被分配过,则其中可能遗留有各种各样的数据。也就是说,使用malloc()函数
的程序开始时(内存空间还没有被重新分配)能正常进行,但经过一段时间(内存空间还已经被重新分配)可能会出现问题。
函数calloc()会将所分配的内存空间中的每一位都初始化为零,也就是说,如果你是为字符类型或整数类型的元素分配内存,那麽这些元素将
保证会被初始化为0;如果你是为指针类型的元素分配内存,那麽这些元素通常会被初始化为空指针;如果你为实型数据分配内存,则这些元素
会被初始化为浮点型的零。
*/
puts("****************动态内存分配和变长数组******************************");
/*变长数组和调用malloc()创建动态数组很类似,但是也有区别:
变长数组是自动存储类别,程序在离开定义变长数组的块时,变长数组所占内存将释放,不必使用free();
用malloc()创建的数组不必局限在一个函数内访问,所以可以这样做:被调函数用malloc创建一个数组并返回指针,供
主调函数访问,然后主调函数在末尾调用free()释放之前被调函数分配的内存。free()不能释放同一块内存两次。
*/
int n =5;
int m =6;
int ar2[n][m]; //n*m的变长数组
int (*p2)[6]; //C99以前的写法,p2指向一个内含6个int值的数组
int (*p3)[m]; //C99以后的写法,p3指向一个内含m个int值的数组
p2 = (int (*)[6])(malloc(n*6*sizeof(int)));
p3 = (int (*)[m])(malloc(n*m*sizeof(int)));
ar2[1][2] = p2[1][2] = 12;
return 0;
}
- storageclassb.c
//storageclassb.c
#include <stdio.h>
#include "diceroll.h"
//和storageclassa.c一起编译
static int total = 0; //静态变量,内部链接,文件作用域
static int rollem(int);
static unsigned long int next = 1; //静态内链接变量,仅本翻译单元可用
void trystat()
{
int fade =1;
static int stay = 1; /*块作用域的静态变量*/
printf("fade=%d stay=%d\n", fade++, stay++);
}
// 计算小计和总计
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;
}
}
void report_count()
{
printf("Loop executed %d times\n", count);
}
void report_roll_count()
{
printf("Loop executed %d times\n", roll_count);
}
//掷骰子,输入骰子的面数,返回掷得的值
//静态函数,属于文件私有
static int rollem(int sides)
{
int count;
count = rand()% sides +1;
return count;
}
//掷dice次骰子,骰子面数是sides,返回总值
int roll_n_dice(int dice,int sides)
{
int d;
int total =0;
if (dice<1)
{
printf("Need at least one dice.\n");
return -1;
}
if (sides <2)
{
printf("Need at least 2 sides.\n");
return -2;
}
for (d=0; d< dice; d++)
total += rollem(sides);
return total;
}
//自己实现的srand1(),模拟srand()库函数生成种子
void srand1(unsigned int seed)
{
next = seed;
}
//自己实现的rand1(),模拟rand()生成随机数
int rand1(void)
{
next = next * 1103515245 + 12345;
return (unsigned int)(next/65536) % 32768;
}
- diceroll.h
//diceroll.h
#ifndef DICEROLL_H_INCLUDED
#define DICEROLL_H_INCLUDED
extern int count; //引用式声明
extern int roll_count; //引用式声明
void trystat();
void accumulate(int k);
int roll_n_dice(int, int);
void srand1(unsigned int);
int rand1(void);
void report_count();
void report_roll_count();
联合编译:
gcc -std=c99 storageclassa.c -o storageclassa.o
gcc -std=c99 storageclassb.c -o storageclassb.o
gcc storageclassa.o storageclassb.o -o storageclass
9.分配内存:malloc()和free()
【知识点】
- 动态分配内存的存储期从调用malloc()开始到调用free()释放内存结束,必须配套使用;
- free()的参数是一个指针,指向malloc()函数返回的地址;
- 不能用free()释放用其他方式(例如声明一个数组)分配的内存;
- malloc()和free()的原型都在stdlib.h头文件中。
【malloc()和calloc()的联系和区别】:
- 函数malloc()和calloc()都可以用来动态分配内存空间,但两者稍有区别。
- calloc(m, n) 本质上等于malloc()再加上元素初始化:
p = malloc(m * n);
memset(p, 0, m * n);
- malloc()函数有一个参数,即要分配的内存空间的大小:
void *malloc(size_t size);
- calloc()函数有两个参数,分别为元素的数目和每个元素的大小,这两个参数的乘积就是要分配的内存空间的大小。
void *calloc(size_t numElements,size_t sizeOfElement);
如果调用成功,函数malloc()和函数calloc()都将返回所分配的内存空间的首地址。 - 函数malloc()和函数calloc()的主要区别是前者不能初始化所分配的内存空间,而后者能。
10. 动态分配内存创建数组和变长数组的区别
变长数组和调用malloc()创建动态数组很类似,但是也有区别:
1. 变长数组是自动存储类别,程序在离开定义变长数组的块时,变长数组所占内存将释放,不必使用free();
2. 用malloc()创建的数组不必局限在一个函数内访问,所以可以这样做:被调函数用malloc创建一个数组并返回指针,供主调函数访问,然后主调函数在末尾调用free()释放之前被调函数分配的内存。free()不能释放同一块内存两次。
int n =5;
int m =6;
int ar2[n][m]; //n*m的变长数组
int (*p2)[6]; //C99以前的写法,p2指向一个内含6个int值的数组
int (*p3)[m]; //C99以后的写法,p3指向一个内含m个int值的数组
p2 = (int (*)[6])(malloc(n*6*sizeof(int)));
p3 = (int (*)[m])(malloc(n*m*sizeof(int)));
ar2[1][2] = p2[1][2] = 12;
free(p2);
free(p3);
11. ANSI C类型限定符
C99以前:const和volatile
C99新增:restrict
C11新增:_Atomic(C11提供一个可选库stdatomic.h,以支持并发程序设计)
- const类型限定符
const可以修饰变量、数组、指针、函数形参中的指针、全局变量。下面依次说明:
- const修饰变量,变量即为常量,不可修改。
const int nochange;
nochange =12; /*错误,不允许修改*/
const int nochage =12; /*初始化const变量没问题*/
2.const修饰数组,表示该数组不能修改。
const int days1[12] = {31,28,18,15};
3.const修饰指针,分为下面三种情形:
(1)const float * pf; /*不能用pf去修改其指向的值,但可以指向别处*/
(2)float * const pt; /*pt指针本身不能改变,即ptr不能指向别处*/
(3)const float * const ptr; /*ptr既不能指向别处,也不能用ptr去修改其指向的值*/
综上所述,const 放在左边的,表示不用用指针去修改其指向的值;const放在右边的,表示指针不能指向别处。
- const修饰函数形参中的指针,表示不能通过指针去修改主调函数中的值。例如下面的声明:
void display(const int array[], int limit);
由于我们传入的是指针或数组名,不论传入哪个传入的都是地址,所以如果不加const,该函数会更改主调函数中的数据。
strcat()函数的声明如下:
char * strcat(char * restrict s1, const char * restrict s2);
在s1末尾添加s2字符串的副本,这改变了s1,但是s2未改变,所以用const去修饰s2。 - const修饰全局数据,能防止全局数据被篡改。在文件中使用全局变量要十分小心,因为任何部分都可能修改它,因此用const修饰全局数据可以避免其被修改。然后在文件中共享const数据要小心,可采用以下两种策略之一:
【策略一】:在一个文件中用const修饰外部变量的定义式声明,在其它文件中用extern const做引用时声明:
/*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 char * MONTHS [];
【策略二】:用static const修饰全局变量并放到头文件中,别的文件引用它即可,这相当于每个文件拥有它的一个副本,而且这个副本只对当前文件可见。如果头文件中去掉了static, 这将导致包含它的所有文件中都有一个相同标识符的定义式声明,这是不允许的。例如:
/*constant.h --定义了一些外部const变量*/
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 --使用定义在别处的外部const变量*/
#include "constant.h"
/*file2.c --使用定义在别处的外部const变量*/
#include "constant.h"
- volatile类型限定符
volatile限定符告诉计算机,代理(而不是变量所在的程序)可以改变变量的值,所以编译器不要进行高速缓存(caching)优化。举个例子,假设有以下代码:
var1 = x;
/*一些不使用x的代码*/
var2 = x;
编译器注意到上面使用了2次x,而且未改变其值,所以它会先将x临时存到寄存器,等到var2需要x时再从寄存器拿出来,以节约时间。通常这很不错,但如果在多个程序或多个线程同时运行的情况下,代理有可能修改x的值,而你并不知道。所以这里用volatile告知编译器,不要去使用caching优化代码。
可以在变量声明中同时使用const和volatile这两个限定符,例如,通常用const把硬件时钟设置为程序不可改变的变量,但是可以通过代理改变(自己改不了,但别人可能会改)。
volatile const int loc;
const volatile char * ploc;
- restrict类型限定符
restrict关键字允许编译器优化某部分代码以支持更好的计算,它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
restrict可以修饰指针变量,例如:
int ar[10];
int * restrict restar = (int *)(malloc(10*sizeof(int)));
int * par = ar;
这里restar是访问malloc()所分配内存唯一且初始的方式,所以可以用restrict修饰它,而par既不是访问ar数组中数据的初始方式,也不是唯一方式,所以不能用restrict修饰。那么restrict有什么用呢?考虑下面的例子:
for (n=0;n<10;n++)
{
par[n] += 5;
restar[n] += 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}
由于之前用restrict修饰了restar,这里编译器可以把涉及restar的语句替换成如下:
restar[n] += 8;
其他变量如果也做类似替换,显然会出现问题,所以restrict可以提示编译器,该指针是访问数据的唯一且初始的方式,和该指针相关的计算可以做一些优化。
restrict限定符还可以修饰函数形参中的指针。这意味着编译器可以假定函数体内的其他标识符不会修改该指针指向的数据,而且编译可以尝试对齐优化。例如,C库有两个库函数memcpy()和memmove()都可以把一个位置上的字节拷贝到另一个位置,C99中这两个函数的原型如下:
void * memcpy(void *restrict s1, constr void * restrict s2, size_t n);
void * memmove(void * s1, const void * s2, size_t n)
这两个函数都从位置s2拷贝n个字节到s1位置,但是memcpy()函数要求两个位置不能重叠,但是memmove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问数据的唯一方式,所以它们一定不能访问相同块的数据,这保证了memcpy()的要求。
- _Atomic类型限定符
并发程序设计中,如果一个线程要对一个原子类型的对象执行原子操作,其他线程不能访问该对象,此时要用_Atomic修饰该变量:
_Atomic int hogs; //原子类型的变量
atomic_store(&hogs, 12); //stdatomic.h中的宏,存储过程是个原子过程