第九章 存储类别、链接和内存管理


存储类别

C提供了多种不同的模型或存储类别(storage class) 在内存中储存数据。

从硬件上看,储存值需要占用一定的物理内存,C语言把这样的一块内存称为对象(object)

从软件上看,程序需要一种方法访问对象。

程序通过标识符或表达式指定内存中的对象。那些指定对象的标识符或表达式被称为左值。

/* 通过定义标识符来指定内存中的对象 */
int entity = 3;
/* 变量名即是一个标识符, 该声明创建了一个名为 entity 的标识符(identifier)
   C程序通过标识符 entity 指定硬件内存中的对象, entity 是一个左值 */

/* 标识符不是指定对象的唯一途径 */
int * pt = &entity;
int ranks[10];
/* pt 是一个标识符, 它指定了一个存储地址的对象, 是一个左值
   *pt 是一个表达式, 它指定的对象与 entity 指定的对象相同, 是一个左值
   (ranks + 2 * entity) 是表达式, 它不指定内存位置上的内容, 不是左值
   *(ranks + 2 * entity) 是表达式, 它指定了 ranks 数组的第7个元素, 是一个左值 */

如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue)

const char * pc = "Behold a string literal!";
/* 标识符pc是一个可修改的左值
   *pc 是一个左值, 但不是一个可修改的左值
   字符串字面量本身指定了储存字符串的对象, 所以它也是一个左值, 但不是可修改的左值 */

作用域(scope)和链接(linkage) 用于描述程序中标识符可被访问的区域。
存储期(storage duration) 用于描述标识符指定的对象在内存中保留了多长时间。


作用域

四种作用域:

  • 块作用域(block scope)
  • 函数作用域(function scope)
  • 函数原型作用域(function prototype scope)
  • 文件作用域(file scope)

块作用域

什么是块:

  • 一对花括号括起来的代码区域是一个块(复合语句)。
  • 函数定义头的形参属于函数体这个块。
  • 整个循环块(从for开始)是它所在块的子块,循环体是整个循环块的子块(C99新特性)。
  • if 语句是一个块,与其相关联的子语句是if语句的子块(C99新特性)。

在块中声明的变量具有块作用域,从定义处到所在的块的末尾,可被该块及其包含的块访问。块作用域的变量又称为局部变量

如果内层块中的变量与外层块中的变量同名,内层块会隐藏外层块的定义,离开内层块后解除隐藏。

// 内层块隐藏外层块的定义
#include<stdio.h>
int main(void)
{
	int x = 30; // 原始的 x
	printf("x in outer block: %d at %p\n", x, &x);
	{
		int x = 77; // 新的 x,隐藏了原始的 x
		printf("x in inner 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,隐藏了原始的 x
		x++;
		printf("x in while loop: %d at %p\n", x, &x);
	}
	printf("x in outer block: %d at %p\n", x, &x);
	return 0;
}
/* while循环的测试条件中使用的是原始的x,每次进入循环都创建了一个新的x */

在这里插入图片描述

C99特性:作为循环或if语句的一部分,即使不使用花括号({}),也是一个块。更完整地说,整个循环是它所在块的子块(sub- block),循环体是整个循环块的子块。
在这里插入图片描述

// forc99.c -- 新的 C99 块规则
#include<stdio.h>
int main(void)
{
	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 index n = %d at %p\n", n, &n);
		int n = 6;  //循环体是循环块的子块,前面声明的两个n被此处的n隐藏
		printf("loop 2: n = %d at %p\n", n, &n);
		n++;
	}
	
	printf("After loop 2, n = %d at %p\n", n, &n);
	return 0;
}
/* 第2个for循环头中声明的n作为循环的索引,隐藏了原始的n。
   然后,在循环体中又声明了一个n,隐藏了索引n */

/* 有些编译器并不支持C99/C11的这些作用域规则, 有些编译会提供激活这些规则的选项,如
   gcc:    gcc –std=c99 forc99.c 
   类似的,gcc或clang都要使用  -std=c1x 或 -std=c11 激活C11特性 */

在这里插入图片描述

函数作用域

仅用于描述goto语句的标签。即使一个标签首次出现在函数的内层块中, 它的作用域也延伸至整个函数。

int main(void)
{
    int i=0;
    {
one:
        printf("i = %d  think you!\n", i++);
        if(i>5)
            goto two;
    }
    goto one;  // 跳转到one标记的位置去执行
two:
    printf("over i = %d", i);
    return 0;
}

函数原型作用域

用于描述函数原型中的形参名,范围是从形参定义处到原型声明结束。

int mighty(int mouse, double large); 

编译器处理函数原型中的形参时只关心它的类型, 而形参名无关紧要。 即使有形参名, 也不必与函数定义中形参名相匹配。 只有在变长数组中, 形参名才有用:

 void use_a_VLA(int n, int m, ar[n][m]); 
 // 方括号中必须使用在函数原型中已声明的形参名

文件作用域

在函数外声明的变量具有文件作用域,从它的定义处到所在文件的末尾均可被访问。 由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)

#include <stdio.h> 
int units = 0; /* 变量units具有文件作用域, main()和critic()函数都可以使用它 */ 
void critic(void); 
int main(void) { 
	... 
	return 0;
} 
void critic(void) { 
	...
}

翻译单元和文件

通常在源代码(.c扩展名) 中包含一个或多个头文件(.h 扩展名) 。 头文件会依次包含其他头文件。 C预处理实际上是用包含的头文件内容替换#include指令。 所以, 源代码文件和所有包含的头文件都可以看成是一个单独文件。 这个文件被称为翻译单元 (translation unit) 。 一个文件作用域变量的实际可见范围是整个翻译单元。

如果程序由多个源代码文件组成, 那么该程序也将由多个翻译单元组成。 每个翻译单元均对应一个源代码文件和它所包含的文件。

链接

C变量有三种链接属性:

  • 无链接:块内声明的变量,属于定义它的块私有
  • 外部链接:默认的文件作用域变量,可被其它翻译单元使用
  • 内部链接:static 修饰的文件作用域变量,只在一个翻译单元中使用

使用 static 关键字修饰文件作用域变量:

int giants = 5;     // 文件作用域, 外部链接, 外部文件可访问
static int dodgers = 3; // 文件作用域, 内部链接, 属于本文件私有
int main(){
	...
	return 0;
}
...

存储期

作用域和链接描述了标识符的可见性。存储期描述了通过这些标识符访问的对象的生存期。C对象有4种存储期

  • 静态存储期
  • 线程存储期
  • 自动存储期
  • 动态分配存储期

静态存储期

具有静态存储期的对象在程序的执行期间一直存在

所有文件作用域变量自动(且必须)具有静态存储期。

static 修饰的块作用域变量具有静态存储期。

字符串常量具有静态存储期

线程存储期

具有线程存储期的对象, 从被声明时到线程结束一直存在。线程存储期用于并发程序设计, 程序执行可被分为多个线程。

以关键字 _Thread_local声明一个对象时,每个线程都获得该变量的私有备份。

自动存储期

程序执行可分为多个块,当程序进入这些块时,为块内定义的变量分配内存;当退出这个块时,释放刚才为变量分配的内存。这些块内的变量就具有自动存储期。

具有自动存储期的都是块作用域的变量(除非使用static修饰),这些变量又叫自动变量。

变长数组稍有不同,它们的存储期从声明处到块的末尾,而不是从块的开始处到块的末尾。

动态分配存储期

动态分配存储期在后面


5种存储类别

在这里插入图片描述

自动变量

属于自动存储类别的变量具有自动存储期、 块作用域且无链接。 默认情况下, 声明在块或函数头中的任何变量都属于自动存储类别。

auto是自动存储类别说明符。auto 只能用于块作用域的变量声明,由于在块或函数头中声明的变量本身就是自动存储类别,所以auto是可缺省的。

使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图。

自动存储类别的变量不会自动初始化。

寄存器变量

对于使用最频繁的变量可以使用 register 请求 编译器将相应变量存放为register 类型。

绝大多数方面, 寄存器变量和自动变量都一样。 它们都是块作用域、 无链接和自动存储期。 由于寄存器变量储存在寄存器而非内存中, 所以无法获取寄存器变量的地址。

因为寄存器的数量有限,所以有可能并不会将其放入寄存器中,但是编译器并不会显示错误。在这种情况下, 寄存器变量就变成普通的自动变量。 即使是这样, 仍然不能对该变量使用地址运算符。

int main(void) { 
    register int quick; 

块作用域的静态变量

静态变量(static variable) 听起来自相矛盾, 像是一个不可变的变量。 实际上, 静态的意思是该变量在内存中原地不动, 并不是说它的值不变。

可以创建具有静态存储期、 块作用域的局部变量。 这些变量和自动变量一样, 具有相同的作用域, 但是程序离开它们所在的块后, 这些变量不会消失。 这种变量具有块作用域、 无链接, 但是具有静态存储期。

void trystat(void) { 
    int fade = 1; 
    static int stay = 1; //块作用域的静态变量声明
    printf("fade = %d and stay = %d\n", fade++, stay++);
}
/* static int stay = 1; 
   这条实际上并不是trystat()函数的一部分。这是因为静态变量 和外部变量在程序被载入内存时已执行完毕。 
   把这条声明放在trystat()函数中是为了告诉编译器只有trystat()函数才能看到该变量。这条声明并未在运行时执行 */

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

“局部静态变量”是描述具有块作用域的静态变量的另一个术语

外部链接的静态变量

外部链接的静态变量具有文件作用域、 外部链接和静态存储期。 该类别有时称为外部存储类别(external storage class) , 属于该类别的变量称为外部变量(external variable)

把变量的定义性声明(defining declaration) 放在在所有函数的外面便创建了外部变量,外部变量可以在本文件中使用,也可以被其它文件使用。

如果一个源代码文件使用的外部变量定义在另一个源代码文件中, 必须用extern在该文件中声明该变量。 为了指出函数使用了某个外部变量, 可以在函数中用关键字extern再次声明。

int Errupt;       /* 外部定义的变量 */
double Up[100];   /* 外部定义的数组 */ 
extern char Coal; /* 如果Coal被定义在另一个文件, 则必须这样声明*/
void next(void);
int main(void) {
	extern int Errupt;  /* 可选的声明 */
	extern double Up[]; /* 可选的声明, 不用指明数组大小, 因为第1次声明已经提供了数组大小信息。*/
	extern char Coal;   /* 可选的声明 */
	/* 此处使用 extern 只是明确表明这个函数要使用这些外部变量或数组 */

	int Errupt;
	/* 此处定义了一个和外部变量同名的局部变量,此时外部变量被隐藏 */
}

初始化外部变量

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

外部名称

C99和C11标准都要求编译器识别局部标识符的前63个字符和外部标识符的前31个字符。 这修订了以前的标准, 即编译器识别局部标识符前31个字 符和外部标识符前6个字符。 你所用的编译器可能还执行以前的规则。 外部变量名比局部变量名的规则严格, 是因为外部变量名还要遵循局部环境规则, 所受的限制更多。

定义和声明

变量定义和变量声明的区别

int tern = 1; /* tern被定义 */ 
int main() { 
    extern int tern; /* 使用在别处定义的tern */
}

tern被声明了两次。 第1次声明为变量预留了存储空间, 该声明构成了变量的定义。 第2次声明只告诉编译器使用之前已创建的tern变量, 所以这不是定义。 第1次声明被称为定义式声明(defining declaration) , 第2次声明被称为引用式声明(referencing declaration) 。 关键字extern表明该声明不是定义, 因为它指示编译器去别处查询其定义。

外部变量只能初始化一次, 且必须在定义该变量时进行。

内部链接的静态变量

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

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

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

多文件

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

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

存储类别说明符

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

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

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

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

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

下面用一个简短的程序使用了5种存储类别。 该程序包含两个文件, 所以必须使用多文件编译

// 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); 
}
// 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;
	}
}

存储类别和函数

函数也有存储类别, 可以是外部函数(默认) 或静态函数。C99 新增了 第 3 种类别——内联函数.

外部函数可以被其他文件的函数访问, 但是静态函数只能用于其定义所在的文件。 假设一个文件中包含了以下函数原型:

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

在同一个程序中, 其他文件中的函数可以调用gamma()和delta(), 但是不能调用beta(), 因为以static存储类别说明符创建的函数属于特定模块私有。 这样做避免了名称冲突的问题, 由于beta()受限于它所在的文件, 所以在其他文件中可以使用与之同名的函数。 通常的做法是: 用 extern 关键字声明定义在其他文件中的函数。 这样做 是为了表明当前文件中使用的函数被定义在别处。 除非使用static关键字, 否则一般函数声明都默认为extern。


随机函数和静态变量

/* rand0.c --生成随机数*/ 
/* 使用 ANSI C 可移植算法 */ 
static unsigned long int next = 1; /* 种子 */ 
unsigned int rand0(void) { 
    /* 生成伪随机数的魔术公式 */     
    next = next * 1103515245 + 12345;         
    return (unsigned int) (next / 65536) % 32768;
}
/* 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;
}
/* s_and_r.c -- 包含 rand1() 和 srand1() 的文件  */
/*       使用 ANSI C 可移植算法   */
static unsigned long int next = 1; /* 种子 */
int rand1(void)
{
	/*生成伪随机数的魔术公式*/
	next = next * 1103515245 + 12345;
	return (unsigned int) (next/65536) % 32768;
}
void srand1(unsigned int seed)
{
	next = seed;
}
/* r_drive1.c -- 测试 rand1() 和 srand1() */
/* 与 s_and_r.c 一起编译 */
#include<stdio.h>
#include<stdlib.h>
extern void 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;
}


掷骰子

#include <stdlib.h> /* 提供rand()的原型 */
int rollem(int sides)
{
	int roll;
	roll = rand() % sides + 1;
	return roll;
}

/* diceroll.c -- 掷骰子模拟程序 */
/* 与 mandydice.c 一起编译 */
#include "diceroll.h"
#include <stdio.h>
#include <stdlib.h>      /* 提供库函数 rand()的原型 */
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;
	int 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;
}
/* manydice.c -- 多次掷骰子的模拟程序 */
/* 与 diceroll.c 一起编译*/
#include <stdio.h>
#include <stdlib.h>		/* 为库函数 srand() 提供原型 */
#include <time.h>		/* 为 time() 提供原型      */
#include "diceroll.h" 	/* 为roll_n_dice()提供原型,为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 many 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-sided dice.\n",
			roll, dice, sides);
		printf("How many sides? Enter 0 to stop.\n");
	}
	printf("The rollem() function was called %d times.\n", roll_count); /* 使用外部变量 */
	printf("GOOD FORTUNE TO YOU!\n");
	return 0;
}

分配内存 malloc() 和 free()

ANSI C 新增一个类型: 指向void的指针。 该类型相当于 一个通用指针。 把指向 void 的指针赋给任意类型的指针变量完全不用考虑类型匹配的问题。

动态分配内存相关的一些函数

void *malloc(size_t size); 
/* 分配所需的内存空间,并返回一个指向它的指针。
   size -- 内存块的大小,以字节为单位。
   该函数返回一个指针,指向已分配的内存。如果分配失败,则返回 NULL。 */

void *calloc(size_t nitems, size_t size);
/* 分配所需的内存空间,并返回一个指向它的指针。
   nitems -- 期待分配内存的元素个数。
   size -- 元素的字节大小。
   该函数返回一个指针,指向已分配的内存。如果分配失败,则返回 NULL。
   malloc 和 calloc 的不同点是,malloc 不会设置内存为零,而 calloc 会设置分配的内存为零  */

void *realloc(void *ptr, size_t size);
/* 尝试调整之前由 malloc 或 calloc 或 realloc 所分配的 ptr 所指向的内存块的大小。
   ptr -- 指向一个要调整大小的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 
   进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。
   size -- 期望调整之后的内存块的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已
   块,则 ptr 所指向的内存块会被释放,并返回一个空指针
   该函数返回一个指针 ,指向重新调整大小的内存。如果调整失败,则返回 NULL。 */

void free(void *ptr); 
/* ptr -- 指向一个要释放的内存块,该内存块之前是通过调用 malloc、calloc 或    
   realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。
   该函数不返回任何值。 */

void exit(int status);
/* 立即终止调用进程。任何属于该进程的打开的文件描述符都会被关闭,该进程的子进程由进程   1 继承,初始化,且会向父进程发送一个 SIGCHLD 信号。
   status -- 返回给父进程的状态值。
   该函数不返回值。 */
/* stdilb.h 标准提供了两个常用 status 以保证在所有操作系统中都能正常工作: 
   EXIT_SUCCESS(或者, 相当于 0) 表示普通的程序结束
   EXIT_FAILURE 表示程序异常中止   */

如果动态分配内存成功会返回一个指向void的指针,为提高阅读性,应该坚持使用强制类型转换。如果动态分配内存失败, 可以调用 exit() 函数结束程序。

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

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

  1. 声明数组时, 用常量表达式表示数组的维度, 用数组名访问数组的元 素。 可以用静态内存或自动内存创建这种数组。
  2. 声明变长数组(C99新增的特性) 时, 用变量表达式表示数组的维度, 用数组名访问数组的元素。 具有这种特性的数组只能在自动内存中创建。
  3. 声明一个指针, 调用malloc(), 将其返回值赋给指针, 使用指针访问数组的元素。该指针可以是静态的或自动的。

使用第2种和第3种方法可以创建动态数组(dynamic array) 。 这种数组和普通数组不同, 可以在程序运行时选择数组的大小和分配内存。

/* 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 maximum 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) putchar('\n');
	}
	if (i % 7 != 0) putchar('\n');
	puts("Done.");
	free(ptd);
	return 0;
}

free() 的重要性

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

calloc()函数

分配内存还可以使用calloc(), 典型的用法如下:
long * newmem;
newmem = (long *)calloc(100, sizeof (long));

和malloc()类似, 在ANSI之前, calloc()也返回指向char的指针; 在ANSI 之后, 返回指向void的指针。 如果要储存不同的类型, 应使用强制类型转换 运算符。 calloc()函数接受两个无符号整数作为参数(ANSI规定是size_t类 型) 。 第1个参数是所需的存储单元数量, 第2个参数是存储单元的大小(以字节为单位) 。 在该例中, long为4字节, 所以, 前面的代码创建了100个4 字节的存储单元, 总共400字节。 用sizeof(long)而不是4, 提高了代码的可移植性。 这样, 在其他long不是 4字节的系统中也能正常工作。

calloc()函数还有一个特性: 它把块中的所有位都设置为0(注意, 在某些硬件系统中, 不是把所有位都设置为0来表示浮点值0) 。 free()函数也可用于释放calloc()分配的内存。 动态内存分配是许多高级程序设计技巧的关键。 我们将在第17章中详细讲解。 有些编译器可能还提供其他内存管理函数, 有些可以移植, 有些不可 以。 读者可以抽时间看一下。

动态内存分配和变长数组

变长数组(VLA) 和调用 malloc()在功能上有些重合。 例如, 两者都可 用于创建在运行时确定大小的数组:

int vlamal()
{
int n;
int * pi;
scanf("%d", &n);
pi = (int *) malloc (n * sizeof(int));
int ar[n];// 变长数组
pi[2] = ar[2] = -5;
...
}

不 同的是, 变长数组是自动存储类型。 因此, 程序在离开变长数组定义 所在的块时(该例中, 即vlamal()函数结束时) , 变长数组占用的内存空间 会被自动释放, 不必使用 free()。 另一方面, 用malloc()创建的数组不必局限 在一个函数内访问。 例如, 可以这样做: 被调函数创建一个数组并返回指针, 供主调函数访问, 然后主调函数在末尾调用free()释放之前被调函数分配的内存。 另外, free()所用的指针变量可以与 malloc()的指针变量不同, 但是两个指针必须储存相同的地址。 但是, 不能释放同一块内存两次。 对多维数组而言, 使用变长数组更方便。 当然, 也可以用 malloc()创建二维数组, 但是语法比较繁琐。 如果编译器不支持变长数组特性, 就只能固 定二维数组的维度,

int n = 5;
int m = 6;
int ar2[n][m]; // n×m的变长数组(VLA)
int (* p2)[6]; // C99之前的写法
int (* p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数组)
ar2[1][2] = p2[1][2] = 12;

先复习一下指针声明。由于malloc()函数返回一个指针,所以p2必须是一个指向合适类型的指针。第1个指针声明:
int (* p2)[6]; // C99之前的写法
表明p2指向一个内含6个int类型值的数组。因此,p2[i]代表一个由6个整数构成的元素,p2[i][j]代表一个整数。
第2个指针声明用一个变量指定p3所指向数组的大小。因此,p3代表一个指向变长数组的指针,这行代码不能在C90标准中运行。

存储类别和动态内存分配

存储类别和动态内存分配有何联系?我们来看一个理想化模型。可以认为程序把它可用的内存分为 3部分:一部分供具有外部链接、内部链接和无链接的静态变量使用;一部分供自动变量使用;一部分供动态内存分配。
静态存储类别所用的内存数量在编译时确定,只要程序还在运行,就可访问储存在该部分的数据。该类别的变量在程序开始执行时被创建,在程序结束时被销毁。
然而,自动存储类别的变量在程序进入变量定义所在块时存在,在程序离开块时消失。因此,随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少。这部分的内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁。

动态分配的内存在调用 malloc()或相关函数时存在,在调用 free()后释放。这部分的内存由程序员管理,而不是一套规则。所以内存块可以在一个函数中创建,在另一个函数中销毁。正是因为这样,这部分的内存用于动态内存分配会支离破碎。也就是说,未使用的内存块分散在已使用的内存块之间。另外,使用动态内存通常比使用栈内存慢。
总而言之,程序把静态对象、自动对象和动态分配的对象储存在不同的区域。

// 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 * pcl; pi = (int *)malloc(sizeof(int));
	*pi = 35;
	pcl = (char *)malloc(strlen("Dynamic String") + 1);
	strcpy(pcl, "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", pcl, pcl);
	printf(" %s at %p\n", "Quoted String", "Quoted String");
	free(pi);
	free(pcl);
	return 0;
}

ANSI C类型限定符

我们通常用数据类型、存储类别、类型限定符来描述一个变量。

类型限定符:
const (C90)
volatile(C90)
restrict(C99)
_Atomic(C99)

C99 为类型限定符增加了一个新属性:可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略

const const const int n = 6; 
const int n = 6; // 与上面相同

// 所以可以这样做
typedef const int zip;
const zip q = 8; 

const类型限定符

第七章 → const 与指针

对全局数据使用const

使用全局变量是危险的,程序的任何部分都能更改数据。解决办法是把全局变量设置为const。在文件间共享const数据可以采用两个策略。

第一种方案,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字)

/* file1.c -- 定义了一些外部const变量 */
const double PI = 3.14159;
const char * MONTHS[12] = { "January", "February", "March", "April", "May", "June", "July","August", "September", "October", "November", "December" };

/* file2.c -- 使用定义在别处的外部const变量 */
extern const double PI;
extern const * MONTHS [];

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

/* 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"

这二种方案必须在头文件中用关键字 static 声明全局 const 变量。如果去掉 static,那么在file1.c和file2.c中包含constant.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做(有些编译器允许)。实际上,这种方案相当于给每个文件提供了一个单独的数据副本,每个副本只对该文件可见。

volatile类型限定符

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

// volatile的语法和const一样:
volatile int loc1;   /* loc1 是一个易变的位置 */
volatile int * ploc; /* ploc 是一个指向易变的位置的指针 */

/* 以上代码把loc1声明为volatile变量,把ploc声明为指向volatile变量的指针。*/

为何ANSI委员把volatile关键字放入标准?原因是它涉及编译器的优化。例如,假设有下面的代码:

vall = x;
/* 一些不使用 x 的代码 */
val2 = x;

智能的(进行优化的)编译器会注意到以上代码使用了两次 x,但并未改变它的值。于是编译器把 x 的值临时储存在寄存器中,然后在val2需要使用x时,才从寄存器中(而不是从原始内存位置上)读取x的值,以节约时间。这个过程被称为高速缓存(caching)。通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存。这是在 ANSI 之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。

可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示:

volatile const int loc;
const volatile int * ploc;

restrict类型限定符

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

int ar[10];
int * restrict restar = (int *)malloc(10 * sizeof(int));
int * par = ar;

这里,指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。

for (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;
这是因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。

在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

restrict 限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。例如,C 库有两个函数用于把一个位置上的字节拷贝到另一个位置。在C99中,这两个函数的原型是:

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

这两个函数都从位置s2把n字节拷贝到位置s1。memcpy()函数要求两个位置不重叠,但是memove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足了memcpy()无重叠的要求。memmove()函数允许重叠,它在拷贝数据时不得不更小心,以防在使用数据之前就先覆盖了数据。

restrict 关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。

_Atomac类型限定符(C11)

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

int hogs;   // 普通声明
hogs = 12;  // 普通赋值
// 可以替换成:
_Atomic int hogs;      // hogs 是一个原子类型的变量
atomic_store(&hogs, 12);  // stdatomic.h中的宏

这里,在hogs中储存12是一个原子过程,其他线程不能访问hogs。
编写这种代码的前提是,编译器要支持这一新特性。

旧关键字的新位置

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数(指针类型)的初始方括号中。

类型限定符新位置

// 旧式语法的声明:
void ofmouth(int * const a1, int * restrict a2, int n); 
// 新的等价语法:
void ofmouth(int a1[const], int a2[restrict], int n); 
/* 该声明表明a1是一个指向int的const指针,a2是一个restrict指针 */
/* 根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。 */

static 新位置

double stick(double ar[static 20]);

static 的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值