ANSI C类型限定符
我们通常使用类型和储存类别来修饰C中的变量,在标准之中还有这样一些特殊的限定符可以帮助我们更好的修饰C中的变量,它们包括
const
、volatile
、restrict
、_Atomic
。下面来分别详述它们的用处。
1. const:恒常性修饰符
使用
const
修饰符修饰的变量不能更改其值,而且对于const
修饰的指针类型有很多值得关注的形式。
指针与常量
- 首先由于
const
类型的变量不可以修改,所以我们也不希望通过普通指针通过指向const
来修改其内容,所以我们需要使用指向const的指针
,形式大致为:
const int *p;
或者int const *p
(*在const的右侧),这时候的指针可以指向对应类型常量,而且也不能通过其修改指向的常量的内容。- 但是我们也想要不可以改变指向(不能修改指针本身值)的指针,我们使用
常量指针
,形式如:
int *const p;
(*在const的左侧),这时候指针可以指向int
类型的变量,而且不能更改它的指向。- 我们也能结合这两种形式,获得指向常量的常量指针:
const int * const p;
,它可以指向常量,同时自身也作为常量。
全局变量与常量
全局变量的使用在某些方面很方便,但是如果通过某些函数来访问和修改是很危险的,所以我们可以通过
const
限定符来使得我们不能对静态变量进行修改,提高了程序的安全性。但是仅仅使用
const
修饰,在多文件的程序协同中也有诸多不便,我们有两种策略:
使用外部变量常用策略,一个文件中定义式声明,一个文件中引用式声明。
// another.c const int a = 5;// 定义式声明
// main.c #include <stdio.h> int main (void) { extern const int a;// 引用式声明 printf("a = %d", a); return 0; } /** a = 5 * **/
把
const
放入到头文件中,其他文件包含头文件。// main.h const static int a = 1;
// another.h #include "main.h" #include <stdio.h> void fun() { printf("a = %d\n", a); printf("a of another.c is stored in %p\n", &a);// 通过引用main.h有了常量a }
#include <stdio.h> #include "main.h" int main (void) { extern void fun();// 引用式声明查找another.c中的fun函数 fun(); printf("a = %d\n", a); printf("a of main.c is stored in %p\n", &a); return 0; } /** a = 1 a of another.c is stored in 0040506C a = 1 a of main.c is stored in 00405044 * **/
我们通过
#include
指令包含定义有全局变量的头文件,就可以实现对其的访问,但是其中我们要注意到两点:第一点,包含头文件的操作实际上是将对应头文件中内容替换到原指令处,我们也不需要额外的声明,即可直接使用变量,但是两个源文件都包含了该头文件,此时这个变量将会被创建两次(因为她们被static
修饰符修饰,两个源文件属于不同翻译单元),通过输出来看,她们在内存中占据的空间并不相同。第二点,为什么我们要在头文件中使用static
修饰符?因为如果不使用,导致两个文件中都有同样的全局变量a,(static
能修饰能让他们只在自己的翻译单元可见)这是不允许的,会导致程序奔溃。这两种方法各有利弊,后者较为简便,但是在全局变量占用空间较大时,它会同样的拷贝两份浪费储存空间,这时候还是使用第一种方法吧。
2. volatile:易变性修饰符
volatile
修饰符告诉编译器,代理可以修改该变量的值,而且不通过该程序进行修改。它和const
相互是并不冲突的,我们可能不希望在本程序中修改该变量,但是希望代理来修改。此外``volatile`对于编译器的一些优化也有很好的用途:
a = x;b = x
对于这样的代码,两次使用x的值而且未对x进行修改,所以编译器在优化时可以将其放入寄存器找中进行高速缓存,但是如果x通过代理发生改变后就不能使用这样的方法进行优化了。但是因为提出volatile
关键字,在不使用的情况下默认不会被代理改变,所以编译器会进行这样的优化。
3. restrict:唯一访问途径
restrict
可以对指针进行使用,假定该指针是访问该对象(一段内存空间)的唯一途径,借此编译器也可以进行一定程度的优化,例如:#include <stdlib.h> int main (void) { int* restrict p = (int *)malloc(sizeof(int)); int a = 5; int *p1 = &a; *p += 2; *p1 += 3; *p += 3; // 编译器会优化为*p += 5; free(p); return 0; }
一般情况下,编译器不会对这样的情形进行优化,因为它不能确定p1是否与p指向同一片内存空间,是否在其中这片空间的数据在中间被修改了。但是如果使用
restrict
修饰符,那么在假定p作为对象的唯一访问途径的情况下编译器自然会精心相应的优化。但是,注意restrict
基于程序员自己的判断,编译器并不会为你检查是否有其他指针也可以对对象中内容进行修改,如果不可以那么将造成令人迷惑的运算错误。例如常见的一些函数的实现都有利用该关键字:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
4. _Atomic: 线程独占
该修饰符使得一个线程对一个原子对象进行操作时,其他线程不能访问该对象。
5. 新标准关键字的新位置
C的新标准允许吧类型限定符和储存类别说明符中的
static
放入到函数原型和函数头的形式参数中的方括号中去,旧式声明可能如下:
void old(int *const a1, int *restrict a2);
新式可以这样:
void new(int a1[const], int a2[restrict]);
同样也有一种很为特殊的用法:
void fun(int a[static 20]);
其中指定了数组的大小至少为20个元素,这样便利与编译器根据这些休息优化编码。