【问题标题】:How to do a bit representation in a C-standard way?如何以 C 标准方式进行位表示?
【发布时间】:2015-05-10 08:16:15
【问题描述】:

根据 C 标准,整数类型的值表示是实现定义的。所以5 可能不会表示为00000000000000000000000000000101-111111111111111111111111111111111,因为我们通常假设在32 位2 的补码中。因此,即使运算符~<<>> 定义明确,它们将处理的位模式也是实现定义的。我能找到的唯一定义的位模式是 "§5.2.1/3 所有位都设置为 0 的字节,称为空字符,应存在于基本执行字符集中;它用于终止字符串.".

所以我的问题是 - 是否有独立于实现的方法将整数类型转换为位模式?

我们总是可以从一个空字符开始并对其进行足够多的位操作以使其达到所需的值,但我觉得它太麻烦了。我也意识到几乎所有实现都将使用 2 的补码表示,但我想知道如何以纯 C 标准方式进行。就我个人而言,由于设备驱动程序编程的问题,我个人觉得这个话题非常有趣,迄今为止编写的所有代码都假定了一个特定的实现。

【问题讨论】:

  • “...他们将处理的值是实现定义的...”这是什么意思?
  • 到底想做什么?这个问题的答案取决于您正在执行的操作。话虽如此,我认为通常不会在设备驱动程序级别上解决这个问题。如果您切换到具有不同整数表示的不同硬件,则很可能无论如何都必须重写驱动程序。
  • 你能举个例子说明你想完成什么吗?
  • @JoachimPileborg : 那么格雷码呢?不是“二进制”,但C标准是否禁止用格雷码表示整数?
  • 要访问单个位,您可以使用~(~0<<1) << n 而不是1 << n。如果连0 都没有保存,你必须改用(0^0)

标签: c bit-manipulation standards bit


【解决方案1】:

一般来说,在大多数情况下,适应不寻常的平台并不难(如果您不想简单地假设 8 位 char、2 的补码、无填充、无陷阱,并截断无符号到有符号的转换),该标准大多提供了足够的保证(不过,检查某些实现细节的一些宏会有所帮助)。

就严格符合程序可以观察到的(位域之外)而言,5 始终编码为00...0101。这不一定是物理表示(无论这应该意味着什么),而是可移植代码可以观察到的。例如,在内部使用格雷码的机器必须为按位运算符和移位模拟“纯二进制符号”。

对于有符号类型的负值,允许使用不同的编码,当重新解释为对应的无符号类型时,这会导致不同的(但对每种情况都有明确定义)的结果。例如,对于有符号整数n,严格符合代码必须区分(unsigned)n*(unsigned *)&n:对于没有填充位的二进制补码,它们相等,但如果n 为负数,则对于其他编码则不同。

此外,可能存在填充位,并且有符号整数类型可能具有比其对应的无符号对应物更多的填充位(但并非相反,从有符号到无符号的类型双关总是有效的)。 sizeof 不能用于获取非填充位数,例如要获得一个无符号值,其中仅设置了符号位(相应的有符号类型),必须使用类似的东西:

#define TYPE_PUN(to, from, x) ( *(to *)&(from){(x)} )
unsigned sign_bit = TYPE_PUN(unsigned, int, INT_MIN) &
                    TYPE_PUN(unsigned, int, -1) & ~1u;

(可能有更好的方法)而不是

unsigned sign_bit = 1u << sizeof sign_bit * CHAR_BIT - 1;

因为这可能会移动超过宽度。 (我不知道给出宽度的常量表达式,但是上面的 sign_bit 可以右移直到它为 0 来确定它,Gcc 可以对其进行常量折叠。)填充位可以通过memcpying 检查进入unsigned char 数组,尽管它们可能看起来“摇摆不定”:两次读取相同的填充位可能会产生不同的结果。

如果你想要一个有符号整数(小端序)的位模式(没有填充位):

int print_bits_u(unsigned n) {
    for(; n; n>>=1) {
        putchar(n&1 ? '1' : '0'); // n&1 never traps
    }
    return 0;
}

int print_bits(int n) {
    return print_bits_u(*(unsigned *)&n & INT_MAX);
    /* This masks padding bits if int has more of them than unsigned int.
     * Note that INT_MAX is promoted to unsigned int here. */
}

int print_bits_2scomp(int n) {
    return print_bits_u(n);
}

print_bits 对负数给出不同的结果,具体取决于所使用的表示形式(它给出原始位模式),print_bits_2scomp 给出二进制补码表示(如果@987654338,宽度可能比signed int 的宽度更大) @ 的填充位更少)。

在使用按位运算符以及从无符号到有符号的类型双关语时,必须注意不要生成陷阱表示,请参阅下面如何可能生成这些表示(例如,*(int *)&amp;sign_bit 可以使用二进制补码进行陷阱,而 @ 987654340@可以用反码进行陷印)。

无符号到有符号整数转换(如果转换后的值在目标类型中不可表示)始终是实现定义的,我希望非 2 的补码机器更有可能不同于常见的定义,尽管在技术上,它也可能成为 2 的补码实现的问题。

从 C11 (n1570) 6.2.6.2:

(1) 对于unsigned char以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(后者不需要任何一个)。如果有 N 个值位,每个位应代表 12N-1之间的 2 的不同幂>,以便该类型的对象能够使用纯二进制表示表示从 02N-1 的值;这应称为值表示。未指定任何填充位的值。

(2) 对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。不需要任何填充位; signed char 不应有任何填充位。应该有一个符号位。作为值位的每个位都应与相应无符号类型的对象表示中的相同位具有相同的值(如果有符号中存在 M 个值位 type 和 N 在无符号类型,然后 M≤N )。如果符号位为零,则不应影响结果值。如果符号位为1,则按以下方式之一修改该值:

  • 符号位为0的对应值取反(符号和幅度);
  • 符号位的值是 -(2M)二的补码);
  • 符号位的值是 -(2M-1)ones'补码)。

这些应用中的哪一个是实现定义的,无论符号位为 1 且所有值位为零(对于前两个),还是符号位和所有值位为 1(对于一个补码)的值是陷阱表示或正常值。在符号和幅度以及反码的情况下,如果这种表示是正常值,则称为负零。

【讨论】:

  • 赞成该标准限制了表示。我很乐意被反驳,但我相信所有现代平台都使用二进制补码。当然,本文en.wikipedia.org/wiki/Signed_number_representations 中实现其他格式的所有硬件实际上都是博物馆作品。位操作运算符,尤其是 >> 的行为方式存在差异,但唯一可以找到不使用补码的计算机的地方是爷爷的地下室。
  • @DanAllen,我不知道,也从未见过有人在这里知道(这个问题在 cmets 中不时提出)。我很确定没有更新的机器。
  • 这个答案是错误的。 print_bits_u 调用中的 INT_MAX 掩码屏蔽了符号位,sign_bit 的初始化程序可能设置了值位,当解释为 int 时这些位是填充位(因此也需要一些掩码)。如果我找到合适的面具,我会解决这个问题。
  • 我开始相信可移植代码不存在此掩码。一个实现可以有 int 具有 30 个值位和 1 个符号位,以及 1 个填充位,当且仅当设置了符号位时(如果恰好设置了一个符号位和填充位,它是陷阱表示)。如果填充位在读取为unsigned int 时是一个值位,则无法确定哪个位是哪个,我认为...
  • @Cuadue:这个名字可能不是很好选择,但用法是正确的。为了在相应的有符号和无符号类型之间进行类型双关,定义了显示的代码。对于floatint 之间的类型双关语(即使它们的大小相同),这会破坏严格的别名。
【解决方案2】:

为了补充 mafso 的出色答案,ANSI C rationale 的一部分谈到了这一点:

委员会已明确将 C 语言限制为二进制架构,理由是这种限制在任何情况下都是隐含的:

  • 位域由多个位指定,没有提及“无效整数”表示。此类位域的唯一合理编码是二进制。
  • printf 的整数格式不建议提供“无效整数”值,这意味着任何按位操作的结果都会产生一个整数结果,该结果可由 printf 打印。
  • 所有指定整数常量的方法(十进制、十六进制和八进制)都指定一个整数值。没有定义独立于整数的方法来指定“位串常量”。只有二​​进制编码才能在位串和整数值之间提供完整的一对一映射。

对二进制计数系统的限制排除了格雷码等好奇心,并使得 无符号类型的位运算符的可能算术定义。

标准的相关部分可能是这句话:

3.1.2.5 类型

[...]

char 类型,有符号和无符号整数类型,以及 枚举类型统称为整数类型。这 整数类型的表示应使用纯类型定义值 二进制计数系统。

【讨论】:

    【解决方案3】:

    如果您想获得给定int 的位模式,那么按位运算符是您的朋友。如果您想将 int 转换为其 2 补码表示,那么算术运算符是您的朋友。这两种表示可以不同,因为它是由实现定义的:

    Std Draft 2011. 6.5/4. 一些运算符(一元运算符 ~ 和 二元运算符 >、&、^ 和 |,统称为 位运算符)必须具有具有整数的操作数 类型。这些运算符产生的值取决于内部 整数的表示,并具有实现定义和 有符号类型的未定义方面。

    所以这意味着i&lt;&lt;1 将有效地将位模式向左移动一个位置,但产生的值可能与i*2 不同(即使i 的值很小)。

    【讨论】:

    • 如果i*2(算术结果)为非负且在范围内,i&lt;&lt;1i*2 相等。
    • These operators yield values that depend on the internal representations of integer。你可以有整数的格雷码,而 Gray
    • 没有。未定义的方面并不意味着一切都是未定义的。移位运算符是根据值定义的,而不是位模式。而且这不是允许格雷码的意图(事实上,从 C99 Rationale V5.10 6.2.6.2 开始,它的意图是禁止它:对二进制计数系统的限制排除了格雷码这样的好奇心,并使之成为可能无符号类型的位运算符的算术定义。),因此在内部使用格雷码的机器必须表现得好像它使用了三种允许的编码之一。
    • 对!我犯了一个严重的错误 6.5.7/4 The result of E1
    • 是的,它是根据表示中的值 定义的(尽管标准没有说明“左移”的含义)。但是表示也是根据&lt;&lt; 的所有定义结果的值来定义的,所以没关系。 n&lt;&lt;kn*pow(2, k) 用于定义它的所有情况。 (参见 C11 (n1570) 6.2.6.2)。)
    猜你喜欢
    • 2010-11-29
    • 1970-01-01
    • 1970-01-01
    • 2014-08-19
    • 2021-11-29
    • 1970-01-01
    • 2023-03-24
    • 2011-03-09
    • 1970-01-01
    相关资源
    最近更新 更多