通过联合在 C 和 C++ 中输入双关结构

Type punning a struct in C and C++ via a union

我已经在 gcc 和 g++ 中使用 pedantic 编译了这个,我在任何一个中都没有收到警告:

#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)说:

If a union contains several structures that share a common initial sequence, and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them. Two structures share a common initial sequence if corresponding members have compatible types for a sequence of one or more initial members.

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

是的,这很好;您问题中引用的粗体部分涵盖了这种情况。

在 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):

If the member used to read the contents of a union object is not the same as the member last used to store a value in the object, the appropriate part of the object representation of the value is reinterpreted as an object representation in the new type as described in 6.2.6 (a process sometimes called ‘‘type punning’’). This might be a trap representation.


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

Two standard-layout structs share a common initial sequence if corresponding members have layout-compatible types and either neither member is a bit-field or both are bit-fields with the same width for a sequence of one or more initial members.

[basic.types]/p9:

If two types T1 and T2 are the same type, then T1 and T2 are layout-compatible types. [ Note: Layout-compatible enumerations are described in 7.2. Layout-compatible standard-layout structs and standard-layout unions are described in 9.2. —end note ]

struct a *struct b * 既不是结构也不是联合也不是枚举;因此,它们只有在类型相同时才与布局兼容,而事实并非如此。

确实 ([basic.compound]/p3)

Pointers to cv-qualified and cv-unqualified versions (3.9.3) of layout-compatible types shall have the same value representation and alignment requirements (3.11).

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

可以 做的(我以前也被这个困扰过)是将结构的初始指针声明为 void* 并进行转换。由于 void 是可转换的 to/from 任何指针类型,你只会被迫支付丑陋税,而不是冒险 gcc 重新排序你的操作 (我已经看到发生 -即使您使用联合),由于某些版本中的编译器错误。作为@T.C。正确地指出,给定类型的布局兼容性意味着它们在语言级别是可转换的;即使类型可能偶然具有相同的大小,它们也不一定是布局兼容的;这可能会让一些贪婪的编译器基于此假设一些其他事情。

我也有类似的问题,我想我可以回答你的问题。

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

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

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

One special guarantee is made in order to simplify the use of unions: if a union contains several structures that share a common initial sequence (see below), and if the union object currently contains one of these structures, it is permitted to inspect the common initial part of any of them anywhere that a declaration of the completed type of the union is visible.

最后一句话有点模棱两可,因为您可以用多种方式解释 "visible",但是,根据委员会的说法,这意味着应该对 的对象执行检查有问题的联合类型.

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

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 具有相同初始序列的将拥有它具有相同的布局(在 99.9% 的情况下)。实际上,即使在您的原始设置中,它们也应该具有相同的布局,因为指向 struct astruct b 的指针应该具有相同的大小。所以,如果你的编译器在你打破 strict aliasing 时没有变得讨厌,你可以或多或少安全地在它们之间进行类型转换,或者按照你正在使用的方式在联合中使用它们他们现在。

EDIT:正如@underscore_d 在对该答案的评论中指出的那样,因为 C++ 标准中的相应条款没有行 "anywhere that a declaration of the completed type of the union is visible" 在适当的部分,C++ 标准可能与 C89 标准在该主题上具有相同的立场。