当询问C语言中常见的未定义行为时,人们有时会提到严格的别名规则。
他们在说什么
遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字长的缓冲区(如指向uint32\u ts或uint16\u ts的指针)上。当您将结构覆盖到这样的缓冲区上,或者通过指针转换将缓冲区覆盖到这样的结构上时,很容易违反严格的别名规则
因此,在这种设置中,如果我想向某个对象发送消息,我必须有两个不兼容的指针指向同一块内存。然后我可能会天真地编写如下代码:
类型定义结构消息
{
无符号整数a;
无符号整数b;
}味精;
无效发送字(uint32_t);
内部主(空)
{
//从系统中获取32位缓冲区
uint32_t*buff=malloc(sizeof(Msg));
//通过消息为缓冲区添加别名
味精*味精=(味精*)(浅黄色);
//发一串信息
对于(整数i=0;i<;10;++i)
{
msg->;a=i;
msg->;b=i+1;
发送字(buff[0]);
SendWord(buff[1]);
}
}
严格的别名规则使此设置非法:取消引用为不兼容类型或C 2011 6.5第7段所允许的其他类型之一的对象添加别名的指针是未定义的行为。不幸的是,您仍然可以这样编码,可能会得到一些警告,让它编译好,但在运行代码时却会出现奇怪的意外行为
(GCC给出别名警告的能力似乎有点不一致,有时给我们友好的警告,有时不友好。)
要了解为什么这种行为是未定义的,我们必须考虑严格的别名规则为编译器带来了什么。基本上,使用此规则,它不必考虑插入指令来刷新buff每次循环的内容。相反,在优化时,使用一些令人烦恼的关于别名的非强制假设,它可以忽略这些指令,在循环运行之前将buff[0]和buff[1]加载到CPU寄存器中一次,并加速循环体。在引入严格的别名之前,编译器必须生活在一种偏执的状态中,buff的内容可能会被前面的任何内存存储改变。因此,为了获得额外的性能优势,并且假设大多数人不键入双关语指针,引入了严格的别名规则
请记住,如果您认为该示例是人为设计的,那么如果您将缓冲区传递给另一个为您执行发送的函数,则可能会发生这种情况
无效发送消息(uint32\u t*buff,大小32)
{
对于(int i=0;i<;size32;++i)
{
发送字(buff[i]);
}
}
并重写前面的循环以利用这个方便的函数
用于(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中是明确允许的
联合{ 味精; 无符号整数作为缓冲区[sizeof(Msg)/sizeof(无符号整数)]; }; -
您可以在编译器中禁用严格别名(gcc中的f[no-]严格别名))
-
您可以使用
char*作为别名,而不是系统的单词。规则允许对char*进行例外处理(包括signed char和unsigned char)。通常假定char*别名为其他类型。然而,这不会以另一种方式起作用:没有假设您的结构会为字符缓冲区别名
初学者要当心
当两种类型相互叠加时,这只是一个潜在雷区。您还应该了解尾端、单词对齐以及如何通过正确打包结构来处理对齐问题
脚注
1C 2011 6.5 7允许左值访问的类型有:
- 与对象的有效类型兼容的类型
- 与对象的有效类型兼容的类型的限定版本
- 与对象的有效类型相对应的有符号或无符号类型
- 一种类型,它是与对象的有效类型的限定版本相对应的有符号或无符号类型
- 在其成员中包含上述类型之一的聚合或联合类型(递归地包括子聚合或包含的联合的成员),或
- 字符类型