继承自 C 的优良传统, C++ 也是一门非常靠近底层的语言, 可是实在是太靠近了, 很多问题语言本身没有提供解决方案, 可执行代码贴近机器, 运行时没有虚拟机来反馈错误, 跑着跑着就毫无征兆地崩溃了, 简直比过山车还刺激.
    虽然 C++ 加入了异常机制来处理很多运行时错误, 但是异常机制的功效非常受限, 很多错误还没办法用原生异常手段捕捉, 比如整数除 0 错误. 下面这段代码

#include <iostream>

int main()
{
    try {
        int x, y;
        std::cin >> x >> y;
        std::cout << x / y << std::endl;
    } catch (...) {
        std::cerr << "attempt to divide integer by 0." << std::endl;
    }
    return 0;
}

输入 "1 0" 则会导致程序挂掉, 而那对 try-catch 还呆在那里好像什么事情都没发生一样. 像 Python 一类有虚拟机环境支持的语言, 都会毫无悬念地捕获除 0 错误.

使用信号

不过, 底层自然有底层的办法, 而且有虚拟机的环境也并非在每个整数除法指令之前都添上一句 if 0 == divisor: raise 之类的挫语句来触发异常. 这得益于硬件体系中的中断机制. 简而言之, 当发生整数除 0 之类的错误时, 硬件会触发中断, 这时操作系统会根据上下文查出是哪个进程不给力了, 然后给这个进程发出一个信号. 某些时候也可以手动给进程发信号, 比如恼怒的用户发现某个程序卡死的时候果断 kill 掉这个进程, 这也是信号的一种.
    这次就不是 C 标准了, 而是 POSIX 标准. 它规定了哪些信号进程不处理也不会有太大问题, 有些信号进程想处理也是不行的, 还有一些信号是错误中断, 如果程序处理了它们, 那么程序能继续执行, 否则直接杀掉.
    不过, 这些错误处理默认过程都是不存在的, 需要通过调用 signal 函数配置. 方法类似下面这个例子

#include <csignal>
#include <cstdlib>
#include <iostream>

void handle_div_0(int)
{
    std::cerr << "attempt to divide integer by 0." << std::endl;
    exit(1);
}

int main()
{
    if (SIG_ERR == signal(SIGFPE, handle_div_0)) {
        std::cerr << "fail to setup handler." << std::endl;
        return 1;
    }
    int x, y;
    std::cin >> x >> y;
    std::cout << x / y << std::endl;
    return 0;
}

可以看出, signal 接受两个参数, 分别是信号编号和信号处理函数. 成功设置了针对 SIGFPE (吐槽: 为什么是浮点异常 FPE 呢?) 的处理函数 handle_div_0, 如果再发生整数除 0 的惨剧, handle_div_0 就会被调用.
    handle_div_0 的参数是信号码, 也就是 SIGFPE, 忽略它也行.

底层机制

虽然说 handle_div_0 是异常处理过程, 但毕竟是函数都会有调用栈, 能返回. 假如在 handle_div_0 中不调用exit 自寻死路, 而是选择返回, 那么程序会怎么样呢? 运行一下, 当出现错误时, stderr 会死循环般地刷屏.
    实际上, 当错误发生时, 操作系统会在当前错误出现处加载信号处理函数的调用栈帧, 并且把它的返回地址设置为出错的那条指令之前, 这样看起来就像是出错之前的瞬间调用了信号处理函数. 当信号处理函数返回时, 则又会再次执行那条会出错的指令, 除非信号处理函数能通过某些特别的技巧修复指令, 否则退出时会重蹈覆辙.
    上面提到的 "修复指令" 指的是修复 CPU 级别的指令码或者操作数. 把除数 y 变成全局变量, 然后在handle_div_0 中设置 y 为 1, 这样做是于事无补的.

使用异常处理机制

修复指令这种事情简直是天方夜谭, 所以选择输出一跳错误语句并退出也算是不错的方法. 在 C 语言时代, 还可以通过 setjmp 和 longjmp 来跳转程序流程. 不过 setjmp 和 longjmp 操作起来太不方便了, 相比之下 try-catch 要好得多.
    刚才说过, 错误处理函数的调用栈帧直接位于错误发生处所在函数栈帧之上, 因此, 抛出异常能够被外部设置的 try-catch 捕获. 现在定义一个异常类型, 然后在 handle_div_0 中抛出就行.

#include <csignal>
#include <iostream>

struct div_0_exception {};

void handle_div_0(int)
{
    throw div_0_exception();
}

int main()
{
    if (SIG_ERR == signal(SIGFPE, handle_div_0)) {
        std::cerr << "fail to setup handler." << std::endl;
        return 1;
    }
    try {
        int x, y;
        std::cin >> x >> y;
        std::cout << x / y << std::endl;
    } catch (div_0_exception) {
        std::cerr << "attempt to divide integer by 0." << std::endl;
    }
    return 0;
}

更精准的信号处理

上述方法的缺陷在于, 只要发生 SIGFPE 中断, 无论是整数除 0 错误, 还是其它浮点异常, 处理方式是统一的. 不过, POSIX 还规定了一组更精细的信号处理接口, 它们是 sigaction.
    呃... 对它们是 sigaction. 这又是一个雷死人的东西. 在 csignal 中定义了两个同名的东西, 分别是

struct sigaction;

int sigaction(int sig
            , struct sigaction const* restrict act
            , struct sigaction* restrict old_act);

前面那个结构体在设置信号处理函数时用到, 里面存放了一些标志位和信号处理函数指针. 而后面那个函数就是设置信号处理的入口 (如果函数的第三个参数并非 NULL, 并且之前设置过信号处理结构体, 那么会将之前的处理方法写入第三个参数所指向的结构中, 这一点并不需要, 所以后面的例子中这个参数直接传入 NULL, 详情请见man 3 sigaction).
    结构 sigaction 中会有两个函数入口地址, 它们分别是

void (* sa_handler)(int);
void (* sa_sigaction)(int, siginfo_t*, void*);

sa_handler 也就是之前所演示的轻便型信号处理函数; 而 sa_sigaction, 从它接受的参数就能看出, 它能获得更多的上下文信息 (然而, 一看第三个参数的类型是 void* 就知道没有好事, 信息都在第二个参数指向的结构体中).
    既然有两个处理函数, 那么如何决定使用哪一个呢? 在 struct sigaction 中有一个标志位成员 sa_flags, 如果为它置上 SA_SIGINFO 位, 那么就使用 sa_sigaction 作为处理函数.
    siginfo_t 类型中有一个叫做 si_code 的成员, 它为信号类型提供进一步的细分, 比如在 SIGFPE 信号下,si_code 可能有 FPE_INTOVF (整数溢出), FPE_FLTUND (浮点数下溢), FPE_FLTOVF (浮点数上溢) 等各种相关取值, 当然还有现在最关心的整数除 0 信号码 FPE_INTDIV. 如果陷入 SIGFPE 的窘境中, 而 si_code 又恰好是FPE_INTDIV 那么就要果断抛出 0 异常了.
    由于原生的 struct sigaction 居然跟函数重名, 所以下面的例子中会对其包装一下, 提供合适的初始化过程.

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

struct my_sig_action {
    typedef void (* handler_type)(int, siginfo_t*, void*);

explicit my_sig_action(handler_type handler)
    {
        memset(&_sa, 0, sizeof(struct sigaction));
        _sa.sa_sigaction = handler;
        _sa.sa_flags = SA_SIGINFO;
    }

operator struct sigaction const*() const
    {
        return &_sa;
    }
protected:
    struct sigaction _sa;
};

struct div_0_exception {};

void handle_div_0(int sig, siginfo_t* info, void*)
{
    if (FPE_INTDIV == info->si_code)
        throw div_0_exception();
}

int main()
{
    my_sig_action sa(handle_div_0);
    if (0 != sigaction(SIGFPE, sa, NULL)) {
        std::cerr << "fail to setup handler." << std::endl;
        return 1;
    }
    try {
        int x, y;
        std::cin >> x >> y;
        std::cout << x / y << std::endl;
    } catch (div_0_exception) {
        std::cerr << "attempt to divide integer by 0." << std::endl;
    }
    return 0;
}

Post tags:   POSIX  C  Exception Handling  Signal  C++

这还是POSIX方法,悲愤的Windows下悲愤的SEH表示欲哭无泪啊(PS:此处需要一个哭的表情)

http://blog.bitfoc.us/?p=100

C++ 中捕获整数除零错误的更多相关文章

  1. jenkins构建中的除零错误

    一. 除零错误(ZeroDivisionError) 今天在jenkins上运行接口自动化测试任务时,从控制台输出中看到了除零错误,大概是这样的 从上图中,通过分析,可以得出三个结论: 1. jenk ...

  2. mssql sqlserver避免sql脚本中出现除零错误的方法分享

    摘自:http://www.maomao365.com/?p=6612 摘要:下文介绍sql server中,sql脚本避免出现除零错误的方法分享 在各种业务系统开发中,通常会遇到除零的错误,下文分享 ...

  3. 在C#代码中应用Log4Net(四)在Winform和Web中捕获全局异常

    毕竟人不是神,谁写的程序都会有bug,有了bug不可怕,可怕的是出错了,你却不知道错误在哪里.所以我们需要将应用程序中抛出的所有异常都记录起来,不然出了错,找问题就能要了你的命.下面我们主要讨论的是如 ...

  4. iOS 中捕获程序崩溃日志

    iOS 中捕获程序崩溃日志 (2014-04-22 17:35:59) 转载▼     iOS开发中遇到程序崩溃是很正常的事情,如何在程序崩溃时捕获到异常信息并通知开发者,是大多数软件都选择的方法.下 ...

  5. 在DECIMAL(m,n)的设置中,整数的位数不能大于(m-n)

    关于DB2的DECIMAL类型 创建表的时用的是DECIMAL(13,2),我认为它为13个整数位数+2为有效数字,因为在打印银行交易的FORM时遇到了难题.输出和建表的长度不一样,我们以为它会打印出 ...

  6. 有一个很大的整数list,需要求这个list中所有整数的和,写一个可以充分利用多核CPU的代码,来计算结果(转)

    引用 前几天在网上看到一个淘宝的面试题:有一个很大的整数list,需要求这个list中所有整数的和,写一个可以充分利用多核CPU的代码,来计算结果.一:分析题目 从题中可以看到“很大的List”以及“ ...

  7. 1000多个项目中的十大JavaScript错误以及如何避免

    通过统计数据库中的1000多个项目,我们发现在 JavaScript 中最常出现的错误有10个.下面会向大家介绍这些错误发生的原因以及如何防止. 对于这些错误发生的次数,我们是通过收集的数据统计得出的 ...

  8. DHCP获取IP地址过程中捕获的报文—三级网络总结(二)

    上一篇文章主要说了一下知识点中的IP地址的考点,这一篇我打算说说DHCP获取IP地址过程中捕获的报文的这个考点,都是自己的理解,有错误欢迎指正. DHCP是应用层协议,UDP是传输层协议,IP是网络层 ...

  9. 16.1 foreach 循环中捕获变量的变化

    在 foreach 循环内的匿名函数(通常为Lambda表达式)中捕获循环 变量时要格外小心.代码清单16-1就展示了这样一个简单的示例,它看上去似乎会输出 x . y . z . string[] ...

随机推荐

  1. 基于QT开发的第三方库

    基于Qt开发的第三方库 分类: Qt2014-02-12 11:34 1738人阅读 评论(0) 收藏 举报 QT第三方库   目录(?)[+]   文章来源:http://blog.csdn.net ...

  2. Java7新语法 -try-with-resources

    http://docs.oracle.com/javase/7/docs/technotes/guides/language/try-with-resources.html The try-with- ...

  3. SSH自动部署(转)

    我的是windows环境,目前开发的过程中,有些项目需要一下子部署到很多的linux服务器上.写了个脚本能够自动上传文件和执行部署任务.完成这个任务需要的条件包括SSH配置和一个执行脚本. 准备 1. ...

  4. 学习javascript基础知识系列第二节 - this用法

    通过一段代码学习javascript基础知识系列 第二节 - this用法 this是面向对象语言中的一个重要概念,在JAVA,C#等大型语言中,this固定指向运行时的当前对象.但是在javascr ...

  5. BOT、BT、PPP形式介绍(3)

    PPP     20世纪90年代后,一种崭新的融资模式-PPP模式(Public-Private-Partnership,即“公共部门-私人企业-合作”的模式)在西方特别是欧洲流行起来,在公共基础设施 ...

  6. 左侧高亮(js)

    //左导航高亮 (function(){ var leftnav = $('#leftnav');  //左侧ul var pi = __preset.curpathid; if(pi=='/acco ...

  7. codeforces C. Cd and pwd commands 执行命令行

    执行命令来改变路径 cd 并显示路径命令 pwd 一个节目的 抽样: input 7 pwd cd /home/vasya pwd cd .. pwd cd vasya/../petya pwd ou ...

  8. linux提取指定行至指定位置

    grep查找ERROR,定位位置 awk打印到指定行数 sed打印到文本末尾 awk打印到文本末尾 方法一 #!/bin/csh -f if(-f errorlog.rpt) then rm -rf ...

  9. jquery商城类封装插件

    自从解决了定时器的问题后,什么都好弄了 这是仿苏宁商城banner的,当然我没弄得那么好啦,但是我想就是那个缩略图,我没弄好吧,方法我猜想是通过把所有li都放进数组,然后通过遍历,就可以做出相应的效果 ...

  10. F5(调试)和服务器控件

    一.调试 背景: 今天调试的时候发现我进入的网址是http://×××.com:7813/webaspx/System/Login.aspx(由于代码在公司,我就没有截图,等了半天显示无法加载该页面) ...