C语言中有符号变量和无符号变量的计算方式是怎样的?

19

我在C规范中读到,无符号变量(特别是unsigned short int)在整数溢出时会执行所谓的回绕(wrap around)操作,但除了我发现有未定义行为(undefined behavior)之外,我找不到任何关于有符号变量的内容。

我的教授告诉我,它们的值也会被回绕(也许他只是指gcc)。我原以为位只是被截断了,留下的位给了我一些奇怪的值!

什么是回绕操作,它与截断位有何不同。


4
除非你的教授在他关于值环绕的陈述中特别说明了具体的C语言实现方式,有可能包括有关优化或语义方面的特定标志,否则他是错误的。 - Eric Postpischil
@EricPostpischil,您所说的特定C实现是什么意思?它通常是怎样的? - orustammanapov
6
C标准没有定义整数溢出时会发生什么。但是,C实现可以定义会发生什么事情。 C实现可以超越C标准。例如,GCC可以发出一条语句,如“当整数溢出发生且编译时使用-fSomething开关时,结果将被包装成二进制补码。”或者,如果GCC没有说明这一点,那么Fred Doe可以查看GCC源代码,根据需要进行修改,并发布一个以特定方式运行的新版本GCC - Eric Postpischil
4个回答

29
在C语言中,有符号整数变量不具备环绕行为。在算术计算期间发生有符号整数溢出会产生未定义的行为。顺便提一下,你提到的GCC编译器以实现严格的溢出语义优化而闻名,这意味着它利用了这种未定义行为情况所提供的自由:GCC编译器假定有符号整数值永远不会环绕。这意味着实际上GCC是其中一个编译器,在其中你不能依靠有符号整数类型的环绕行为。
例如,GCC编译器可以假设对于变量int i,以下条件成立:
if (i > 0 && i + 1 > 0)

等同于仅仅

if (i > 0)

这正是“严格的溢出语义”所指的内容。

无符号整数类型实现模运算。模数等于类型的值表示中包含的位数的2^N次方,其中N是位数。因此,无符号整数类型在溢出时确实似乎会“环绕”。

然而,C语言从不使用小于int/unsigned int的域进行算术计算。在任何计算开始之前,你在问题中提到的unsigned short int类型通常会被提升为int类型(假设unsigned short的范围适合int的范围)。这意味着1)使用unsigned short int进行的计算将在int的域中执行,在int溢出时发生溢出,2)这种计算期间的溢出将导致未定义行为,而不是环绕行为。

例如,以下代码会产生一个环绕:

unsigned i = USHRT_MAX;
i *= INT_MAX; /* <- unsigned arithmetic, overflows, wraps around */

当这段代码执行时

unsigned short i = USHRT_MAX;
i *= INT_MAX; /* <- signed arithmetic, overflows, produces undefined behavior */

导致未定义行为。

如果没有 int 溢出发生,并且结果被转换回 unsigned short int 类型,那么它将再次通过模运算 2^N 进行缩减,这会使得值看起来像是被包裹了一样。


“unsigned i” 代表什么?你已经解释得非常好了,能否在这个例子中演示一下:“short int y = 511, z = 512; y*=z;” - orustammanapov
6
unsigned i 表示 unsigned int i,就像 long i 表示 long int i 一样。在这种情况下,默认为 int。至于您的例子,y*=z 被解释为 y = (short) ((int) y * (int) z),这可以简化为 y = (short) 261632。请注意,在此情况下,在算术运算期间没有溢出(它们完全适合 int),但在转换回 short 时会发生溢出。 在这种情况下的行为取决于实现 - AnT stands with Russia
1
我想知道将整数溢出无限制视为未定义行为,而不是说编译器可以在涉及整数类型的计算中包含额外的精度,可以任意重写任何或所有超出精度位,只要它们与符号位不一致,并且可以随意将这种额外精度存储在非易失性整数变量中,是否有多少现实优势?这样的规则允许编译器将i+1 > i视为无条件真,但不允许编译器否定因果关系。超越这一点带来了多少优势? - supercat

12

假设你有一个只有3位的数据类型。这使你能够表示8个不同的值,从0到7。如果你将7加1,则会“回绕”回到0,因为你没有足够的位来表示值8(1000)。

对于无符号类型,这种行为是明确定义的。但是对于有符号类型,这种行为是未定义的,因为有多种表示有符号值的方法,并且溢出的结果将根据该方法进行不同的解释。

表示-幅度法:最高位表示符号;0表示正数,1表示负数。如果我的类型再次为三位,则我可以表示如下的有符号值:

000  =  0
001  =  1
010  =  2
011  =  3
100  = -0
101  = -1
110  = -2
111  = -3

由于一位用于表示符号,因此我只有两位来编码0到3的值。如果我将3加1,则会溢出并得到-0作为结果。是的,0有两种表示方式,一种是正数,一种是负数。你不太可能经常遇到符号数表示法。

反码:负值是正值的按位取反。同样,使用三位类型:

000  =  0
001  =  1
010  =  2
011  =  3
100  = -3
101  = -2
110  = -1 
111  = -0

我有三个比特位来编码我的值,但范围是[-3, 3]。如果我将3加1,则会溢出并得到结果-3。这与上面的带符号大小结果不同。同样,使用此方法,存在两种编码方式可表示0。

二进制补码:负数的二进制补码是正数按位取反再加1。在三位系统中:

000  =  0
001  =  1
010  =  2
011  =  3
100  = -4
101  = -3
110  = -2
111  = -1

如果我将3加1,结果会溢出为-4,这与先前的两种方法不同。请注意,我们的值范围略大[-4, 3],而且0只有一种表示方法。

补码可能是最常用的表示有符号数的方法,但它并不是唯一的方法,因此C标准不能保证当您溢出有符号整数类型时会发生什么。因此,它将行为定义为未定义,以便编译器不必处理多个表示的解释。


7
不同的符号表示法是让有符号算术溢出未定义的最初原因。现在,编译器已经开始利用未定义行为进行优化,因此即使您知道目标是二进制补码架构,也不能确定有符号溢出会产生二进制补码结果。请参见例如http://www.airs.com/blog/archives/120。 - Pascal Cuoq

5

未定义行为源于早期的可移植性问题,因为有符号整数类型可以表示为符号和大小、反码或二进制补码。

现在,所有架构都将整数表示为二进制补码,会出现回绕现象。但要小心:由于编译器假定您不会运行未定义行为,所以在优化开启时可能会遇到奇怪的错误。


这是否意味着即使使用有符号整数,我也会得到一个环绕? - orustammanapov
3
不行。即使底层硬件具有处理溢出或发生何种包装的固有行为,C实现也不必使用它。C标准规定,有符号整数溢出时的行为是未定义的。这意味着C实现可能会对代码进行优化,而不考虑在溢出情况下会发生什么。这可能会导致行为像值已包装一样发生,或者可能会导致行为像发生了陷阱一样发生,或者它可以产生与包装产生的不同值。 - Eric Postpischil

3
在有符号的8位整数中,环绕的直觉定义可能看起来像是从+127到-128--用二进制补码表示:0111111(127)和1000000(-128)。正如您所看到的,这是增加二进制数据的自然进展--不考虑它表示有符号或无符号的整数。反直觉地,实际上溢出发生在从-1(11111111)到0(00000000)的无符号整数的环绕意义下移动时。
这并没有回答有符号整数溢出时正确行为的更深层次的问题,因为根据标准,没有“正确”的行为。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接