精通C语言ANSI C 类型限定符const,volatile,restrict,_Atomic
Posted 从善若水
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了精通C语言ANSI C 类型限定符const,volatile,restrict,_Atomic相关的知识,希望对你有一定的参考价值。
本人就职于国际知名终端厂商,负责modem芯片研发。
在5G早期负责终端数据业务层、核心网相关的开发工作,目前牵头6G算力网络技术标准研究。
文章目录
ANSI C 类型限定符
我们通常使用类型和存储类别来描述一个变量。C90还新增了两个属性:恒常性 和易变形 。使用关键字const表示恒常性,使用关键字volatile表示易变形。以这两个关键字创建的类型是限定类型(qualified type)。C99新增第三个限定符restrict,用于提高编译器优化。C11新增第四个限定符_Atomic。对于编译器来说_Atomic是一个可选的支持项,由stdatomic.h管理,以支持并发程序设计。
C99还为限定符增加了一个新属性:idempotent(幂等)!听起来感觉好厉害,其实意思是说可以在一条声明中多次使用一个限定符,多余的限定符将被忽略。
const const const int CSRS = 6; //与const int CSRS = 6;相同
有了这个新属性,我们就可以编写类似下面的code:
typedef const int CSRS;
const CSRS c = 8;
1️⃣const 类型限定符
以const关键字声明的变量,其值不能通过赋值或递增、递减来修改,以下code是错误的:
const int CSRS;
CSRS = 6; //error
这样的code编译器会报错。但是,可以初始化const变量,下面的code正确:
const int CSRS = 6;
在指针和形参声明中使用const
使用const定义指针,看下面的code
/*case 1*/
const int *CSRS_p;
/*case 2*/
int const *CSRS_p;
/*case 3*/
int *const CSRS_p;
/*case 4*/
const int *const CSRS_p;
每种指针的含义:
- case1 和 case2 含义一样(也就是说const放在 * 左边都是一样的),CSRS_p指向不能被改变的值,但是CSRS_p本身的值可以改变
- 对于case 3,CSRS_p指向的值可以被改变,但是CSRS_p本身的值不能改变
- 对于case4,CSRS_p指向的值不能被改变,而且CSRS_p本身的值也不能改变
说白了就是 const放在 * 左侧任意位置,限定了指针指向的数据不能改变;const放在 * 的右侧,限定了指针本身不能改变
使用const声明函数形参,看下面的code:
void star_CSRS_blog(const int user_id[] , int n);
上面code声明的形参user_id[],表示在这个函数中不希望数组user_id中的值被修改,所以使用const声明数组user_id。
在函数原型和函数头,形参声明const int user_id[] 与 const int *user_id是相同的
我们来看一个ANSI C中函数strcat()的原型👇
char *strcat(char *restrict s1 ,const char * restrict s2);
这个函数在第一个字符串的末尾添加第二个字符串的副本。这更改了第一个字符串,但是未更改第二个字符串,上面的声明体现了这一点。
2️⃣volatile 类型限定符
volatile限定符告诉编译器,这个变量的值随时可能被其它程序或者线程修改,请不要尝试对该变量做优化。volatile的语法与const一样,看下面的code:
volatile int CSRS; /*CSRS是一个易变的位置*/
volatile int *CSRS_p; /*CSRS_p是一个指向易变的位置的指针*/
这个限定符对编译器有重要的作用,看下面的code:
CSRS1 = x;
/* 一些不使用x的code */
CSRS2 = x;
👇👇👇
开启优化功能的编译器在看到以上code使用了两次 x,但是并没有修改它的值。于是编译器会把x 的值临时存储在寄存器中,然后在CSRS2 需要使用x 时直接从寄存器获取,这样可以节省时间提高程序性能(这个过程也被称为caching)。
通常情况caching是一个不错的优化方案,但是如果一些程序或线程在以上两条语句之间修改了x 的值,就不能这样优化了。而volatile限定符就是告诉编译器会有这种情况发送,此时编译器就不会采取这样的优化策略了。
volatile也可以和const一起使用。考虑这样的场景,程序A只读取时钟,程序B实时的修改时钟。那么我们在程序A中可以声明这样的变量👇
volatile const int time;
const volatile int *time_p;
上面code中的变量保证在程序A中时钟信息是只读的,但是在程序B修改了时钟值后程序A可以立即获取到正确的值。
3️⃣restrict 类型限定符
restrict 关键字允许编译器优化某部分代码以更好地支持计算,它只能用于指针,表明该指针是访问数据对象唯一且初始的方式,想要搞明白先看下面的例子👇
int CSRS[5];
int * restrict star_CSRS_p = (int *)malloc(5*sizeof(int));
int * CSRS_p = CSRS;
👆上面的code,指针star_CSRS_p是访问malloc()函数所分配内存的唯一且初始的方式,因此可以使用restrict关键字限定它。而指针CSRS_p既不是访问数组CSRS的唯一方式,也不是初始方式,所以不能把它设置成restrict。
再看一个例子👇
for(int n=0;n<5;++n)
{
CSRS_p[n] += 5;
star_CSRS_p[n] += 5;
CSRS[n] *= 2;
CSRS_p[n] += 3;
star_CSRS_p[n] += 3;
}
上例中,由于之前声明了star_CSRS_p是访问数据对象唯一且初始方式,所以编译器可以把涉及star_CSRS_p的两条语句替换成下面的这条语句:
star_CSRS_p[n] += 8;
但是,如果把CSRS_p相关的两条语句替换成下面的这条语句,将会导致计算错误:
CSRS_p[n] += 8;
这是因为for循环在CSRS_p的两次访问相同的数据对象之间,用CSRS修改了该数据的值。
restrict 限定符还会用在函数形参中的指针。这意味着编译器可以假定在函数体内其它标识符不会修改该指针指向的数据,而且编译器还可尝试对其进行优化。例如 ANSI C库中的函数:
void * memcpy(void * restrict s1 , const void * restrict s2 ,size_t n);
restrict关键字有两个读者:
- 编译器:
该关键字告诉编译器可以自由假定一些优化方案 - 读者:
该关键字告知用户要使用满足restrict 要求的参数,编译器不会检查用户是否遵循这一限制,但是无视它后果自负
4️⃣_Atomic 类型限定符
C11专为并发程序设计引入的一个限定词。当一个线程对一个原子类型的变量执行原子操作时,其它线程不能访问该对象。我们对原子类型变量的操作都是通过定义在 stdatomic.h 和 threads.h中的各种函数宏。例如下面的code:
int hogs; //普通声明
hogs = 12; // 普通赋值
_Atomic int CSRS; //CSRS是一个原子类型的变量
atomic_store(&CSRS , 12); //stdatomic.h中的宏函数
但是这一新特性是可选的(不要求编译器强制实现),如果要使用需要确认编译器支持这一新特性。
➰旧关键词新用法
看下面的code👇
void starCSRS(int user_id[const restrict] , int n);
// 等价于
void starCSRS(int * const restrict user_id , int n);
根据新标准,在声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符。
新标准为static 引入一种与以前用法不相关的新用法。现在,static 除了表示静态存储类别变量的作用域和链接外,新的用法告诉编译器如何使用形式参数。看下面的code👇
void starCSRS(int user_id[static 20]);
static 的这种用法表明,函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素。这种用法的目的是让编译器使用这些信息优化函数的编码。
为什么给static 新增一个完全不同的用法呢?
主要是C 标准委员会考虑,如果新增关键字会导致,之前用新关键字作为标识符的程序无效。所以他们尽量使用现有的关键字,不再新增关键字。
以上是关于精通C语言ANSI C 类型限定符const,volatile,restrict,_Atomic的主要内容,如果未能解决你的问题,请参考以下文章