一元减号和有符号到无符号的转换

Posted

技术标签:

【中文标题】一元减号和有符号到无符号的转换【英文标题】:Unary minus and signed-to-unsigned conversion 【发布时间】:2011-05-30 23:34:52 【问题描述】:

这在技术上是否总是正确的:

unsigned abs(int n)

    if (n >= 0) 
        return n;
     else 
        return -n;
    

在我看来,如果 -INT_MIN > INT_MAX,“-n”表达式可能会在 n == INT_MIN 时溢出,因为 -INT_MIN 超出范围。但是在我的编译器上这似乎工作正常......这是一个实现细节还是可以依赖的行为?

加长版

一点上下文:我正在为 GMP 整数类型 (mpz_t) 编写 C++ 包装器,并从现有的 GMP C++ 包装器(称为 mpz_class)中汲取灵感。当处理带符号整数的 mpz_t 加法时,代码如下:

static void eval(mpz_ptr z, signed long int l, mpz_srcptr w)

  if (l >= 0)
    mpz_add_ui(z, w, l);
  else
    mpz_sub_ui(z, w, -l);

换句话说,如果有符号整数是正数,则使用无符号加法程序将其相加,如果有符号整数为负数,则使用无符号减法程序将其相加。两个 *_ui 例程都将 unsigned long 作为最后一个参数。是表达式

-l

有溢出的风险?

【问题讨论】:

负的补码整数比正的多一个,所以是的,它可以溢出。 【参考方案1】:

当今大多数计算机都使用二补数刻度,这意味着负数比正数大一,例如从 -128 到 127。这意味着如果您可以表示正数,则负数可以表示负数数无忧。

【讨论】:

我想他问的是相反的情况;即,在某些情况下,将给定的负数转换为正数是否会溢出。 这不是说在做abs(-128)的时候会尝试构建整数+128,这是不可表示的吗? @bluescami:是的,+128(在这个虚构的 8 位 int 系统中)溢出到 -128。 但据我所知,有符号整数溢出是 C/C++ 中未定义的行为? @Justin:啊,那么应该更仔细地阅读这个问题。无论如何,我希望他能从答案中学到一些东西。【参考方案2】:

是的,它会溢出到自己身上。

#include <stdio.h>
#include <limits.h>
int main(int argc, char**argv) 
    int foo = INT_MIN;
    if (-foo == INT_MIN) printf("overflow\n");
    return 0;

打印“溢出”

但是,这只是典型的行为,标准没有要求。如果您希望安全起见,请参阅接受的答案以了解如何操作。

【讨论】:

这是标准定义的吗? 或者更确切地说,它溢出到零。而零恰好具有它既不是负数也不是正数的好特性。因此,试图找到零的负值当然会让你直接回到零。 如果溢出,则行为未定义。 我手头没有引用,但我知道 C 不需要补码,我认为 C++ 在这方面遵循 C。当我再次在家时,我可以引用 ISO C99。 C99 §6.5/5:“如果在计算表达式期间发生异常条件(即,如果结果未在数学上定义或不在其类型的可表示值),行为未定义。”【参考方案3】:

如果您想避免溢出,您应该首先将n 转换为无符号整数,然后对其应用一元减号。

unsigned abs(int n) 
  if (n >= 0)
    return n;
  return -((unsigned)n);

在您的原始代码中,否定发生在类型转换之前,因此如果n &lt; -INT_MAX,则行为未定义。

当对无符号表达式求反时,永远不会溢出。相反,对于 x 的适当值,结果将取模 2^x

【讨论】:

我不确定我是否完全理解这一点...这种行为是否依赖于二进制补码? 不,它没有。它适用于符合 ISO C90 或 ISO C99 的任何环境,并且这些标准都不需要二进制补码算法。诀窍是通过完全在无符号算术中计算有趣的情况来避免对负整数的任何依赖。 好吧,也许我正在慢慢理解这一点......让我试试:1)在强制转换之后,无符号值与原始值模 2**nbits 2) 与减号运算符一致执行另一个模运算 好的,现在我也得到了减号部分,引用了 C++ 标准:“无符号量的负数是通过从 2**n 中减去它的值来计算的,其中 n 是提升的操作数”。 显然如此,至少在 C++ (4.7.2) 中:“如果目标类型是无符号的,则结果值是与源整数一致的最小无符号整数(模 2**n 其中 n是用来表示无符号类型的位数)"。【参考方案4】:

也许它可以处理 2 的补数的对称范围:

#include <limits.h>

unsigned int abs(int n)

  unsigned int m;

  if(n == INT_MIN)
    m = INT_MAX + 1UL;
  else if(n < 0)
    m = -n;
  else 
    m = n;

  return m;

【讨论】:

假设 _MAX 和 _MIN 最多相差 1(但当然可以概括),这将起作用。 它们最多相差一个。 C 仅允许 3 种可能的符号表示选择:二进制补码、二进制补码和符号/大小(分别为 1、0 和 0 的差)。 @R.. 谢谢你的信息,我是迟早要问这个的:) @bruce:您的类型/限制不匹配。将LONG_MIN 更改为INT_MIN,将LONG_MAX 更改为INT_MAX。您可能还应该更正第一种情况以使用-(unsigned)INT_MIN 而不是INT_MAX+1UL,以便它适用于任何表示。 @R.. 谢谢。但我想知道 'INT_MAX + 1' 和 '-INT_MAX' 之间的区别,前者不起作用吗?【参考方案5】:

非常好的问题,它揭示了 C89、C99 和 C++ 之间的差异。这是对这些标准的一些评论。

在 C89 中,n 是一个 int:

(unsigned)n

对于所有 n 都没有很好的定义:对有符号或无符号 int 的转换没有限制,除非非负有符号 int 的表示与相同值的无符号 int 的表示相同,前提是值是有代表性的。

这被认为是一个缺陷,在 C99 中,不幸的是尝试将编码限制为二进制补码、一个补码或具有相同位数的有符号幅度是错误的。不幸的是,C 委员会没有太多的数学知识,并且完全搞砸了规范:一方面它由于循环定义而格式错误,因此不规范,另一方面,如果你原谅这个错误,它是一个严重的过度约束,例如,它排除了 BCD 表示(在旧的 IBM 大型机上用 C 语言使用),并且还允许程序员通过摆弄表示的位来破解整数的值(这是非常糟糕的)。

C++ 在提供更好的规范方面遇到了一些麻烦,但是它也遇到了同样的循环定义错误。

粗略地说,值 v 的表示形式是一个带有 sizeof(v) 个元素的 unsigned char 数组。 unsigned char 具有两个元素的幂,并且需要足够大以确保它忠实地编码任何别名数据结构。 unsigned char 中的位数很好地定义为可表示值数量的二进制 log。

通过规范位置编码方案,如果任何无符号值具有从 0 到 2^n-1 的两个值的幂,则它的位数同样可以很好地定义。

不幸的是,委员会想询问代表是否存在任何“漏洞”。例如,您可以在 x86 机器上使用 31 位整数吗?我说很遗憾,因为这是一个格式错误的问题,而且答案同样不正确。

提出这个问题的正确方法是询问表示是否已满。 不可能谈论有符号整数的“表示的位”,因为规范没有从表示到值,而是相反。这可能会让很多程序员感到困惑,他们错误地认为表示是从底层位到某个值的映射:表示是从值到位的映射。

一个表示是满的,如果它是一个满射,也就是说,它在表示空间的整个范围上。如果表示已满,则没有“漏洞”,即未使用的位。然而,这还不是全部。 8 位数组的 255 个值的表示不能满,但没有未使用的位。没有洞。

问题是这样的:考虑一个无符号整数,那么有两种不同的按位表示。有从规范编码确定的定义良好的 log base 2 位数组,然后是由 unsigned char 数组的别名给出的物理表示的位数组。即使这个表示是完整的,这两种位之间也没有没有对应关系

我们都知道,逻辑表示的“高位”可以在某些机器上位于物理表示的一端,而在其他机器上则位于另一端:这称为字节序。但事实上,没有理由不能按任何顺序排列这些位,事实上,根本没有理由让这些位对齐!只需考虑将最大值加 1 模加 1 作为表示即可看到这一点。

所以现在的问题是,对于有符号整数,没有没有规范的逻辑表示,而是有几种常见的表示:例如二进制补码。然而,如上所述,这与物理表示无关。 C 委员会只是无法理解值和物理表示之间的对应关系不能通过谈论位来指定必须完全通过谈论函数的属性来指定

因为没有这样做,所以 C99 标准包含非规范性的胡言乱语,因此所有有符号和无符号整数转换行为的规则也是非规范性的胡言乱语。

所以不清楚

(unsigned)n

实际上会为负值产生所需的结果。

【讨论】:

在完成时指定整数表示可能是一个错误,但你在这里错了:从有符号到无符号的转换是根据值定义的(“重复地加或减一大于最大值可以在新类型中表示的值”),因此定义明确 你的咆哮可能有道理,但结论是错误的。该标准绝对将转换为无符号的结果指定为归约模一加上目标类型中的最大可能值。【参考方案6】:

C 中不存在无符号整数溢出之类的东西。它们的算术被明确定义为以它们的 max+1 为模的计算,它们可以“换行”,但从技术上讲,这不被视为溢出。所以你代码的转换部分没问题,尽管在极端情况下你可能会遇到令人惊讶的结果。

您的代码中唯一可能溢出的地方是签名类型的-。对于有符号类型,只有一个值可能没有正对应,即最小值。事实上,你必须做一个特殊的检查,例如int

if (INT_MIN < -INT_MAX && n == INT_MIN ) /*do something special*/

【讨论】:

【参考方案7】:

这应该避免未定义的行为,并适用于有符号 int 的所有表示(2 的补码、1 的补码、符号和幅度):

unsigned myabs(int v)

  return (v >= 0) ? (unsigned)v : (unsigned)-(v+1)+1;

现代编译器能够删除多余的-1+1 并识别用于计算有符号整数的绝对值的习语。

这是 gcc 产生的:

_myabs:
    movl    4(%esp), %eax
    cltd
    xorl    %edx, %eax
    subl    %edx, %eax
    ret

【讨论】:

以上是关于一元减号和有符号到无符号的转换的主要内容,如果未能解决你的问题,请参考以下文章

C中的有符号到无符号转换 - 它总是安全的吗?

byte[] 到无符号 BigInteger?

无符号整型和有符号整形转换

C ++十六进制字符串到无符号整数[重复]

为啥减去无符号和有符号后符号不同?

C++11 标准是不是保证零值有符号整数的一元减号为零?