我原本认为访问 union
成员变量时,除最后一个已设置的成员外,访问其他成员是未定义行为,但我似乎找不到可靠的参考资料(除了一些回答声称这是未定义行为,但没有任何标准支持)。
因此,这是否为未定义行为?
我原本认为访问 union
成员变量时,除最后一个已设置的成员外,访问其他成员是未定义行为,但我似乎找不到可靠的参考资料(除了一些回答声称这是未定义行为,但没有任何标准支持)。
因此,这是否为未定义行为?
6.5.2.3 结构体和联合体成员
95)如果用于读取联合对象内容的成员与用于在对象中最后存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型的对象表示,如6.2.6所述(有时称为“类型游戏”)。这可能是一个陷阱表示。
C++的情况:
9.5 联合体 [class.union]
在联合体中,最多只能有一个非静态数据成员处于活动状态,也就是说,在任何时候,联合体中最多只能存储一个非静态数据成员的值。
C++后来有了语言,允许使用包含共同初始序列的struct
的union;然而这并不允许类型转换。
要确定C++中是否允许联合体类型转换,我们需要进一步搜索。回想一下c99是C++11的规范参考(而C99有类似于C11的语言允许联合体类型转换):
4 - 类型T的对象表示是由该类型T的对象占用的N个无符号字符对象序列,其中N等于sizeof(T)。对象的值表示是保存T类型值的一组位。对于可平凡复制类型,值表示是对象表示中确定一个值的一组位,该值是实现定义的一组离散元素之一。42
42)意图是C++的内存模型与ISO/IEC 9899编程语言C的内存模型兼容。
当我们阅读以下内容时,它变得特别有趣
类型T的对象的生命周期从以下情况开始: — 获得了适合类型T的对齐和大小的存储空间,并且 — 如果对象具有非平凡的初始化,则其初始化完成。
因此,对于包含在联合中的原始类型(其本身具有平凡的初始化),对象的生命周期至少包括联合本身的生命周期。这使我们能够调用
如果类型T的对象位于地址A处,则类型为cv T*的指针,其值为地址A,被称为指向该对象的指针,不管该值是如何获得的。
假设我们感兴趣的操作是类型转换,即获取非活动联合成员的值,并且根据上述内容,我们拥有对该成员所引用的对象的有效引用,则该操作是左值到右值的转换:
非函数、非数组类型T的glvalue可以转换为prvalue。如果T是不完整的类型,则需要进行此转换的程序是非法的。如果glvalue所引用的对象不是类型T的对象,也不是派生自类型T的对象,或者对象未初始化,则需要进行此转换的程序具有未定义的行为。
问题在于,非活动联合成员是否由存储初始化为活动联合成员。据我所知,这不是这种情况,因此尽管如果:memcpy
实现(使用unsigned char
左值访问对象),禁止在int *p = 0; const int *const *pp = &p;
后访问*p
(尽管从int **
到const int *const *
的隐式转换是有效的),甚至禁止在struct S s; const S &c = s;
后访问c
。新的表述是否允许这些操作?还有关于[lval]的内容。 - user743382&
运算符的含义。我认为生成的指针应该可用于访问该成员,至少在下一次直接或间接使用任何其他成员lvalue之前,但在gcc中,指针甚至不能使用那么长时间,这引发了一个问题,即&
运算符应该意味着什么。 - supercatgcc文档将此列在实现定义的行为下
- 使用不同类型的成员访问联合对象的成员(C90 6.3.2.3)。
对象表示中相关字节被视为使用于访问的类型的对象。请参见类型转换。这可能是一个陷阱表示。
表明这不是C标准所要求的。
2016年1月5日:通过评论,我被链接到了C99缺陷报告#283,该报告在C标准文档中添加了类似的文本作为脚注:
78a)如果用于访问联合对象内容的成员与用于在对象中存储值的最后一个成员不同,则该值的对象表示的适当部分将根据6.2.6中描述的方式重新解释为新类型的对象表示(有时称为“类型游戏”)。这可能是一个陷阱。
不确定它是否澄清了很多问题,考虑到脚注对于标准来说并非规范性的。
C++11在§9.2 / 19给出了类似的要求/允许:为了简化对联合体的使用,提供了一个特殊的保证:如果一个联合体包含多个结构,这些结构共享一个公共初始顺序(见下文),并且如果该联合体对象当前包含其中之一,则允许在任何声明联合体的完整类型可见的地方检查它们中任何一个的公共初始部分。 如果相应成员具有兼容类型(对于位域,宽度相同)的话,则两个结构共享公共初始序列,用于一个或多个初始成员的序列。
虽然两者都没有直接说明,但两者都表明“允许”(读取)成员的“检查”仅在以下情况下才被“允许”,即1)它是最近写入的成员的一部分,或者2)是公共初始序列的一部分。如果标准布局联合包含两个或多个共享公共初始序列的标准布局结构,并且如果标准布局联合体对象当前包含其中之一,则允许检查其中任何一个的公共初始部分。 如果相应成员具有布局兼容类型,并且没有成员是位字段 或者两个成员都是具有相同宽度的位字段,则两个标准布局结构分享共同的初始序列,用于一个或多个初始成员的序列。
目前可用的答案中还没有提到的是6.2.5节第21段中的脚注37:
请注意,聚合类型不包括联合类型,因为具有联合类型的对象一次只能包含一个成员。
这个要求似乎明确暗示着您不能在一个成员中写入并在另一个成员中读取。 在这种情况下,由于缺乏规范,可能会出现未定义行为。
我将用一个示例来说明这个问题。
假设我们有以下联合:
union A{
int x;
short y[2];
};
我假设sizeof(int)
为4,sizeof(short)
为2。
当你写union A a = {10}
时,它将创建一个新的A类型变量,并将值10放入其中。
你的内存应该看起来像这样:(请记住,联合体成员都占用相同的位置)
| x | | y[0] | y[1] | ----------------------------------------- a-> |0000 0000|0000 0000|0000 0000|0000 1010| -----------------------------------------
正如你所看到的,a.x的值为10,a.y1的值也是10,而a.y[0]的值为0。
现在,如果我这样做会发生什么呢?
a.y[0] = 37;
我们的内存将会是这样的:
| x | | y[0] | y[1] | ----------------------------------------- a-> |0000 0000|0010 0101|0000 0000|0000 1010| -----------------------------------------
这将把a.x的值转换为2424842(十进制)。
现在,如果你的联合体有一个浮点数或双精度浮点数,你的内存映射将会更加混乱,因为你存储精确数字的方式不同。 你可以在这里获得更多信息。