如果两个指针引用同一内存地址,我们称一个指针是另一个指针的别名。别名并不罕见,不过可能会引发一些问题。下面的代码声明了两个指针,并把它们都指向同一地址:
int num = 5;
int* p1 = #
int* p2 = #
当编译器为指针生成代码时,除非特别指定,它必须假设可能会存在别名。使用别名会对编译器生成代码有所限制,如果两个指针引用同一位置,那么任何一个都可能修改这个位置。当编译器生成读写这个位置的代码时,它就不能通过把值放入寄存器来优化性能。对于每次引用,它只能执行机器级别的加载和保存操作。频繁的加载/保存会很低效,在某些情况下,编译器还必须关心操作执行的顺序。
强别名是另一种别名,它不允许一种类型的指针成为另一种类型的指针的别名。下面的代码中,一个整数指针是一个浮点数指针的别名,这破坏了强别名的规则。这段代码判断一个数是否为负数,相比将它的参数跟0比较来判断正负,这种方法执行速度更快:
float number = 3.25f;
unsigned int *ptrValue = (unsigned int *)&number;
unsigned int result = (*ptrValue & 0x80000000) == 0;
注意 强别名规则对符号或修饰符不起作用,下面都是合法的强别名:
int num; const int *ptr1 = # int *ptr2 = # int volatile ptr3 = #
不过,有些情况下,对同样的数据采用不同的表现形式也是有用的,为了避免别名问题,可以采用这几种技术:
- 使用联合体;
- 关闭强别名;
- 使用
char
指针。
两种数据类型的联合体可以避开强别名的问题,后面中会讨论这个主题。如果编译器有禁用强别名的选项,就可以关闭它。GCC编译器有如下的编译器选项:
-fno-strict-aliasing
可以关闭强别名;-fstrict-aliasing
可以打开强别名;-Wstrict-aliasing
可以打开跟强别名相关的警告信息。
需要关闭强别名的代码可能意味着差劲的内存访问实践,如果可能的话,花些时间解决这些问题,而不是关闭强别名。
注意 编译器并非总能准确地报告别名相关的警告,有时候会漏报,有时候会虚报,最终还是要靠程序员定位别名问题。
编译器总是假定char
指针是任意对象的潜在别名,所以,大部分情况下可以安全地使用。不过,把其他数据类型的指针转换成char
指针,再把char
指针转换成其他数据类型的指针,则会导致未定义的行为,应该避免这么做。
用联合体以多种方式表示值
C是类型语言,在声明变量时就得为其指定类型。可以存在不同类型的多个变量,有时候,可能需要把一种类型转换成另一种类型,这一般是通过类型转换实现的,不过也可以使用联合体。类型双关就是指这种绕开类型系统的技术。
如果转换涉及指针,可能会产生严重问题。为了说明这种技术,我们会用到三个不同的函数,这些函数会判断一个浮点数是否为正。
第一个函数用了浮点数和无符号整数的联合体,如下所示,函数先把浮点数赋给联合体,然后再获取整数来执行测试:
typedef union _conversion {
float fNum;
unsigned int uiNum;
} Conversion;
int isPositive1(float number) {
Conversion conversion = { .fNum =number};
return (conversion.uiNum & 0x80000000) == 0;
}
这样可以正确工作,也不会涉及别名,因为没有用到指针。下面这个版本用了两种数据类型的指针的联合体,将浮点数的地址赋给第一个指针,然后解引整数指针来执行测试。这样破坏了强别名规则:
typedef union _conversion2 {
float *fNum;
unsigned int *uiNum;
} Conversion2;
int isPositive2(float number) {
Conversion2 conversion;
conversion.fNum =&number;
return (*conversion.uiNum & 0x80000000) == 0;
}
下面这个函数没有用联合体,而且破坏了强别名规则,因为ptrValue
指针和number
共享了同一个地址:
int isPositive3(float number) {
unsigned int *ptrValue = (unsigned int *)&number;
return (*ptrValue & 0x80000000) == 0;
}
这三个函数所用的方法做了如下假设:
- 表示浮点数用的是IEEE-754浮点数标准;
- 以特定方式布局浮点数;
- 正确对齐了整数和浮点数指针。
不过,这些假设不一定有效。类似方法可以优化性能,但不一定可移植。如果移植性变得重要,执行浮点数比较是更好的办法。
强别名
编译器不会强制使用强别名,它只会产生警告。编译器假设两个或更多不同类型的指针永远不会引用同一个对象,这也包括除名字外其他都相同的结构体的指针。有了强别名,编译器可以做某些类型的优化。如果这个假设不正确,就会产生不可预期的结果。
即使两个结构体的字段完全一样,但如果名字不同的话,这两种结构体的指针就不应该引用同一对象。在下面的例子中,我们假设person
和employee
指针永远不会引用同一对象:
typedef struct _person {
char* firstName;
char* lastName;
unsigned int age;
} Person;
typedef struct _employee {
char* firstName;
char* lastName;
unsigned int age;
} Employee;
Person* person;
Employee* employee;
不过,如果定义了同一个结构体的两个类型,那么指向不同名字的指针可以引用同一对象:
typedef struct _person {
char* firstName;
char* lastName;
unsigned int age;
} Person;
typedef Person Employee;
Person* person;
Employee* employee;
使用restrict关键字
C编译器默认假设指针有别名,用restrict
关键字可以在声明指针时告诉编译器这个指针没有别名,这样就允许编译器产生更高效的代码。很多情况下这是通过缓存指针实现的,不过要记住这只是个建议,编译器也可以选择不优化代码。如果用了别名,那么执行代码会导致未定义行为,编译器不会因为破坏强别名假设而提供任何警告信息。
注意 新开发的代码应该尽量对指针声明使用
restrict
关键字,这样会产生更高效的代码,而修改已有代码可能就不划算了。
下面这个函数说明restrict
关键字的定义和使用,该函数把两个向量相加,并将结果存在第一个向量中:
void add(int size, double * restrict arr1, const double * restrict arr2) {
for (int i = 0; i < size; i++) {
arr1[i] += arr2[i];
}
}
两个指针参数都用了restrict
关键字,但是它们不应该引用同一块内存,下面是函数的正确用法:
double vector1[] = {1.1, 2.2, 3.3, 4.4};
double vector2[] = {1.1, 2.2, 3.3, 4.4};
add(4,vector1,vector2);
在下面的代码中,两个参数用了同一个向量,这样的调用是不正确的。第一个调用语句用了别名,而第二个调用语句则使用了同一个向量两次:
double vector1[] = {1.1, 2.2, 3.3, 4.4};
double *vector3 = vector1;
add(4,vector1,vector3);
add(4,vector1,vector1);
尽管这么做有时候能正确工作,但是调用函数的结果是不可靠的。
一些标准C函数用了restrict
关键字,包括:
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
char *strcpy(char * restrict s1, const char * restrict s2);
char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
int printf(const char * restrict format, ... );
int sprintf(char * restrict s, const char * restrict format, ... );
int snprintf(char * restrict s, size_t n, const char * restrict format, ... );
int scanf(const char * restrict format, ...);
restrict
关键字隐含了两层含义:
- 对于编译器来说,这意味着它可以执行某些代码优化;
- 对于程序员来说,这意味着这些指针不能有别名,否则操作的结果将是未定义的。
评论前必须登录!
注册