问题 一元减号和签名到无符号转换


这在技术上是否正确:

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

有溢出的危险吗?


6936
2017-12-27 00:55


起源

还有一个负二进制补码整数而不是正整数,所以是的,它可以溢出。 - James K Polk


答案:


如果你想避免溢出,你应该先施展 n 到unsigned int然后将一元减号应用于它。

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

在原始代码中,否定在类型转换之前发生,因此如果行为是未定义的 n < -INT_MAX

当否定无符号表达式时,永远不会溢出。相反,结果将是模数 2^x,适当的价值 x


10
2017-12-27 01:31



不,不。它适用于符合ISO C90或ISO C99的任何环境,这些标准都不需要两个补码算法。诀窍是通过在无符号算术中完全计算有趣的情况来避免对负整数的任何依赖。 - Roland Illig
好吧,也许我正在慢慢理解这个...让我尝试:1)在演员表演之后,无符号值是全等模2 ** nbits到原始值2)用减号运算符执行另一个模运算 - bluescarni
好吧,现在我也得到了负部分,引用了C ++标准:“无符号数量的负数是通过从2 ** n减去其值来计算的,其中n是提升操作数中的位数”。 - bluescarni
显然是这样,至少在C ++(4.7.2)中:“如果目标类型是无符号的,则结果值是与源整数一致的最小无符号整数(模2 ** n,其中n是用于表示的位数)无符号类型)“。 - bluescarni
@ysth:是的,确实如此。 - caf


在C中没有无符号整数溢出这样的事情。它们的算术明确定义为以max + 1为模的计算,它们可以“换行”但从技术上讲这不算是溢出。因此,代码的转换部分很好,但在极端情况下,您可能会遇到令人惊讶的结果。

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

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

3
2017-12-27 08:35





今天的大多数计算机都使用两个补码数字,这意味着负数部分比正数大一个,例如从-128到127.这意味着如果你能用负数表示正数,你可以毫无顾虑地代表负数。 。


2
2017-12-27 01:00



+1好点好放 - Nick Moore
我认为他在问相反的情况;即,在某些情况下,将给定的负数转换为正数是否会溢出。 - Justin Spahr-Summers
这是不是意味着在做abs(-128)时,会尝试构建整数+128,这是不可表示的? - bluescarni
@bluescami:是的,+ 128(在这个虚构的8位int系统中)溢出到-128。 - ysth
但据我记得,有符号整数溢出是C / C ++中未定义的行为? - bluescarni


也许它可以应对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;
}

0
2017-12-27 02:10



这可以假设_MAX和_MIN最多相差1(但当然可以推广)。 - bluescarni
它们最多只有一个不同。 C只允许3种可能的有符号表示选择:二进制补码,一补码和符号/幅度(分别为1,0和0)。 - R..
@R ..感谢您的信息,我的意思是迟早要问:) - bluescarni
@bruce:你的类型/限制不匹配。更改 LONG_MIN 至 INT_MIN 和 LONG_MAX 至 INT_MAX。您可能还应该更正要使用的第一个案例 -(unsigned)INT_MIN 代替 INT_MAX+1UL所以它适用于任何表示。 - R..
@R ..谢谢。但是我想知道'INT_MAX + 1'和'-INT_MAX'之间的区别,不是前者有效吗? - bruce


这应该避免未定义的行为,并使用signed 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

0
2017-09-20 10:02





是的,它会自行溢出。

#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;
}

打印“溢出”

然而,这仅仅是标准所不需要的典型行为。如果您希望安全播放,请参阅接受的答案。


-1
2017-12-27 01:12



这是由标准定义的吗? - Justin Spahr-Summers
或者更确切地说,它溢出为零。零恰好恰好具有既不消极也不积极的优良特性。所以试图找到零的负值当然会导致你直接回零。 - slebetman
如果它溢出,则行为未定义。 - Roland Illig
我手边没有引用,但我知道C不需要两个补码,我认为C ++在这方面遵循C.当我再次回到家时,我可以引用ISO C99。 - Roland Illig
C99§6.5/ 5:“如果是 特殊情况 在评估表达式期间(即,如果结果未在数学上定义或不在其类型的可表示值范围内),则行为未定义。 - Adam Rosenfield


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

在C89中,其中n是int:

(unsigned)n

没有为所有n定义良好:对signed或unsigned int的转换没有限制,除非非负signed int的表示与相同值的unsigned int的表示相同,前提是该值是可表示的。

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

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

粗略地说,值v的表示是具有sizeof(v)元素的unsigned char数组。 unsigned char具有两个元素的幂,并且要求足够大以确保它忠实地编码任何别名数据结构。无符号字符中的位数被明确定义为可表示的值数的二进制日志。

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

不幸的是,委员会想要询问代表中是否存在任何“漏洞”。例如,你在x86机器上有31位整数吗?我不幸地说,因为这是一个形成错误的问题,答案同样不合适。

提出这个问题的正确方法是询问表示是否已满。不是这样 可能 谈论有符号整数的“表示位”,因为规范不是从表示到值,而是另一种方式。这可能会使许多错误认为表示是从底层位到某个值的映射的程序员感到困惑:表示是从值到位的映射。

如果表示是一个表示,则表示已满,即它表示在表示空间的整个范围内。如果表示已满,则没有“空洞”,即未使用的位。然而,并非全部。对8位数组的255个值的表示不能满,但没有未使用的位。没有洞。

问题是:考虑一个unsigned int,然后有两个不同的按位表示。存在由规范编码确定的明确定义的对数基数2比特的数组,然后存在由无符号字符数组的别名给出的物理表示的比特数组。即使这种表示已满,也有 没有通信 两种比特之间。

我们都知道逻辑表示的“高阶位”可以位于某些机器上的物理表示的一端,而另一端则位于其他机器上:它称为endian-ness。但实际上没有理由根本不能以任何顺序置换比特,事实上根本就没有理由这些比特排成一行!只需考虑添加1模数最大值加1作为表示来看到这一点。

所以现在的问题是对于有符号的整数 没有 规范的逻辑表示,而不是有几个常见的:例如,两个补码。但是如上所述 无关 物理表征。 C委员会无法理解价值观与物理表征之间的对应关系 不能通过谈论比特来指定。它 必须完全通过谈论函数的属性来指定

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

因此,目前尚不清楚

(unsigned)n

实际上会产生负值的预期结果。


-2
2017-12-27 06:35



指定整数表示可能是一个错误,但你错了:从有符号到无符号的转换是根据值定义的(“重复加或减一个可以在新的值中表示的最大值输入“),因而定义明确 - Christoph
你的咆哮可能有价值,但结论是错误的。标准绝对指定转换为无符号的结果作为减少模1加上目标类型中的最大可能值。 - R..
好的,点了! - Yttrill