在C语言中,我们经常会看见constvolatilerestrict,其实他们都是ANSI C的类型限定词。使用这些关键字,我们就可以创建受限类型(qualified type)。下面我们一一介绍。

1. C类型限定词介绍

我们知道对于一个变量,都有类型和存储类两个属性,前者表明变量类型,后者表明变量的存储位置。C90中新增加了两个变量的属性:不变性与易变性。不变性用const关键字来声明,易变性用volatile关键字声明。而C99中又添加了一个限定词restrict,主要用于编译器的优化。

C99中也增加了限定词一个新的属性:他们是幂等的(idempotent)。所谓幂等是指可以在一个声明中不止一次的使用同一个限定词,多余的将被忽略掉。比如const const const int n = 6; 和const int n = 6; 是等价的。

2. const

相比其他两个限定词,这个限定词我们应该都更熟悉一点。正如其名字,这个限定词用于定义一些常量(constant),所谓常量,自然是不能改变的量。所以,如果变量声明中带有关键字const,则不能通过赋值、增量或减量等运算来改变它的值。我觉得,对于const的使用,可以分两大类:(1)修饰非指针(2)修饰非指针。

2.1 修饰非指针

修饰非指针的场合使用起来很简单,在变量类型前加上const关键字即可。

const int nochange1;     /* 把nochange1变量限定为常量 */
nochange1 = 12;         /* 错误,不允许改变常量 */

const int nochange2 = 12;   /* OK */

在使用const修饰一个变量后,这个变量就不可以再被改变,所以在修饰非指针的场合,我们一定要像上面的nochange2变量那样,将变量的声明和初始化放在一起。

2.2 修饰指针

关于const修饰指针的问题其实我已经在以前的博客《解读C指针》中讲过,分为两种:指针本身为常量和指针指向的变量是常量。判断方法如下:const位于*号左边,则表示指针指向的数据是常量;const位于*的右边,则表示指针本身为常量。比如:

const float * pf; // pf指向一个常量浮点值,也可定义为:float const *pf;
float * const pt; // pt是一个常量指针
const float * const ptr;

上面的例子中,pf指向的值必须是不可变的,但是pf本身的值却是可以变的。例如,它可以指向另一个const值。而后面的指针pt本身的值是不可以变的,它必须总是指向同一个地址,但地址所指向的值可以改变。而最后的指针ptr则表示它总是指向同一个位置,并且他所指位置存储的值也不能改变。

3. volatile

限定词volatile告诉编译器该变量除了可被程序改变以外还可被其他地方改变。比如某个指针指向的地址里面存着当前时间,那么不管程序做什么,该地址的值都会随着时间改变。volatile的语法和const一样。考虑下面代码的编译过程:

var1 = x;
var2 = x;
volatile var3 = x;

编译器在编译var1的时候先把x临时存储在一个寄存器中,接着,它发现var2也需要x的值,那么他就会通过从寄存器获取x的值而不是从内存中的位置来读取该值以节省时间。这个过程就是所谓的缓存(caching)。但是,编译var3的时候,虽然同样也是获取x的值,但是编译器会从存储x的内存位置去获取x的值,而不是临时寄存器,因为volatile关键字告诉编译器该值可能被程序以外的其他代理改变。所以,一个变量是不是应该由volatile来修饰,我们需要根据具体的场景来判断。在嵌入式C编程里面该限定词使用的特别多,因为很多地址都是可能被其他并行运行的程序改变的。

4. restrict

这个关键字只能用于指针,表明该指针是访问一个数据对象的惟一且初始的方式。看下面例子:

#include <stdio.h>

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

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

	return 0;
}
  • 指针restar是访问有malloc分配的内存的惟一且初始方式,因此,他可以由关键字restrict限定。然而,par指针既不是初始的,也不是访问数组ar中数据的惟一方式,因此不可以把它限定为restrict。
  • 编译器在编译的时候,知道了restar是访问它指向数据的惟一初始方式,就会将上面的restar[n] += 5; 和restar[n] += 3; 优化为一句restar[n] += 8 。但显然par语句是不能这样优化的,因为ar在par两次访问数组数据之间更改了数据的值。

restrict限定词大多用在函数原型中,这里举一个例子(此处为C99中的函数原型):

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

这两个函数的作用都是都是从位置s2把n个字节的数据复制到位置s1.声明中也只差了restrict关键字。这是因为memcpy()函数要求两个位置之间不能重叠(overlap),但memmove()没有这个要求。把s1和s2声明为restrict意味着每个指针都是相应数据惟一访问方式,因此它们不能访问同一数据块。这满足了不能有重叠的要求。