具有相同成员类型的 C 结构是不是保证在内存中具有相同的布局?
Posted
技术标签:
【中文标题】具有相同成员类型的 C 结构是不是保证在内存中具有相同的布局?【英文标题】:Are C-structs with the same members types guaranteed to have the same layout in memory?具有相同成员类型的 C 结构是否保证在内存中具有相同的布局? 【发布时间】:2013-11-17 06:02:28 【问题描述】:基本上,如果我有
typedef struct
int x;
int y;
A;
typedef struct
int h;
int k;
B;
我有A a
,C 标准是否保证((B*)&a)->k
与a.y
相同?
【问题讨论】:
不,我认为标准不能保证这一点。在实践中,编译器会按照您的意愿和期望进行,但标准并不能保证这一点。这是未定义的行为;任何事情都可能发生。 【参考方案1】:具有相同成员类型的 C 结构是否保证在内存中具有相同的布局?
几乎是的。对我来说足够近了。
来自 n1516,第 6.5.2.3 节,第 6 段:
...如果一个联合包含多个共享一个公共初始序列的结构...,并且如果联合对象当前包含这些结构之一,则允许在任何位置检查其中任何一个的公共初始部分联合的已完成类型的声明是可见的。如果对应的成员对于一个或多个初始成员的序列具有兼容的类型(并且对于位域,具有相同的宽度),则两个结构共享一个共同的初始序列。
这意味着如果你有以下代码:
struct a
int x;
int y;
;
struct b
int h;
int k;
;
union
struct a a;
struct b b;
u;
如果你分配给u.a
,标准说你可以从u.b
读取相应的值。它扩展了合理性的界限,建议struct a
和struct b
可以有不同的布局,给定这个要求。这样的系统将是极端病态的。
请记住,该标准还保证:
结构绝不是陷阱表示。
结构中的字段地址增加(a.x
总是在a.y
之前)。
第一个字段的偏移量始终为零。
但是,这很重要!
你改写了这个问题,
C 标准是否保证
((B*)&a)->k
与 a.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:这就是我躺着的东西。无论如何,这种语言从 ANSI C 时代就已经存在(第 3.3.2.3 节)。 如果包含struct a
和struct b
的完整联合type 声明在代码检查结构成员时可见,则conforming 和非-buggy 编译器会识别出别名的可能性。一些只希望在适合他们的标准时遵守标准的编译器编写者会破坏这样的代码,即使标准保证它会起作用;这仅仅意味着他们的编译器不符合要求。
@supercat 是的,但我知道没有一个编译器(在优化期间使用严格的别名)实现了这个规则,所以不能依赖它。将来可能会删除该条款。无论如何,标准大多是垃圾,大多数编译器并没有真正遵循它们。
@wonder.mice:x
在两者中具有相同的类型是不够的。问题是a
的类型为struct a
,而您通过struct b
类型访问它。这是一个链接,它向您展示了编译器将如何基于别名进行优化:gcc.godbolt.org/z/7PMjbT 尝试删除 -fstrict-aliasing
并查看生成的代码如何变化。【参考方案2】:
捎带其他回复,并带有关于第 6.5.2.3 节的警告。显然对于anywhere that a declaration of the completed type of the union is visible
的确切措辞存在一些争论,至少GCC doesn't implement it as written。有一些切线的 C WG 缺陷报告 here 和 here 以及来自委员会的后续 cmets。
最近我试图找出其他编译器(特别是 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++ 中排除 - 在这里:***.com/q/34616086/2757035
一点都不重要!通过与您链接的第二次 GCC 讨论,我们看到 C++ 可能故意拒绝了这一点 - 而 C 在添加这个措辞之前并没有真正考虑过,从未真正认真对待它,并且可能正在扭转它:gcc.gnu.org/bugzilla/show_bug.cgi?id=65892 从那里,我们到了 C++ DR 1719 open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1719,这表明了一个重大的措辞变化,这似乎使 C++ 对此类 struct
s 的确切位置的看法非常清楚。我已经收集了这个以及更多关于我的链接问题的答案
@curiousguy:对于无法识别从另一种类型的指针或左值派生一种类型的指针或左值的行为相对于涉及的其他操作的顺序的编译器,CIS 规则很有用这些类型,需要有一种方法告诉编译器“这个指针将识别这些结构类型中的一种,我不知道是哪一种,但我需要能够使用其中一个的 CIS 成员来访问他们都是”。除了声明联合类型之外,让联合声明服务于该目的将避免引入新指令的需要......
...为此目的。请注意,6.5p7 的编写方式,给定struct foo int x; *p, it;
,类似p=&it; p->x=4;
的东西会调用UB,因为它使用int
类型的左值来修改struct foo
类型的对象,但标准的作者期望编译器编写者不会愚蠢到假装他们不应该将其视为已定义。该标准从未做出任何合理的尝试来完全指定针对任何特定平台和目的的实现应支持的全部语义。无意义的“有效类型”规则甚至不能......
...处理非字符类型结构成员的最基本操作。如果要调整 6.5p7 来说明在函数或循环的任何特定执行期间更改的任何存储字节必须在其生命周期内仅通过从同一对象或执行期间派生的左值访问相同数组的元素,并且所有与字节相关的派生左值的使用都在下一次使用与该字节相关的父级之前,人们可以放弃与“有效类型”有关的所有事情,并使事情变得更简单和更多强大。【参考方案3】:
这种别名特别需要union
类型。 C11 §6.5.2.3/6:
为了简化联合的使用,我们做了一个特殊的保证:如果联合包含多个共享相同初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,它允许在任何可见联合的完整类型声明的任何地方检查它们中任何一个的公共初始部分。如果相应的成员具有兼容的类型(并且,对于位域,两个结构共享一个公共的初始序列) , 相同的宽度) 用于一个或多个初始成员的序列。
这个例子如下:
以下不是有效片段(因为联合类型不是 在函数 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
。一般来说,这种union
依赖对于编译器来说是一种极其病态的行为。如果联合类型的存在会以某种方式影响具体的内存模型,那么最好不要尝试去描绘它。
我想目的是内存访问验证器(想想 Valgrind 的 steroids)可以根据这些“严格”规则检查潜在的别名错误。
【讨论】:
C++ 可能没有规定联合声明是必需的,但它的行为仍然与 C 相同——不允许通过 GCC 和 Clang 对指向union
成员的“裸”指针进行别名。请参阅@ecatmur 对我的问题的测试,了解为什么这个子句被排除在 C++ 之外:***.com/q/34616086/2757035 非常欢迎读者对这种差异有任何想法。我怀疑这个子句 应该 被添加到 C++ 中,只是因为从 C99 中添加的“继承”而意外省略了(C99 没有它)。
@underscore_d C++ 中故意省略了可见性部分,因为它被广泛认为是荒谬且无法实现的(或者至少与任何实现的实际考虑相去甚远)。别名分析是编译器后端的一部分,声明可见性通常只在前端知道。
@underscore_d 讨论中的人基本上都在那里“记录在案”。 Andrew Pinski 是 GCC 后端的铁杆人物。 Martin Sebor 是活跃的 C 委员会成员。 Jonathan Wakely 是一位活跃的 C++ 委员会成员和语言/库实施者。那个页面比我能写的任何东西都更权威、更清晰、更完整。
@underscore_d N685 的意图并不是特别清楚,因为它并没有深入探讨为什么它的提议词实际上解决了问题。省略了 N685 措辞的 C++ 对于使用指向初始子序列的指针可以做什么也未确定(或者可能最终达成共识)。反射器报价显示某人从实用性而非标准中得出适当的规则。 C 和 C++ 委员会(通过 Martin 和 Clark)将尝试达成共识并敲定措辞,以便标准最终能说出它的含义。
...作者并不打算在 6.5p7 中完全描述编译器应该支持的所有情况。相反,他们希望编译器编写者能够更好地判断他们应该将对派生指针或左值的访问识别为对原始值的访问或潜在访问的情况。问题在于,一些编译器编写者有一个扭曲的想法,即该标准旨在全面描述程序员应该从 quality 实现中获得的所有行为,尽管其基本原理表明事实并非如此。 以上是关于具有相同成员类型的 C 结构是不是保证在内存中具有相同的布局?的主要内容,如果未能解决你的问题,请参考以下文章