【问题标题】:Aliasing, Type-punning, Unions, Structs and Bit Fields in C99C99 中的别名、类型双关、联合、结构和位字段
【发布时间】:2014-07-03 00:02:38
【问题描述】:

this question的回复中收到以下声明后:

...您试图覆盖valuebits,并将数据填充到联合的一个替代方案中并将其从另一个替代方案中取出是未定义

我对 C99 中的类型双关语允许什么(以及什么是谨慎的)更加好奇。环顾四周后,我在Is type-punning through a union unspecified in C99... 的帖子中发现了很多有用的信息。

从 cmets 和那里发布的答案中可以看出很多东西。为了清楚起见(并作为完整性检查),我想根据我对 C99 标准的理解创建一个示例。下面是我创建的示例代码,虽然它按预期运行,但我想确定我的断言是正确的。

以下代码包含我在 cmets 中的断言。这是我对 C99 中类型双关语的理解。我的cmets正确吗?如果不是,您能解释一下原因吗?

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM_BYTES   sizeof(uint32_t)
typedef union
{
    uint32_t fourByteValue;
    uint8_t  byteValue[NUM_BYTES];
    struct
    {
        unsigned int firstBitSpecified  :   1;
        unsigned int secondBitSpecified :   1;
        unsigned int thirdBitSpecified  :   1;
        unsigned int fourthBitSpecified :   1;
        unsigned int paddingBits        :   4;
        uint8_t  oneByteStructValue;
        uint16_t twoByteStructValue;
    };
} U;

int main (void)
{
    const char border[] = "==============================\n";
    U myUnion;
    uint8_t byte;
    uint32_t fourBytes;
    uint8_t i;

    myUnion.fourByteValue = 0x00FFFFFF;
    fourBytes = myUnion.fourByteValue;  /* 1. This is not type-punning. */
    printf("No type-punning fourByteValue:\n%s"
           "fourBytes\t= 0x%.4x\n\n", border, fourBytes);


    printf("Type-punning byteValue:\n%s", border);
    for (i = 0; i < NUM_BYTES; i++)
    {
        byte = myUnion.byteValue[i];   /* 2. Type-punning allowed by C99, 
                                             no unspecified values. */
        printf ("byte[%d]\t\t= 0x%.2x\n", i, byte);
    }
    printf("\n");

    myUnion.byteValue[3] = 0xff;
    fourBytes = myUnion.fourByteValue; /* 3. Type-punning allowed by C99 
                                             but all other 'byteValue's
                                             are now unspecified values. */
    printf("Type-punning fourByteValue:\n%s"
           "fourBytes\t= 0x%.4x\n\n", border, fourBytes);

    myUnion.firstBitSpecified = 0;
    myUnion.thirdBitSpecified = 0;
    fourBytes = myUnion.fourByteValue; /* 4. Again, this would be allowed, but 
                                             the bit that was just assigned
                                             a value of 0 is implementation
                                             defined AND all other bits are
                                             unspecified values. */
    printf("Type-punning firstBitSpecified:\n%s"
           "fourBytes\t= 0x%.4x\n\n", border, fourBytes);

    myUnion.fourByteValue = 0x00000001;
    fourBytes = myUnion.firstBitSpecified; /* 5. Type-punning allowed, although
                                                 which bit you get is implementation
                                                 specific. */
    printf("No type-punning, firstBitSpecified:\n%s"
           "fourBytes\t= 0x%.4x\n\n", border, fourBytes);
    fourBytes = myUnion.secondBitSpecified;
    printf("No type-punning, secondBitSpecified:\n%s"
           "fourBytes\t= 0x%.4x\n\n", border, fourBytes);

    return (EXIT_SUCCESS);
}

以上代码是在 64 位 Windows 7 机器上使用 mingw32-gcc.exe -Wall -g -std=c99 编译的。运行代码后,我收到以下输出:

No type-punning fourByteValue:
==============================
fourBytes       = 0xffffff

Type-punning byteValue:
==============================
byte[0]         = 0xff
byte[1]         = 0xff
byte[2]         = 0xff
byte[3]         = 0x00

Type-punning fourByteValue:
==============================
fourBytes       = 0xffffffff

Type-punning firstBitSpecified:
==============================
fourBytes       = 0xfffffffa

No type-punning, firstBitSpecified:
==============================
fourBytes       = 0x0001

No type-punning, secondBitSpecified:
==============================
fourBytes       = 0x0000

【问题讨论】:

  • 请注意,您永远不能依赖位字段的布局顺序。
  • 确实如此。这就是为什么我在代码中的 cmets 中对其进行了注释“......刚刚分配的位是实现定义的......”

标签: c c99 language-lawyer


【解决方案1】:

我对该帖子中链接的脚注的阅读是,从未指定通过联合进行类型双关。来自this,标准说:

除了一个例外,如果联合对象的成员在 值已存储在对象的不同成员中, 行为是实现定义的。

脚注并没有改变这一点。出现这种情况的原因是 C 不保证 (a) 数字类型的字节顺序,或 (b) struct 的成员在内存中的顺序,除非第一个成员必须是字节-与struct 的“开始”对齐(这样您就可以进行他们在 GTK 中所做的那种转换以实现多态性)。

有问题的脚注涉及这一行:

当一个值存储在联合类型对象的成员中时, 不对应的对象表示的字节 成员但确实对应于其他成员采用未指定的值,但是 联合对象的值不应因此成为陷阱 代表

它是这样说的:

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

“重新解释为新类型中的对象表示”是实现定义的(因为所有类型的解释,在逐字节级别上,始终是实现定义的,考虑到字节序等)。脚注只是添加了更多细节,以指出当您通过联合与类型系统混淆时可能会发生令人惊讶的事情,包括导致陷阱表示。寻找 here 以了解“陷阱表示”的定义:

陷阱表示是一组位,当解释为 特定类型的值,导致未定义的行为。陷阱 表示最常见于浮点和指针 值,但理论上,几乎任何类型都可能有陷阱 申述。未初始化的对象可能包含陷阱 表示。这给出了与旧规则相同的行为:访问 未初始化的对象会产生未定义的行为。

标准对访问未初始化的唯一保证 数据是 unsigned char 类型没有陷阱表示, 和 该填充没有陷阱表示。

因此,通过在您的帖子中将 uint_8 替换为 unsigned char,您可以避免未定义的行为,并最终获得特定于实现的行为。然而,正如现在所写的那样,标准并未禁止 UB。

这在您链接的帖子中的引用中明确说明:

最后,从 C90 到 C99 的变化之一是删除任何 限制在最后一个存储时访问联合的一个成员 到另一个。 理由是该行为会 取决于值的表示。

根据定义,标准从未定义基础表示。

【讨论】:

  • 实现定义不同于未定义。 “未定义的行为”是指鼻恶魔。未指定或实现定义的行为意味着,尽管标准不打算指定行为,但其他正确的程序不会因为它引发未指定或实现定义的行为而导致不正确。
  • @tmyklebu 鼻恶魔不是通过联合实现类型双关的允许实现吗?
  • 这个评论系统不能让我简单地说“不”,所以我会稍微啰嗦一下。
  • @tmyklebu uint_8 的行为是否与 unsigned char 相同?我假设unsigned char 正是unsigned char,而unsigned char 的行为与uint_8 相同的事实只是实现的一个(通用)怪癖。
  • 不确定。 (最终,我不觉得这些东西很令人兴奋。类型双关语在实践中有效,因为大多数架构不会为整数类型选择愚蠢的表示,而陷阱表示,当它们存在时,非常罕见 --- Itanic 上的 NaT 是我知道的唯一例子。我会说可以安全地假设 uint_8unsigned char 相同,但我不知道标准是否指定或暗示它。)
猜你喜欢
  • 1970-01-01
  • 2019-06-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-21
相关资源
最近更新 更多