C_Primer第12章 存储类型、链接和内存管理

本章介绍以下内容

  • 关键字:auto、extern、static、register、const、volatile、restricted、_Thread_local、_Atomic
  • 函数:rand()、srand()、time()、malloc()、calloc()、free()
  • 如何确定变量的作用域(可见的范围)和生命期(它丰在多长时间)
  • 设计更复杂的程序

C语言能让程序员恰到好下地控制程序,这是它的优势之一。程序员通过C的内存管理系统指定变量的作用域和生命期。实现对程序的控制。合理使用内存储存数据是设计程序的一个要点。

11.1 存储类型

可以用存储期(storage duratin)描述对象,把谓存储期是指对象在内存中保留了多长时间。标识符用于访问对象,可能用作用域(scope )和链接(linkage)描述标识符,标识符的作用域和链接表明了程序的哪些 部分可以使用它。不同的存储类型具有不同的存储期、作用域和链接。标识符可以在源代码的多文件中共享、可用于特写文件的任意函数、可仅限于特写函数中使用,甚至只在函数中的某部分使用。对象可存在于程序的执行期,也呆以仅存在于它所在函数的执行期。对于并发编程,对象可以在特写线程的执行期存在。可以通过函数调用的方式显式分配和释放内存。

12.1.1 作用域

12.1.2 链接

12.1.3 存储期

12.1.4 自动变量

属于自动存储类型的变量具有自动存储期、块作用域且无链接。默认情况下,声明在块或函数头中的任何变量都属于自动存储类型。为了更清楚地表达你的意图(例如,为了表明有意覆盖一个外部变量定义,或者强调不要把该变量改为其他存储类型),可以显式使用关键字auto如下所示:

int main(void)
{
    auto int plox;
}

关键字auto是存储类型说明符(storage-class specifier)。auto关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用auto作为存储类型说明 符。

如果内层块中声明的变量与外层块中的变量同名会怎样?内层块会隐藏外层块定义。但是离开内层块后,外层块变量的作用域又回到了原来的作用域。程序清单12.1演示了这一过程。

程序清单12.1 hiding.c程序

// hiding.c --块中的变量
#include <stdio.h>
int loop( int );
int main()
{
	int x = 30; //原始的x
	printf("x in outer block: %d at %p\n",x ,&x);
	{
		int x =77; //新的x,隐藏了原始的 x
		printf("x in outer block: %d at %p\n",x ,&x);
	}
	printf("x in outer block: %d at %p\n",x ,&x);
	while (x++ < 33)  //原始的x
	{
		int x = 100;
		x++;
		printf("x in outer block: %d at %p\n",x ,&x);
	}
	printf("x in outer block: %d at %p\n",x ,&x);
	
	return 0;
}

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已经消失
}

程序输出

x in outer block: 30 at 0028FF3C
x in outer block: 77 at 0028FF38
x in outer block: 30 at 0028FF3C
x in outer block: 101 at 0028FF34
x in outer block: 101 at 0028FF34
x in outer block: 101 at 0028FF34
x in outer block: 34 at 0028FF3C

1.没有花括号的块

前面提到一个C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块,更完整地说,整个循环是它所在块的子块(sub-block),循环估是整个循环块的子块。与此类似,if语句是一个块,与其相关联的子语句是if语句 的子块。这些规则会影响到声明的变量和这些变量的作用域。程序清单 12.2演示了for循环中该特性的用法。

程序清单12.2 forc99.c程序

//forc99.c --新的C99规则
#include <stdio.h>
int main()
{
	int n = 8;
	printf("     Initially,n = %d at %p\n",n,&n);
	for (int n = 1; n < 3; n++)
		printf("     loop 1: n = %d at %p\n",n,&n);
	printf("After loop 1,n = %d at %p\n",n,&n);
	for (int n = 1; n < 3; n++)
	{
		printf("     loop 2: n = %d at %p\n",n,&n);
		int  n = 6;
		printf("     loop 2: n = %d at %p\n",n,&n);
		n++;
    }
	printf("After loop 2,n = %d at %p\n",n,&n);
}

程序输出:

     Initially,n = 8 at 0028FF3C
     loop 1: n = 1 at 0028FF38
     loop 1: n = 2 at 0028FF38
After loop 1,n = 8 at 0028FF3C
     loop 2: n = 1 at 0028FF34
     loop 2: n = 6 at 0028FF30
     loop 2: n = 2 at 0028FF34
     loop 2: n = 6 at 0028FF30
After loop 2,n = 8 at 0028FF3C

第1个for循环头中声明的n,其作用域到循环末尾。而且隐藏了原始的n,但是,离开循环后,原始的n又起作用了。

第2个for循环头中声明的n作为循环的索引,隐藏了原始的n。然后,在循环体中又声明了一个n,隐藏索引n。结束一迭代后,声明在循环何中的n消失,循环头使用的索引n进行测试。当整个循环结束时,原始的n又起作用了。现次提醒读者注意,没必要在程序中使用相同的变量名。如果用了,各变量的情况发上所述。

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变量第次都是1.这表明了初始化的不同。每次调用trystat()都会初始化fade,但是stay只在编译trystat()时被初始化一次。如果未显式初始化静态变量,它们会初始化为0。

下面两个声明很相似:

int fade = 1;

static int stay = 1;

第1条声明 确实是trystat()函数的一部分,每次调用该函数时都会执行这条声明。这是运行时的行为。

第2条声明实际上并不是trystat()函数的一部分。如果逐步调度该程序会发现。程序似乎跳过了这条声明。这是因为静态变量和外部变量在程序被入内存忆执行完毕。把这条声明放在trystat()函数中是为了告诉编译顺只有trystat()函数才能看到该变量。这条声明并未在运行是执行。

不能在函数的形参中使用static”

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

“局部静态变量”是描述具有块作用域的静态变量的另一个术语。阅读一些老的C文献时会发现。这种存储类型以称为内部静态存储类型(internal static storage class)。这里的内部指的函数内部,而非内部链接。

12.1.7 外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。该类型有时称为外部存储类型(external storage class)。属于该类型的变量称为外部变量(external variable)。把变量的定义性声明(defining declaration)放在在所有函数的外面便创建了外部变量。当然,为了指出该函数使用了外部变量,可以在函数中用关键字extern再次声明。如果一个源代码文件使用的外部变量定义 在另一个源代码文件中,则必须用extern在该文件中声明该变量。如下所示:

#include <stdio.h>

int Errupt;					/*外部定义的变量*/
double Up[100]              /*外部定义的数组*/
extern char Coal;			/*如果Coal被定义在另一个文件,*/
							/* 则必须这样声明*/
void next(void);

int main(void)
{
	extern int Errupt;		/*可选的声明*/
	extern double Up[];		/*可选的声明*/
	...
}
void next (void)
{
	...
}

外部变量具有静态存储期。因此,无论程序执行到main()、next()还是其他函数,数组Up及其值都一直存在。

下面3个示例演示了外部和自动变量的一些使用情况。示例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()可见。

/*示例1 */
int Hocus;
int magic();
int main(void)
{
	extern int Hocus;  //Hocus 之前已声明为外部变量
	...
}
int magic ()
{
						//并未在该函数中声明Hocus,但是仍可使用该变量
	...
}

在示例3中,创建了4个独立的变量。main()中的Hocus变量默认是自动变量,属于main()私有。magic()中的Hocus变量被显式声明为自动,只有magic()可用。外部变量Hocus对main()和 magic()均不可见,但是对该文件中未创建局部Hocus变量的其他函数可见。最后,Pocus是外部变量,magic()可见,便是main()i不可见,因为Pocus被声明在main()后面。

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

这3个示例演示了外部变量的作用域是:从声明处到文件结尾。除此之外,琮说明了外部变量的生命期。外部变量Hocus和Pocus在程序运行中一直存在,因为它们不受限于任何函数,不会在某个函数返回后就消失。

1.初始化外部变量

外部变量和自动 变量类似,也可以被被显式初始化。与自动变量不同的是,如果未初始化外部变量,它们会被自动 初始经为0. 这一原则也适用于外部定义的数组元素。与自动 变量的情况不同,只能使用常量表达式初始化文件域变量:

2.使用外部变量

下面来看一个使用外部变量的示例。假设有两个函数main()和critic()。它们都要访问变量units。

可以把units声明在这两个函数的上面。如程序清单12.4所示(注意:该例的目的是演示外部变量的工作原理,并非它的典型用法)。

程序清单 12.4 globalc程序

/*global.c --使用外部变量*/
#include <stdio.h>
int units = 0; /*外部变量*/
void critic(void);
int main(void)
{
	extern int units; /* 可选的重复声明*/
	printf("How namy pounds to a firkin of buuter\n");
	scanf("%D",&units);
	while(units != 56)
	   critic();
	printf("You must have looked it up!\n");
}
void critic(void)
{
	/*删除了可选的重复声明*/
	printf("No lock ,my friend, Try again.\n");
	scanf("%d",&units);
}

程序输出

How namy pounds to a firkin of buuter
12
No lock ,my friend, Try again.
56
You must have looked it up!

12.1.8 内部链接的静态变量

该存储类别的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(这点与外部变量相同),用存储类型说明 符static定义的变量具有这种存储类别。

static int svil = 1; //静态变量,内部链接
int main(void)
{

这种变量过去称为外部静态变量(external static variable),但是这个术语有点自相矛盾(这些变量具有内部链接)。但是,没有合适的新简称,所以只能用内部链接的静态变量(static variable with internal linkage)。普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存储类别说明符extern,在函数中重复声明任何具有文件作用域的变量。这样的声明产东会改变链接属性。考虑下在的代码:

int traveler = 1; //外部链接
static int stayome = 1; //内部链接
int mian()
{
    extern int traveler;     //使用定义在别处的traveler
    extern int stayhome;     //使用定义在别处的stayhome
    ...
}

对于该程序所在的翻译单元,trveler和stayhome都具有文件作用域,但是只有traveler可用于其他翻译单元(因为它具有外部链接)。这两个声明都使用了extern关键字,指明了main()中使用的这两个变量的定义都在别处,但是这并未改变stayhome的内部链接属性。

12.1.9 多文件

只有当程序由多个翻译单元组成时,才体现区别内部链接和外部链接的重要性。接下来简要介绍一下。

复杂的C程序通常由多个单独的源代码谁的组成。有时,这此文件可能要共享一个处部变量。C通过在一个文件中进行定义式声明 ,然后在其他文件中进行引用式声明来实现共享。也就是说,除了一个定义式声明外,其他声明都要使用extern关键字。而且,只有定义式声明才能初始化变量。

注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它(用extern关键字)。敢就是说,在某文件中对外部变量进行定义式声明只是单方面允许其他文件使用该变量,其他文件在用extern声明之前不能直接使用它。

过去,不同的编译器遵循不同的规则。例如,许多UNIX系统允许在多个文件中不使用extern关键字声明变量,前提是只有一个带初始化的声明。编译器会把文件中一个带初始化的声明视为变量的定义。

12.1.10 存储类别说明符

关键字static和extern的含义取决于上下文。C语言有6个关键字作为存储类型说明:auto、register、extern、_Thread_local和typedef。typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因。尤其是,在绝大多数情况下,不能在声明中使用多个存储类型说明符,所以这意味着不能使用多个存储类型说明符作为typedef的一部分。唯一例外的_Thread_local,它可以和static或extern一起使用。

auto说明符表明变量是自动存储期,只能用于块作用域的变量声明中。由 于在块中声明的变量本身就具有自动存储期,所以使用auto主要是为了明确表达这要使用与外部变量同名的局部就是不一样的意图。

register说明符也只用于块作用域的变量,它把变量归为寄存哭喊 存储类别,请求最快速度访问该变量 。同时,还保护了该变量的地址不被获取。

用static说明符创建的对象具有静态存储期,载入程序时创建对象,当程序结束时对象消失。如果static用于文件作用或声明,作用域受限于该文件。如果static用于块作用域声明,作用域则受限于该块,因此,只要程序在运行对象就存在并保留其值,但是只有在执行块内的代码时,才能通过标识符访问,块作用域的静态变量无链接,文件作用域 的静态变量具有内部链接.

extern说明符表明声明的变量定义在别处。如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接,这取决于该变量的定义式声明。

小结:存储类型

自动变量具有块作用域、无链接、自动存储期,它们是局部变量,属于其定义所在块(通常指函数)私有。寄存器变量的属性和自动变量相同,但是编译器会使用更快的内存或寄存器储存它们。不能获取寄存器变量的地址。

具有静态存储期的变量可以具有外部链接、内部链接或无链接。在同一个文件所有函数的外部声明的变量是外部变量、具有文件作用哉、外部链接和静态存储期。如果在这种声明前面加上关键字static,那么其声明的变量具有文件作用域、内部链接和静态存储期。如果在函数中用static 声明一个变量,则该变量具有作用域 无链接 静态存储期。

具有自动存储期的变量、程序在进入该变量的声明所在块时才为其分配 内存,在退出该块时释放之前分配的内存。如果未初始化、、自动变量中是垃圾值。程序在编译时为具有静态存储斯的变量分配内存,并在程序的运行过程中一直保留这块内存。如果未初始化,这样的变量会被设置为0.

具有块作用域的变量是局部的,属于包含该声明的块私有。具有文件作用域的变量对文件(或翻译单元)中位于其声明后面的所有函数可见。具有外部链接的文件作用域变量,可用于该程序的其他翻译单元,具有内部链接的文件作用域变量,只能用于其声明所在的文件内。

一面用一个简短的程序使用了5种存储类别。该程序包含两个文件(程序清单12.5和程序清单12.6),所以必须使用多文件编译。该示例仅为了让读者熟悉5种存储类型的用法,并不是提供设计模型,好的设计可以不需要 使用文件作用域变量。

程序清单 12.5 parta.c程序

// parta.c --不同的存储类别
// 与partb.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 executed %d times\n",count);
}

程序清单 12.6 partb.c程序

//partb.c --程序的其余部分
//与parta.c 一起编译
#include <stdio.h>

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

static int total = 0; 	//静态定义,内部链接
void accumulate(int k );	//函数原型

void accumulate(int k)   //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;
	}
}

程序输出:

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.1.11 存储类别和函数

函数也有存储类型,可以是外部函数(默认)或静态函数。C99新增了第3种类别---内联函数,将在第16章中介绍。外部函数可以被其他文件的函数访问,但是静态函数只能用于定义所在的文件。假设一个文件中包含了以下函数原型:

double gamma(double);        /* 该函数默认为外部函数 */
static double beta(int,int);
extern double delta(double,int);

在同一个程序中,其他文件中的函数可以调用gamma()和delta(),但是不能调用beta(),因为以static存储类型说明符创建的函数属于特写模块私有。这样做避免了名称冲突的问题,由于beta()受限于它所在的文件,所以在其他谁的中可以使用与之同名的函数。

通常的做法是:用extern关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。除非使用static关键字,否则一般函数声明都默认为extern。

12.2.12 存储类别的选择

12.2 随机数函数和静态变量

学习了不同存储类别的概念后,我们来看几个相关的程序。首先,来看一个使用内部链接的静态变量的函数:随机数函数。ANSI  C库提供了rand()函数生成随机数。生成随机数有多种算法,ANSI C 允许C实现针对特定机器使用最佳算法。然而,ANSI C 标准还提供了一个可移植的标准算法,在不同系统中生成相同的随机数。实际上,rand()是“伪随机数生成器”,意思是可预测生成数字的实际序列。但是,数字在其取值范围内均匀分布。

为了看清楚程序内部情况,我们使用可移植的ANSI 版本,而不是编译器内置的rand()函数。可移植版本的方案开始于一个“种子”数字。该函数使用种子生成新的数。这个新数又成为新的种子。然后新种子可用于生成更新的种子,以此类推。该方案要行之有效。随机数函数必须记录它上一次被调用时所使用的种子。这里需要一个静态变量。程序清单12.7演示了版本0.

程序清单12.7 rand0.c函数文件

/* rand-.c --生成随机数*/
/* 使用 ANSI C可移植算法*/
static unsigned long int next = 1; /* 种子 */

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

一个正整数对5取模运算,产生的余数范围必在0~4之间。所以,对正整数进行模a的取余运算,得到的余数范围在0~a-1,如果对取余运算的结果加上b,得到的结果就在b~a+b-1之间。所以,我们要得到[M,N]区间内的随机数,函数rand0()就要对(N-M+1)取余运算,再加上M。

在程序清单12.7中,静态变量next的初始值是1,其值在每次调用rand0()函数时都会被修改(通过魔术公式)。该函数是用于返回一个0~32767之间的值。注意,next是具有内部链接的静态变量(并非无链接)。这是为了方便稍后扩展本例,供同一个文件中其他函数共享。

程序清单12.8是测试rand0()函数的一个简单的驱动程序。

程序清单 12.8 r_drive0.c  驱动程序

/* r_drive0.c --测试 rand0()函数*/
/* 与rand0.c 一起编译*/
#include <stdio.h>
extern unsigned int rand0(void);

int main(void)
{
	int count;
	for (count = 0; count < 5; count++)
		printf("%d\n",rand0());
	return 0;
}

该程序也需要多文件编译。程序清单 12.7 和程序清单 12.8分别使用一个文件。程序12.8中的extern关键字提醒读者rand0()被定义在其他文件中,达个文件中不要求写出该函数原型。输出如下:

16838
5758
10113
17515
31051

程序输出的数字看上去是随机的,再将运行程序扣,输出如下:

16838
5758
10113
17515
31051

看来,这两次的输出完全相同,这体现了“伪随机”的一个方面。每次主程序运行,都开始了相同的种子1.可以引入另一个函数srand1()重置种子来解决这个问题。关键是要让next成为只供rand1()和srand1()访问的内部链接静态变量(srand1()相当于C库中的srand()函数)。把srand1()加入rand1()所在的文件中。程序清单12.9给出了修改后的文件。

程序清单 12.9 s_and_r.c文件程序

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

unsigned 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() 和strnd1()*/
/* 与s_and_r.c  一起编译*/
#include <stdio.h>
#include <time.h>
extern unsigned int 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("%d\n",rand1());
		printf("Please enter next seed (q to quit):\n");
	}
	printf("Done\n");
	
	return 0;
}
*/

int main(void)
{
	int count;
	unsigned seed;
	seed = (unsigned int)time(0);
	
	srand1(seed); /*重置种子*/
	for (count = 0; count < 10; count++)
	printf("%d\n",rand1());
	return 0;
}

12.3 掷骰子

我们将要模拟一个非常流行的游戏----掷骰子。骰子的形式多种多样,最普遍的是使用两个6面骰子。在一些冒险游戏中,会使用5种骰子:4面、6面、8面、12面和20面。聪明的古希腊人证明了只有6种正多面体,它们的所有面者阴相同的形状和大小。各种不同类型的骨子就是根据这些正多面体发展而来。也可以做成其他面数的,但是其所有的面不会都相等。因此各个面朝上的几率就不同。

计算机计算不用考虑几何的限制。所以可以设计任意面数的电子骰子。我们先从6面开始。

我们相获得1~6的随机数。然而,rand()生成的随机数在-~RAND_MAX之间。RAND_MAX被定义在stdlib.中,其值通常是INT_MAX。因此进行一些调整,方法如下。

1,把随机数求模6,获得的整数在0~5之间。

2.结果加1,新值在1~6之间。

3,为方便以后扩展,把第1步中的数字6替换成骰子面数。

下面的代码实现了这3个步骤:

我们还想用一个函数提示用户选择任意面数的骰子,并返回点数总和。如程序清单12.11所示。

程序清单12.11 diceroll.c程序

int rollen(int sides)  /*该函数属于该文件私有*/
{
	int roll;
	roll =rand() % sides +1;
	++roll_count;  			/*计算函数调用次数*/
	return roll;
}

我们还想用一个函数提示用户选择任意面数的骰子,并返回点数总和。如程序清单12.11所示。

程序清单12.11 diceroll.c程序

/* diceroll.c --掷骰子模拟程序*/
		/* 与mandydice.c 一起编译*/

#include "diceroll.h"
#include <stdio.h>
#include <stdlib.h>  /*提供库函数rand() 的原型*/

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

static int rollen(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 leaset 1 dice.\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),而不是声明该函数。我们效仿这一做法,把roll_n_dice()函数的原型放在diceroll.h头文件中。把文件名放在双引号中而不是尖括号中,指示编译器在本地查找文件,而不是到编译器存入标准头文件的位置去查找文件。“本地查找”的含义取决于具体的实现。一些常见的实现把头文件与源代码文件或工程谁的(如果编译器使用它们的话)放在相同的目录。程序清单12.12 是头文件中的内容 。

程序清单12.12 diceroll.h文件

//diceroll.h
extern int roll_count;

int roll_n_dice(int dice,int sides);

该头文件包含一个函数原型和一个extern声明。同于direroll.c文件包含了该文件,direroll.c实际上包含了roll_count的两个声明:

extern int roll_count;

int roll_count = 0; 

这样做没问题。一个变量只能有一个定义式声明,但是带extern的声明是引用式声明,可以有多个引用式声明 。

使用roll_n_dice()函数的程序都要包含diceroll.h头文件。包含该头文件后,程序便可使用roll_n_dice()函数和roll_count 变量。如程序清单12.13所示。

程序清单12.13 manydice.c文件

/* manydice.c --多次掷骰子模拟程序*/
		/* 与diceroll.c 一起编译*/

#include <stdio.h>
#include <stdlib.h>  	/*提供库函数rand() 的原型*/
#include <time.h>  		/*为time()提供原型*/
#include "diceroll.h"	/*为roll_ndice()提供原型,为roll_count变量提供声明*/

int main(void)
{
	int dice,roll;
	int sides;
	int status;
	
	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 name dice?\n");
		if((status = scanf("%d",&dice)) !=1) //输入的不是数字
		{
			if(status == EOF)
				break;
			else
			{
				printf("You should have entered an integer.");
				printf("Let's begin again.\n");
				while (getchar() != '\n')
					continue;             /*处理错误的输入*/
				printf("How many sides? Enter 0 to stop\n");
				continue; 						/*进入循环的下一轮迭代*/
			}
		}
		roll = roll_n_dice(dice,sides);
		printf("You have rolled a %d using %d %d-sideed dice.\n",
				roll,dice,sides);
		printf("How many sides? Enter 0 to stop\n");
		
	}
	printf("The rollem() function was callwd %d times.\n",
	     roll_count);        /*使用外部变量*/
	printf("GOOD FORTUNE TO YOU!\n");
	
	return 0;
}

 要与包含程序清单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 rolled a 7 using 2 6-sideed dice.
How many sides? Enter 0 to stop
6
How many dice?
2
You have rolled a 8 using 2 6-sideed dice.
How many sides? Enter 0 to stop
6
How many dice?
2
You have rolled a 10 using 2 6-sideed dice.
How many sides? Enter 0 to stop
0
The rollem() function was callwd 6 times.
GOOD FORTUNE TO YOU!

因为该程序使用了srand()随机生成随机数种子,所以大多数情况下,,即使输入相同也很难得到相同的输出。注意,manydice.c中的main()访问了定义在diceroll.c中的roll_count变量。

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

我们前面讨论的存储类型有一个共同之处:在确定用哪种存储类型后,根据自己制定好的内存管理规则,将自动选择其作用域和存储期。然后,还有更灵活地选择,即用库函数分配 和管理内存。

首先,回顾一下内存分配。所有程序都必须预留足够的内存来储存程序使用的数据。这些内存中有些是自动分配的。例如,以下声明:

float x;

char place[] = "Dancing Oxen Creek";

为一个float类型的值和一个字符串预留了中够的内存,或者可以显式指定分配一定数量的内存:

int plates[100];

该声明预留了100个内存位置,每个位置都用于存储 int类型的值。声明还为内存提供了一个标识符。因此,可以使用x或place识别数据。回忆一下。静态数据在程序载入内存时分配,而自动数据在程序执行块时分配,并在程序离开该块时销毁。

C能做的不止这些。可以在程序运行时分配更多的内存。主要的工具是malloc()函数,该函数接受一个参数:所需要的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说,malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。因为char表示1字节,malloc()的返回类型通常被定义为指向char的指针。然而,从ANSI C 标准开始,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]访问第2个元素,以此类推。根据前面所学的知识,可以使用数组表表示指针,也可以用指针来表示数组。

现在,我们有3种创建数组的方法。

声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素。可以用静态内存或自动内丰创建这种数组。

声明变长数组(C99新增的特性)时,用变量表达试表示数组的维度,用数组名访问数组的元素。具有这各占特性的数组只能在自动内存中创建。

声明一个指针,调用malloc(),将其返回值 赋给指针,使用指针访问数组的元素。该指针可以是静态的或自动的。

使用第2种和第3种方法可以创建动态数组(dynamic array),这种数组和普通数组不同,可以在程序运行选择数组的大小和分配内存。例如,假设n是一个整形变量。在C99之前不能这样做:

double item[n]; /* C99之前:n不允许是变量*/

但是,可以这样做:

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

如你所见,这比变长数组更灵活。

通常,malloc()要与free()配套使用,free()函数的参数是之前malloc()返回的地址,该函数释放之前malloc()分配 的内存。malloc()和 free()的原型都在stdlib.h产学研文件中。

使用malloc(),程序可以在运行时才确定数组大小。另外,如果内存分配 失败,可以调用exti()函数结束程序,其原型在stdlib.h中。EXIT_FAILURE,表示系统异常中止EXIT_SUCCESS,表示普通程序结束。

程序清单12.14 dyn_arr.c程序

/* 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?");
	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个元素的数组*/
	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)   //输出7次,打印换行,0~6。
			putchar('\n');
	}
	if(i % 7 !=0)  //不足7次,打印换行。
		putchar('\n');
	puts("Done.");
	free(ptd);
	
	return 0;
		
}

下面是该程序的运行示例。程序通过交互的方式让用户先确定数组的大小,我们设置数组大小为5,虽然我们后来输入了6个数,但程序也只处理前5个数。

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

该程序通过以下代码获取数组的大小:

if(scanf("%d",&max) != 1)
    {
        puts("Number not correctly entered -- bye.");
        exit(EXIT_FAILURE);
    }

接下来,分配足够的内存空间以储存用户要存入的所有数,然后把动态分配的内存地址赋给指针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(),不要依赖操作每户来清理。

使用动态数组有什么好处?从本例来看,使用动态数组给程序带来了更多灵活性。

12.4.1 free()的重要性

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配 的内存数量只会增加,除非用free() 进行释放。例如,假设有个创建数组临时副本的函数,其代码框架如下:

...
int main()
{
	double glad[2000];
	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()*/
}

第1次调用 gobble时,它创建了指针temp,并调用malloc()分配了16000字节的内存(假设double为8字节)。假设如代码注释所示,遗漏了free()。当函数结束时,作为自动变量的指针temp也会消失。便是它所指向的16000字节的内存却仍然存在。由于temp指针已被销毁,所以说这块内存,它也不能被重复使用,因为代码中没有调用free()释放这块内存。

第2次调用 gobble()时,它又创建了指针temp,并调用malloc()分配 了16000字节的内存。第一次分配的16000字节内存已经不可用,所以malloc()分配了另外一块16000字节的内存。当函数结束时,该内存块也无法再访问和再使用。

循环要执行1000次,所以在循环结束时,内存池中有16000万字节被用。实际上,也许在循环结束之臆忆耗尽的呢的内存。这类问题被称为内存泄漏(memory leak)。在函数末尾处调用free()函数可避免这类问题发生。

12.4.2 calloc()函数

12.4.3 动态内存分配和变长数组

12.4.4 存储类型和动态内存分配

静态存储类别所用的内存数量在编译时确定,保要程序琮在运行,就可访问储存在该部分的数据。该类型的变量在程序开始执行时被创建,在程序结束时被销毁。

然而,自动存储类型的变量在程序进入变量定义所在块时存在,在程序离开发起人时消失 。

动态分配的内存在调用maloc蔌相关函数存丰。在调用free()后释放。使用动态内存通常比使用栈内存慢。

总而言之,程序把静态对象、自动对象和动态分配的对象储存在不同的区域。

程序清单 12.15 where.c 程序

//where.c --数据被储存在何处?

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int static_store = 30;
const char * pcg = "String Literal";

int main()
{
	int auto_store = 40;
	char auto_string [] = "Auto char Array";
	int * pi;
	char * pc1;
	
	pi = (int *) malloc(sizeof(int));
	*pi = 35;
	pc1 = (char *)malloc(strlen("Dynamic String") + 1);
	strcpy(pc1,"Dynamic String");
	printf("static_store: %d at %p\n",static_store,&static_store);
	printf("auto_store:   %d at %p\n",auto_store,&auto_store);
	printf("         *pi: %d at %p\n",*pi,pi);
	
	printf("  %s at %p\n",pcg,pcg);
	printf(" %s at %p\n",auto_string,auto_string);
	printf("  %s at %p\n",pc1,pc1);
	
	printf("   %s at %p\n","Quoted String","Auoted String");
	
	
	return 0;
	
	
}

程序输出:

static_store: 30 at 00404004
auto_store:   40 at 0028FF34
         *pi: 35 at 006E1718
  String Literal at 00405064
 Auto char Array at 0028FF24
  Dynamic String at 006E1748
   Quoted String at 004050D2

如上所示,静态数据(包含字符串字面量)占用一个区域,自动变量占用另一个区域,动态分配的数据占用第3个区域(通常被称为内存堆或自由内存)。l

12.5 ANSI C 类型限定符

第4章和第10章中介绍过const。以const关键字声明对象,其值不能通过赋值或递增、递减来修改。在ANSI兼容的编译器中,以下代码:

const int nohcange; /*限定nochange 的值不能被修改*/

nochange = 12; /*不允许*/

编译器会报错。但是,可吧初始化const变量。因此,下面的代码没问题。

const int nochange = 12; /* 没问题*/

该声明让nochange成为只读变量。初始化后,就不能改变它的值 。

可以用const关键字创建不允许修改的数组:

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

1.在指针和形参声明中使用const

声明变通变量和数组时使用const关键字很简单。指针则复杂一些,因为要区分是限定指针本身为const 还是限定指针指向的值为const 。下面的声明:

const float *pf; /* pf指向一个float类型的const 值 */

创建 了pf指向的值不能被改变,而pt本身的值可以改变。例如,可以设置该指针指向其他const值。相比之下,下面的声明:

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

创建的指针pt本身的值不能更改。pt必须指向同一个地址,但是它所指向的值可以改变。下面的声明:

const float *const ptr;

表明ptr既不能指向别处,它所指向的值也不能改变。

还可把const放在第3个位置:

float const * pfc; //与const float * pfc;相同

如注释所示,把const放在类型名之后、*之前,说明该指针不能用于改变它所指向的值。简而言之,const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。

const关键字的常见用法是声明为函数形参的指针。假设有一个函数要调用display()显示一个数组的内容。要把数组名作为实参传递给该函数。但是数组名是一个地址。该函数可能会更改主调函数中的数据,但是下央的原型保证了数据不会被更改:

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

在函数原型和函数头,形参声明const int array[]与consg int *array相同,所以该声明表明不能更改array指向的数据。

ANSI C库遵循这种做法。如果一个指针仅用于给函数访问值,应将其声明为一个指向const限定类型的指针。如果要用指针更改主调函数中的数据,就不使用const关键字。例如,ANSI C 中的strcat()原型如下。

char *strcat(char * restrict s1,const char * restrict s2);

 strcat()函数在第1个字符串的末尾添加第2个字符串的副本。这更改了第1个字符串,但是未更改第2个字符串。上面的声明体现了这一点。

2, 对全局数据使用const

前面讲过,使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分都能更改数据。如果把数据设置为const,就可以避免这样的危险,因此用const限定符声明全局数据很合理。可以创建 const哟变量、const数组和const结构。

然而,在文件间共享const数据要小心。可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明。在其他文件中使用引用式声明(用extern关键字)。

另一种方案是,把const变量放在一个头文件中,然后在其他文件中包含该头文件。

12.5.2 volatile 类型限定符

volatile限定符告知计算机,代理(而不是变量所在的程序 )可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做寿什么,地址上的值都随时间的变化而改变。或者一个地址用于接受中一台计算机传入的信息。

volatile的语法和const一样:

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

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

12.5.3 restrict 类型限定符

restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。

12.5.1 _Atomic 类型限定符(C11)

并发程序设计把程序执行分成可以同是执行多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可先的头文件stdatomic.h 和threads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象 执行原子操作时,其他线程不能访问廖对象。

12.5.5 旧关键字的新位置

发布了139 篇原创文章 · 获赞 38 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/tjjingpan/article/details/90437654