为什么当d == 0时'd / = d'不抛出除以零的异常?


81

我不太明白为什么我没有被零例外除以:

int d = 0;
d /= d;

我本来希望得到除以零的除法运算,但是反而d == 1

为什么在什么时候不d /= d将被零除d == 0


25
那是未定义的行为。
LF

51
没有除以零的例外之类的东西。
–πάνταῥεῖ

15
为了澄清一些意见:当您看到有关“零除数异常”的消息时,操作系统将告诉您出了点问题。这不是C ++异常。在C ++中,throw语句引发异常。没有别的(除非您处在行为不确定的领域)。
皮特·贝克尔,

9
在C ++中,没有“除以零例外”之类的东西。
AlgirdasPreidžius

6
@ user11659763 “这就是为什么它是未定义的行为:它完全取决于目标。” -这不是什么不确定的行为方式在所有; 您要描述的是实现定义的行为。未定义的行为是一个非常强大的陈述。
marcelm

Answers:


108

C ++没有要捕获的“零除”异常。您观察到的行为是编译器优化的结果:

  1. 编译器假定未发生未定义行为
  2. C ++中的零除是未定义的行为
  3. 因此,假定导致被零除的代码不会这样做。
    • 并且,假定必须导致零除的代码永远不会发生
  4. 因此,编译器推断出由于未发生未定义行为,因此该代码(d == 0)中未定义行为的条件一定不会发生
  5. 因此,d / d必须始终等于1。

然而...

我们可以强制编译器以零次调整代码来触发除以零的“实际”除法。

volatile int d = 0;
d /= d; //What happens?

所以现在问题仍然存在:既然我们基本上已经迫使编译器允许这种情况发生,那会发生什么?这是未定义的行为-但是我们现在阻止了编译器针对这种未定义的行为进行优化。

通常,它取决于目标环境。这不会触发软件异常,但是可以(取决于目标CPU)触发硬件异常(零整数分割),无法以传统方式捕获软件异常。对于x86 CPU和大多数其他(但不是全部!)架构,绝对是这种情况。

但是,有一些处理硬件异常(如果发生)的方法,而不仅仅是让程序崩溃:在这篇文章中寻找一些可能适用的方法:捕获异常:除以零。请注意,它们因编译器而异。


25
@Adrian这两个都很好,因为行为是不确定的。从字面上看,一切正常。
Jesper Juhl,

9
“在C ++中零除是未定义的行为”->请注意,编译器无法针对IEE754下的浮点类型进行此优化。必须将d设置为NaN。
Bathsheba,

6
@RichardHodges不允许在IEEE754下运行的编译器进行双重优化:必须生成NaN。
Bathsheba

2
@formerlyknownas:这不是“优化UB”的问题-“任何事情都有可能发生”的情况仍然存在;只是那个生产1是完全有效的任何东西。获得14684554的原因必须是编译器进一步优化-它传播初始d==0条件,因此不仅可以得出“这是1或UB”的结论,而且实际上可以得出“这是UB,周期”的结论。因此,生成加载该常量的代码甚至不费吹灰之力1
hmakholm在Monica

1
人们总是建议使用volatile来防止优化,但是构成volatile读写的是实现定义的。
philipxy

38

只是为了补充其他答案,以零除是未定义的行为这一事实意味着编译器在发生这种情况的情况下可以自由执行任何操作

  • 编译器可以假定0 / 0 == 1并相应地进行优化。这实际上是这里所做的。
  • 如果愿意,编译器还可以假设0 / 0 == 42并设置d为该值。
  • 编译器还可以确定的值d不确定,从而使变量未初始化,以便其值可以是先前写入分配给它的内存中的任何值。注释中在其他编译器上观察到的一些意外值可能是由那些编译器执行此类操作引起的。
  • 每当发生被零除的情况时,编译器还可以决定终止程序或引发异常。因为对于该程序,编译器可以确定将始终发生这种情况,所以它可以简单地发出代码以引发异常(或完全中止执行)​​,并将其余函数视为不可访问的代码。
  • 编译器也可以选择停止程序并开始玩单人纸牌游戏,而不是在被零除时引发异常。这也属于“不确定行为”的范畴。
  • 原则上,每当被零除时,编译器甚至可能发出导致计算机爆炸的代码。在C ++标准中没有什么可以禁止这种情况。(对于某些应用,例如导弹飞行控制器,甚至可以认为这是理想的安全功能!)
  • 此外,该标准明确允许未定义的行为“时间旅行”,因此编译器还可以执行上述任何操作(或其他操作)被零除之前发生。基本上,只要不改变程序的可观察行为,该标准就允许编译器自由地对操作进行重新排序-但是,如果执行程序会导致未定义的行为,则即使最终要求也被明确放弃。因此,实际上,在某个时刻会触发未定义行为的任​​何程序执行的整个行为都是未定义的!
  • 由于上述原因,编译器还可以简单地假设未发生未定义的行为,因为在某些输入上以未定义的方式运行的程序的一种允许行为是,其行为就像输入是某种东西一样。否则。也就是说,即使d在编译时不知道的原始值,编译器仍可以假定它永远不会为零,并相应地优化代码。在OP代码的特定情况下,仅假设,这实际上与编译器是没有区别的0 / 0 == 1,但是例如,编译器还可以假设puts()inif (d == 0) puts("About to divide by zero!"); d /= d;永远不会执行!

29

C ++标准未定义零除整数的行为。这是需要抛出异常。

(浮点除以零也未定义,但IEEE754对其进行了定义。)

您的编译器正在d /= d有效地d = 1进行优化,这是一个合理的选择。可以进行此优化,因为可以假设代码中没有未定义的行为-d不可能为零。


3
重要的是要特别清楚,否则还会发生其他事情,哎呀,这种行为不能依靠。
海德

2
当你说这是合理的编译器假设“即d不可能是零,”你还以为编译器不看行:int d = 0;?? :)
阿德里安·摩尔

6
编译器看到了,但可能不在乎。对于像这样的极端情况,已经疯狂的复杂编译器所需的额外代码复杂性可能不值得。
user4581301

1
@ user4581301两者结合使用可以检测到中毒的分支,从而可以修剪更多的代码。因此,它将很有用。
Deduplicator

3
因此,如果您编写了“ int d = 0; if(d == 0)printf(“ d = 0 \ n”); d / = d;“,编译器也可以删除printf。
gnasher729

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.