`
NeuronR
  • 浏览: 58969 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

C++ 中处理除零错误

阅读更多

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

 

    继承自 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 中设置 y1, 这样做是于事无补的.

使用异常处理机制

    修复指令这种事情简直是天方夜谭, 所以选择输出一跳错误语句并退出也算是不错的方法. 在 C 语言时代, 还可以通过 setjmplongjmp 来跳转程序流程. 不过 setjmplongjmp 操作起来太不方便了, 相比之下 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).

    结构 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; 
}

 

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

分享到:
评论
2 楼 NeuronR 2010-10-31  
之前自搭 blog, 太累. 最近在研究 C艹 语言的错误处理
1 楼 lwwin 2010-10-31  
话说很久没有看到你更新BLOG了,最近在研究LINUX开发^^?

相关推荐

    一个专门处理C++异常的类和例子

    在C++编程中,异常处理是一项关键技能,它允许程序员优雅地处理运行时错误,而不会导致程序崩溃。本文将详细介绍CExceptionLogger类及其在处理C++异常中的应用,同时提供一个实例来演示如何使用此类。 ...

    c++出错提示中英文对照详解

    C++出错提示中英文对照详解 C++编程语言中,错误提示是开发者在编写代码时经常遇到的问题,本文...这些错误提示是C++编程中常见的错误,开发者需要注意这些错误,并对其进行正确的处理,以确保代码的正确性和可靠性。

    Visual C++中的异常处理浅析

    SEH使用`__try`、`__except`和`__finally`关键字,它可以捕获系统级的异常,如内存溢出或除零错误等。SEH的优势在于它可以处理C语言级别的异常,而不仅仅是C++的异常。但是,SEH不支持异常类型检查,所以使用时需...

    从零开始学C++

    第14章可能涉及异常处理,这是C++中处理运行时错误的方式,通过try-catch块捕获和处理异常,以保证程序的健壮性。 第15章和第16章可能会探讨更高级的话题,如内存管理(指针、引用)、文件操作、预处理器宏、命名...

    C++从零开始 C++从零开始

    7. **异常处理**:通过try、catch和throw关键字,C++提供了异常处理机制,用于捕获和处理程序运行时可能出现的错误。 8. **命名空间**:命名空间用于避免命名冲突,使得大型项目中的代码组织更加有序。 9. **内存...

    C++ 各种异常处理机制 演示代码

    其次,Windows SEH是一种更底层的异常处理机制,主要用于处理硬件异常,如除零错误、访问非法内存等。SEH使用`__try`、`__except`和`__finally`关键字。虽然SEH通常用于处理硬件级别的异常,但也可以结合C++的异常...

    关于C++异常处理和win32结构化异常处理

    相比之下,Win32结构化异常处理是一种更底层的异常处理机制,主要用于处理硬件异常,如访问非法内存、除零错误等。它使用`__try...__except`结构来捕获和响应这些异常。与C++异常处理不同,SEH不会自动调用析构函数...

    C++异常处理的编程方法

    SEH的强大功能之一是它能够处理硬件异常,比如除零错误、访问违规等,而C++的标准异常处理机制通常只能处理软件异常。在实际开发中,如果需要结合使用SEH和C++异常处理机制,需要对两者都有深入了解,并且要处理好...

    C++ Java异常处理比较

    C++的异常处理还支持异常规范(exception specification),即函数声明中可以指定可能抛出的异常类型,但这一特性在C++11标准后被认为不推荐使用,因为其可能导致编译时错误过于严格。 异常与异常处理器的绑定方面...

    C++异常处理 王胜祥

    1. **异常的基本概念**:在C++中,异常是程序执行时遇到的不寻常情况,如除零错误、数组越界等。当出现异常时,程序会抛出一个对象,这个对象被称为异常。 2. **try、catch和throw关键字**:C++异常处理机制主要...

    C++从零开始

    - 异常处理是C++中处理错误的一种机制,通过try、catch和throw关键字进行。 - 当出现异常时,程序会跳转到相应的catch块进行处理,避免程序崩溃。 7. **内存管理** - 动态内存分配:使用new和delete关键字进行...

    mfc 简易计算器(有除零报错功能)

    对于除零错误,会在执行除法操作前检查除数是否为零,如果是,则抛出异常或者显示错误信息。 在压缩包中的“P030M简易计算器”,很可能是源代码文件或者编译后的可执行文件。源代码文件可能包含多个.CPP和.H头文件...

    c++异常处理机制描述

    SEH是Windows平台特有的,它可以处理硬件异常(如除零错误)和其他操作系统级别的异常。在VC++中,可以使用`__try`、`__except`和`__finally`关键字来实现SEH。 C++异常与SEH的区别在于,C++异常是基于对象的,而...

    c++(程序设计)从零开始

    "C++(程序设计)从零开始"这个教程旨在为初学者提供一个全面的学习路径,帮助他们从无到有掌握C++编程。 教程内容可能包括以下几个主要部分: 1. **基础语法**:学习C++之前,首先会介绍基本的编程概念,如变量、...

    零起点Visual C++程序设计培训教程电子教案

    C++中的try、catch和throw关键字提供了处理运行时错误的能力,能够帮助编写更健壮的代码。 本教程的电子教案可能涵盖了以上所有内容,并可能以实例和练习的形式,让你在实践中学习和巩固知识。通过深入学习和实践,...

    零基础学C++ppt

    【零基础学C++】是针对初学者设计的一套学习资料,旨在帮助毫无编程经验的人逐步掌握C++语言的基础知识。课程内容深入浅出,旨在让学习者能够快速上手并理解C++的核心概念。 首先,从【第2章 开始C++之旅.ppt】开始...

    C++:C++异常处理教程

    在C++编程中,异常处理是一种重要的机制,用于处理程序运行时可能发生的错误或异常情况。这些异常可能是由多种原因导致的,例如资源不足、操作失败、逻辑错误等。异常处理的主要目的是让程序在遇到问题时能够优雅地...

Global site tag (gtag.js) - Google Analytics