解引用types的指针会破坏严格的别名规则
我使用下面的一段代码作为更大程序的一部分从文件中读取数据。
double data_read(FILE *stream,int code) { char data[8]; switch(code) { case 0x08: return (unsigned char)fgetc(stream); case 0x09: return (signed char)fgetc(stream); case 0x0b: data[1] = fgetc(stream); data[0] = fgetc(stream); return *(short*)data; case 0x0c: for(int i=3;i>=0;i--) data[i] = fgetc(stream); return *(int*)data; case 0x0d: for(int i=3;i>=0;i--) data[i] = fgetc(stream); return *(float*)data; case 0x0e: for(int i=7;i>=0;i--) data[i] = fgetc(stream); return *(double*)data; } die("data read failed"); return 1; }
现在我被告知使用-O2
和我得到以下gcc警告: warning: dereferencing type-punned pointer will break strict-aliasing rules
谷歌我发现了两个正交的答案:
- 结论:没有必要担心, gcc试图比实际的法律更服从法律。
VS
- 所以基本上如果你有一个int *和一个浮点数*他们不允许指向相同的内存位置。 如果你的代码不尊重这个,那么编译器的优化器很可能会破坏你的代码。
最后我不想忽视这些警告。 你会推荐什么?
[更新]我用真实的function代替玩具的例子。
它看起来很像你真的想使用fread:
int data; fread(&data, sizeof(data), 1, stream);
也就是说,如果你确实想要去读取字符的path,然后将它们重新解释为一个int,那么在C中(而不是在C ++中)这样做的安全方法是使用联合:
union { char theChars[4]; int theInt; } myunion; for(int i=0; i<4; i++) myunion.theChars[i] = fgetc(stream); return myunion.theInt;
我不知道为什么原始代码中的data
长度是3.我假设你想要4个字节; 至less我不知道任何int是3个字节的系统。
请注意,您的代码和我的代码都非常不便携。
编辑:如果你想从文件中读取各种长度的整数,可移植的,尝试这样的事情:
unsigned result=0; for(int i=0; i<4; i++) result = (result << 8) | fgetc(stream);
(注意:在真正的程序中,您还需要testingfgetc()的EOF返回值。)
无论系统的字节顺序如何 ,它都会以little-endian格式从文件中读取一个4字节的无符号数。 它应该适用于任何无符号的系统,至less有4个字节。
如果你想要endian中性,不要使用指针或联合; 使用位移代替。
发生此问题是因为您通过double*
访问char数组:
char data[8]; ... return *(double*)data;
但是gcc认为你的程序不会通过不同types的指针访问variables。 这个假设被称为严格别名,并允许编译器进行一些优化:
如果编译器知道你的*(double*)
可以与data[]
没有任何重叠,就可以将所有的东西重新sorting:
return *(double*)data; for(int i=7;i>=0;i--) data[i] = fgetc(stream);
该循环最有可能被优化,最终只是:
return *(double*)data;
这会导致您的数据未初始化。 在这种情况下,编译器可能会看到你的指针重叠,但如果你已经声明了char* data
,它可能会有错误。
但是,严格别名规则说char *和void *可以指向任何types。 所以你可以把它改写成:
double data; ... *(((char*)&data) + i) = fgetc(stream); ... return data;
严格的别名警告对于理解或修复非常重要。 它们会导致那些内部不可能复制的错误,因为它们只发生在一台特定的操作系统上的一个特定的编译器上,并且只发生在一个特定的机器上,而且只发生在满月和一年一次等等。
使用联合不是在这里做的正确的事情。 从一个未成文的联盟成员读取是未定义的 – 即编译器可以自由执行优化,这将打破你的代码(如优化远离写入)。
本文档总结了这种情况: http : //dbp-consulting.com/tutorials/StrictAliasing.html
有几个不同的解决scheme,但最便携/安全的是使用memcpy()。 (函数调用可能会被优化,所以效率并不像看起来那样低)。例如,将其replace为:
return *(short*)data;
有了这个:
short temp; memcpy(&temp, data, sizeof(temp)); return temp;
基本上你可以阅读海湾合作委员会的消息作为你正在寻找麻烦的人,不要说我没有警告你 。
将一个三字节字符数组转换为int
是我见过的最糟糕的事情之一。 通常你的int
至less有4个字节。 所以对于第四(也许更多,如果int
更宽),你会得到随机数据。 然后你把所有这一切都double
。
只是没有这一点。 与你正在做的事情相比,gcc警告的别名问题是无辜的。
C标准的作者想让编译器编写者在理论上可能的情况下生成高效的代码,但不太可能使用看起来不相关的指针来访问全局variables的值。 这个想法并不是禁止通过在单个expression式中引用和解引用指针来实现types的双关操作,而是说给定类似的东西:
int x; int foo(double *d) { x++; *d=1234; return x; }
编译器有权假定写入* d不会影响x。 标准的作者想要列出一些情况,像上面这样的函数接收到一个来自未知源的指针将不得不假设它可能是一个看起来不相关的全局的别名,而不要求types完全匹配。 不幸的是,虽然理由强烈地暗示,标准的作者打算描述一个编译器如果没有理由相信事情可能混淆的情况下的最小一致性标准,那么规则就不会要求编译器在它的情况下识别别名是明显的,gcc的作者已经决定,他们宁愿生成最小的程序,它可以符合标准的糟糕的语言,而不是生成实际有用的代码,而不是在明显的情况下识别别名(虽然仍然可以假定事物看上去不像是别名,但他们宁愿要求程序员使用memcpy
,因此需要一个编译器来允许未知来源的指针可能是别名几乎任何事情,从而阻碍优化。
显然这个标准允许sizeof(char *)与sizeof(int *)不同,所以当你尝试直接强制转换时gcc会抱怨。 void *有点特别,因为所有的东西都可以在void *中来回转换。 在实践中,我不知道许多架构/编译器的指针并不总是相同的所有types,但海湾合作委员会是正确的发出警告,即使它很烦人。
我认为安全的方法是
int i, *p = &i; char *q = (char*)&p[0];
要么
char *q = (char*)(void*)p;
你也可以试试这个,看看你得到了什么:
char *q = reinterpret_cast<char*>(p);