预计阅读本页时间:-
您已经知道一个变量是以它的类型和存储类表征的。C90增加了两个属性:不变性和易变性。这些属性是通过关键字const和volatile声明的,这样就创建了受限类型(qualified type)。C99标准添加了第三个限定词restrict,用以方便编译器优化。
C99授予类型限定词一个新属性:它们现在是幂等的(idempotent)!这听起来像一个强大的功能,其实只意味着可以在一个声明中不止一次地使用同一限定词,多余的将被忽略掉:
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
例如,这使下列序列可以被接受:
12.7.1 类型限定词const
第4章“字符串和格式化输入/输出”和第10章“数组和指针”已经介绍过const。回顾一下,如果变量声明中带有关键字const,则不能通过赋值、增量或减量运算来修改该变量的值。在与ANSI兼容的编译器中,下面的代码将产生一个错误信息:
然而,可以初始化一个const变量。因此,下面的代码是对的:
上面的声明使nochange成为一个只读变量。在初始化以后,不可以再改变它。
例如,可以用关键字const创建一组程序不可以改变的数据:
一、在指针和参量声明中使用const
在声明一个简单变量和数组时使用关键字const很简单。指针则要复杂一些,因为不得不把让指针本身成为const与让指针指向的值成为const区分开来。下面的声明表明pf指向的值必须是不变的:
但pf本身的值可以改变。例如,它可以指向另一个const值。相反,下面的声明表明指针pt本身的值不可以改变:
它必须总是指向同一个地址,但所指向的值可以改变。最后,下面的声明:
意味着ptr必须总是指向同一个位置,并且它所指位置存储的值也不能改变。
还有第三种放置const关键字的方法:
正如注释所表示的那样,把const放在类型名的后边和*的前边,意味着指针不能够用来改变它所指向的值。总而言之,一个位于*左边任意位置的const使得数据成为常量,而一个位于*右边的const使得指针自身成为常量。
这个新关键字的一个常见用法是声明作为函数形式参量的指针。例如,假定一个名为display()的函数显示一个数组的内容。为了使用它,您可能会把数组名作为实际参数传送,但数组名是一个地址,这样做将允许函数改变调用函数中的数据。下面的原型防止了这样的情况发生:
在函数原型和函数头部,参量声明const int array[]与const int* array相同,因此该声明表明array指向的数据是不可变的。
ANSI C库遵循这一惯例。如果指针只是用来让函数访问值,将把它声明为const受限指针。如果指针被用来改变调用函数中的数据,则不使用关键字const。例如,ANSI C中strcat()声明如下:
回忆一下,函数strcat()在第一个字符串的末尾处添加第二个字符串的一份拷贝。这改变了第一个字符串,但不改变第二个字符串。该声明也体现了这一点。
二、对全局数据使用const
回忆一下,使用全局变量被认为是一个冒险的方法,因为它暴露了数据,使程序的任何部分都可以错误地修改数据。如果数据是const的,这种危险就不存在了,因此对全局数据使用const限定词是很合理的。可以有const变量、const数组和const结构(结构是将要在第14章中讨论的复合数据类型)。
然而,在文件之间共享const数据时要小心。可以使用两个策略。首先是遵循外部变量的惯用规则:在一个文件中进行定义声明,在其他文件中进行引用声明(使用关键字extern):
其次是将常量放在一个include文件中。这时还必须使用静态外部存储类:
如果不使用关键字static,在文件filel.c和file2.c中包含constant.h将导致每个文件都有同一标识符的定义声明,ANSI标准不支持这样做(然而,一些编译器的确支持这样做)。通过使每个标识符成为静态外部的,实际上给了每个文件一个独立的数据拷贝。如果文件想使用该数据来与另一个文件通话,这样做就不行了,因为每个文件都只能看到它自己的拷贝。然而,由于数据是不变的(通过使用关键字const)和相同的(通过使两个文件都包含同样的头文件),这就不是问题了。
使用头文件的好处是不必惦记着在一个文件中进行定义声明,在下一个文件中进行引用声明;全部文件都包含同一个头文件。缺点在于复制了数据。在前述的例子中,这不构成一个真正的问题;但如果常量数据包含着巨大的数组,它可能就是一个问题了。
12.7.2 类型限定词volatile
限定词volatile告诉编译器该变量除了可被程序改变以外还可被其他代理改变。典型地,它被用于硬件地址和与其他并行运行的程序共享的数据。例如,一个地址中可能保存着当前的时钟时间。不管程序做些什么,该地址的值都会随着时间而改变。另一种情况是一个地址被用来接收来自其他计算机的信息。
语法同const:
这些语句声明loc1是一个volatile值,并且ploc指向一个volatile值。
您可能以为volatile是一个有趣的概念,但您也可能奇怪为什么ANSI觉得有必要把volatile作为一个关键字。原因是它可以方便编译器优化。例如,假定有如下代码:
一个聪明的(优化的)编译器可能注意到您两次使用了×,而没有改变它的值。它将把×临时存储在一个寄存器中。接着,当val2需要×时,可以通过从寄存器而非初始的内存位置中读取该值以节省时间。这个过程被称为缓存(caching)。通常,缓存是一个好的优化方式,但如果在两个语句间其他代理改变了x的话就不是这样了。如果没有规定volatile关键字,编译器将无从得知这种改变是否可能发生。因此,为了安全起见,编译器不使用缓存。那是在ANSI以前的情形。然而现在,如果在声明中没有使用关键字volatile,编译器就可以假定一个值在使用过程中没有被修改,它就可以试着优化代码。
一个值可以同时是const和volatile。例如,硬件时钟一般设定为不能由程序改变,这一点使它成为const;但它被程序以外的代理改变,这使它成为volatile的。只需在声明中同时使用这两个限定词,如下所示;顺序并不重要:
12.7.3 类型限定词restrict
关键字restrict通过允许编译器优化某几种代码增强了计算支持。它只可用于指针,并表明指针是访问一个数据对象的惟一且初始的方式。为了清楚为何这样做有用,我们需要看一些例子。考虑下面的例子:
这里,指针restar是访问由malloc()分配的内存的惟一且初始的方式。因此,它可以由关键字restrict限定。然而,par指针既不是初始的,也不是访问数组ar中数据的惟一方式,因此不可以把它限定为restrict。
现在考虑下面这个更加复杂的例子,其中n是一个int:
知道了restar是访问它所指向数据块的惟一初始方式,编译器就可以用具有同样效果的一条语句来代替包含restar的两个语句:
然而,将两个包含par的语句精简为一个语句将导致计算错误:
出现错误结果的原因是循环在par两次访问同一个数据之间,使用ar改变了该数据的值。
没有关键字restrict,编译器将不得不设想比较糟的那种情形,也就是在两次使用指针之间,其他标识符可能改变了数据的值。使用了关键字restrict以后,编译器可以放心地寻找计算的捷径。
可以将关键字restrict作为指针型函数参量的限定词使用。这意味着编译器可以假定在函数体内没有其他标识符修改指针指向的数据,因而可以试着优化代码,反之则不然。例如,C库中有两个函数可以从一个位置把字节复制到另一个位置。在C99标准下,它们的原型如下:
每一个函数都从位置s2把n个字节复制到位置s1。函数memcpy()要求两个位置之间不重叠,但memmove()没有这个要求。把s1和s2声明为restrict意味着每个指针都是相应数据的惟一访问方式,因此它们不能访问同一数据块。这满足了不能有重叠的要求。函数memmove()允许重叠,它不得不在复制数据时更小心,以防在使用数据前就覆盖了数据。
关键字restrict有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个读者是用户,它告诉用户仅使用满足restrict要求的参数。一般,编译器无法检查您是否遵循了这一限制,如果您蔑视它也就是在让自己冒险。
12.7.4 旧关键字的新位置
C99允许将类型限定词和存储类限定词static放在函数原型和函数头部的形式参量所属的初始方括号内。对于类型限定词的情形,这样做为已有功能提供了一个可选语法。例如,下面是一个使用旧语法的声明:
它表明al是一个指向int的const指针。回忆一下,这意味着该指针是不变的,而不是它所指向的数据不变。还表明a2是一个受限指针,如上一节所述。等价的新语法如下:
static的情形是不同的,因为它引发了一些新问题。例如,考虑如下原型:
使用static表明在函数调用中,实际参数将是一个指向数组首元素的指针,该数组至少具有20个元素。这样做的目的是允许编译器使用这个信息来优化函数的代码。
与restrict相同,关键字static有两个读者。一个是编译器,它告诉编译器可以自由地做一些有关优化的假定。另一个是用户,它告诉用户仅使用满足static要求的参数。