您已经知道一个变量是以它的类型与存储类表征的。C90增长了两个属性:不变性和易变性。这些属性是经过关键字const和volatile声明的,这样就建立了受限类型(qualified type)。C99标准添加了第三个限定词resrict,用以方便编译器优化。数组
C99授予类型限定词一个新属性:它们如今是幂等的(idempotent)!这听起来像一个强大的功能,其实只意味着能够在一个声明 中不止一次地使用同一限定词,多余的将被忽略掉:缓存
const const const int n = 6; //至关于const int n = 6;安全
例如,这使下列序列能够被接受:ide
typedef const int zip;函数
const zip q=8;优化
12.7.1 类型限定词constspa
回顾一下,若是变量声明中带有关键字const,则不能经过赋值、增量或减量运算来修改该变量的值。在与ANSI 编译器中,下面的代码将产生一个错误信息:代理
const int nochange; //把m限定为常量指针
nochange = 12; //不容许rest
然而,能够初始化一个const变量。所以,下面的代码是正确的:
const int nochange = 12; //能够
上面的声明使nochange成为一个只读变量。在初始化之后,不能够再改变它。
例如,能够用关键字const建立一组程序不能够改变的数据:
const int days1[12] = {31,28,31,30,31,30,31,31,30,31,30,31};
1、在指针和参量声明中const
指针要复杂一些,由于不得不把让指针成为const与让指针指向的值成为const区分开来。下面的声明代表pf指向的值必须是不变的:
const float * pf; /*pf指向一个常量浮点数值*/
但pf自己的值能够改变。例如,它能够指向另外一个const值。相反,下面的声明代表指针pt自己的值不能够改变:
float * const pt; /*pt是一个常量指针*/
它必须老是指向同一个地址,但所指向的值能够改变。最后,下面的声明:
const float * const ptr;
意味着ptr必须老是指向同一个位置,而且它所指位置存储的值也不能改变。
还有第三种放置const关键字的方法:
float const * pfc; //等同于const float * pfc;
总而言之,一个位于*左边任意位置的const使得数据成为常量,而一个位于*右边的const使得指针自身成为常量。
这个新关键字的一个常见用法是声明做为函数形式参量的指针。例如,假定一个名为display()的函数显示一个数组的内容。为了使用它,您可能会把数组名做为实际参数传送,但数组名是一个地址,这样作将容许函数改变调用函数中的数据。下面的原型防止了这样的状况发生:
void display(const int array[],int limit);
在函数原型和函数头部,参量声明const int array[]与const int * array相同,所以该声明代表array指向的数据是不可变的。
ANSI C库遵循这一惯例。若是指针只是用来记函数访问值,将把它声明为const受限指针。若是指针被用来改变调用函数中的数据,则不使用关键字const。例如,ANSI C 中的strcat()声明以下:
char * strcat(char *,const char *);
回忆下,函数strcat()在第一个字符串的末尾处添加第二个字符串的一份拷贝。这改变了第一个字符串,但不改变第二个字符串。该声明也体现了这一点。
2、对全局数据使用const
回忆一下,使用全局变量被认为是一个冒险的方法,由于它暴露了数据,使程序的任何部分均可以错误地修改数据。若是数据是const的,这种危险就不存在了,所以对全局数据使用const限定词是很合理的。
然而,在文件之间共享const数据时要当心。可使用两个策略。首先是遵循外部变量的惯用规则:在一个文件中进行定义声明,在其余文件中进行引用声明(关键字extern):
/*file 1.c --定义一些全局常量*/ const double PI = 3.14159; const char * MONTHS[12] = {"January","February","March","April","May","June","July", "August","September","October","November","December"}; /*file 2.c --使用在其余文件中定义的全局常量*/ extern const double PI; extern const char * MONTHS[];
其次是将常量放在一个include文件中。这时还必须使用静态外部存储类:
/*constant.h --定义一些全局常量*/ static const double PI = 3.14159; static const char * [12] = {...}; /*file1.c --使用在其余文件中定义的全局常量*/ #include "constant.h" /*file2.c --使用在其余文件中定义的全局常量*/ #include "constant.h"
若是不使用关键字static,在文件file1.c和file2.c中包含constant.h将致使每一个文件都有同一标识符的定义声明,ANSI C 不支持这样作。经过使每一个标识符成为静态的外部的,实际上给了每一个文件一个独立的数据拷贝。若是文件想使用该数据与另外一文件通话,这样作就不行了,由于每一个文件都只能看到它本身的拷贝。然而,因为数据是不变的(const)和相同的(经过使两个文件都包含一样的头文件),这就不是问题了。
使用头文件的好处是不用惦记着在一个文件中进行定义声明,在下一个文件中进行引用声明;所有文件都包含同一个头文件。缺点在于复制了数据。在前述的例子中,这不构成一个问题,但若是常量数据包含着巨大的数组,它可能就是一个问题了。
12.7.2 类型限定词
限定词volatile告诉编译器该变量除了可被程序改变外还能够被其余代理改变。典型地,它被用于硬件地址和其余并行运行的程序共享的数据 。例如,一个地址中可能保存着当前的时钟时间。无论程序作什么,该地址的值都会随着时间而改变。另外一种状况是,一个地址被用来接收来自其余计算机的信息。
语法同const
volatile int locl; /* locl是一个易变的位置*/
volatile int ploc; /*ploc指向一个易变的位置*/
这些语句声明locl是一个volatile值,而且ploc指向一个volatile值。
您可能认为volatile是一个有趣的概念,但您也可能奇怪为何ANSI C 以为有必要把volatile做为一个关键字。缘由是它能够方便编译器优化。例如,假定有以下代码:
val1=x;
/*一些不使用x的代码x*/
val2=x;
一个聪明的(优化的)编译器可能注意到您两次使用了x,而没有改变它的值。它将把x临时存储在一个寄存器中。接着当VAL2须要x时,可能经过寄存器而非初始的内存位置中读取该值以节省时间。这个过程被称为缓存(caching)。一般,缓存是一个好的优化方式,但若是在两个语句间其余代理改变了x的话就不是这样了。若是没有规定volatile关键字,编译器将无从得知这种改变是否可能发生。所以,为了安全起见,编译器不使用缓存。那是在ANSI之前的情形。然而如今,若是在声明中没有使用volatile,编译器就能够假定一个值在使用过程当中没有被修改,它就能够试着优化代码 。
一个值能够同时是const和volatile。例如,硬件时钟通常设定为不能由程序改变,这一点使它成为const;但它被程序之外的代理改变,这使它成为volatile的。只需在声明中同时使用这两个限定词,以下所示:顺序并不重要:
volatile const int loc;
const volatile int * ploc;
12.7.3 类型限定词restrict
关键字restrict经过容许编译优化某几种代码加强了计算支持。它只可用于指针,并代表指针是访问一个数据对象的唯一且初始的方式。考虑下面的例子:
int ar[10]; int * restrict restar = (int *)malloc(10 * sizeof(int)); int * par = ar;
这里,指针 restar是访问由malloc()分配 的内存的唯一且初始的方式 。所以,它能够由关键字restrict限定。然而,par指针既不是初始的,也不 是访问数组ar中数据的唯一方式,所以不能够把它限定为restrict。
如今考虑下面这个更加复杂的例子,其中n是一个Int:
for (n=0; n<10; n++) { par[n] += 5; restar += 5; ar[n] *= 2; par[n] += 3; restar[n] += 3; }
知道了restar是访问它所指向数据块的唯一初始方式,编译器就能够用具备一样效果的一条语句来代替包含restar的两个语句:
restar[n] += 8; /*能够进行替换*/
然而,将两个包含par的语句精简为一个语句将致使计算错误:
par[n] += 8; /*给出错误的结果*/
出现错误结果的缘由是循环在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()要求两个位置之间不重叠,但memmove()没有这个要求。把s1和s2声明为restrict意味着每一个指针都是相应数据的唯一访问方式,所以它们不能访问同一数据块。这知足了不能有重叠 的要求。函数memmove()容许重叠,它不得不在复制数据时更加当心,以防在使用数据前就覆盖了数据。
关键字restrict有两个读者,一个是编译器,它告诉编译器能够自由地作一些有关优化的假定。另外一个读者是用户,它告诉用户仅使用知足restrict要求的参数。通常,编译器没法假定您是否遵循了这一限制,若是您蔑视它也就是在让本身冒险。
12.7.4 旧关键字的新位置
C99容许将类型限定词和存储类限定词static放在函数原型和函数头部的形式参量所属的初始方括号内。对于类型限定词的情形,这样作为已有功能提供了一个可选语法。例如,下面是一个使用旧语法的声明:
void ofmouth( int * const a1, int * restrict a2, int n) //之前的风格
它代表a1是一个指向int的const指针。回忆一下,这意味着该指针是不变的,而不是它所指向的数据不变。还代表a2是一个受限指针,如上一节所述。等价的新语法以下:
void ofmouth( int a1[const], int a2 [restrict], int n); //C99容许
static的情形是不一样的,由于它引起了一些新问题。例如,考虑以下原型:
double stick (double ar[static 20]);
使用static代表在函数调用中,实际参数将是一个指向数组首元素的指针,该数组至少具备20个元素。这样的目的是容许编译器使用这个信息来优化函数的代码。
与restrict相同,关键字static有两个读者,一个是编译器,它告诉编译器能够自由地作一些有关优化的假定。另外一个是用户,它告诉用户仅使用知足static要求的参数。