具有相同成员类型的C结构体是否保证在内存中具有相同的布局?

21

基本上,如果我有

typedef struct {
    int x;
    int y;
} A;

typedef struct {
    int h;
    int k;
} B;

我有一个A a,C标准是否保证((B*)&a)->ka.y相同?


1
不,我认为标准并不能保证这一点。实际上,编译器会按照你想要和期望的方式执行,但标准并不保证它。这是未定义行为;任何事情都可能发生。 - Jonathan Leffler
4个回答

17
同样成员类型的C结构体在内存中保证具有相同的布局。几乎是这样,对我来说足够了。
根据n1516,第6.5.2.3节,第6段的规定:
如果一个联合(union)包含多个共享一个公共初始序列的结构体……,并且如果联合对象当前包含其中的一个结构体,则允许在任何完成联合类型声明可见的地方检查它们任意一个的公共初始部分。如果对应成员的类型兼容(对于位域,宽度相同),则两个结构体共享一个公共初始序列,可以由一系列一个或多个初始成员组成。
这意味着如果你有以下代码:
struct a {
    int x;
    int y;
};

struct b {
    int h;
    int k;
};

union {
    struct a a;
    struct b b;
} u;
如果您给a赋值,标准规定您可以从b中读取相应的值。 基于此要求,建议认为struct astruct b具有不同的布局是难以置信的。这样的系统将极端异常。
请记住,标准还保证:
- 结构体从不是陷阱表示形式。 - 结构体中字段的地址增加(a.x始终在a.y之前)。 - 第一个字段的偏移量始终为零。
但是,这很重要!
你改变了问题的措辞,
“C标准是否保证((B*)&a)->ka.y相同?”
没有!它非常明确地说明它们不同!
struct a { int x; };
struct b { int x; };
int test(int value)
{
    struct a a;
    a.x = value;
    return ((struct b *) &a)->x;
}

这是一个别名违规。


为什么是N1516?我指的是N1570... - Potatoswatter
3
@Potatoswatter:这是我随手拿到的。无论如何,这种语言自 ANSI C 时代以来就一直存在(第3.3.2.3节)。 - Dietrich Epp
1
如果在代码检查结构成员的地方可见包含struct astruct b的完整联合类型声明,符合规范且没有错误的编译器将会识别别名的可能性。一些编译器作者只想在适合自己的情况下遵守标准,即使标准保证它能够工作,他们也会破坏这样的代码;这仅意味着他们的编译器不符合规范。 - supercat
2
@supercat 是的,但是我所知道的使用严格别名优化的编译器都没有实现这个规则,所以不能依赖它。将来可能会删除这个条款。标准大多数都是垃圾,大多数编译器并不真正遵循它们。 - user1143634
1
@wonder.mice:仅仅让x在两个中具有相同的类型是不够的。问题在于a的类型为struct a,而你正在通过struct b类型进行访问。这里有一个链接,展示了编译器如何基于别名进行优化:https://gcc.godbolt.org/z/7PMjbT 尝试移除-fstrict-aliasing选项,看看生成的代码如何变化。 - Dietrich Epp
显示剩余6条评论

8
跟随其他回复,对6.5.2.3节的警告。显然,关于“在联合体的完成类型的声明可见的任何地方”,有一些争议,至少GCC没有按照书面要求实现。C WG有一些相关的缺陷报告herehere,委员会有后续评论。
最近,我尝试使用标准中的以下代码来了解其他编译器(特别是GCC 4.8.2、ICC 14和clang 3.4)如何解释它:
// Undefined, result could (realistically) be either -1 or 1
struct t1 { int m; } s1;
struct t2 { int m; } s2;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union {
        struct t1 s1;
        struct t2 s2;
    } u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1,clang: -1,ICC: 1,并警告关于别名违规的问题。

// Global union declaration, result should be 1 according to a literal reading of 6.5.2.3/6
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
};
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    union u u;
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1,clang: -1,ICC: 1但警告关于别名违规的问题。

// Global union definition, result should be 1 as well.
struct t1 { int m; } s1;
struct t2 { int m; } s2;
union u {
    struct t1 s1;
    struct t2 s2;
} u;
int f(struct t1 *p1, struct t2 *p2) {
    if (p1->m < 0)
        p2->m = -p2->m;
    return p1->m;
}
int g() {
    u.s1.m = -1;
    return f(&u.s1,&u.s2);
}

GCC: -1,clang: -1,ICC: 1,无警告。

当然,没有严格别名优化时,三个编译器每次都返回预期结果。由于在任何情况下,clang和gcc都没有区分出不同的结果,因此唯一真正的信息来自ICC在最后一个例子中缺少诊断的事实。这也与上述第一个缺陷报告中标准委员会给出的示例相一致。

换句话说,C语言的这个方面是一个真正的雷区,即使您完全遵循标准,您也必须小心您的编译器是否做正确的事情。更糟糕的是,这对结构体应该在内存中兼容的直觉是符合常理的。


非常感谢提供的链接,但遗憾的是它们大部分都不重要。值得一提的是,我与少数(外行)人讨论后的共识似乎是这意味着函数必须传递union,而不是包含类型的原始指针。然而,在我看来,这违背了使用union的初衷。关于这个条款,我有一个问题 - 特别是它在C++中引起了显著的(也许是意外的?)排除 - 在这里:https://dev59.com/s1sW5IYBdhLWcg3wwZcP - underscore_d
1
一点也不无关紧要!通过你的第二个GCC讨论链接,我们发现C++可能故意拒绝了这个 - 而C在添加这个措辞之前并没有真正考虑过,从来没有真正认真对待过,并且可能正在扭转这种情况:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=65892 从那里,我们进入了C++ DR 1719 http://open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1719,其中建议进行重大措辞变更,似乎使C ++对这些“结构体”可以被“玩弄”的确切位置的看法非常清晰。我已经将此及更多内容收集到我的链接问题的答案中。 - underscore_d
1
@curiousguy:对于那些无法识别从一个指针或lvalue派生另一个指针或lvalue的编译器,使CIS规则有用,需要一种告诉编译器“这个指针将标识其中一个结构类型,我不知道哪一个,但我需要能够使用一个的CIS成员来访问它们所有的CIS成员”的方法。除了声明联合类型之外,使用联合声明来实现这个目的可以避免引入新的指令... - supercat
1
请注意,6.5p7的写法,给定“struct foo {int x;} *p, it;”,像这样的p=&it; p->x=4;将会调用未定义行为,因为它使用了类型为int的左值来修改类型为struct foo的对象,但标准的作者们期望编译器编写人员不会如此愚蠢,以至于假装他们不应该将其视为已定义。标准从未试图合理地指定任何特定平台和目的的实现所需支持的全部语义范围。荒谬的“有效类型”规则甚至不能... - supercat
1
处理非字符类型结构成员的最基本操作。如果将6.5p7进行微调,使其说任何存储字节在函数或循环的任何特定执行期间发生更改时,必须通过派生自同一对象或相同数组的lvalue在其生命周期内独占地访问,并且与字节相关的派生lvalue的所有使用都在与该字节相关的父级别的下一次使用之前,就可以摆脱与“有效类型”有关的所有内容,使事情变得更简单、更强大。 - supercat
显示剩余4条评论

3
这种特定的别名需要使用 union 类型。C11 §6.5.2.3/6 规定:为了简化联合的使用,有一个特殊的保证,即如果一个联合体包含多个共享公共初始序列的结构体(参考下面),并且如果联合体对象当前包含其中一个结构体,则可以在任何可见联合体类型的已完成声明中检查它们中的任何一个的公共初始部分。如果对应成员具有兼容的类型(对于位域来说,还需要相同的宽度),则两个结构体共享一个公共初始序列,该示例遵循此规则。

The following is not a valid fragment (because the union type is not visible within function f):

struct t1 { int m; };
struct t2 { int m; };
int f(struct t1 *p1, struct t2 *p2)
{
    if (p1->m < 0)
          p2->m = -p2->m;
    return p1->m;
}

int g() {
    union {
          struct t1 s1;
          struct t2 s2;
    } u;
    /* ... */
    return f(&u.s1, &u.s2);}
}
要求似乎是:1.被别名化的对象存储在一个“union”内,2.该“union”类型的定义在作用域内。
就C++中相应的初始子序列关系而言,不需要使用“union”。一般来说,这样的“联合”依赖关系对于编译器来说会是一种极端的病态行为。如果存在某种方式可以使联合类型的存在影响到具体的内存模型,最好不要尝试去描绘它。
我认为意图是一个内存访问验证器(类似于Valgrind的工具)可以根据这些“严格”的规则检查潜在的别名错误。

2
@underscore_d 在 C++ 中故意省略了可见性部分,因为它被广泛认为是荒谬和难以实现的(或者至少远离任何实现的实际考虑)。别名分析是编译器后端的一部分,声明可见性通常只在前端知道。 - Potatoswatter
1
@underscore_d 那次讨论中的人本质上是“公开表态”的。Andrew Pinski是一位GCC后端专家。Martin Sebor是一位积极的C委员会成员。Jonathan Wakely是一位积极的C++委员会成员和语言/库实现者。那个页面比我写的任何东西都更权威,更清晰,更完整。 - Potatoswatter
谢谢!JW是我之前见过的唯一一个人。因为我不习惯总结这些类型的情况,你认为以下陈述准确吗?(A)讨论证实,一直以来,没有“可见声明”部分的措辞旨在通过局部可见的“联合”的实例进行限定struct的双关语。 (B) N685是对此的误读,应用于union_type_和别名,强制实现大多数人不同意并忽略的复杂性。(C)引用的C++反射器显示了忽略N685的有意决定。 - underscore_d
1
@underscore_d N685的意图并不是特别清晰,因为它没有深入探讨其提出的词语实际上是如何解决问题的。省略了N685措辞的C++也未决定(或者可能最终达成共识)指针可以对初始子序列进行什么操作。反射器引用显示某人从实践中推导出适当的规则,而不是标准。C和C++委员会(通过Martin和Clark)将尝试找到共识并制定措辞,以便标准最终能够表达其意思。 - Potatoswatter
1
作者并没有打算让6.5p7完全描述编译器应该支持的所有情况。相反,他们期望编译器编写者能够更好地判断何时应该将对派生指针或lvalue的访问视为对原始值的访问或潜在访问。问题在于,一些编译器编写者已经产生了一种扭曲的想法,即标准旨在完全描述程序员应该从优质实现中期望的所有行为,尽管理由清楚表明这不是情况。 - supercat
显示剩余7条评论

0
我想进一步扩展@Dietrich Epp的答案。这里是来自C99的引用:
6.7.2.1点14 ...指向联合对象的指针,经过适当转换,指向其每个成员...反之亦然。
这意味着我们可以将内存从一个包含它的结构体复制到联合体中:
struct a
{
    int foo;
    char bar;
};

struct b
{
    int foo;
    char bar;
};

union ab
{
    struct a a;
    struct b b;
};

void test(struct a *aa)
{
    union ab ab;
    memcpy(&ab, aa, sizeof *aa);

    // ...
}

C99还规定:

6.5.2.3第5点 为了简化联合的使用,提供了一个特殊保证:如果一个联合包含多个共享公共初始序列的结构体...,并且如果联合对象当前包含其中之一,则允许在任何可见联合完整类型声明的地方检查它们中任何一个的公共初始部分。如果对应成员具有兼容类型,则两个结构体共享公共初始序列...,对于一个或多个初始成员的序列。

这意味着在memcpy之后以下内容也将是合法的:

ab.a.bar;
ab.b.bar;

结构体可以在单独的翻译单元中初始化,而复制是在标准库中完成的(不受编译器控制)。 因此,memcpy会逐字节复制类型为struct a的对象的值,编译器必须确保结果对两个结构体都有效。 编译器除了生成读取这两行相应内存偏移量的指令外,无法执行其他任何操作,因此地址需要相同。
尽管并未明确说明,但我认为该标准意味着具有相同成员类型的C结构在内存中具有相同的布局。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接