我正在尝试将联合位作为不同的数据类型进行访问。例如:
typedef union {
uint64_t x;
uint32_t y[2];
}test;
test testdata;
testdata.x = 0xa;
printf("uint64_t: %016lx\nuint32_t: %08x %08x\n",testdata.x,testdata.y[0],testdata.y[1]);
printf("Addresses:\nuint64_t: %016lx\nuint32_t: %p %p\n",&testdata.x,&testdata.y[0],&testdata.y[1]);
输出为
uint64_t: 000000000000000a
uint32_t: 0000000a 00000000
Addresses:
uint64_t: 00007ffe09d594e0
uint32_t: 0x7ffe09d594e0 0x7ffe09d594e4
y
指向的起始地址与x
的起始地址相同。由于两个字段使用相同的位置,因此x
的值不应该为00000000 0000000a
吗?
为什么这没有发生?在具有不同数据类型的不同字段的联盟中,如何进行内部转换?
需要执行什么操作才能使用联合以与uint64_t中相同的顺序检索uint32_t中的原始位?
编辑: 如注释中所述,C ++提供了未定义的行为。 它在C中如何工作?我们真的可以做到吗?
答案 0 :(得分:6)
我将首先解释您的实施过程中发生的情况。
您正在uint64_t
值和2个uint32_t
值的数组之间进行 type punning 。根据结果,您的系统是低位字节序的,并通过简单地重新解释字节表示形式来高兴地接受这种类型的修剪。并且0x0a
的字节表示为小端uint64_t
:
Byte number 0 1 2 3 4 5 6 7
Value 0x0a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
little endian中的最低有效字节具有最低地址。现在很明显为什么uint32_t[2]
表示为{ 0x0a, 0x00 }
。
但是您所做的只是合法的C语言。
C11表示为6.5.2.3结构和联合成员:
3后缀表达式,后跟.。运算符和标识符指定成员 结构或联合对象。该值是命名成员的值, 95),如果是,则为左值 第一个表达式是左值。
95)注释明确指出:
如果用于读取联合对象的内容的成员与上次使用该成员的成员不同 将值存储在对象中,值的对象表示的适当部分将重新解释 作为新类型的对象表示形式(如6.2.6中所述(有时称为“类型 punning’)。这可能是陷阱的表示形式。
因此,即使注释不是规范性的,它们的目的也是弄清楚标准的解释方式=>您的代码有效,并且在定义uint64_t
和{{1}的小端系统上具有定义的行为}类型。
C ++在这方面更加严格。用于C ++ 17的n4659草案在[basic.lval]中说:
8如果程序尝试通过除以下任意一个以外的glvalue来访问对象的存储值 以下类型的行为未定义: 56
(8.1)—对象的动态类型,
(8.2)—对象的动态类型的cv限定版本,
(8.3)—与对象的动态类型相似的类型(定义见7.5),
(8.4)—类型,它是与对象的动态类型相对应的有符号或无符号类型,
(8.5)—一种类型,是与动态类型的CV限定版本相对应的有符号或无符号类型 对象的
(8.6)—集合或联合类型,在其元素中包括上述类型之一或非静态 数据成员(包括递归地包括子聚合的元素或非静态数据成员或 包含工会),
(8.7)—一种类型,它是对象的动态类型的(可能是cv限定的)基类类型,
(8.8)— char,unsigned char或std :: byte类型。
注释 56 明确说:
此列表的目的是指定对象可能会别名也可能不会别名的那些情况。
因为 punning 从未在C ++标准中引用,并且struct / union部分不包含C的 re-interpretation 的等效项,这意味着读入C ++的成员值(不是最后一个写入的成员)会调用未定义的行为。
当然,常见的编译器实现可以同时编译C和C ++,并且它们中的大多数甚至在C ++源代码中也接受C习惯用法,这与gcc C ++编译器欣然接受C ++源文件中的VLA的原因相同。毕竟,未定义的行为包括预期的结果...但是您不应依赖于此作为可移植代码。