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

这里可(不)能抛出异常

阅读更多

 

    先来回顾上一篇开头处的一段代码的结尾处

EXIT_FREE_BUFFER:
    free(buffer);
EXIT_CLOSE_DST:
    fclose(dstf);
EXIT_CLOSE_SRC:
    fclose(srcf);
    return ret;

    有一个问题是, 当有错误发生时 (ret 为非零值) 如果同时 free, fclose 这样的函数又出错, 那这个时候如何处理两个不同的错误呢? 答案是, 看一下手头任何一本 C 语言手册, 它都会说只要语言的使用者不手贱传递一个错误的指针到函数或者瞎掰似的连续释放两次, 函数都不会出错, 此外, 这些函数都返回 void 类型, 换句话说, 它们不会标记任何错误.

 

    首先简单说一下 C++ 怎么处理资源释放问题, 那就是 RAII 机制, C++ 在栈上构造出的每一个对象, 在栈退出时都会析构掉, 甚至可以说, C++ 区别于任何其它一门面向对象程序设计语言的地方就是, 在对象机制上的自动化, 即对象的析构函数以明确的时序被调用. 参照下面的代码可以窥出其中的端倪

 

#include <iostream>

struct echo {
    int n;

    echo(int nn)
        : n(nn)
    {
        std::cout << "constructing " << n << std::endl;
    }

    ~echo()
    {
        std::cout << "destructing " << n << std::endl;
    }
};

int main(int argc, char* argv[])
{
    echo e0(0);
    if (argc > 1) {
        echo e1(1);
    }
    return 0;
}

    而且, 即使产生异常, 在函数非正常退栈时也会析构掉已经构造的对象. 比如

 

#include <iostream>

struct echo {
    int n;

    echo(int nn)
        : n(nn)
    {
        std::cout << "constructing " << n << std::endl;
    }

    ~echo()
    {
        std::cout << "destructing " << n << std::endl;
    }
};

void func_throw()
{
    echo e(0);
    throw int(0);
}

int main()
{
    try {
        func_throw();
    } catch (int) {
        std::cout << "int caught." << std::endl;
    }
    return 0;
}

    而且可以看出来, 析构对象的工作是在 catch 之前发生的. 那么, 很明显的类比是, C++ 应该有一个不抛出异常协约, 任何类型的析构函数和 operator delete应该声明 throw().

    设想一下你正在喝水, 这时手一滑, 产生了一个杯具悲剧异常, 好, 那么就直接跳到清理阶段, 也就是无论是否喝到了水, 都得把资源释放掉; 然而不幸的是这时不能再用扫帚了, 因为申请扫帚就是请求另一份资源, 而请求资源就会有潜在的资源无法满足的情形发生, 从而产生另一个错误, 于是你只好用肉体解决; 当然, 这样一来, 如果某个刹那你稍有闪失, 让玻璃边缘与皮肤切面来个正交接触, 从而发生一个手割破了异常, 好了, 这个时候就没办法处理了不是吗?

 

    但是 C++ 的世界显然不是这样的, 在处理错误的过程中不允许发生第二个错误, 这样的规定看起来简直是反人类的. 然而, 无论是面向过程编程, 还是面向对象编程, 或是面向显示器编程, 从一开始就认定计算机处理错误的能力非常有限, 出现一个错误, 那就处理这个错误, 如果此时又出现另外一个错误, 那么程序崩溃, 比如下面这段代码

 

#include <string>
#include <iostream>

struct throw_on_dtor {
    ~throw_on_dtor()
    {
        throw int(0);
    }
};

void func_throws()
{
    throw_on_dtor t;
    throw std::string("func_throw");
}

void wrapper0()
{
    try {
        func_throws();
    } catch (int) {
        std::cout << "0: int caught." << std::endl;
    } catch (std::string) {
        std::cout << "0: string caught." << std::endl;
    }
}

void wrapper1()
{
    try {
        wrapper0();
    } catch (int) {
        std::cout << "1: int caught." << std::endl;
    } catch (std::string) {
        std::cout << "1: string caught." << std::endl;
    }
}

void wrapper2()
{
    try {
        wrapper1();
    } catch (int) {
        std::cout << "1: int caught." << std::endl;
    } catch (std::string) {
        std::cout << "1: string caught." << std::endl;
    }
}

int main()
{
    try {
        wrapper2();
        return 0;
    } catch (...) {
        std::cout << "main: exception caught." << std::endl;
        return 1;
    }
}

    这里包了很多层, 每一层都全副武装, 然而这样都是徒劳的, catch 已经无法阻止异常了. 不过看看控制台的输出, 很有意思的是, 贯穿堆栈而下的是 throw_on_dtor 析构时甩出来的 int, 大家有兴趣可以再做一下试验, 时序上第二个抛出的异常将是摧毁程序的必备良药, 而实际上再多抛任何异常, 这些异常对象甚至都不会被构造.

    想想自诩为万物之灵的人类处理异常的方法则好得多, 割破手? 没问题, 把清扫杯具的事情压到栈里面去, 先把血止了, 然后再来处理玻璃碎片. 只有在极端情况下, 比如血有病患者同时又找不到创口贴资源和止血药品, 这时才会引发严重的问题, 但不管怎么说, 只要人不死掉, 总会想到办法来一个个处理问题的, 正如侏罗纪公园那句名言 "Life will find a way". 而自诩为万物之灵的人类创造出来的计算机则并没有这个灵性.

 

    不过幸好, 析构函数并不会真的像真实环境中这样这么环境复杂, 对于计算机而言, 关闭文件, 释放内存, 退临界区这种操作就不应该发生异常. 当然说应该不应该是一回事, 说是不是确实肯定以及保证不出错不抛异常是另一回事. 在上面某处的 "应该" 二字我设了粗体, 确实存在这样的问题, 在某个应该予以确保的环境却险象环生. 对于 C++ 而言, 就是本该声明 throw() 的析构函数却没有这么做, 而且并不是因为写代码的人的疏忽, 很多析构函数后面都非常非常明确地给了注释, 标出了 never throw, 如果有兴趣可以运行下面这个脚本

find /usr/include/ -type f | awk '{print "cat", $1;}' | sh | grep never\ throw

    但注释是注释, 很多情况下前面真的并没有 throw().

 

    为什么应该却没人这么干呢? 回想一下 C++ 中处理除零错误这篇文章中所看到的东西吧, 看起来一个整数除法也能轻易抛出异常不是吗? 那么是不是应该来这样规定: "析构函数里面, 不能够使用整数除法"? 析构函数你们到底要闹哪样啊? 这是我的 Blog, 又不是冷笑话.

    说到底不是整数除法的问题, 而是信号中断的那些事儿. 除零这种事情相对而言可控性是很高的了, 而且哪有那么多人真没事做在析构的时候摆弄除法运算, 撑死了用加减法搞搞指针偏移就行了. 关键是, 信号很可能是外部来的, 比如, 进程中断.

 

    为什么 kill -9 比直接 kill 要给力得多, 而且有些程序 killkill 不掉呢? 因为 -9, 也就是 SIGKILL 不允许设置信号处理函数的, 而直接 kill 传递的是 SIGTERM, 这个的信号处理函数可以有. 假定我们设计一个正常的程序, 而参照整数除零异常, 类似地, 可以在 C++ 程序中设置一个进程中断异常, 在处理 SIGTERM 时抛出一个, 接到后做一些比如用户设置保留之类的处理然后退出, 听起来非常完美. 不过, 在可能有这样的情况出现, 程序内部刚刚发生了一个异常, 并且这时正在执行某些应该不会产生异常的对象析构时, 从系统的某个地方突然窜出来一个 SIGTERM, 暗无天日的堆栈猛然间打开了一道口子, 只见进程中断异常一路狂飙冲到栈底, 把程序给弄崩溃了. 虽然非常小概率, 但是无论怎么说, 这都是潜在可能的结局.

 

    C++ 标准中有一个非常邪恶的, 如果函数不声明抛出什么异常, 那么就是可能抛出任何异常的阴谋, 大抵也就是这个原因. 看到这个规定不可避免会脱口而出, 我擦叻, 那岂不是随便一个 C 函数都能抛出异常? 但是生活在石器时代的 C 语言连什么是异常都不知道, 还抛什么抛? 任何异常都能抛, C 真的有这凶残能力吗? 现在很明了了, 确实是这样, 信号让一切都这么简单, 想抛就抛, 随心所欲.

 

    最后请大家自问一下, 真的相信自己的运气足够好, 信号中断永远不会发生在异常来袭的时候吗?

分享到:
评论

相关推荐

    关于在SQL中抛出异常的写法

    总的来说,熟练掌握在SQL中抛出异常的方法,能显著提升开发质量和效率,尤其是在处理报表这类复杂逻辑时。通过合理使用`RAISERROR`和`THROW`,以及配合`TRY...CATCH`,我们可以更好地控制错误处理,确保程序的稳定性...

    Java 自定义异常和抛出异常

    另一方面,`throws`关键字用于方法声明,表示该方法可能会抛出异常,但不会在这里处理。这是将异常处理推迟到调用者的地方。例如: ```java public void readFile(String fileName) throws FileNotFoundException {...

    c#异常含异常格式,抛出异常和自定义异常

    这里,`try`块包含可能抛出异常的代码,`catch`块用于捕获特定类型的异常并进行处理,`finally`块则确保某些资源的释放或清理工作。 接下来,我们讨论“抛出异常”。在C#中,可以通过`throw`关键字来抛出一个异常。...

    抛出异常的事例

    如果`toantitone`方法抛出异常,`main`方法中的`catch`块会捕获它,并通过`e.printStackTrace()`打印堆栈跟踪,帮助调试。`finally`块确保无论是否发生异常,都会执行一段代码(在这里是打印"over")。 `toantitone...

    C++抛出异常技巧讲解

    C++语言与其他编程语言一样,其中也包含有关于异常的处理。我们在这里将会为大家详细讲解一下有关C++抛出异常的实现方法,及异常的应用方式。希望大家可以从中获得些帮助,以提高对此的理解程度。

    Java异常处理-throw手动抛出异常对象

    // 这里抛出异常后,后面的代码不会执行了 } } public class Test { public static void main(String[] args) { Student s = new Student(); s.regist(-1); // 调用regist方法,传入负数 } } 情况2 2、手动抛普通的...

    android jni抛出异常

    描述中的"这是我我自己写的android jni里面抛出异常的demo"暗示了有一个实际的示例代码,尽管未提供具体代码,但我们可以想象它可能包含了上述步骤,即定义了一个JNI函数,然后在该函数内部遇到错误条件时抛出异常。...

    JAVA抛出异常的实验.doc

    ### JAVA 抛出异常实验知识点解析 #### 实验一:函数`normal()`的返回值分析 根据提供的实验内容,我们来逐步分析`normal()`函数的行为: ```java int normal(){ try{ return 10; } catch(Exception e){ ...

    Python语言基础:异常的抛出.pptx

    异常处理是Python编程中不可或缺的一部分,它能帮助我们编写健壮的代码,即使在遇到错误时也能有条不紊地进行错误报告和恢复。通过`raise`语句,我们可以精确地控制何时以及如何抛出异常,从而实现更精细的错误管理...

    浅谈python抛出异常、自定义异常, 传递异常

    开发者可以通过 `raise` 关键字来主动抛出异常,通过不带参数的 `raise` 来传递异常,并且可以轻松地定义符合自己需求的自定义异常类型。掌握这些知识对于编写健壮、易于维护的Python程序至关重要。

    Java编程中使用throw关键字抛出异常的用法简介

    `throw`关键字是Java中用于手动抛出异常的关键字,通常在检测到某个条件不符合预期或者需要提供特定错误信息时使用。以下是对`throw`关键字使用的一些详细解释和示例。 ### 抛出异常的基本语法 在Java中,`throw`...

    一个构造函数抛出异常引发的“餐具”

    1. 在构造函数中,确保所有可能抛出异常的操作都被包围在try-catch块中,即使在抛出异常后也能正确关闭和清理资源。 2. 使用try-with-resources语句,如果`WMQMessageConsumer`实现了`AutoCloseable`接口,以确保在...

    Python 异常的捕获、异常的传递与主动抛出异常操作示例

    本篇将详细探讨Python中的异常捕获、异常的传递以及如何主动抛出异常。 ### 异常的捕获 异常捕获是通过`try-except`语句来实现的。当`try`块中的代码发生异常时,程序会立即跳转到相应的`except`块进行处理。在`...

    Python使用lambda抛出异常实现方法解析

    这里创建了一个空的生成器表达式,然后调用其`throw()`方法来抛出异常。这种方法适用于所有版本的Python。 #### 方法二:编写一定会抛出异常的表达式 如果异常的具体信息并不重要,只需要触发异常即可,那么可以...

    Java中关于子类覆盖父类的抛出异常问题

    通常,我们说子类覆盖父类方法时,抛出的异常不能比父类更"宽泛",这里的"宽泛"指的是异常类型的继承关系。 首先,让我们澄清一下Java中的异常处理概念。Java异常是通过`try-catch-finally`块来管理的,异常是程序...

    java try…catch嵌套捕获异常的实例

    此外,`finally`块总是最后执行,无论是否抛出异常,它都是必不可少的资源清理环节。 在`CatchException_03.java`这个文件中,可能包含了实际的嵌套`try-catch`异常处理代码实例。配合`Java.jpg`可能是一个解释或...

    易语言简单处理TRY异常

    在这里放置可能抛出异常的代码 .如果发生错误 (错误号 &lt;&gt; 0) ; 错误处理代码 输出 ("发生错误:" + 错误信息 (错误号)) .结束如果 try_退出区域 ``` 在TRY块内,如果发生了错误,错误号会被设置,并可以通过`...

    捕获不可达的异常

    // 无论是否抛出异常,这里的代码都会执行 // 可以用来释放资源或进行其他必要的操作 } ``` 在这个例子中,如果`obj`是`null`并且尝试调用`toString()`方法,将会抛出`NullPointerException`。`catch`块捕获这个...

    C#中的异常

    当一个异常条件发生时,开发人员可以通过`throw`语句来显式抛出异常。例如: ```csharp if (value == null) throw new ArgumentNullException("value", "The value cannot be null."); ``` 这里`...

    java异常处理

    // 这里会抛出异常 } } catch (Exception e) { System.out.println("异常,继续抛出"); throw e; // 再次抛出异常 } } } ``` 在这个例子中,`test1`方法通过`throws`声明了可能抛出`Exception`异常。在方法...

Global site tag (gtag.js) - Google Analytics