【问题标题】:Arithmetic with unsigned variables in CC中无符号变量的算术运算
【发布时间】:2016-12-17 21:06:11
【问题描述】:

例如,无符号字符如何将值从-128 获取到+127?据我了解,最高有效位用于表示数字的符号,而 char 的其余位用于表示数字的大小。现在,7 位的最大可能幅度是127,那么范围不应该是从-127+127 吗? -128怎么会是结果?

其次,以下行为背后的位级逻辑是什么

#include <stdio.h>

int main()
{
    signed char x = 127;
    x += 1;
    printf("%i", x);
}

输出:

-128

正如你所见,x 变成了-128,但为什么呢?这种行为背后的算法是什么?

【问题讨论】:

  • 查找 One's Complement 和 Two's Complement 以了解整数类型在内存中的表示方式。
  • 您需要阅读二进制数的补码。不用在这里问
  • “据我了解,最高位是用来表示数字的符号的。” --> C 指定表示有符号整数的 3 种方法中的 1 种包括二进制补码(最常见)。在这种情况下,最高有效位代表-256
  • C 标准现在允许三种不同的有符号整数表示。您所描述的符号幅度是其中之一,但不是很常见。最常用的称为 2 的补语,它的负数范围更大。
  • 位级逻辑:十进制127 是二进制01111111。添加1 得到二进制10000000。清除第 7 位的正整数已变为第 7 位设置的负整数(2 的补码),十进制值 -128。但如前所述,在 C 语言中,有符号整数是未定义的行为,因为这不是表示有符号整数的唯一方法。

标签: c undefined-behavior unsigned twos-complement


【解决方案1】:

这基于名为Two's Complement 的东西。这里的想法是,给定一些二进制数,它的两个补码将是一个补码(翻转所有位)加一。我们可以看一个简单的例子,我们求13的补码,我们可以写成0b0110101101 (flip) -&gt; 10010 (+1) --&gt; 10011

现在,虽然如果我们像往常一样将其解释为二进制数,我们会在十进制中读取19,但我们必须知道该数字是以二进制补码形式写入的,以便颠倒过程并得到先前的数字@987654330 @。所以,从这里我们可以看到我们已经表示了+13 = 01101-13 = 10011这样的东西,注意正数以0开头,它与1对称。使用此表示时,这将是一个常数,正数总是以0 开头,负数总是以1 开头。值得注意的另一件事是,我在 13 的原始表示之前添加了 0 前缀,这将是正确表示它的二进制补码所必需的。您可以尝试通过相同的示例而不这样做并验证它的必要性。

现在,让我们看一下这样表示的几个值,

╔══════╦════════════════╦════════════════════════╗
║ Bits ║ Unsigned Value ║ Two's Complement Value ║
╠══════╬════════════════╬════════════════════════╣
║ 011  ║ 3              ║ 3                      ║
╠══════╬════════════════╬════════════════════════╣
║ 010  ║ 2              ║ 2                      ║
╠══════╬════════════════╬════════════════════════╣
║ 001  ║ 1              ║ 1                      ║
╠══════╬════════════════╬════════════════════════╣
║ 000  ║ 0              ║ 0                      ║
╠══════╬════════════════╬════════════════════════╣
║ 111  ║ 7              ║ -1                     ║
╠══════╬════════════════╬════════════════════════╣
║ 110  ║ 6              ║ -2                     ║
╠══════╬════════════════╬════════════════════════╣
║ 101  ║ 5              ║ -3                     ║
╠══════╬════════════════╬════════════════════════╣
║ 100  ║ 4              ║ -4                     ║
╚══════╩════════════════╩════════════════════════╝

如您所见,它的工作原理与我们之前的预期一样,但是您现在可以开始了解您发现的“错误”是如何发生的。二进制补码中 4 位表示的上限是十进制值 3。让我们看看我们如何通过简单地添加1 来达到-43 = 0b011 因此是 3+1 = 0b100,从表中可以看出,它映射到二的补码上的 -4(而不是 4)。你的情况就是这个确切的问题,但有更多的位。像这样的有符号表示是循环的,所以在顶部溢出会产生底部值。让我们看看你的情况

127 = 0b01111111
127 + 1 = 0b10000000

您可以看到它以1 开头,因此它是负数(!),如果您解决二进制补码,您将看到它表示 -128(因为下限总是大于上限)。

并非每个硬件都以相同的方式实现事物,英特尔、AMD、ARM 以及据我所知,通用 CPU 的所有主要架构都在其 ALU 中使用二进制补码,但有些硬件使用其他实现整数签名的技术,所以基本上你描述的行为是未定义的。另一个值得注意的有趣的事情是IEEE's standard for floating point arithmetic,实现了一个基于exponent bias 的签名浮点数。

最后,由于我们在这里讨论的是 C,请注意编译器可以优化未定义的行为,this blog post 中描述了此类优化的一个很好的示例。

【讨论】:

  • 二进制补码算法的选择是芯片制造商的决定,通常不是编程语言的决定。也就是说,CPU 通常会确定算术是使用补码(目前为止最常见)还是使用补码或符号幅度算术来实现的。编程语言可以模拟这些算术模式中的任何一种,但需要付出一定的代价。
  • @JonathanLeffler 感谢您的更正,我已对结论段进行了一些修正,请告诉我您的想法。
  • 行为未定义。这个答案是错误的。不会发生溢出。看到这个正确答案:stackoverflow.com/a/41205803/4082723
  • @JonathanLeffler:执行二进制加法、减法和乘法的自然方法是使用补码格式。以任何其他格式执行这些操作需要使用二进制补码硬件并向其添加其他内容。与其他格式相比,操作员更容易理解符号幅值格式的闪烁灯显示,并且将补码数字转换为符号幅值比转换二进制补码数字更容易。如果一台机器有 16 个寄存器并附有闪烁灯,...
  • ...有一个电路来执行一个的补码数学和十六个电路来显示一个一个的补码寄存器作为符号大小可能比有十六个电路来显示一个二的 -补码寄存器作为符号幅度。然而,在闪烁光显示器之类的东西之外,符号幅度和二进制补码在几乎所有可以想象的方式上都不如二进制补码。
【解决方案2】:

在 C 中,运算符 += 的行为是通过 =+ 运算符的等效组合来定义的。例如。根据定义,您的x += 1 代表x = x + 1。由于x 具有窄类型signed char,因此在任何算术开始之前将其提升为int。这意味着子表达式x + 1int 类型的域中计算。之后,结果(int 类型)被转换回signed char 并存储回x

因此,在您的情况下,您的 x += 1 实际上相当于

x = (signed char) ((int) x + 1);

(int) x + 1 子表达式不会溢出。它成功地产生了int 类型的值128。但是,此值不适合signed char 类型的范围,当此值转换回signed char 类型时,会导致实现定义的行为。在您的平台上,这种实现定义的行为会产生 -128 类型的 signed char 值。

【讨论】:

  • 一个正确的答案是+0,而一个没有触及实际问题并且不回答问题的答案是+6,这真的很令人失望。
猜你喜欢
  • 1970-01-01
  • 2023-03-23
  • 1970-01-01
  • 1970-01-01
  • 2012-10-10
  • 1970-01-01
  • 2021-12-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多