C 类型限定符
1. Introduction
C 语言中的大部分类型都可以用称为限定符(qualifier)的关键字 const、 volatile、 restrict、 _Atomic 加以限定。这些限定符可以单独使用,也可以组合使用。
const 和 volatile 在 C89/C90 版本定义,restrict 在 C99 版本定义,_Atomic 在 C11 版本定义,_Atomic 对于编译器而言是可选的支持特性,表示原子类型。
一个类型不加限定符的版本和加限定符的版本是两个不同的类型,但是有相同的表示方法和对齐要求。
因此,int 是一个类型,const int 是另一个类型,限定的结果是产生一个新的类型,而不是“仅仅加入一个修饰,使其具有只读属性”。
程序员可以通过限定符提供一定的信息,以便 C 实现可以更好地优化程序。当编译器编译程序时,看到这些限定符就知道哪些优化是可以做的,哪些优化是不可以做的。另外,还可以检查出程序中的不当操作
类型限定符有以下的使用规则:
a. 类型限定符和类型指定符的顺序可交换
const int i;
int const i;
上述两种表示方式是等价的,但是处于可读性的考虑,通常采用第一种表示方式。
b. 派生类型不会继承类型限定符
指针派生自它所指向的类型。类型 const int 可以派生出 const int *,该指针的类型是“指向一个 const int 类型的指针”,而不是具有 const 限定的指针。
如果要生成 const 指针,需要对指针单独限定:
int * const cp; // cp 是 const 指针,派生自无限定的 int 类型
const int * const cpc; // cpc 是 const 指针,派生自 const int 类型
struct 类型派生自它的成员类型,如下所示:
struct t {const int i; const float f;}
struct t 派生自两个成员类型 const int, const float,但是 struct t 不是 const 限定的类型。
c. 同一个类型限定符同时出现多次,相当于只出现一次
两个声明指定符连用是非法的,但是两个类型限定符连用是合法的:
int int i; // 非法
const volatile const const int i; // 等效于 const volatile int i
int function(const const int i); // 等效于 int function(const int i);
2. const
const 比较准确的含义是"只读",而不是“常量”,“常量”在 C 语言中有特定的含义,与 const 无关。
const 有以下几点需要了解:
a. 优化
当程序中出现 const int i = 0
这样的代码时,它表示程序不准备修改对象 i 的存储值,编译器可以根据这种意图提供适当的优化。
比如,编译器可以在第一次访问对象 i 时将它缓存起来,以后只需要使用这个缓存值而不需要浪费时间重新读取。
b. 安全
编译器会检查代码是否违反了 const 的限定规则,对变量做了不适当的修改操作。
但是用 const 限定的对象并非不可改写的,可以通过强制类型转换来绕过这种限制。
例如:
int x = 0;
const int *p = &x;
(*p)++; //S1
(*(int*)p)++; //S2
S1 是非法的,S2 是合法的,S2 的做法是非常危险的,其后果未定义。
c.作用范围
const 限定符有其作用范围,如果多段代码共享同一个对象,或者多个线程共享同一个对象,那么这个对象可能在一个范围内是 const 限定的,在另一个范围内则不是。
例如:
void f(const char* c)
{
*c = 'x'; //S1,非法
}
void g(void)
{
char a[3];
f(a);
}
上述数组 a 的元素在 g() 中是可修改的,在 f() 中是只读的, S1 代码段的操作是非法的。
3. volatile
与 const 相反,volatile 的含义是告诉 C 实现,对象的值会改变,并且是以不受控制的方式改变。
如果一个类型是 “volatile 限定的类型”,则意味着该类型所定义的对象,它的值不单单会被当前程序的代码修改,还可能潜在地被其他程序或代码修改。
因此,编译器不能在编译时对访问该对象的代码做优化处理,对这种对象的处理不能依赖于缓存特性。
例如,它可能对应一个硬件的端口,或者几个程序或线程公用的存储位置等等。
int i;
interrupt_handler()
{
i = 1;
}
void function()
{
i = 0;
while (0 == i);
}
虽然本意是在中断服务函数中将 i 赋值为 1,但实际上 function 函数中的 while 循环可能永远不会退出。
因为编译器看到在 while 循环前 i 被赋值为 0,while(0 == i)
的条件比较结果肯定为 1,如果打开了编译器的优化选项,while(0 == i)
会被优化为 while(1)
。
解决方案就是在声明 i 的时候加入 volatile 限定符:volatile int i;
这样编译器就不会做出不适当的优化。
4. restrict
restrict 限定符仅适用于指针类型。
如果一个指针类型是 restrict 限定的,则它所指向的对象和该指针有一种特殊的联系:在一个代码块内(函数体或者复合语句),所有到这个对象的引用必须直接或者间接通过这个指针进行。
基于上述保证的条件,编译器就可以在代码块的开始处安全地缓存“该指针所指向的对象”的值,读取和更新操作只针对这个缓存值进行。在退出代码块之前,再将缓存的值写回到指针所指向的对象。
如果没有这个限定符,则意味着在当前块内,还可能存在着其他指向这个对象的指针,因此,缓存对象的值是不安全的。
以下两个代码段:
void f1(int *p1, int *p2, int n)
{
for (int i = 0; i < n; i++) {
*p1 ++;
*p2 ++
}
}
void f2(int * restrict p1, int * restrict p2, int n)
{
for (int i = 0; i < n; i++) {
*p1 ++;
*p2 ++
}
}
用相同的方法调用上述 f1 和 f2:
int i = 0;
f1(&i, &i, 10)
int i = 0;
f2(&i, &i, 10)
f1 调用完成后,i 的值是 90;f2 调用完成后,i 的值是 45
原因在于,在 f2 中,参数 p1 和 p2 都被声明为 restrict 限定,但调用时他们又指向了同一个对象,这违背了 restrict 的约定。
因此在 f2 内部,for 循环里的两个累加过程都认为只有自己在改变对象的值,因此都使用了缓存值,而不是实际访问对象的值。
当退出 for 循环时他们各自的值都是 45,退出 f2 函数前,这两个缓存值被各自刷新到 i 中,导致 i 的值是 45。