为什么 'd /= d' 在 d == 0 时不抛出被零除异常?

Why doesn't 'd /= d' throw a division by zero exception when d == 0?

我不太明白为什么我没有得到被零除的异常:

int d = 0;
d /= d;

我希望得到除以零的异常,但 d == 1

为什么 d /= dd == 0 时不抛出被零除异常?

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

(浮点数除以零也未定义,但 IEEE754 定义了它。)

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

C++ 没有要捕获的 "Division by Zero" 异常。您观察到的行为是编译器优化的结果:

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

然而...

我们可以强制编译器触发 "real" 除以零,只需稍微调整您的代码。

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

所以现在问题仍然存在:现在我们基本上已经强制编译器允许这种情况发生,会发生什么?这是未定义的行为——但我们现在已经阻止编译器围绕这个未定义的行为进行优化。

主要取决于目标环境。这不会触发软件异常,但它可以(取决于目标CPU)触发硬件异常(整数除以零),这不能以传统方式捕获软件异常可以被捕获。对于 x86 CPU 和大多数其他(但不是全部!)架构来说,情况确实如此。

但是,有一些方法可以处理硬件异常(如果发生)而不是让程序崩溃:查看此 post 了解一些可能适用的方法:Catching exception: divide by zero.请注意,它们因编译器而异。

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

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

请注意,在这种情况下(以及其他情况下),您可以通过使用 boost 安全数字让您的代码生成 C++ 异常。 https://github.com/boostorg/safe_numerics

了解发生了什么的最简单方法是查看程序集输出

int divide(int num) {
    return num/num;
}

将为 x86-64 生成

divide(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, 1
        pop     rbp
        ret

如您所见,这里没有除法运算, 但是我们有 mov eax, 1.

这里有一个link要重现:https://godbolt.org/z/MbY6Wqh4T