捕获异常:除以零

Posted

技术标签:

【中文标题】捕获异常:除以零【英文标题】:Catching exception: divide by zero 【发布时间】:2011-05-25 08:32:51 【问题描述】:

当我尝试除以 0 时,以下代码未捕获异常。我需要抛出异常,还是计算机在运行时自动抛出异常?

int i = 0;

cin >> i;  // what if someone enters zero?

try 
    i = 5/i;

catch (std::logic_error e) 

    cerr << e.what();

【问题讨论】:

为什么不直接检查i 是否为零? 你为什么要除以 ze— OH SHI— 不,不是,我编辑了我的问题。 这个问题和所有随后的答案都是一个很好的研究,可以很好地研究所有异常情况。 【参考方案1】:

您需要自己检查并抛出异常。整数除以零在标准 C++ 中也不例外。

浮点除以零也不是,但至少有特定的处理方法。

ISO 标准中列出的例外情况是:

namespace std 
    class logic_error;
        class domain_error;
        class invalid_argument;
        class length_error;
        class out_of_range;
    class runtime_error;
        class range_error;
        class overflow_error;
        class underflow_error;

您可以非常有说服力地争论overflow_error(IEEE754 浮点生成的无穷大可能被认为是溢出)或domain_error(它输入值的问题)将是非常适合指示除以零。

但是,5.6 部分(C++11,虽然我认为这与之前的迭代没有改变)明确指出:

如果/% 的第二个操作数为零,则行为未定义。

所以,它可能抛出那些(或任何其他)异常。它还可以格式化您的硬盘并嘲笑:-)


如果你想实现这样的野兽,你可以在以下程序中使用类似intDivEx 的东西(使用溢出变体):

#include <iostream>
#include <stdexcept>

// Integer division/remainder, catching divide by zero.

inline int intDivEx (int numerator, int denominator) 
    if (denominator == 0)
        throw std::overflow_error("Divide by zero exception");
    return numerator / denominator;


inline int intModEx (int numerator, int denominator) 
    if (denominator == 0)
        throw std::overflow_error("Divide by zero exception");
    return numerator % denominator;


int main (void) 
    int i = 42;

    try  i = intDivEx (10, 0); 
    catch (std::overflow_error &e) 
        std::cout << e.what() << " -> ";
    
    std::cout << i << std::endl;

    try  i = intDivEx (10, 2); 
    catch (std::overflow_error &e) 
        std::cout << e.what() << " -> ";
    
    std::cout << i << std::endl;

    return 0;

这个输出:

Divide by zero exception -> 42
5

你可以看到它抛出并捕获了除以零的异常(保持返回变量不变)。


% 等效项几乎完全相同:

【讨论】:

谢谢,我正在寻找系统抛出异常的情况,这些在c++中可能吗? @user33424,是的,有可能,例如在std::bad_alloc 上查看我的答案,这是由new 抛出的 hmm 所以当你决定使用异常类时不必仔细选择,因为它们都采用相同的参数。 @user 但是你应该选择一个让你的意图清楚地知道出了什么问题的人。例如,抛出 bad_alloc invalid_argument 是糟糕的设计。 由于函数的域是定义函数的有效输入集,domain_error 在这里抛出不是更合适吗?【参考方案2】:

使用 ExcessPhase 的 cmets 更新

GCC(至少 4.8 版)可以让您模拟这种行为:

#include <signal.h>
#include <memory>
#include <iostream>

int main() 
    std::shared_ptr<void(int)> handler(
        signal(SIGFPE, [](int signum) throw std::logic_error("FPE"); ),
        [](__sighandler_t f)  signal(SIGFPE, f); );

    int i = 0;

    std::cin >> i;  // what if someone enters zero?

    try 
        i = 5/i;
    
    catch (std::logic_error e) 
        std::cerr << e.what();
    

这会设置一个引发异常的新信号处理程序,并为旧信号处理程序设置一个shared_ptr,并使用自定义“删除”函数在旧处理程序超出范围时恢复它。

您至少需要使用以下选项进行编译:

g++ -c Foo.cc -o Foo.o -fnon-call-exceptions -std=c++11

Visual C++ 也可以让你做类似的事情:

#include <eh.h>
#include <memory>

int main() 
    std::shared_ptr<void(unsigned, EXCEPTION_POINTERS*)> handler(
        _set_se_translator([](unsigned u, EXCEPTION_POINTERS* p) 
            switch(u) 
                case FLT_DIVIDE_BY_ZERO:
                case INT_DIVIDE_BY_ZERO:
                    throw std::logic_error("Divide by zero");
                    break;
                ...
                default:
                    throw std::logic_error("SEH exception");
            
        ),
        [](_se_translator_function f)  _set_se_translator(f); );

    int i = 0;

    try 
        i = 5 / i;
     catch(std::logic_error e) 
        std::cerr << e.what();
    

当然,您可以跳过所有 C++11 风格,将它们放入传统的 RAII 管理结构中。

【讨论】:

设置和恢复信号处理程序应该使用 RAII!您也不能假设默认信号处理程序是您临时替换的。 为什么你使用shared_ptr?据我了解,使用 RAII 卸载处理程序。但是如果 U 不共享这个指针,最好使用 unique_ptr,甚至自定义 RAII struct。 FLT_DIVIDE_BY_ZERO 和 SIGFPE 不只是针对浮点异常吗? @Tom 是的,它是关于捕捉被零除,但我想说的是我认为 FLT_DIVIDE_BY_ZERO 和 SIGFPE 只是用于浮点异常,并不认为 integer 除以零(本文的主题)可以被这两个信号中的任何一个捕获。 啊,我明白你的意思,但是不,SIGFPE 也适用于整数除以零。单一 Unix 规范将 SIGFPE 定义为“错误的算术运算”。 FLT_DIVIDE_BY_ZERO 确实仅用于浮点,并且有相应的 INT_DIVIDE_BY_ZERO;我会更新答案。【参考方案3】:

据我所知,C++ 规范没有提到任何关于除以零例外的内容。我相信你需要自己做......

Stroustrup 在“C++ 的设计和演变”(Addison Wesley,1994 年)中说,“低级事件,例如算术溢出和除以零,被假定由专用的低级机制处理,而不是而不是异常。这使得 C++ 在算术方面能够与其他语言的行为相匹配。它还避免了在诸如除以零之类的事件是异步的大量流水线架构上发生的问题。"`

【讨论】:

【参考方案4】:

您需要使用throw 关键字手动抛出异常。

例子:

#include <iostream>
using namespace std;

double division(int a, int b)

   if( b == 0 )
   
      throw "Division by zero condition!";
   
   return (a/b);


int main ()

   int x = 50;
   int y = 0;
   double z = 0;

   try 
     z = division(x, y);
     cout << z << endl;
   catch (const char* msg) 
     cerr << msg << endl;
   

   return 0;

【讨论】:

【参考方案5】:

你应该检查i = 0,然后不要除。

(可以选择在检查后抛出异常并稍后处理)。

更多信息请访问:http://www.cprogramming.com/tutorial/exceptions.html

【讨论】:

【参考方案6】:

setjmp + longjmp

https://***.com/a/25601100/895245 提到了从信号处理程序中抛出 C++ 异常的可能性,但 Throwing an exception from within a signal handler 提到了几个警告,所以我会非常小心。

作为另一种潜在危险的可能性,您还可以尝试使用旧的 C setjmp + longjmp 机制,如下所示:C handle signal SIGFPE and continue execution

main.cpp

#include <csetjmp>
#include <csignal>
#include <cstring>
#include <iostream>

jmp_buf fpe;

void handler(int signum) 
    longjmp(fpe, 1);


int main() 
    volatile int i, j;
    for(i = 0; i < 10; i++) 
        struct sigaction act;
        struct sigaction oldact;
        memset(&act, 0, sizeof(act));
        act.sa_handler = handler;
        act.sa_flags = SA_NODEFER | SA_NOMASK;
        sigaction(SIGFPE, &act, &oldact);
        if (0 == setjmp(fpe)) 
            std::cout << "before divide" << std::endl;
            j = i / 0;
            sigaction(SIGFPE, &oldact, &act);
         else 
            std::cout << "after longjmp" << std::endl;
            sigaction(SIGFPE, &oldact, &act);
        
    
    return 0;

编译运行:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o main.out main.cpp
./main.out

输出:

i = 0
before divide
after longjmp
i = 1
before divide
after longjmp
i = 2
before divide
after longjmp

man longjmp 表示您可以通过信号处理程序 longjmp,但有一些注意事项:

POSIX.1-2008 Technical Corrigendum 2 将 longjmp() 和 siglongjmp() 添加到异步信号安全函数列表中。但是,该标准建议避免在信号处理程序中使用这些函数,并继续指出 指出如果这些函数是从中断对非异步信号安全函数(或某些等效函数,例如从初始返回时发生的与 exit(3) 等效的步骤)的调用的信号处理程序调用的 调用 main()),如果程序随后调用非异步信号安全函数,则行为未定义。避免未定义行为的唯一方法是确保以下条件之一:

从信号处理程序长跳转后,程序不会调用任何非异步信号安全函数,也不会从对 main() 的初始调用返回。

在每次调用非异步信号安全函数期间,必须阻止其处理程序执行长跳转的任何信号,并且在从初始调用返回 main() 后不会调用非异步信号安全函数。

另请参阅:Longjmp out of signal handler?

但是Throwing an exception from within a signal handler 提到这对 C++ 有进一步的危险:

setjmp 和 longjmp 与异常和 RAII(ctors/dtors)不兼容。 :( 你可能会因此而导致资源泄漏。

所以你也必须非常小心。

我认为信号处理程序很难,除非您确切知道自己在做什么,否则应尽可能避免使用它们。

检测浮点零除法

也可以通过 glibc 调用来检测浮点除以零:

#include <cfenv>

feenableexcept(FE_INVALID);

如图:What is the difference between quiet NaN and signaling NaN?

这使得它提高 SIGFPE 以及整数除以零,而不是静默 qnan 和设置标志。

【讨论】:

【参考方案7】:

这个呢?用 Clang 测试,GCC 抛出 SIGILL。

#include <iostream>
#include <cassert>

int main()

    unsigned int x = 42;
    unsigned int y = x;
    y -= x;
    x /= y;
    
    std::cout << x << " != "<< *(&x) << std::endl;
    assert (*(&x) == x);

【讨论】:

【参考方案8】:

do i need to throw an exception or does the computer automatically throws one at runtime?

您需要自己 throw 异常和 catch 它。例如

try 
  //...
  throw int();

catch(int i)  

或者catch你的代码抛出的异常。

try 
    int *p = new int();

catch (std::bad_alloc e) 
    cerr << e.what();

在你的情况下,我不确定是否有任何标准异常意味着除以零。如果没有此类异常,则可以使用,

catch(...)   // catch 'any' exception

【讨论】:

除以零是未定义的行为。 不知道此功能存在的人(在 Windows 和 OSX 中已有 20 多年了)不应该在这里回答,也不应该投票或称自己为软件工程师。【参考方案9】:

你可以只做assert(2 * i != i),它会抛出一个断言。如果你需要更高级的东西,你可以编写自己的异常类。

【讨论】:

-1 我真的不喜欢这个解决方案。这比assert(i != 0) 容易吗?我没有考虑过边界案例,但如果看到断言正确陈述并非易事,那么你不应该放它。 此外,由于NDEBUG 的存在,assert 在生产代码中经常被停用 - assert() 通常是一种仅用于捕获问题的开发方法。在任何情况下,抛出一个断言不是抛出一个异常。前者会中止你的程序,而不是生成你可以捕获的东西。

以上是关于捕获异常:除以零的主要内容,如果未能解决你的问题,请参考以下文章

C++异常如何写

C++异常如何写

Python 异常处理-Python零基础入门教程

Java学习笔记3.10.2 异常处理 - 异常捕获

由于未捕获的异常“NSInvalidArgumentException”而终止应用程序,原因:“UICollectionView 必须使用非零布局参数初始化

C++:捕获除以零错误