您的位置:首页 > 其它

C Primer Plus之存储类、链接和内存管理

2015-12-24 20:18 204 查看
存储时期即生存周期——变量在内存中保留的时间

变量的作用域和链接一起表明程序的哪些部分可以通过变量名来使用该变量。

注意:生存期和作用域是两个不同的概念。

作用域

作用域描述了程序中可以访问一个标识符的一个或多个区域。一个C变量的作用域可以是代码块作用域、函数原型作用域,或者文件作用域。

在代码块中定义的变量具有代码块作用域,从该变量被定义的地方到包含该定义的代码块的末尾该变量均可见。其次,函数的形式参量尽管在函数的开始花括号前进行定义,同样也具有代码块作用域,隶属于包含函数体的代码块。

传统上,具有代码块作用域的变量都必须在代码块的开始处进行声明。C99放宽了这一规则,允许在一个代码块中任何位置声明变量。一个新的可能是变量声明可以出现在for循环的控制部分,即:

for(int i = 0; i < 10; i++)
printf("A C99 feature: i = %d", i);


函数原型作用域适用于函数原型中使用的变量名。函数原型作用域从变量定义处一直到原型声明的末尾。这意味着编译器在处理一个函数原型的参数时,它所关心的只是该参数的类型;您使用设么名字通常是无关紧要的,不需要使它们和在函数定义中使用的变量名保持一致。

一个在所有函数之外定义的变量具有文件作用域。具有文件作用域的变量从它定义处到包含该定义的文件结尾处都是可见的。文件作用域变量也被称为全局变量。

链接

一个C变量具有下列链接之一:

外部链接

内部链接

空链接

具有代码块作用域或者函数原型作用域的变量有空链接,意味着它们是由其定义所在的代码块或函数原型所私有的。具有文件作用域的变量可能有内部或者外部链接。一个具有外部链接的变量可以在一个多文件程序的任何地方使用。一个具有内部链接的变量可以在一个文件的任何地方使用。

问题:怎样知道一个文件作用域变量具有内部链接还是外部链接?

答:看看在外部定义中是否使用了存储类说明符static,例:

int giants = 5;                // 文件作用域,外部链接
static int dodgers = 3;    // 文件作用域,内部链接
int main(void)
{
....
}
.....


存储时期

一个C变量有以下两种存储时期之一:静态存储时期和自动存储时期。如果一个变量具有静态存储时期,它在程序执行期间将一直存在。具有文件作用域的变量自动具有静态存储时期。(注意:对于具有文件作用域的变量,关键词static表明链接类型,并非存储时期)。

具有代码块作用域的变量一般情况下具有自动存储时期,但也可创建具有代码块作用域,兼具静态存储时期的局部变量。在程序进入定义这些变量的代码块时,将为这些变量分配内存;当退出这个代码块时,分配的内存将被释放。该思想把自动变量使用的内存视为一个可以重复使用的工作区或者暂存内存。

1° 自动变量

属于自动存储类的变量具有自动存储时期、代码块作用域和空链接。默认情况下,在代码块或函数的头部定义的任意变量都属于自动存储类。然而,也可以显示地使用关键字auto使您的这个意图更清晰。

问题:如果在内层代码块定义了一个具有和外层代码块变量同一名字的变量,将发生什么?

答:在内层代码块定义的名字是内层代码块所使用的变量。我们称之为内层定义覆盖了外部定义,但当运行离开内层代码块时,外部变量重新恢复作用。

除非您显示地初始化自动变量,否则它不会被自动初始化,不要指望这个值是0。

2° 寄存器变量

寄存器变量具有代码块作用域、空链接以及自动存储时期。但是,由于寄存器变量多是存储在一个寄存器而非内存中,所以无法获得寄存器变量的地址。通过使用存储类说明符register可以声明寄存器变量。

3° 具有代码块作用域的静态变量

“静态”一词是指变量的位置固定不动。此类变量和自动变量具有相同的作用域,但当包含这些变量的函数完成工作时,它们并不消失。因为如果一个变量具有静态存储时期,它在程序执行期间将一直存在。即此类变量具有代码块作用域、空链接,却有静态存储时期。从一次函数调用到下一次调用,计算机都记录着它们的值。这样的变量通过使用存储类说明符static在代码块内声明创建。

如果不显示地对静态变量进行初始化,它们将被初始化为0。静态变量和外部变量(?)在程序调入内存时已经就位(即在程序载入内存时,就被载入内存中)。

注意:对函数参量不能使用static,例:

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


4° 具有外部链接的静态变量

具有外部链接的静态变量具有文件作用域、外部链接和静态存储时期。这一类型有时被称为外部存储类,这一类型的变量被称为外部变量。把变量的定义声明放在所有函数之外,即创建了一个外部变量。为了使程序更加清晰,可以在使用外部变量的函数中通过使用extern关键字来再次声明它(但也可以完全省略,它们出现在那里,作用只不过是表明函数使用了这些变量。如果函数中的声明漏掉了extern,就会建立一个独立的自动变量)。如果变量是在别的文件中定义的,使用extern来声明该变量就是必须的。

外部变量的作用域:从声明的位置开始到文件结尾为止。它们也说明了外部变量的生存期。外部变量存在的时间与程序运行时间一样,并且它们不局限于任一函数,在一个特定函数返回时并不消失。

如果您不对外部变量进行初始化,它们将自动被赋初值0。不同于自动变量,只可以用常量表达式来初始化文件作用域变量。

变量定义与变量声明的区别,举例说明:

int tern = 1;
main()
{
extern int tern;


第一次声明称为定义声明,第二次声明称为引用声明。关键字extern表明该声明不是一个定义,因为它指示编译器参考其他地方。

注意:不要用关键字extern来进行外部定义;只用它来引用一个已经存在的外部定义。一个外部变量只可进行一次初始化,而且一定是在变量被定义时进行。

extern char permis = 'Y'  /* 错误 */


5° 具有内部链接的静态变量

这种存储类的变量具有静态存储时期、文件作用域以及内部链接。通过使用存储类说明符static在所有函数外部进行定义来创建一个这样的变量。

普通的外部变量可以被程序的任一文件中所包含的函数使用,而具有内部链接的静态变量只可以被与它在同一个文件中的函数使用。可以在函数中使用存储类说明符extern来再次声明任何具有文件作用域的变量。这样的声明并不改变链接。

C语言中有5个作为存储类说明符的关键字:

auto

register

static

extern

typedef

特别地,不可以在一个声明中使用一个以上存储类说明符,这意味着不能将其他任一存储类说明符作为typedef的一部分。

存储类和函数

函数也有存储类。函数可以是外部的(默认情况下)或者静态的。外部函数可被其他文件中的函数调用,而静态函数只可以在定义它的文件中使用。例如,考虑一个包含如下函数声明的文件:

double gamma();            /* 默认为外部的 */
static double beta();
extern double delta();


随机数函数和静态变量

随机数函数使用一个具有内部链接的静态变量。ANSI C程序提供了rand()函数来产生随机数。事实上,rand()是一个“伪随机数发生器”,这意味着可以预测数字的实际顺序(计算机不具有自发性),但这些数字在可能的取值范围内均匀地分布。

我们使用可移植的ASCI版本程序,举例说明如下:

#include <stdio.h>
extern int rand1(void);
extern void srand1(unsigned int x);

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("%hd\n", rand1());
printf("Please enter next seed (q to quit): \n");
}
printf("Done\n");

return 0;
}


View Code
自动重置种子:

ANSI C有一个函数time()可以返回系统时间。时间单位由系统决定,但有用的一点是返回值为数值类型,并且随着时间变化。其确切类型与系统有关,名称为time_t,但您可以对它进行类型指派。例如:

#include <time.h>               // 为time()函数提供ANSI原型
srand1((unsigned int)time(0));


通常,time()的参数是一个time_t类型对象的地址。那种情形下,时间值也存储在那个地址中。然而,您也可以传送空指针(0)作为参数。此时,时间值仅通过返回值机制(?)提供。

掷骰子:

ANSI C函数srand()和rand()的原型在stdlib.h头文件中。rand()产生的是从0到RAND_MAX范围内的整数;RAND_MAX在stdlib.h中定义,它通常是INT_MAX。

#include "diceroll.h"


以上代码的解释:将文件名置于双引号而非尖括号中,是为了指示编译器在本地寻找文件,而不是到编译器存放标准头文件的标准位置去寻找文件。在“本地寻找”的意义取决于具体的C实现。一些常见的解释是将头文件与源代码文件放在同一个目录或文件夹中,或者与工程文件(如果编译器使用它们)放在同一个目录或文件夹中。

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

C可以在程序运行时分配更多的内存。主要工具是函数malloc()。

malloc()接受一个参数:所需内存字节数。然后malloc()找到可用内存中一个大小适合的块。内存是匿名的,即malloc()分配了内存,但没有为它指定名字。然而,它却可以返回那块内存第一个字节的地址。因此,您可以把那个地址赋值给一个指针变量,并使用该指针来访问那块内存。

“通用指针”——指向void的指针。

需要使用指向不同类型的指针时,可采用void指针。 函数malloc()可用来返回数组指针、结构指针等等,因此一般需要把返回值的类型指派为适当的类型。在ANSI C中,为了程序清晰应对指针进行类型指派,但是将void指针值赋值给其他类型的指针并不构成类型冲突。如果malloc()找不到所需的空间,它将返回空指针。

举例说明如下:

double * ptd;
ptd = (double *)malloc(30 * sizeof(double)); // ptd是作为指向一个double类型值的指针,而不是指向30个double类型值的数据块的指针。


使用malloc()可以创建一个动态数组,即一个在程序运行时才分配内存并可在程序运行时选择大小的数组。

也可以使用malloc()来定义一个二维数组:

int (*p2)[6];
p2 = (int (*)[6])malloc(n * 6 *sizeof(int));


一般地,对应每个malloc()调用,应该调用一次free()。函数free()的参数是先前malloc()返回的地址,它释放先前分配的内存。这样,所分配内存的持续时间从调用malloc()分配内存开始,到调用free()释放内存以供再使用为止。free()的参数应是一指针,指向由先前malloc()分配的内存块;不能使用free()来释放通过其他方式(例如声明一个数组)分配的内存。在头文件stdlib.h中有malloc()和free()的原型。

exit()——该函数的原型也在stdlib.h中,用来在内存分配失败时结束程序。值EXIT_FAILURE也在这个头文件中定义。标准库提供了两个保证能够在所有操作系统下工作的返回值:EXIT_SUCCESS(或者,等同于0)指示程序正常终止,EXIT_FAILURE指示程序异常终止。

举例如下:

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

int main(void)
{
double * ptd;
int max;
int number;
int i = 0;

puts("What is the maximum 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);
}
puts("Enter the value (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个数
putchar('\n');
}
if(i % 7 != 0)
putchar('\n');
puts("Done.");
free(ptd);

return 0;
}


在C中类型指派(double *)是可选的,而在C++中必须有,因此使用类型指派将使把C程序移植到C++更容易。

free()的重要性

在编译程序时,静态变量的内存数量是固定的,在程序运行时也不会改变。自动变量使用的内存数量在程序执行时自动增加或者减少。但被分配的内存所使用内存数量只会增加,除非您记得使用free()。我们通过一个例子来说明内存泄露(内存溢出)的问题:

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


分析:第一次调用gobble()时,它创建了指针temp,并使用malloc()为之分配16000字节的内存(设double是8个字节)。假定我们如暗示的那样没有使用free()。当函数终止时,指针temp作为一个自动变量消失了,但它所指向的16000个字节的内存仍旧存在。我们无法访问这些内存,因为地址不见了。由于没有调用free(),不可以再使用它。

第二次调用gobble(),它又创建了一个temp,再次使用malloc()分配16000个字节的内存。第一个16000个字节的块已不可用,因此malloc()不得不再找一个16000个字节的块。当函数终止时,这个内存块也无法访问,不可再利用。

但循环执行了1000次,因此在循环最终结束时,已经有1600万字节的内存从内存池中移走。事实上,在到达这一步前,程序很可能已经内存溢出了。这类问题被称为内存泄露,可以通过在函数末尾处调用free()防止该问题出现。

函数calloc()

内存分配还可以使用calloc()。例如:

long * newmem;
newmem = (long *)calloc(100, sizeof(long));


与malloc()类似,calloc()在ANSI以前的版本中返回一个char指针,在ANSI中返回一个void指针(即通用指针)。这个新函数接受两个参数,都应是无符号的整数。第一个参数是所需内存单元的数量,第二个参数是每个单元以字节计的大小。

calloc()还有一个特性:它将块中的全部位都置为0。函数free()也可以用来释放由calloc()分配的内存。

ANSI C的类型限定词

C90增加了两个属性:不变性和易变性。这些属性是通过关键字const和volatile声明的,这样就创建了受限类型。C99标准添加了第三个限定词restrict,用以方便编译器优化。C99授予类型限定词一个新属性:它们现在是幂等的,其实只意味着可以在一个声明中不止一次地使用同一限定词,多余的将被忽略掉,如:

const const const int n = 6;  // 相当于:const int n = 6;


类型限定词volatile

限定词volatile告诉编译器该变量除了可被程序改变以外还可被其他代理改变。典型地,它被用于硬件地址和与其他并行运行的程序共享的数据。

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


volatile可以方便编译器优化。例如:

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


一个聪明的(优化的)编译器可能注意到您两次使用了x,而没有改变它的值。它将把x临时存储在一个寄存器中。接着,当val2需要x时,可以通过从寄存器而非初始的内存位置中读取该值以节省时间。这个过程被称为缓存。通常,缓存是一个好的优化方式,但如果在两个语句间其他代理改变了x的话就不是这样了。如果没有规定volatile关键字,编译器将无从得知这种改变是否可能发生。因此,为了安全起见,编译器不使用缓存。然而现在,如果在声明中没有使用关键字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)); // 指针restar是访问由malloc()分配的内存的唯一且初始的方式
int * par = ar; // par指针既不是初始的,也不是访问数组ar中数据的唯一方式


for(int n = 0; n < 10; n++)
{
par
+= 5;
restar
+= 5;
ar
*= 5;
par
+= 3;
restar
+= 3;
}


因为restar是访问它所指向数据块的唯一且初始方式,编译器就可以用具有同样效果的一条语句来代替包含restar的两个语句:

restar
+= 8;


没有关键字restrict,编译器将不得不设想比较糟的那种情形,也就是在两次使用指针之间,其他标识符可能改变了数据的值。使用了关键字restrict以后,编译器可以放心地寻找计算的捷径。

可以将关键字restrict作为指针类型函数参量的限定词使用。这意味着编译器可以假定在函数体内没有其他标识符修改指针指向的数据,因而可以试着优化代码,反之则不然。

C99允许将类型限定词和存储类限定词static放在函数原型和函数头部的形式参量所属的初始方括号内。对于类型限定词的情形,这样做为已有功能提供了一个可选语法。例如:

void ofmouth(int * const a1, int * restrict a2, int n);  // 以前的风格


等价的新语法如下:

void ofmouth(int a1[const], int a2[restrict], int n); // C99允许


static的情形是不同的,因为它引发了一些新问题。例如:

double stick(double ar[static 20]);


使用static表明在函数调用中,实际参数将是一个指向数组首元素的指针,该数组至少具有20个元素。这样做的目的是允许编译器使用这个信息来优化函数的代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: