如果它们有相同的对齐方式,将 int 指针强制转换为 float 指针是否合法?
如果它们有相同的对齐方式,将 int 指针强制转换为 float 指针是否合法?
当询问C语言中的常见未定义行为时,人们有时会提到严格别名规则。
他们在谈些什么?
我发现最好的解释是由Mike Acton提供的《了解Strict Aliasing》。这篇文章着重于PS3开发,但实质上只是GCC。
从这篇文章中得知:
“Strict aliasing是C(或C ++)编译器做出的一种假设,即指向不同类型的对象的指针解引用永远不会引用同一内存位置(即别名)。"
因此,如果您有一个指向包含int的内存的int*,然后指向该内存的float*并将其用作float,则会破坏该规则。 如果您的代码不遵守此规则,则编译器的优化器很可能会破坏您的代码。
例外是一个char*,它被允许指向任何类型。
一个典型的遇到严格别名问题的情况是将结构体(比如设备/网络消息)覆盖在系统的字大小缓冲区(比如指向uint32_t或uint16_t的指针)上。当你将结构体覆盖在这样的缓冲区上,或通过指针转换将缓冲区覆盖在这样的结构体上时,你很容易违反严格别名规则。
因此,在这种设置中,如果我想向某个东西发送消息,我必须有两个不兼容的指针指向相同的内存块。然后,我可能会天真地编写类似下面这样的代码:
```
typedef struct Msg { unsigned int a; unsigned int b; } Msg; void SendWord(uint32_t); int main(void) { // Get a 32-bit buffer from the system uint32_t* buff = malloc(sizeof(Msg)); // Alias that buffer through message Msg* msg = (Msg*)(buff); // Send a bunch of messages for (int i = 0; i < 10; ++i) { msg->a = i; msg->b = i+1; SendWord(buff[0]); SendWord(buff[1]); } }
```
严格别名规则使得这种设置非法:对别名不兼容类型的对象进行解引用,或者对C 2011 6.5段71允许的其他类型之一进行解引用,导致未定义行为。不幸的是,你仍然可以以这种方式编写代码,也许会得到一些警告,编译也可以通过,但当你运行代码时会出现奇怪的意外行为。
(GCC在给予别名警告的能力方面似乎有一些不一致之处,有时会给我们一个友好的警告,有时可能不会。)
要了解为什么这种行为是未定义的,我们必须考虑严格别名规则为编译器带来了什么好处。基本上,有了这个规则,编译器就不必考虑在每次循环运行时插入刷新buff
内容的指令。相反,在优化时,通过某些令人讨厌的未强制执行的关于别名的假设,它可以省略那些指令,将buff[0]
和buff[1]
在循环运行前装入CPU寄存器一次,从而加快循环体的运行速度。在引入严格别名之前,编译器必须处于buff
的内容可以通过任何前面的内存存储更改的偏执状态。因此,为了获得额外的性能优势,并假定大多数人不会类型转换指针,引入了严格别名规则。
请记住,如果你认为这个例子是人为的,那么即使在将缓冲区传递给另一个为你发送的函数时,这种情况也可能发生,如果改为以下方式,
```
void SendMessage(uint32_t* buff, size_t size32) { for (int i = 0; i < size32; ++i) { SendWord(buff[i]); } }
```
并重写我们之前的循环以利用这个方便的函数
```
for (int i = 0; i < 10; ++i) { msg->a = i; msg->b = i+1; SendMessage(buff, 2); }
```
编译器可能能够或智能到尝试内联SendMessage,它可以选择加载或不加载buff。如果SendMessage
是另一个API的一部分,它可能有指令来加载buff
的内容。另一方面,也许你在C++中,这是一些编译器认为可以内联的模板头文件实现。或者这只是你在.c文件中为自己方便而编写的东西。无论如何,未定义的行为仍然可能发生。即使我们知道底层的一些情况,违反规则仍然是一种违反,因此不能保证有良好定义的行为。因此,仅仅通过包装一个以单词为分隔符的缓冲区的函数并不能帮助。
那么我该怎么办呢?
-
使用联合。大多数编译器都支持这种做法,而不会对严格别名做出抱怨。这在C99中是允许的,而在C11中则被明确允许。
union { Msg msg; unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)]; };
-
在编译器中禁用严格别名(在gcc中使用f[no-]strict-aliasing)
-
使用
char*
代替系统的单词进行别名处理。规则允许char*
(包括signed char
和unsigned char
)作为一个例外。总是假设char*
与其他类型是别名。但是这种方法不能用于反过来:不假定您的结构体别名为一组字符缓冲区。
初学者要小心
将两种类型叠加在一起时,还有很多潜在的陷阱。您还应该了解字节序、字对齐问题以及如何通过正确地压缩结构体解决对齐问题。
脚注
1 C 2011 6.5 7允许左值访问的类型为:
- 与对象的有效类型兼容的类型,
- 与对象的有效类型兼容的带限定符的类型,
- 对象的有效类型的有符号或无符号对应类型,
- 对象的有效类型的带限定符的有符号或无符号对应类型,
- 包括上述类型之一的聚合或联合类型(对于一个子聚合或包含的联合的成员,也是递归的),或
- 字符类型。