C

C 知识量:16 - 74 - 317

12.4 ANSI C类型限定符><

简介- 12.4.1 -

通常用类型和存储类别来描述一个变量。例如:

static int n = 100;

其中,static是存储类别;int是数据类型。

  • C90新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const和volatile来声明,以这两个关键字创建的类型是限定类型(qualified type)。

  • C99标准新增了第3个限定符:restrict,用于提高编译器优化。

  • C11标准新增了第4个限定符:_Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。

此外,C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotent)!意思是可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略,例如:

const const const int n = 100; // 与 const int n = 100;相同

const类型限定符- 12.4.2 -

以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。例如:

const int number1 = 100; /* 初始化赋值,没问题 */
const int number2;       /* 创建后不能被修改 */
number2= 200;            /* 不允许 */

1、在指针和形参声明中使用const。

在指针中使用const要复杂一些,因为要区分是限定指针本身为const,还是限定指针指向的值为const。这里只需要记住以下重点内容:const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。例如:

const float * p;        // p指向的值不能被改变,而p本身的值可以改变。
float const * p;        // 与上面的效果相同。
float * const p;        // 指针p本身的值不能更改,但是它所指向的值可以改变。 
const float * const p;  // p既不能指向别处,它所指向的值也不能改变。

const关键字还常用于声明函数形参的指针。例如:下面的原型保证了数据不会被更改。

void display(const int array[], int limit);

形参声明const int array[]与constint * array相同,所以该声明表明不能更改array指向的数据。

在具体操作中,如果一个指针仅用于给函数访问值,应将其声明为一个指向const限定类型的指针;如果要用指针更改主调函数中的数据,就不使用const关键字。

2、对全局数据使用const。

使用全局变量具有风险,因为程序的任何部分都能更改数据。如果把数据设置为const,就可避免这样的危险。

在文件间共享const数据可以采用两个策略:

第1个策略是:遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字)。例如:

在文件file1中使用定义式声明:

const double PI = 3.14159;

在文件file2中使用引用式声明:

extern const double PI;

第2个策略是:把const变量放在一个头文件中,然后在其他文件中包含该头文件。例如:

在头文件head.h中:

static const double PI = 3.14159;

在file1中:

#include "head.h"

在file2中:

#include "head.h"

这种方案必须在头文件中用关键字static声明全局const变量。如果去掉static,那么在file1和file2中包含head.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做。

volatile类型限定符- 12.4.3 -

volatile的含义是易变的、不稳定的。该限定符表明:代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算机传入的信息。

volatile的使用方法如下所示:

volatile int n;       // n 是一个易变的位置
volatile int * pn;    // pn 是一个指向易变的位置的指针

volatile常用于编译器的优化,例如对于以下代码:

val1 = x;
val2 = x;

编译器会注意到以上代码使用了两次x,但并未改变它的值。于是编译器把x的值临时储存在寄存器中,然后在val2需要使用x时,才从寄存器中(而不是从原始内存位置上)读取x的值,以节约时间,这个过程被称为高速缓存。

高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。

可以同时用const和volatile限定一个值,但是只能在声明中同时使用这两个限定符,它们的顺序不重要,例如:

volatile const int n;
const volatile int * pn;

restrict类型限定符- 12.4.4 -

restrict关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。对于以下代码:

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;
}

指针restar是访问由malloc()所分配内存的唯一且初始的方式,因此,可以用restrict关键字限定它;而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式,所以不用把它设置为restrict。

对于for循环内的代码,编译器可以把涉及restar的两条语句替换成下面这条语句,效果相同:

restar[n] += 8;

但是,与par相关的两条语句不能这样替换,因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。

如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。

此外,restrict限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。

restrict关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。需要注意:编译器不会检查用户是否遵循这一限制。

_Atomic类型限定符- 12.4.5 -

并发程序设计把程序执行分成可以同时执行的多个线程,这涉及如何管理访问相同数据的不同线程问题。C11通过包含可选的头文件stdatomic.h和threads.h,提供了一些可选的(不是必须实现的)管理方法。需要注意的是:要通过各种宏函数来访问原子类型,而且,当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。例如:

_Atomic int n;            // n 是一个原子类型的变量
atomic_store(&n, 10);     // stdatomic.h中的宏函数

在n中储存10是一个原子过程,此时,其他线程不能访问n。以上操作需要编译器对该特性的支持。