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

时间:2010-12-27 00:55:44

标签: c++ c

这在技术上是否正确:

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

在我看来,如果-INT_MIN> INT_MAX,当n == INT_MIN时,“-n”表达式可能会溢出,因为-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

有溢出的风险吗?

7 个答案:

答案 0 :(得分:10)

如果你想避免溢出,你应该首先将n转换为unsigned int,然后将一元减号应用于它。

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

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

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

答案 1 :(得分:3)

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

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

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

答案 2 :(得分:2)

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

答案 3 :(得分:0)

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

答案 4 :(得分:0)

这应该避免未定义的行为,并使用signed int的所有表示形式(2的补码,1&#39的补码,符号和幅度):

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

答案 5 :(得分:-1)

是的,它会自行溢出。

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

打印“溢出”

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

答案 6 :(得分:-1)

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

在C89中,其中n是int:

(unsigned)n

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

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

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

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

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

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

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

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

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

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

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

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

因此不清楚

(unsigned)n

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