【问题标题】:Interpreting bits in union fields as different datatypes in C/C++在 C/C++ 中将联合字段中的位解释为不同的数据类型
【发布时间】:2018-07-02 07:19:43
【问题描述】:

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

    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 的确切原始位?

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

【问题讨论】:

    标签: c++ c bit-manipulation unions


    【解决方案1】:

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

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

    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 明确地说:

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

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


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

    【讨论】:

    • 值得注意的是,这是 C++ 不适合与硬件相关的编程的原因之一,在这种编程中,您经常必须通过联合在不同的整数类型之间键入双关语(例如:32 位寄存器但 CPU 是 16 位)或在某种类型和字节类型uint8_t 之间(在进行任何形式的序列化/反序列化时)。这不仅使 C++ 对于与硬件相关的编程变得异常繁琐;当您获取从 C 编写的硬件寄存器映射时,这也会导致 C++ 调用令人讨厌的 UB 错误。
    • 我强烈反对。实际上,这适用于所有健全的编译器。 C++ 是除 C 之外唯一适用于内核工作和硬实时系统的语言。而且您可以获得更好的零成本抽象和语言内置的 RAII。
    • 由于我们将其读取为 uint32_t,根据字节序,每次从 'uint64_t' 从 LSB 到 MSB 的读取将读取 4 个字节而不是 2 个字节,对吧?
    • @Lundin:C 标准没有尝试描述何时(如果有)通过从另一种类型派生的指针或左值进行的访问应该被识别为通过原始类型的访问,而是将其视为实施质量问题。作者接受这种低质量的“符合”实现是无用的[在基本原理中描述]意味着该标准不仅没有努力详尽地描述编译器应该识别这种派生左值的情况——它甚至没有定义明显的像someAggregate.member这样的情况。
    • 因此,可能有适合嵌入式或系统编程的高质量 C 实现,也可能有设计迟钝但不适合此类目的但“符合”的实现,或者甚至任何目的。如果有人正在努力生成一个最适合平台上的嵌入式或系统编程的高质量实现,标准和平台规范一起可能会为他们提供他们需要的所有信息,但前提是他们避免做与该目的相反的事情。
    猜你喜欢
    • 2014-12-30
    • 2018-07-25
    • 2020-10-26
    • 1970-01-01
    • 2015-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多