C 类型限定符
我们通常用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别使用关键字const
和volatile
来声明,以这两个关键字创建的类型是限定类型(qualified type)。C99标准新增了第3个限定符:restrict
,用于提高编译器优化。C11标准新增了第四个限定符:_Atomic
。C11提供一个可选库,有stdatomic.h
管理,以支持并发程序设计,而且_Atomic
是可选支持项。
C99为类型限定符增加了一个新属性:幂等性(idempotent),表示可以再一条声明中多次使用同一个限定符,多余的限定符将被忽略:
const const const int n = 6; // 等同于 const int n = 6;
const 类型限定符(C90)
以const
关键字声明的对象,其值不能通过赋值或递增、递减来修改。在ANSI
兼容的编译器中,以下代码
const int nochange; // 限定nochange的值不能被修改
nochange = 12; // 不允许
编译器会报错。但是可以初始化const
变量,即下面代码没问题
const int nochage = 12; // 编译通过
该声明让nochange
成为只读变量。初始化后,就不能在改变它的值。
在指针和形参声明中使用const
const
限定符可以用来修饰普通变量、数组和指针,但修饰指针时要区分是限定指针本身为const
还是限定指针指向的值为const
。
const float *pf; // pf指向一个float类型的const值。 pf指向的值不能被改变,pf本身的值可以改变。
float const *pfc; // 等同于 const float *pfc;
float * const pt; // pt是一个const指针。 pt本身的值不能改变,但其所指向的值可以改变。
const float * const ptr; // 表明ptr本身不能改变,其指向的值也不能改变。
如注释所示,把const
放在类型名之后、*之前,表明该指针不能用于改变它所指向的值。简而言之,const
放在*左侧任意位置,限定了指针指向的数据不能改变;const
放在*的右侧,限定了指针本身不能改变。
const
关键字的常见用法是声明为函数形参的指针。
void display(const int array[], int limit); // array数组不会被修改。const int array[] 等同于 const int *array
对全局数据使用const
全局变量会暴露数据,导致程序的任何部分都能更改数据。如果把数据设置为const
,就可避免这样的危险。因此,用const
限定符声明全局数据很合理,可以创建const
变量、const
数据和const
结构
文件间共享const
数据有两个策略
遵循外部变量的常用规则
即在一个文件中使用定义式声明,在其他文件使用引用式声明(用extern
关键字)
const变量放在都文件,然后其他文件包含头文件
这种方案必须在头文件中用关键字static
声明全局const
变量,如果去掉static
,那么包含头文件的每个文件都会有一个相同标识符的定义式声明,即每个文件都有一个单独的数据副本,由于每个副本只对该文件可见,故无法用这些数据和其他文件通信。
头文件的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其他文件中使用引用式声明。所有文件都只需包含同一个头文件。但是它的缺点是,数据是重复的。对于简单的数据而言,这没有问题,但是如果const
数据包含庞大的数组,就不能视而不见了。
C
中的const
与C++
中的const
有何异同?
未完待续
C
中define
和const
的区别?
编译器处理方式不同
define
宏定义在预处理阶段展开,而const
常量是在编译运行阶段使用类型和安全检查不同
define
宏定义没有类型,不做任何类型检查,仅仅是替换展开const
常量有具体的类型,在编译阶段会执行类型检查内存分配不同
define
宏仅仅是展开,不会分配内存const
常量会在内存中分配空间,可以是堆或是栈常量和只读变量
C中定义常量是用
enum
或者define
宏,常量不需要分配内存空间const
修饰的是只读变量,不是常量,不可以作为数组维数,也不能放在case
关键字后面。
volatile 类型限定符(C90)
volatile
本意为易变的,表明代理(非变量所在程序)可以改变该变量的值。通常编译器为了优化减少读取内存会将变量由内存读入寄存器。但如果使用volatile
声明变量时,系统总是重新内存读取数据,即不进行优化。如果不适用volatile
,则编译器可能会声明语句进行优化。简单说就是volatile
关键字影响编译器编译结果,volatile
声明的变量表明该变量随时可能变化,相关运算不要优化,以免出错。
假设有以下代码
val1 = x;
/* 一些不适应x的代码 */
val2 = x;
智能的(进行优化的)编译器会注意到以上代码使用了两次x
,但并未改变它的值。于是编译器把x
的值临时储存在寄存器中,然后val2
使用x
时,从寄存器中(而不是原始内存位置上)读取x
的值,以节约时间。这个过程被称为高速缓存(caching)。通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x
的值,就不能这样优化了。如果没有volatile
关键字,编译器会假定变量的值在使用过程中不变,尝试优化代码。
可以同时是有const
和volatile
限定一个值。例如,通常用const
把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用1volatile
。只能在声明中同时使用这两个限定符,顺序不重要
volatile const int loc;
const volatile int * ploc;
restrict 类型限定符(C99)
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[n] = 5;
ar[n] *= 2;
par[n] += 3;
restar[n] += 3;
}
由于之前生命了restar
是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及restar
的两条语句替换成下面这条语句,效果相同:
restar[n] += 8; /* 可以进行替换 */
但是,如果把与par
相关的两条语句替换成下面的语句,将导致计算错误:
par[n] += 8; /* 给出错误的结果 */
这是因为for
循环在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()
函数要求两个位置不重叠,但是memove()
没有这样的要求。声明s1
和s2
为restrict
说明这两个指针是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足memcpy()
函数无重叠的要求。memmove()
允许重叠,它在拷贝数据时不得不更小心,以防止在使用数据之前就先覆盖了数据。
restrict
关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict
要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。
_Atomic 类型限定符(C11)
并发程序设计把程序执行分成可以同时执行的多个线程。这个程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.h
和threads.h
,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问院子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该队形。例如,下面的代码:
int hogs; // 普通声明
hogs = 12; // 普通赋值
// 替换为
_Atomic int hogs; // hogs是一个原子类型的变量
atomic_store(&hogs, 12); // stdatomic.h 中的宏
这里,在hogs
中储存12
是一个原子过程,其他线程不能访问hogs
。
编写这种代码的前提是,编译器支持这一新特性。
参考
- C Primer Plus (第六版) 中文版
- C99中的restrict关键字