12类型限定符

参考资料:
C语言中文网:http://c.biancheng.net/
《C语言编程魔法书基于C11标准》
视频教程:C语音深入剖析班(国嵌 唐老师主讲)

const

const限定符用于修饰一个对象,表明该对象是一个常量。

const修饰的对象只能初始化一次,之后它的值就不能被修改。

例如:

int const a = 100;		//这里定义常量a,它具有int类型
const float b = 10.5f;	//这里定义常量b,它具有float类型

上面的例子都是定义了一个常量

上面代码中,无论是a还是b都不能对它们的值进行修改。但如果常量定义在函数中,那么是可以间接通过指针进行修改的,不过C语言的规定中,这个是未定义的行为,也就是用该行为造成的不良后果无法预测。

#include <stdio.h>

int main(int argc,const char * argv[]) {
    int const a = 10;	//定义了常量a
    void *p = (void *)&a;	//这里通过万用指针p指向a的地址
    *(int *)p += 10;		//然后通过指针去修改a的值
    printf("a = %d\n",a);
    return 0;
}

运行结果

a = 20

这里的值被修改成20是因为,常量a的存储空间是在栈空间上,也就是函数中,栈空间本身是一个可读可写的存储空间,因此在这种情况下即便C语言编译器会对常量对象在编程语言语法上进行保护,但也不能保证在运行时常量对象的值一定不变。

上面的这种修改方式叫做指针解引用,解引用也就是通过对指针对象做间接操作以访问该指针所指向对象的值。说白了就是“解脱”(这里的解脱不是真的解脱)了原有变量对它的引用,改用指针引用例如int a = 10;int *p = &a;那么这时p对a的操作也是解引用。

C语言标准明确指出,通过指针解引用的方式去修改一个常量对象,其行为是未定义的,所以会导致很多无法预测的后果。

因为常量被存储了在栈空间上,所以可以被修改使用,但如果常量被定义在了文件作用域中,那么结果就不一样了。

#include <stdio.h>

int const a = 20;	//定义了常量a

int main(int argc,const char * argv[]) {
    void *p = (void *)&a;	//这里通过万用指针p指向a的地址
    p = (void *)&a;		//然后通过指针去修改a的值
    *(int *)p += 20;
    printf("a = %d\n",a);
    return 0;
}

运行结果:

Segmentation fault (core dumped)

直接就段错误了,因为该常量被分配到了常量存储区,该存储区是只读存储区,所以在修改的时候直接就报段错误了。

修饰普通对象

const修饰一个普通对象时,该对象的值将不能被修改。当一个const修饰一个复合类型对象时(比如一个结构体对象),那么该结构体对象中的所有成员的值都不能被修改。

#include <stdio.h>

int main(int argc, const char* argv[]) {
	//声明了一个int类型的常量对象a,但没有对它初始化
	int const a;
	//即便如此,我们也不能再对常量对象a进行赋值,因此以下这条表达式是错误的
	//a = 100;
	//声明了一个常量枚举对象e,并用MY_ENUM2枚举值对它初始化
	enum {MY_ENUM1,MY_ENUM2} const e = MY_ENUM2;
	//定义了一个匿名结构体,并用它声明了一个常量结构体对象s,这里的const也能放在struct前面
	struct {
		int a;
		struct {
			float f;
			double d;
		}inner;
	} const s = { .a = 10,.inner = {10.5f,-20.5} };
	//这里对结构体对象s中的任一成员,包括其内嵌类型的成员,都不能修改
	//s.inner.d = 30;	//这条语句是错误的
	printf("result = %.1f\n", e + s.a + s.inner.f - s.inner.d);
	struct Object {
		int a, b;
		const int c;	//这里成员对象c是一个常量
	};
	struct Object obj = { 10,20,0 };
	obj.a += obj.b;	//这条语句没问题,因为只有c是常量
	//以下这条语句将会出现编译报错
	//因为Object结构体中的成员对象c是常量,所以它的值不能被修改
	//obj.c += 10;
	return 0;
}

运行结果:

result = 42.0

修饰数组

因为数组实际上只是一个地址,是一个指向数组首元素的地址,所以const并不能修饰数组,但可以修饰数组中的元素。

#include <stdio.h>

int main(int argc, const char* argv[]) {
	//声明了一个带有5个const int类型元素的数组对象a
	//这里的const修饰的是a[i],而不是a自身
    //因为a[]会隐式转换成*a所以就变成了int const *a
	int const a[] = { 1,2,3,4,5 };
	//以下这条语句将产生编译错误,
	//因为数组对象a中的每个元素都是常量,不能被修改
	//a[0]++;

	//这里首先定义了一个匿名枚举,然后用该枚举类型声明了一个
	//带有3个常量元素的数组对象e。e的每个元素类型为const枚举类型,
	//这里的const也能放在enum之前
	enum {MY_ENUM1,MY_ENUM2,MY_ENUM3};
	const e[] = { MY_ENUM1,MY_ENUM2,MY_ENUM3 };

	//e[1] = MY_ENUM3;	//这条语句也是无法通过编译的

	//这里定义了一个UN联合体类型,并用该类型声明了一个带有2个常量元素的数组对象u。
	//u的每个元素类型为const union UN,这里的const也能放在union之前。
	union UN { int a; float f; };
	union UN const u[] = { {.a = 10},{.f = 2.5f} };
	//u[1].f = 0.0f;	//这条语句也无法通过编译
	printf("The value is: %.1f\n", a[0] + e[1] + u[1].f);
	return 0;
}

运行结果:

The value is: 4.5

const修饰一个数组对象时,产生作用的是该数组中的每个元素。

修饰指针

const用在指针上,那么就会有各种组合的变化,比如修饰指针指向的数据为常量,或者修饰指针自己本身的值为常量,或者两个都是常量。

int * const p1;	//指向指针对象p1为常量,也就是p1的值为常量
int const * p2;	//指向指针对象p2做间接操作后的值作为常量,也就是指针指向的值为常量
int const * const p3;	//指定指针对象p3为常量,并且对它做间接引用后的值也作为常量
#include <stdio.h>

//这里定义了一个dummy函数
static void dummy(int a) {
	printf("param a = %d\n", a);
}

int main(int argc, const char* argv[]) {
	//声明一个常量对象a,并将它初始化为10
	const int a = 10;
	//声明一个普通对象b,并将它初始化为20
	int b = 20;
	//声明一个变量对象p,并用对象a的地址对它初始化
	int const* p = &a;
	//*p = 20;	//这句非法,(*p)是一个常量,其值不能被修改
	p = &b;	//正确
	//*p = 30;	//这句非法,因为const修饰了(*p)为常量,所以就算b是一个普通对象也无法修改
	//声明了一个常量指针对象q,并用对象b的地址对其初始化
	int* const q = &b;
	//q = NULL;	//这句非法,q是一个常量,其值不能被修改,这里const修饰的是q这个对象,而不是*q
	*q += 10;

	//这里声明了一个变量指针对象cpp,并用指针对象p的地址对它初始化,
	//注意,这里的const修饰的是(**cpp),但*cpp也是const修饰的。
	//此外,这里(*cpp)的类型为const int *,(**cpp)的类型为const int
	int const** cpp = &p;	//cpp的类型为const int **
	*cpp = &a;				//这里通过间接操作,使得指针对象p又指向了a,*cpp可修改,因为*cpp的类型为const int *,那么是cpp指向的数据不能进行修改而已
	if (p == &a)
		puts("p points to a!");	//*cpp = &a把p的指向修改成了&a
	cpp = NULL;		//这句没问题
	//**cpp = 0;		//这句错误,因为(**cpp)是一个常量,其值不允许被修改

	//这里声明了一个常量指针对象cqq,并用指针对象q的地址对它初始化。
	//cqq前的const修饰的是cqq,说明cqq自身是一个常量
	//int*后面的const修饰的是(*cqq),说明(*cqq)也是一个常量。
	//也就是说,这里(*cqq)的类型为int * const,(**cqq)的类型为int
	int* const* const cqq = &q;	//cqq的类型为int * const * const
	**cqq += 100;	//这句没问题,(**cqq)的类型是int,不是一个常量,因为const只是修饰了*cqq为常量
	printf("b = %d\n", b);
	//*cqq = NULL;	//这句错误,(*cqq)的类型为int * const,是一个常量
	//cqq = NULL;	//这句错误,cqq的类型为int * const * const,是一个常量

	//声明一个数组对象arr,它包含有3个int类型的元素
	int arr[3] = { 1,2,3 };

	//这里声明一个常量指针对象pArray,指向数组对象arr的地址,
	//pArray的类型为int (* const)[3],const修饰的是pArray对象标识符,
	//这也说明const修饰的类型为int(*)[3],即pArray自身是一个常量
	int(* const pArray)[3] = &arr;
	(*pArray)[1] = 0;	//OK
	//pArray = NULL;		//这句非法,pArray是常量,其值不允许被修改
	printf("arr[1] = %d\n", arr[1]);

	//这里声明了一个指向函数的指针常量对象pFunc。
	//这里的const修饰的是pFunc标识符,说明pFunc自身是一个常量。
	//const所修饰的类型则是void (*)(int)
	void(* const pFunc)(int) = &dummy;
	pFunc(100);		//没问题
	//pFunc = NULL;	//语法错误,pFunc是常量,其值不允许被修改
	return 0;
}

运行结果

p points to a!
b = 130
arr[1] = 0
param a = 100

const修饰的是那个对象,可由const*的位置可得,例如上面例子中的int const* p = &a; const修饰的是*p,那么p的类型是int const **p的类型是int const,所以const修饰的只是p指向的数据,所以p指向的数据是常量,又例如int* const q = &b; const修饰的是q,所以q的类型是int * const *q的类型是int,所以const修饰的是q,也就是q的值不能修改,但*q指向的数据可修改。

总结起来就是如果const*左边,那么就是指针的对象无法修改,也就是不能改指向的地址(不能换对象,要从一而终),const*右边表示指针指向的对象不能修改,也就是指向的对象是一个常量(不过可换对象指向,但指向后的对象就又成为常量了)

const修饰指针的隐式转换问题

类型安全的问题。比如说,我们声明了一个常量对象const int a = 10;,如果用它赋值给另一个普通变量对象,这没有问题,如:int b = a;,不过如果用在指针上面就要注意类型了,比如int *p = &a;,这时a是常量,如果用*p = 20;那么是会违背C语言的设计的,所以在面对指针的时候,我们要注意赋值号左右的类型进行赋值。对一个常量对象做取地址操作之后的指针类型是在const后面直接加*号,例如:const type obj; 也就是如const int a;&obj的类型就是const type*,那么&a就等于const int *这个就是&a后的类型,然后我们进行赋值的时候也要赋值给const int *的对象。

高限定类型是可以隐式转换成低限制类型的,但低限制类型是不能转换成高限制类型的,也就是例如:等号右边是const int *类型不能隐式转换为等号左边的int *类型,然而等号右边如果是int *类型,那么可以隐式转换为const int *类型与等号左边的表达式匹配。不过不建议这样去做,因为编译器会报警告,在C编程中,最好的是左右的类型匹配,避免出现不知名的BUG。

#include <stdio.h>

int main(int argc, const char* argv[]) {
	//这里先声明一个常量对象a
	const int a = 10;
	//这里声明一个普通指针对象p,初始化为空
	int* p = NULL;

	//这里声明一个const int**的指针对象pp,指向p的首地址。
	//这里用投射操作做类型强制转换就是因为int**隐式转换为const int**类型。会报警告,投射也叫说强制类型转换
	const int** pp = (const int**)&p;//p类型是*p,所以&p的类型为**p

	//这里大家注意,由于*pp的类型是const int*,
	//&a的类型也是const int*,所以两者完全兼容。
	//这条语句执行之后,p也就被间接指向了常量对象a的地址
	*pp = &a;
	if (p == &a)
		puts("p points to a!");
	//最后通过指针对象p来间接修改常量对象a的值
	*p = 10;
	printf("a = %d\n", a);
	return 0;
}

运行结果:

p points to a!
a = 10

修饰函数形参类型为数组的对象

一个函数的形参可以被表达为一个数组类型,但它本质仍然是一个指针,既然是一个指针,那么它跟原生的数组对象就会有所不同。原生的数组对象本身是不可被修改的,因此没有所谓的用限定符修饰数组对象的这个概念,然而对于指针则不同,指针对象的值是可被修改的。如果我们要对以数组类型呈现的函数形参对象施加const限定,使得该形参值无法被修改,那么我们只需要将const限定符放置在[]下标操作符里面即可。如果[]中函数数值字面量或其它标识符,那么const放在它们的前面,即左侧位置即可。

#include <stdio.h>

//这里,形参a相当于int * const类型
//形参b相当于const int *类型
//形参c相当于int const * const类型
static void Fun(int a[const 5], const int b[3], const int c[static const 4]) {	//VS编译器只识别中间const int b[3]这种定义方式
	a[0]++;		//OK
	//a = NULL;	//错误,a是一个常量指针对象
	//b[0]++;	//错误,b[0]是const int类型,一个常量
	//c[0]++;	//错误,c[0]是const int类型,是一个常量
	//c = NULL;	//错误,c本身是一个常量,不能被修改
	printf("The sum is:%d\n", a[0] + b[1] + c[2]);
	b = NULL;	//OK
}

int main(int argc, const char* argv[]) {
	int a[] = { 1,2,3,4,5 };
	int b[] = { 7,8,9 };
	int c[] = { 10,11,12,13 };
	Fun(a, b, c);
	return 0;
}

运行结果

The sum is:22

看每个形参的本质类型是直接将[]去掉,把里面的static等存储类说明符也全部去掉,只留限定符,然后把标识符变为*号即可。另外,如果我们对使用数组下标操作符的对象类型是否为一个常量看不清,可以把数组下标形式变为间接操作形式,比如a[0]++;可转换为(* (a + 0) )++;,语义是相同的,这样我们就能看到a[0]的类型其实与(*a)一样,都是int类型,而这里const修饰的是a自身。

volatile

volatile限定符修饰一个对象时,指明该对象的值可能会被异步修改,这暗示了编译器不要对该对象做寄存器暂存优化,在读写它的时候总需要显式地从它的存储器地址中获取值。一般C语言实现会将一个函数中多次出现的同一对象的值尽可能地存放在寄存器中,如果该对象的内容可以存放在寄存器中(部超过一个寄存器所能容纳的字节数),且寄存器数量足够,那么就会放到寄存器中。毕竟,寄存器访问比读写内存快太多了。说白了这个限定符就识告诉编译器,不要对我设置的这个变量进行优化。

volatile一般用于多个线程所共享的资源,包括用于数据同步的锁对象。volatile修饰对象时所摆放的位置与它所起的修饰效果同const一样,此外volatile可以与const一同使用,不过场合不多,如:int data = *(const volatile int*)0xff800000UL;表示从0xff800000地址多映射的外设存储器中获取int类型的相关数据。这里使用volatile限定符表示在内次出现*(const volatile int*)0xff800000UL时,都要显式地读取该地址中的内容,而不是在第一次读取之后就默认将该值存放在CPU的寄存器中,然后后续的读取都直接从该寄存器中获取数据。

#include <stdio.h>
#include <pthread.h>

//这里定义了一个Fun函数,其形参a的类型为:int * const volatile
static void Fun(int a[static volatile const 2]) {
    printf("a[0] = %d,a[1] = %d\n", a[0], a[1]);
}

//这里声明一个普通的int类型静态对象
static int normalInt;
//这里声明了一个volatile的int类型静态对象
static volatile int volatileInt;
//这个函数用于用户线程执行例程
static void* ThreadProc(void* param) {

    //做一个1000000次循环
    //每次分别将normalInt与volatileInt递增,
    //最后看主线程中这两个值的变化
    for (int i = 0; i < 1000000; i++) {
        normalInt++;	//编译器会做优化,变成寄存器值
        volatileInt++;
    }
    return NULL;
}

int main(int argc, const char* argv[]) {
    //在主线程中,一开始将normalInt与volatileInt初始化为0
    normalInt = 0;
    volatileInt = 0;

    pthread_t threadID;
    //我们用pthread API创建一个用户线程,
    //该线程中normalInt与volatileInt在不断变化
    //开启一个线程
    pthread_create(&threadID, NULL, &ThreadProc, NULL);
    //这里循环10000次,明显少于用户线程的1000000次,
    //这样,用户线程在对这两个考察对象的最终修改不会做综合
    while (volatileInt < 10000);

    //我们在Release模式下能观察到,normalInt对象的值始终为0
    //而volatileInt则得到了当前修改后的值
    printf("normalInt = %d\n", normalInt);
    printf("volatileInt = %d\n", volatileInt);
    //这里声明一个指针对象p,(*p)的类型为volatile int
    volatile int* p = &volatileInt;
    //这里声明了一个指针对象q,它自身是volatile的。
    //(*q)的类型则是int,不是volatile的
    int* volatile q = &normalInt;
    Fun((int[]) { *p, * q });
    return 0;
}

运行结果

normalInt = 0
volatileInt = 120765
a[0] = 354528,a[1] = 0

我们要在release模式下才能行,只有这样编译器才会对代码做出优化,我们才能看到效果,Gcc编译要添加-O2例如:gcc main.c -o main.o -lpthread -O2clang就要:clang main.c -o main.o -lpthread -O2

normalInt为0是因为编译器会做优化,变成寄存器值,既然是寄存器的值,那么就存在寄存器中,而不是存在内存中,然而normalInt这个变量是存在内存中的,所以在函数的循环中修改的只是寄存器中的值,所以最后导致内存中的normalInt并没有修改。

register

register限定符暗示编译器将相应的变量保存到寄存器中进行加速优化,也就是和volatile限定符相反的作用,但是否对变量保存到寄存器中,那么就看编译器的实际优化情况了。

register限定符的限制

  • register变量必须是能被CPU所接受的类型,这通常意味着register变量必须是一个单个的值,并且长度应该小于或等于整型的长度。不过,有些高级的机器的寄存器也能存放浮点数。
  • 因为register变量可能不存放在内存中(当优化成功时),所以不能使用&来获取register限定过的变量的地址。
  • 局部变量和函数的形参可以作为寄存器变量,其它的不行(如全局变量)。
  • 局部静态变量不能定义为寄存器变量。不能写成:register static int a,b,c;
  • 由于寄存器的数量有限,不能定义任意多个寄存器变量,而且某些寄存器只能接受特定的数据(如指针和浮点数),因此真正起作用的register限定符的数目和类型都依赖于运行程序的机器,而任何多余的register限定符都将被编译器所忽略。
  • 简单的说就是,想优化就写上,至于又不优化全看编译器的心情。
#include <stdio.h>


//编译器会报错,因为全局变量的生命周期是程序的整个生命周期,也就是程序结束,全局变量才会被释放,如果全局变量能放到寄存器中,那么就有个问题,就是这个寄存器是不能释放了这个值,这个值要一直存在,那么这个寄存器就不能给其它数据使用,假如C语言运行把全局变量放到寄存器中,那么如果设置了多个全局变量的寄存器值,那么这个CPU就无法进行其它工作了,所以C语言直接就不允许全局变量被设置成寄存器变量
//register int m = 0;

void f1() {
	int i = 0;
	i++;
	printf("%d\n",i);
}
void f2() {
	static int i = 0;
	i++;
	printf("%d\n",i);
}

int main(int argc,const char * argv[]) {
	auto int i = 0;
	register int j = 0;
	static int k = 0;	//生命周期是程序的整个周期,程序结束该变量才会被回收

	printf("%p\n",&i);
	//printf("%x\n",&j);	//编译器会报错,因为该变量是寄存器变量,数据都在寄存器中,只有在内存中的变量才可以获取到地址,寄存器没有地址
	printf("%p\n",&k);

	for (i=0;i<5;i++) {
		f1();	//打印的5次都是1,因为函数中的i是局部变量,所以函数调用完后都会被回收,重新调用时又重新分配
	}

	for (i=0;i<5;i++) {
		f2();	//打印的5次分别是1,2,3,4,5,因为函数中的i是全局静态区的变量,所以它只是被初始化1次,也就是只有第一次调用f2函数的时候才会去初始化i,后面编译器看因为已经初始化过i了,所以就不再进行初始化为0,所以每次的相加都会被保留下来,所以打印的是1,2,3,4,5
	}
}

运行结果

0x7fffeb8ae61c
0x601038
1
1
1
1
1
1
2
3
4
5

restrict

用于修饰一个指针类型的对象,而不能用于修饰一个普通对象。使用restric限定符可暗示编译器对通过指针访问的数据进行优化,比如说我们可以直接将受restrict限定符的指针所读取到的值存放在寄存器中,后续再次出现对该指针的访问时可直接拿寄存器中的数据,而不需要做真正的访存操作。

#include <stdio.h>
#include <stdint.h>

int main(int argc, const char* argv[]) {
	//这里声明一个数组,它含有8个uint8_t类型的元素
	uint8_t bytes[] = { 0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80 };
	//声明一个指向int32_t类型的指针对象p,它直接指向bytes数组的首地址
	int32_t* p = (int32_t*)bytes;
	
	printf("First, *p = 0x%.8X\n", *p);

	//声明一个指向int32_t类型的指针对象q,它直接指向bytes[2]位置元素的地址
	int32_t* q = (int32_t*)&bytes[2];
	printf("First,*q = 0x%.8X\n", *q);

	*q += 16;

	printf("Now, *p = %0x%.8X\n", *p);
	return 0;
}

运行结果

First, *p = 0x40302010
First, *q = 0x60504030
Now, *p = 0x40402010

从上面的代码中可看到bytes[2]bytes[3]部分数据会出现重叠,p所指对象的高2字节与q所指对象的低2字节重叠,也就是说(*p)(*q)所表示的对象不是相互独立的,在这种情况下,不能使用restrict限定符去修饰。

如果允许这样去做,那么就如果修饰的是(*p),也就是把(*p)中的0x40302010读到寄存器中,然后(*q)修改时肯定是会影响到(*p)中的值的,这时(*p)指向的内存中的数据改了,但寄存器中的依然是0x40302010,假设我们修饰的是(*q),那么道理也是一样的(*p)修改了内存中的值,不能及时反映到寄存器中,所以这个也是为什么对重叠的两个指针对象不能做restrict修饰的原因。

能被restrict修饰的指针对象,它所指向的存储空间应该是当前执行环境下唯一的,没有其他指针与它指向同一个存储空间,并且也不存在任何与其他指针所指向的存储空间有重叠的情况。说白了就是告诉编译器,我所指向的内存归我所有,谁也不能和我抢,只有我能改动这片内存,其他指针不能改。

#include <stdio.h>
#include <stdint.h>

//这里参数a的类型为:int * const volatile restrict
static void Fun(int a[static const volatile restrict 2]) {
	printf("The result is:%d\n",a[0] + a[1]);
}
/*
下面是利用restrict限定符的存储数据拷贝函数
param pDst 指向目的存储空间
param pSrc 指向源存储空间
param count 指定要拷贝多少个int32_t的数据元素
*/
void MyfastMemCpy(int32_t * restrict pDst,const int32_t* restrict pSrc,size_t count) {
	//如果元素个数为偶数,我们直接2个元素2个元素进行拷贝,以提升效率
	if((count & 1) == 0) {
		const size_t nLoop = count / 2;
		uint64_t* restrict p = (uint64_t *)pDst;
		const uint64_t* restrict q = (const uint64_t*)pSrc;
		for(size_t i = 0;i < nLoop;i++)
			p[i] = q[i];
	} else {
		for(size_t i = 0;i < count;i++)
			pDst[i] = pSrc[i];
	}
}

int main(int argc,const char * argv[]) {
	//声明了两个int类型对象a和b
	int a = 10,b = 20;

	//声明了一个指向int类型的受restrict限定的指针对象p,
	//用对象b的地址对它初始化
	int* restrict p = &a;
	int* restrict q = &b;

	//以下这条语句是非法的!两个restrict限定的指针不能指向同一个存储空间。
	//尽管编译器对此不会有任何警告,但可能会引发未定义的结果
	p = q;
	Fun((int[]){*p,*q});
	//以下这条语句是非法的,restrict不能用于修饰非指针类型
	//restrict int x = 0;
	//以下这条语句是非法的,(*t)类型为int,不是指针类型
	//restrict int *t = NULL;
	//下面定义了两个数组,均含有1024个元素
	int32_t dst[1024] = {0};
	int32_t src[1024];
	//对src数组元素做初始化
	for(int i = 0;i < 1024;i++)
		src[i] = i;
	//调用我们自制的高效存储器拷贝函数
	MyfastMemCpy(dst,src,1024);
	//最后我们验证以下结果
	for(int i = 0;i < 1024;i++) {
		if(src[i] != dst[i]) {
			puts("Result not equal!");
			return -1;
		}
	}
	puts("Result equal!");
}

运行结果

The result is:40
Result equal!

像上面的代码中,我们破坏了restrict的要求,不过编译器无法识别,因为要实现这种识别对编译器来说开销会比较大,那么就会与C语言设计的高性能相违背,所以编译器不去实现这要的检查,需要编写人员自己去遵守规则。restrict一般用于函数形参,提示函数使用者所传入的对象地址要保持唯一性。

_Atomic

_Atomic限定符是在C11标准中引入的。该限定符修饰变量为原子类型变量,_Atomic一般修饰的是非指针对象类型,所以不牵涉限定指向数组的指针以及指向函数的指针这些比较特殊的类型表达式。

原子对象的意思就是当原子对象的读和写的时候都是不可被打断的,也就是说,如果原子对象在操作,就算硬件发一个中断信号过来,该中断信号也不能立即触发处理器的中断执行操作,处理器必须执行完整条原子操作之后才可以进入中断执行操作。在使用原子操作的时候不同担心当前的执行线程会被切换,因为中断处理不会发生。原子对象往往用于多核多线程并行计算中对多个线程共享变量的计算。

对于多个处理器核心对同一个存储空间的访问,存储器控制器会去仲裁当前那个原子操作先进行访存操作,哪个后进行,这些访存操作都会被串行化,所以这对于多核多线程并行计算的数据同步而言是必须的处理器特征。我们无法通过简单的开关中断去控制各个核心同时执行不同线程的行为与状态,所以在多核心多线程并行计算的环境下,原子操作时唯一的数据同步手段。另外,像互斥体、信号量等都时基于原子操作。

在使用原子操作时,应当包含<stdatomic.h>标准库头文件,该头文件中已经预定义了一些当前主流处理器所能支持的原子对象类型,此外还有相应的原子操作函数。在实际使用原子类型时应当避免直接使用_Atomic(类型名)这种形式,也就是禁止_Atomic a = 10;这种使用方法,因为头文件中的类型是由汇编编写的,性能会特别的高,所以推荐使用头文件中的。我们应该直接用<stdatomic.h>头文件中已经定义好的原子类型。当前C11标准中所罗列的能够支持原子对象类型的基本类型均为整数类型,也就是说除整数类型外的其他类型都无法作为原子对象类型(包括浮点类型)。

常用的原子对象类型:atomic_boolatomic_charatomic_scharatomic_ucharatomic_ushortatomic_shortatomic_intatomic_uintatomic_longatomic_ulongatomic_char16_tatomic_char32_tatomic_wchar_tatomic_intptr_tatomic_uintptr_tatomic_size_tatomic_ptrdiff_t等。想atomic_int类型就被定义为_Atomic(int),也就类似于_Atomic int a;

原子对象的初始化与普通对象也不同,在<stdatomic.h>头文件中定义了两个接口,分别用于对全局原子对象与函数内局部原子对象进行初始化。另外,对原子对象的读写也不应该直接用赋值操作符,而是需要通过使用atomic_load函数进行读,atomic_store函数进行写。

#include <stdio.h>
#include <stdatomic.h>

//这里声明了一个int类型的静态原子对象sIntAtom
//通过ATOMIC_VAR_INIT宏函数对其初始化为100
//typedef _Atomic int atomic_int;这个就是atomic_int的原型
static atomic_int sIntAtom = ATOMIC_VAR_INIT(100);

int main(int argc,const char * argv[]) {
    //这里在main函数中声明了局部原子对象a
    atomic_int a;

    //我们通过atomic_init函数对原子对象a进行初始化为10
    atomic_init(&a,10);
    //我们通过atomic_store函数将原子对象a修改为20
    atomic_store(&a,20);

    //我们通过atomic_load函数将原子对象a的值加载到普通对象b中
    int b = atomic_load(&a);

    //利用atomic_fetch_add函数,对原子对象sIntAtom与普通对象b做原子加法操作。
    //此时返回的结果是做原子加法操作之前的sIntAtom的值,其实这时sIntAtom的值已经是120了,不过该函数返回之前的值
    int oldValue = atomic_fetch_add(&sIntAtom,b);

    printf("oldValue = %d\n",oldValue);

    //将原子加法操作之后的sIntAtom原子对象的值,加载到对象b中
    b = atomic_load(&sIntAtom);
    printf("sIntAtom = %d\n",b);
}

运行结果

oldValue = 100
sIntAtom = 120

除了原子加法操作外,还有atomic_fetch_sub(原子减法操作)atomic_fetch_or(原子按位或操作)atomic_fetch_xor(原子按位异或操作)atomic_fetch_and(原子按位操作),这些算术逻辑原子操作都不能用于atomic_bool类型,即布尔原子类型。

原子对象的初始化函数不是原子操作,也就是说atomic_init函数可以被打断。因此对原子对象做初始化时应当统一在一个线程中完成(通常是主线程),然后再做线程分派调度。另外,对原子对象进行初始化操作用ATOMIC_VAR_INITatomic_init这两个接口,并且其他对原子操作的,必须是已经初始化的原子对象,否则结果可能是未知的。

#include <stdio.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stdint.h>
#include <pthread.h>

//声明一个静态unsigned long long类型的原子对象,初始化为0
//用于存放原子计算操作的求和计算结果
static volatile atomic_ullong sAtomResult = ATOMIC_VAR_INIT(0);

//声明一个静态的int类型的原子对象,初始化为0
//用于存放原子计算操作的当前计算组的行索引
static volatile atomic_int sAtomIndex = ATOMIC_VAR_INIT(0);

//声明一个静态普通的uint64_t类型对象,并将它初始化为0
//用于存放普通计算操作的求和计算结果
static volatile uint64_t sNormalResult = 0;

//声明一个静态普通的int类型对象,并将它初始化为0
//用于存放普通计算操作中当前计算组的行索引
static volatile int sNormalIndex = 0;

//由于这个标志在用户线程中只写,且在主线程中只读
//因此在这两者线程中并不会产生数据竞争,所以无需使用原子对象
static volatile bool sIsThreadComplete = false;

//声明即将用于计算的二维数组
static int sArray[10000][100];

//定义普通计算操作的线程
static void* NormalSumProc(void* param) {
    //这里使用一个currIndex对象,使得sNormalIndex在每次迭代中仅被读取一次
    //减少外部修改的干扰
    int currIndex;

    //在每次迭代时,先读取当前行索引的值,然后立即对它做递增操作
    while((currIndex = sNormalIndex++) < 10000) {
        //得到当前行索引之后,对当前行的数组做求和计算
        uint64_t sum = 0;
        //在线程中做求和运算
        for (int i = 0;i < 100; i++) {
            sum += sArray[currIndex][i];
        }
        
        sNormalResult += sum;
    }
    //线程计算结束,将sIsThreadComplete标志置为true
    sIsThreadComplete = true;

    return NULL;
}
//定义原子操作计算的线程
static void* AtomSumProc(void* param) {
    int currIndex;
    while((currIndex = atomic_fetch_add(&sAtomIndex,1)) < 10000) {
        uint64_t sum = 0;
        for (int i = 0;i < 100; i++) {
            sum += sArray[currIndex][i];
        }
        atomic_fetch_add(&sAtomResult,sum);
    } 
    sIsThreadComplete = true;
    return NULL;
}

int main(int argc,const char* argv[]) {
    //先对sArray数组进行初始化
    for (int i = 0;i < 10000; i++) {
        for (int j = 0;j < 100;j++) {
            sArray[i][j] = 100 * i + j;
        }
    }
    //在主线程中计算出标准正确的计算结果
    uint64_t standardResult = 0;
    for (int i = 0;i < 10000;i++) {
        for (int j = 0;j < 100;j++) {
            standardResult += sArray[i][j];
        }
        printf("The standard result is: %lu\n",standardResult);
    }
    //定义一个线程ID
    pthread_t threadID;
    //创建一个线程,并运行线程函数
    pthread_create(&threadID,NULL,&NormalSumProc,NULL);
    //在主线程中也做类似的计算处理
    int currIndex;
    //使用非原子加法操作对当前数组行索引做后缀递增操作
    while((currIndex = sNormalIndex++) < 10000) {
        uint64_t sum = 0;
        for (int i = 0;i < 100;i++) {
            sum += sArray[currIndex][i];
        }
        sNormalResult += sum;
    }
    //等待线程完成
    while(!sIsThreadComplete);
    if (sNormalResult == standardResult) {
        puts("Normal compute compared equal!");
    } else {
        printf("Normal compute compared not equal: %lu\n",sNormalResult);
    }
    //对原子操作的线程做并行计算
    sIsThreadComplete = false;

    pthread_create(&threadID,NULL,&AtomSumProc,NULL);
    //在主线程中做和原子操作的通用的处理
    while ((currIndex = atomic_fetch_add(&sAtomIndex,1)) < 10000) {
        uint64_t sum = 0;
        for (int i = 0;i < 100;i++) {
            sum += sArray[currIndex][i];
        }
        atomic_fetch_add(&sAtomResult,sum);
    }
    //等待线程完成
    while(!sIsThreadComplete);
    if(atomic_load(&sAtomResult) == standardResult) {
        puts("Atom compute compared equal!");
    } else {
        puts("Atom compute compared not equal!");
    }
    return 0;
}

运行结果

...
The standard result is: 499899505050
The standard result is: 499999500000
Normal compute compared not equal: 506325789250
Atom compute compared equal!

从结果可得,采用普通求和计算往往无法得到正确计算结果,且计算结果的值每次执行还都不一样,这就是因为像++操作、+操作的非原子性造成的。像这类修改操作其实有三个步骤:读取数据、修改数据、存储数据。比如像a++;这个操作,如果用处理器指令来表示的话至少需要三条指令——load reg,[a](将对象a的值加载到reg寄存器中);inc reg(对reg寄存器做递增操作);store reg,[a](将reg寄存器的值再写回对象a中)。对于原子加法操作而言,它们将被组合成一单条指令,并且整个操作过程不能被打断。而对于普通操作,这三条指令每条执行完之后都能被打断,这使得一个线程对寄存器做了修改之后,但在写回之前被其他线程先写回了,然后等待线程再写回就先把前线程修改的内容覆盖了,从而造成了数据的不一致性。说白了就是普通的操作,在多线程的情况下,会造成我生产的数据被别人拿走了,然后后面我拿走的数据可能是别人的,我本想修改自己的数据,却反应慢了点,被别人修改了,然后我只能修改别人的,然而原子操作就是,我生产的数据,一定要我拿走了,别人才能去继续生产数据,原子操作就类似于上测试,要我完全上完了测试,别人才能进来测试,非原子操作就像高速公路上进收费站被插队的场景,我走到了第三名,不过也可能被别人插队。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值