🥭类型限定符
2021-1-24
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
我们通常用类型和存储类别来描述一个变量。
C90 还新增了两个属性:恒常性(constancy)和易变性(volatility)。这两个属性可以分别用关键字const 和 volatile 来声明,以这两个关键字创建的类型是限定类型。
C99标准新增了第3个限定符:restrict,用于提高编译器优化。
C11标准新增了第4个限定符:_Atomic。C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项。 C99 为类型限定符增加了一个新属性:它们现在是幂等的!这个属性听起来很强大,其实意思是可以在一条声明中多 次使用同一个限定符,多余的限定符将被忽略:
有了这个新属性,就可以编写类似下面的代码:
 

const类型限定符

以const关键字声明的对象,其值不能通过赋值或递增、递减来修改。在ANSI兼容的编译器中:
但是,可以初始化const变量。因此,下面的代码没问题:
该声明让nochange成为只读变量。初始化后,就不能再改变它的值。
可以用const关键字创建不允许修改的数组:

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

声明普通变量和数组时使用 const 关键字很简单。指针则复杂一些,因为要区分是限定指针本身为const还是限定指针指向的值为const。下面的声明:
创建了 pf 指向的值不能被改变,而 pt 本身的值可以改变。例如,可以设置该指针指向其他 const值。相比之下,下面的声明:
创建的指针pt本身的值不能更改。pt必须指向同一个地址,但是它所指向的值可以改变。下面的声明:
表明ptr既不能指向别处,它所指向的值也不能改变。
还可以把const放在第3个位置:
简而言之, const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变。
const 关键字的常见用法是声明为函数形参的指针。例如,假设有一个函数要调用 display()显示一个数组的内容。要把数组名作为实际参数传递给该函数,但是数组名是一个地址。该函数可能会更改主调函数中的数据,但是下面的原型保证了数据不会被更改:
在函数原型和函数头,形参声明const int array[]与const int * array相同,所以该声明表明不能更改array指向的数据。
ANSI C库遵循这种做法。如果一个指针仅用于给函数访问值,应将其声明为一个指向const限定类型的指针。如果要用指针更改主调函数中的数据,就不使用const关键字。例如,ANSI C中的strcat()原型如下:
strcat()函数在第1个字符串的末尾添加第2个字符串的副本。这更改了第1个字符串,但是未更改第1个字符串。上面的声明体现了这一点。
 

对全局数据使用const

使用全局变量是一种冒险的方法,因为这样做暴露了数据,程序的任何部分都能更改数据。如果把数据设置为 const,就可避免这样的危险,因此用 const 限定符声明全局数据很合理。可以创建const变量、const数组和const结构
 
然而,在文件间共享const数据要小心。可以采用两个策略。第一,遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字):
 
另一种方案是,把const变量放在一个头文件中,然后在其他文件中包含该头文件:
这种方案必须在头文件中用关键字static声明全局const变量。如果去掉static,那么在file1.c和file2.c中包含constant.h将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样做(然而,有些编译器允许)。实际上,这种方案相当于给每个文件提供了一个单独的数据副本。由于每个副本只对该文件可见,所以无法用这些数据和其他文件通信。不过没关系,它们都是完全相同(每个文件都包含相同的头文件)的const数据(声明时使用了const关键字),这不是问题。
头文件方案的好处是,方便你偷懒,不用惦记着在一个文件中使用定义式声明,在其他文件中使用引用式声明。所有的文件都只需包含同一个头文件即可。但它的缺点是,数据是重复的。对于前面的例子而言,这不算什么问题,但是如果const数据包含庞大的数组,就不能视而不见了
 

volatile类型限定符

volatile 限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据。例如,一个地址上可能储存着当前的时钟时间,无论程序做什么,地址上的值都随时间的变化而改变。或者一个地址用于接受另一台计算机传入的信息。 volatile的语法和const一样:
以上代码把loc1声明为volatile变量,把ploc声明为指向volatile变量的指针。
 
你会觉得volatile是个可有可无的概念,为何ANSI委员把volatile关键字放入标准?原因是它涉及编译器的优化。例如,假设有下面的代码:
智能的(进行优化的)编译器会注意到以上代码使用了两次 x,但并未改变它的值。于是编译器把 x的值临时储存在寄存器中,然后在val2需要使用x时,才从寄存器中(而不是从原始内存位置上)读取x的值,以节约时间。这个过程被称为高速缓存(caching)。通常,高速缓存是个不错的优化方案,但是如果一些其他代理在以上两条语句之间改变了x的值,就不能这样优化了。如果没有volatile关键字,编译器就不知道这种事情是否会发生。因此,为安全起见,编译器不会进行高速缓存。这是在 ANSI 之前的情况。现在,如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码。 可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用 volatile。只能在声明中同时使用这两个限定符,它们的顺序不重要,如下所示:
 

restrict类型限定符

restrict 关键字允许编译器优化某部分代码以更好地支持计算。它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。考虑下面的代码:
这里,指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。
现在考虑下面稍复杂的例子,其中n是int类型:
由于之前声明了 restar 是访问它所指向的数据块的唯一且初始的方式,编译器可以把涉及 restar的两条语句替换成下面这条语句,效果相同:
但是,如果把与par相关的两条语句替换成下面的语句,将导致计算错误:
这是因为for循环在par两次访问相同的数据之间,用ar改变了该数据的值。 在本例中,如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他的标识符可能已经改变了数据)。如果用了restrict关键字,编译器就可以选择捷径优化计算。
restrict 限定符还可用于函数形参中的指针。这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途。例如,C 库有两个函数用于把一个位置上的字节拷贝到另一个位置。在C99中,这两个函数的原型是:
这两个函数都从位置s2把n字节拷贝到位置s1。memcpy()函数要求两个位置不重叠,但是memove()没有这样的要求。声明s1和s2为restrict说明这两个指针都是访问相应数据的唯一方式,所以它们不能访问相同块的数据。这满足了memcpy()无重叠的要求。memmove()函数允许重叠,它在拷贝数据时不得不更小心,以防在使用数据之前就先覆盖了数据。
restrict 关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。总而言之,编译器不会检查用户是否遵循这一限制,但是无视它后果自负。
 

_Atomic类型限定符(C11)

并发程序设计把程序执行分成可以同时执行的多个线程。这给程序设计带来了新的挑战,包括如何管理访问相同数据的不同线程。C11通过包含可选的头文件stdatomic.hthreads.h,提供了一些可选的(不是必须实现的)管理方法。值得注意的是,要通过各种宏函数来访问原子类型。当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。例如,下面的代码:
这里,在hogs中储存12是一个原子过程,其他线程不能访问hogs。
 

旧关键字的新位置

C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中。对于类型限定符而言,这样做为现有功能提供了一个替代的语法。例如,下面是旧式语法的声明:
该声明表明a1是一个指向int的const指针,这意味着不能更改指针本身,可以更改指针指向的数据。除此之外,还表明a2是一个restrict指针。新的等价语法如下:
根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。
static的情况不同,因为新标准为static引入了一种与以前用法不相关的新用法。现在,static除了表明静态存储类别变量的作用域或链接外,新的用法告知编译器如何使用形式参数。例如,考虑下面的原型:
static 的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。为何给static新增一个完全不同的用法?C 标准委员会不愿意创建新的关键字,因为这样会让以前用新关键字作为标识符的程序无效。所以,他们会尽量利用现有的关键字,尽量不添加新的关键字。
restrict 关键字有两个读者。一个是编译器,该关键字告知编译器可以自由假定一些优化方案。另一个读者是用户,该关键字告知用户要使用满足restrict要求的参数。
  • C
  • 分配内存:malloc()和free()文件输入输出
    目录