通过联合键入C和C ++中的结构

时间:2015-02-14 22:52:42

标签: c++ c language-lawyer strict-aliasing type-punning

我用gcc和g ++用迂腐编译了这个,我不会在任何一个中得到警告:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct a {
    struct a *next;
    int i;
};

struct b {
    struct b *next;
    int i;
};

struct c {
    int x, x2, x3;
    union {
        struct a a;
        struct b b;
    } u;
};

void foo(struct b *bar) {
    bar->next->i = 9;
    return;
}

int main(int argc, char *argv[]) {
    struct c c;
    memset(&c, 0, sizeof c);
    c.u.a.next = (struct a *)calloc(1, sizeof(struct a));
    foo(&c.u.b);
    printf("%d\n", c.u.a.next->i);
    return 0;
}

在C和C ++中这是合法的吗?我读过关于打字的类型,但我不明白。 foo(&c.u.b)foo((struct b *)&c.u.a)有什么不同?它们不一样吗?联合中结构的这种例外(来自3.3.2.3中的C89)说:

  

如果联合包含多个共享公共首字母的结构   序列,如果union对象当前包含其中一个   结构,允许检查任何共同的初始部分   他们两个结构共享一个共同的初始序列if   相应的成员具有一个或一个序列的兼容类型   更多初始成员。

在联盟中,struct a的第一个成员是struct a *nextstruct b的第一个成员是struct b *next。如您所见,写入指向struct a *next的指针,然后在foo中读取指向struct b *next的指针。它们兼容吗?它们都是指向结构的指针,指向任何结构的指针应该是相同的大小,所以它们应该是兼容的,布局应该是相同的吗?可以从一个结构中读取i并写入另一个结构吗?我是否提出任何类型的别名或类型 - 违规?

4 个答案:

答案 0 :(得分:5)

在C:

struct astruct b不兼容。即使在

typedef struct s1 { int x; } t1, *tp1;
typedef struct s2 { int x; } t2, *tp2;

s1s2不兼容。 (参见6.7.8 / p5中的示例。)识别不兼容结构的一种简单方法是,如果两个结构类型兼容,则可以将一种类型的某些类型分配给另一种类型。如果您希望编译器在尝试执行此操作时进行投诉,则它们不是兼容类型。

因此,struct a *struct b *也不是兼容类型,因此struct astruct b不共享共同的初始子序列。在其他情况下,你的工会惩罚取决于工会双关的相同规则(6.5.2.3脚注95):

  

如果用于读取union对象内容的成员不是   与上次用于在对象中存储值的成员相同,   值的对象表示的适当部分是   如上所述,重新解释为新类型中的对象表示   在6.2.6(一个有时被称为''type punning''的过程)。这可能是   陷阱表示。


在C ++中,struct astruct b也不共享一个共同的初始子序列。 [class.mem] / p18(引用N4140):

  

两个标准布局结构共享一个共同的初始序列if   相应的成员具有布局兼容类型,两者都没有   member是一个位字段,或者两者都是具有相同宽度的位字段   一个或多个初始成员的序列。

[basic.types] / P9:

  

如果两种类型T1T2属于同一类型,则T1T2为   布局兼容类型。 [注意:与布局兼容的枚举在7.2中描述。与布局兼容的标准布局结构和   标准布局联合在9.2中描述。 - 结束记录]

struct a *struct b *既不是结构,也不是工会或枚举;因此,如果它们是相同的类型,它们只是布局兼容的,它们不是。

确实([basic.compound] / p3)

  

指向cv-qualified和cv-nonqualified version(3.9.3)的   布局兼容类型应具有相同的值表示和   对齐要求(3.11)。

但这并不意味着那些指针类型是布局兼容类型,因为该术语在标准中定义。

答案 1 :(得分:2)

可以做什么(以及之前我已经被它咬过),将结构的初始指针声明为void*并进行投射。由于void可以转换为任何指针类型,因此您只会被迫支付丑陋税,而不会冒险gcc 重新安排您的操作(我已经看到了 - 即使您使用union),由于某些版本中的编译器错误。作为@ T.C。正确地指出,给定类型的布局兼容性意味着在语言层面它们是可转换的;即使类型可能偶然具有相同的大小,它们也不一定是布局兼容的;这可能会让一些贪婪的编译器基于此假设其他一些东西。

答案 2 :(得分:2)

我有一个类似的问题some time ago,我想我可以回答你的问题。

是的,struct astruct b 不是兼容类型,指向它们的指针也不兼容。

是的,从C89标准的过时的角度来看,即使,你所做的事情也是非法的。但是,值得注意的是,如果您颠倒struct astruct b中元素的顺序,则可以访问int i实例的struct c(但是不以任何方式访问其next指针,即bar->i = 9;而不是bar->next->i = 9;),但仅从C89标准的角度来看。

但即使你要颠倒两个struct中元素的顺序,从C99和C11标准的角度来看,你正在做的事情仍然是非法的(由委员会解释) )。在C99中,您引用的标准部分已更改为:

  

为了简化联合的使用,我们做了一个特别的保证:如果一个联合包含几个共享一个共同初始序列的结构(见下文),并且如果联合对象当前包含这些结构中的一个,则允许检查其中任何一个的常见初始部分可以看到完整类型的联合声明

最后一个短语有点含糊不清,因为你可以用几种方式解释“可见”,但是,根据委员会的说法,这意味着应该在关联的联合类型的对象上执行检查 / em>的

因此,在您的情况下,处理此问题的正确方法将是:

struct a {
    int i;
    struct a *next;
};

struct b {
    int i;
    struct b *next;
};

union un {
    struct a a;
    struct b b;
};

struct c {
    int x, x2, x3;
    union un u;
};

/* ... */

void foo(union un *bar) {
    bar.b->next->i = 9; /* This is the "inspection" operation */
    return;
}

/* ... */

foo(&c.u);

从语言律师的角度来看,这一切都很精致有趣,但实际上,如果你不对它们应用不同的打包设置,struct s具有相同的初始序列将使用它相同的布局(99.9%的情况)。实际上,即使在原始设置中,它们也应该具有相同的布局,因为指向struct astruct b的指针应该具有相同的大小。因此,如果你打破严格别名时你的编译器不会变得讨厌,你可以或多或少安全地在它们之间进行类型转换,或者在你现在使用它们的方式中使用它们。

编辑:正如@underscore_d在对此答案的评论中所指出的那样,因为C ++标准中的相应条款没有“在完成联合类型声明的任何地方”可见“在其适当的部分,C ++标准可能与C89标准在主题上具有相同的立场。

答案 3 :(得分:-1)

是的,这很好;你问题中引用的粗体部分涵盖了这个案例。