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

这在技术上是否正确:

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 

有溢出的危险吗?

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

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

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

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

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

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

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

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

也许它可以应对2的补码数的对称范围:

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

这应该避免未定义的行为,并使用signed int的所有表示forms(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 

是的,它会自行溢出。

 #include  #include  int main(int argc, char**argv) { int foo = INT_MIN; if (-foo == INT_MIN) printf("overflow\n"); return 0; } 

打印“溢出”

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

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

在C89中,其中n是int:

 (unsigned)n 

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

这被认为是一个缺陷,并且在C99中, 遗憾的是,尝试将编码限制为具有相同位数的二进制补码,一个补码或带符号幅度。 不幸的是,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 

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