45

作为更大程序的一部分,我使用以下代码从文件中读取数据。

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

谷歌我发现了两个正交的答案:

对比

最后,我不想忽略警告。你会推荐什么?

[更新]我用实际功能替换了玩具示例。

4

7 回答 7

41

出现问题是因为您通过 a 访问 char-array double*

char data[8];
...
return *(double*)data;

但是 gcc 假定您的程序永远不会通过不同类型的指针访问变量。这种假设称为严格别名,并允许编译器进行一些优化:

如果编译器知道您的*(double*)can 绝不与 重叠data[],则允许执行各种操作,例如将代码重新排序为:

return *(double*)data;
for(int i=7;i>=0;i--)
    data[i] = fgetc(stream);

循环很可能被优化掉了,你最终只得到:

return *(double*)data;

这使您的 data[] 未初始化。在这种特殊情况下,编译器可能会看到您的指针重叠,但如果您声明了它char* data,它可能会给出错误。

但是,严格别名规则说 char* 和 void* 可以指向任何类型。因此,您可以将其重写为:

double data;
...
*(((char*)&data) + i) = fgetc(stream);
...
return data;

严格的别名警告对于理解或修复非常重要。它们会导致无法在内部重现的各种错误,因为它们仅发生在特定机器上特定操作系统上的特定编译器上,并且仅在满月和一年一次等情况下发生。

于 2012-10-12T14:49:55.807 回答
26

看起来你真的很想使用 fread:

int data;
fread(&data, sizeof(data), 1, stream);

也就是说,如果您确实想走读取字符的路线,然后将它们重新解释为 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 个字节;至少我不知道任何 int 为 3 个字节的系统。

请注意,您的代码和我的代码都是高度不可移植的。

编辑:如果您想从文件中读取各种长度的整数,请尝试以下操作:

unsigned result=0;
for(int i=0; i<4; i++)
    result = (result << 8) | fgetc(stream);

(注意:在实际程序中,您还需要针对 EOF 测试 fgetc() 的返回值。)

无论系统的字节序是什么,这都会以 little-endian 格式从文件中读取 4 字节无符号。它应该适用于无符号至少为 4 个字节的任何系统。

如果您想保持字节序中立,请不要使用指针或联合;改用位移位。

于 2010-07-14T13:01:13.627 回答
7

在这里使用联合不是正确的做法。从联合的未写成员读取是未定义的 - 即编译器可以自由执行会破坏您的代码的优化(例如优化写入)。

于 2010-12-22T18:31:52.010 回答
7

该文档总结了情况:http ://dbp-consulting.com/tutorials/StrictAliasing.html

那里有几种不同的解决方案,但最便携/安全的一种是使用 memcpy()。(函数调用可能会被优化掉,所以它并不像看起来那么低效。)例如,替换这个:

return *(short*)data;

有了这个:

short temp;
memcpy(&temp, data, sizeof(temp));
return temp;
于 2016-04-28T16:42:59.400 回答
3

基本上你可以像找麻烦的人一样阅读 gcc 的消息,不要说我没有警告你

将三字节字符数组转换为 anint是我见过的最糟糕的事情之一。通常你int至少有 4 个字节。因此,对于第四个(如果int更宽,可能更多),您将获得随机数据。然后你将所有这些都转换为double.

只是不要这样做。与您正在做的事情相比,gcc 警告的别名问题是无辜的。

于 2010-07-14T14:11:57.517 回答
0

C 标准的作者希望让编译器编写者在理论上可行但不太可能使用看似不相关的指针访问全局变量的值的情况下生成高效的代码。这个想法不是通过在单个表达式中强制转换和取消引用指针来禁止类型双关语,而是说给定如下内容:

int x;
int foo(double *d)
{
  x++;
  *d=1234;
  return x;
}

编译器有权假设对 *d 的写入不会影响 x。标准的作者想要列出这样的情况:像上面这样从未知来源接收指针的函数必须假设它可能为看似不相关的全局别名,而不要求类型完全匹配。不幸的是,虽然其基本原理强烈表明标准的作者打算在编译器没有理由相信事物可能出现别名的情况下描述最低一致性的标准,但该规则并未要求编译器在其识别别名的情况下识别别名。很明显并且 gcc 的作者已经决定,他们宁愿生成最小的程序,同时符合标准的编写不佳的语言,而不是生成实际上有用的代码,而不是在明显的情况下识别别名(虽然仍然能够假设看起来不像它们会别名的东西不会)他们宁愿要求程序员使用memcpy,因此需要编译器允许未知来源的指针可能会别名几乎任何东西,从而阻碍优化。

于 2016-04-13T21:04:05.943 回答
-4

显然,该标准允许 sizeof(char*) 与 sizeof(int*) 不同,因此当您尝试直接强制转换时 gcc 会抱怨。void* 有点特别,因为一切都可以在 void* 之间来回转换。在实践中,我不知道很多架构/编译器的指针对于所有类型并不总是相同的,但 gcc 发出警告是正确的,即使它很烦人。

我认为安全的方法是

int i, *p = &i;
char *q = (char*)&p[0];

或者

char *q = (char*)(void*)p;

你也可以试试这个,看看你得到了什么:

char *q = reinterpret_cast<char*>(p);
于 2010-08-16T08:25:20.483 回答