C/C++ 无符号整数溢出

30
我正在阅读一篇关于整数安全的文章。以下是链接:http://ptgmedia.pearsoncmg.com/images/0321335724/samplechapter/seacord_ch05.pdf 在第166页中,有这样一句话:
一个涉及无符号操作数的计算永远不会溢出,因为结果如果不能被表示为无符号整数类型的结果将被模化为比结果类型所能表示的最大值还要大1的数字。
这是什么意思?感谢回复。

12
那很误导人。它会溢出。但是它以一种明确定义的方式进行,即通过他们所解释的包装方式。 - harold
3
@harold:这只是一个语义问题。例如,C++标准的作者说它不会溢出,因为模算术可以使结果保持在范围内;他们只使用该术语来描述有符号溢出,这是一种错误,会导致未定义的行为。 - Mike Seymour
1
@harold 这是来自于n1570标准§6.2.5/9。 - Suraj Jain
4个回答

45

1
我知道有符号溢出是未定义行为,但它不也会绕回来吗? - David G
10
在大多数硬件上是这样的,尽管编译器可能会进行一些不会这样做的优化。 - Pubby
2
就像老式车里程表一样。如果只能读到999,999英里,那么再多走一英里就会归零。 - David Schwartz
C++标准中定义在哪里? - Thomas Weller
@Pubby,我认为需要指出的是,在最近几年中,编译器在优化方面变得更加积极,假设有符号整数溢出不会发生。 十年或二十年前,您可以关闭优化并忽略未定义的行为,因为它可以在大多数机器上运行。 如今,至少一些编译器将积极破坏您的代码,如果您忽略了有符号整数溢出是未定义的事实。但是,gcc选项-fwrapv确实使有符号溢出定义,如果您想要它。 它是非标准的,因此我通常不建议使用。 - cds

7

没有溢出?

这里的“溢出”指的是“产生一个不适合操作数的值”。因为应用了算术模运算,值始终适合操作数,因此不会发生溢出。

换句话说,在实际发生溢出之前,C++已经对该值进行了截断。

模运算?

将一个值对另一个值取模意味着进行除法并获取余数。

例如:

0 % 3 = 0  (0 / 3 = 0, remainder 0)
1 % 3 = 1  (1 / 3 = 0, remainder 1) 
2 % 3 = 2  (2 / 3 = 0, remainder 2)
3 % 3 = 0  (3 / 3 = 1, remainder 0)
4 % 3 = 1  (4 / 3 = 1, remainder 1)
5 % 3 = 2  (5 / 3 = 1, remainder 2)
6 % 3 = 0  (6 / 3 = 2, remainder 0)
...

这个模数是应用于无符号计算结果的,除数是该类型可以容纳的最大值。例如,如果最大值为2^16=32768,则32760 + 9 = (32760 + 9) % (32768+1) = 0


5
这意味着您无法改变无符号计算的符号,但它仍然可能产生意外结果。假设我们有一个8位无符号值:
 uint8_t a = 42;

然后我们再加上240:

 a += 240;

你无法放入,因此你得到了26。

C和C++中明确定义了无符号数学,而有符号数学在技术上要么是未定义的,要么是实现相关的,或者其他一些“你不会预料到可能发生的事情”的措辞(我不知道确切的措辞,但结论是“你不应该依赖于有符号整数值溢出的行为”)。


3
“implementation defined behaviour”指编译器必须记录行为,而“undefined behaviour”则表示编译器可以任意处理。需要注意的是,在C++中,“clearly defined”通常用“well defined”来表达。 - Sebastian Mach
我认为至少还有第三种情况,类似于“实现细节”,但我的观点是我不知道有符号整数运算最终属于哪个级别的“不确定发生什么” - 它只允许“奇怪的结果”还是“任何事情都可能发生”(例如,如果处理器在整数溢出时触发溢出陷阱会发生什么?) - Mats Petersson
“implementation detail”在C++中的说法是“implementation defined”。没有查阅标准,但我认为有符号值溢出会导致未定义行为。 - Sebastian Mach
我曾经认为有微妙的区别:“实现定义”意味着编译器生产者必须告诉你他们做了什么。“实现细节”意味着“由编译器生产者决定,无需解释”。无论哪种方式,“不要溢出有符号整数”都是结论。 - Mats Petersson
“无需解释”与“未定义行为”是相同的。尽管编译器可以解释标准声明为未定义的内容,但它仍然可以解释其操作方式 :) 并且我同意你的结论。 - Sebastian Mach
3
@phresnel,我明白了,虽然这是很久以前的事情,但“无需解释”是“未指定行为”,与“未定义”的行为不同,它会产生合理的结果,这些结果可能仍然会有所不同。 - sukhmel

0

再举一个例子来展示无符号数据类型会环绕而不是溢出:

unsigned int i = std::numeric_limits<unsigned int>::max(); // (say) 4294967295

-ve数赋值给unsigned不建议使用,但为了说明目的,下面我将使用它。

unsigned int j = -1; // 4294967295 wraps around(uses modulo operation)
unsigned int j = -2; // 4294967294

通过对于 max+1 取模(其中 max = 2^n),来可视化无符号的 (0 to max) 范围:

Range         :         0,     1,        2,.......,     max-2,   max-1,       max
.................................................................................
Last-to-First :  -(max+1),  -max, -(max-1),.......,        -3,      -2,        -1

First-to-Last :     max+1, max+2,    max+3,......., max+max-1, max+max, max+max+1

模数加法规则: (A + B) % C = (A % C + B % C) % C

[max + max + 1] % (max + 1) = [(max) + (max + 1)] % (max + 1)
                            = [(max % (max + 1)) + ((max + 1) % (max + 1))] % (max + 1)
                            = [(max % (max + 1)) + 0] % (max + 1)
                            = [max] % (max + 1) 
                            = max

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