首页 文章

取消引用类型惩罚指针将破坏严格别名规则

提问于
浏览
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

谷歌我找到了两个正交的答案:

VS

最后我不想忽视这些警告 . 你会推荐什么?

[update] 我用真实的功能取代了玩具示例 .

7 回答

  • 39

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

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

    也就是说,如果你想要读取字符的路径,然后将它们重新解释为int,那么在C中使用C(但是 not )的安全方法是使用union:

    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()的返回值 . )

    无论系统的字节顺序如何,它都会以小端格式从文件中读取4字节的无符号字符 . 它应该适用于无符号至少为4个字节的任何系统 .

    如果你想要 endpoints 中立,不要使用指针或联合;改为使用位移 .

  • 26

    出现此问题的原因是您通过 double* 访问char数组:

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

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

    如果编译器知道你的 *(double*) 不能与 data[] 重叠,那么它允许各种各样的事情,例如将代码重新排序为:

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

    循环最有可能被优化掉,你最终得到:

    return *(double*)data;
    

    这使您的数据[]未初始化 . 在这种特殊情况下,编译器可能会看到您的指针重叠,但如果您已将其声明为 char* data ,则可能会出现错误 .

    但是,严格别名规则说char *和void *可以指向任何类型 . 所以你可以把它重写成:

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

    严格的别名警告对于理解或修复非常重要 . 它们会导致内部无法重现的错误类型,因为它们只出现在一台特定计算机上某个特定操作系统上的一个特定编译器上,而且只发生在满月和一年一次等等 .

  • 0

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

  • 2

    本文总结了这种情况:http://dbp-consulting.com/tutorials/StrictAliasing.html

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

    return *(short*)data;
    

    有了这个:

    short temp;
    memcpy(&temp, data, sizeof(temp));
    return temp;
    
  • -4

    基本上你可以阅读gcc的消息作为你正在寻找麻烦的人,不要警告你 .

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

    只是做不到这一点 . 与你正在做的事情相比,gcc警告的别名问题是无辜的 .

  • 7

    C标准的作者希望让编译器编写者在理论上可能的情况下生成有效的代码,但不太可能全局变量可能使用看似无关的指针访问其值 . 这个想法不是通过在单个表达式中转换和取消引用指针来禁止类型惩罚,而是说给定类似的东西:

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

    编译器有权假设写入* d不会影响x . 标准的作者想要列出这样的情况,即上面接收来自未知来源的指针的函数必须假设它可能为看似无关的全局变为别名,而不要求这些类型完全匹配 . 不幸的是,虽然理由强烈建议该标准的作者打算描述一个标准在案件最低的一致性,其中一个编译器,否则没有理由认为,事情可能别名,规则没有要求编译器在很明显的情况下认走样和gcc的作者已经决定,他们'd rather generate the smallest program it can while conforming to the poorly-written language of the Standard, than generate code which is actually useful, and instead of recognizing aliasing in cases where it'明摆着(同时仍然能够承担的事情,不要't look like they' LL别名,赢得't) they'宁愿要求程序员使用 memcpy ,因此需要一个编译器,以便对来历不明的指针可能别名任何事情,从而阻碍了优化的可能性 .

  • 7

    显然,标准允许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);
    

相关问题