首页 > 解决方案 > 在 C/C++ 中将联合字段中的位解释为不同的数据类型

问题描述

我正在尝试以不同的数据类型访问联合位。例如:

    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。由于两个字段使用相同的位置,不应该是值x00000000 0000000a

为什么这没有发生?内部转换如何在具有不同数据类型的不同字段的联合中发生?

需要做什么才能使用联合以与 uint64_t 中相同的顺序检索作为 uint32_t 的确切原始位?

编辑:正如评论中提到的,C++ 给出了未定义的行为。它在 C 中是如何工作的?我们真的可以做到吗?

标签: c++cbit-manipulationunions

解决方案


我将首先解释在您的实现中会发生什么。

您正在一个值和一个包含 2 个值的数组之间进行类型双关语。根据结果​​,您的系统是小端的,并且很乐意通过简单地重新解释字节表示来接受这种类型的双关语。小端的字节表示是:uint64_tuint32_t0x0auint64_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 语言中是合法的。

C语言:

C11 表示为 6.5.2.3 结构和联合成员:

3 后缀表达式后跟 . 运算符和标识符指定结构或联合对象的成员。该值是命名成员的值,95)如果第一个表达式是左值,则它是左值。

95)注释明确指出:

如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新类型中的对象表示在 6.2.6 中描述(有时称为“类型双关语”的过程)。这可能是一个陷阱表示。

因此,即使注释不规范,它们的目的是明确解释标准的方式=>您的代码是有效的,并且在小端系统定义uint64_tuint32_t类型上定义了行为。

C++ 语言:

C++ 在这部分更加严格。C++17 的 n4659 草案在 [basic.lval] 中说:

8 如果程序试图通过非下列类型之一的泛左值访问对象的存储值,则行为未定义: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明确地说:

此列表的目的是指定对象可能或可能不别名的情况。

由于在 C++ 标准中从未引用过双关语,并且由于 struct/union 部分不包含对 C 的重新解释的等价物,这意味着在 C++ 中读取不是最后写入的成员的值会调用 undefined行为。


当然,常见的编译器实现同时编译 C 和 C++,并且它们中的大多数即使在 C++ 源代码中也接受 C 习惯用法,原因与 gcc C++ 编译器很乐意在 C++ 源文件中接受 VLA 的原因相同。毕竟,未定义的行为包括预期的结果......但你不应该依赖于可移植代码。


推荐阅读