C++的异常处理

异常处理在C++中的地位是很尴尬的,他不被很多公司或者程序员认可,但是基于某些原因,个人依然觉得异常处理在C++程序中 是非常必要的。

一般来说,异常分为两大类,一个是抛出异常,另一个是接受异常然后处理。

  • 抛出异常使用throw。
  • 接受异常 使用try....catch 语句块。

1.标准异常抛出

一般来说,标准库中类或者函数会在使用错误的时候抛出一些异常,这其实也被人诟病的,毕竟错误的使用库是程序员的责任,标准库不应该为此承担。举一个string的栗子:

try{
 std::string wrolen("234");
 std::string it = wrolen.substr(10);
}catch(std::exception& e){
    std::cout << "Something Wrong:"<<e.what() <<"\n";
}

这个例子将只有3个字符的wrolen,取其10个字符长度的子串,自然会发生错误。
结果为
what(): basic_string::substr: __pos (which is 10) > this->size() (which is 3)

使用try--catch后,程序不会立刻奔溃,而是执行打印语句,这说明在try语句中有string的异常被抛出。而我们刚有接住了这个异常。

我们来看完整的测试代码

#include <iostream>
#include <string>

void doExceptionCatch()
{
    try{
        std::string wrolen("234");
        std::string it = wrolen.substr(10);
    }catch(std::exception &e){
        std::cout << "Something Wrong:"<<e.what() <<"\n";
    }
}

int main(int argc, char const *argv[])
{
  
    try{
        doExceptionCatch();
    }
    catch(std::out_of_range &e){
        std::cout << "In main handle Something Wrong:"<<e.what() <<"\n";
    }
    return 0;
}

测试结果
Something Wrong:basic_string::substr: __pos (which is 10) > this->size() (which is 3)

在doExceptionCatch函数中使用了catch后,本来的异常已经被处理了,因此,回到main函数后,没有catch到任何异常,故没有打印。

一个特意的改动是,在doExceptionCatch函数的catch中加一个throw关键字,于是这个异常会被继续上抛,被main捕获。

事实上,标准库中定义了很多种异常,实际编码中,你可以在你认为会产生异常的地方将它们抛出来。当然了,一定是确实产生了异常。

看下面的例子:

int returnInt(unsigned int val)
{
    return val > 2147483647 ? throw std::out_of_range("too big for a interger!"):val;
}

int main(int argc, char const *argv[])
{
    try{
        int intme = returnInt(2147483648);
        std::cout << "In main intme :"<<intme <<"\n";
    }catch(std::out_of_range &e){
        std::cout << "In main handle Something Wrong:"<<e.what() <<"\n";
    }
    return 0;
}

我们知道一个int的最大数值是2147483647,因此当用户输入的数比这个还大,那么我们则抛出一个超出范围的异常。这合情合理。当函数returnInt没有抛出异常时,程序将正常执行,打印输入的数字,否则不会执行打印,而是直接进入到catch语句块。实现了类似(事实上也是)goto语句的功能。

2.自定义的异常抛出

标准库当然不会具体到项目的方方面面,可能你还需要自行定义一些异常情况,比如网络中断的处理,文件无法打开的处理等,那么你就可以自己定义异常了。一般来说,我们会继承标准库的std::exception来自定义异常类。

class MyException:public std::exception
{
public:
    //重载标准exception的what函数
    const char* what() const noexcept{
        return "Error for my Diy exception!";
    }

};
void throwMyexception() noexcept(false)//noexcept(false)表明该函数可以抛出异常,throw(MyException),在C++11中已经废弃了这种写法
{
    //这里模拟发生的异常,然后抛出自定义异常
    if(!false){
        throw MyException();
    }
}
int main(int argc, char const *argv[])
{
    try{
        throwMyexception();
    }catch(MyException & e){
        std::cout << "my own exception is : "<< e.what() <<"\n";
    }
    return 0;
}

值得一提的是noexcept 关键字,该关键字在C++11中引入,用于标识是否会抛出异常。

3.异常捕获

上面的例子中,已经涉及到如何捕获异常了,这里做个说明,不管是标准异常还是自定义的 异常,只要是可能存在异常抛出的位置,我们都可以使用try---catch语句块取处理异常。

4.异常处理的时机

异常处理不是神丹妙药,它不能也不应该作为你的软件出错后的救命稻草(如果软件要奔溃,就该让它早点奔溃),它应该作为软件在不该发生某些问题下的补充处理,虽然与此同时,这样的补充会引发代码膨胀,甚至于滥用。

在《程序员的修炼》一书中,作者对异常的使用时机是这样认为的,如果去掉异常处理的部分,程序和之前表现一致,则说明异常处理是正确的引入了,否则,如果一个原本异常的代码,在异常处理后变正常了,则说明异常处理做了不该做的事。

故此,我的理解是,异常处理不应该试图修复异常本身,而是应该将问题尽早暴露然后即时的对此做出处理,这个处理不是修复,修复应该是正常代码该做的事。比如本文第一个例子中,代码错误的取用了超出字符串长度的子串,这里就不该使用异常处理(好吧,我给自己打脸了),更好的方案是先判断字符串的长度,然后再取子串。第二个例子,用户(调用者)错误的输入了大于最大int型的数值,那么可以使用异常,异常处理里绝不是要把这个输入值变得小一些,使之合法,而是忽视用户的调用输入,并给予提示。那么为什么不使用if语句直接判断输入值是否大于最大int呢?这就涉及到异常的代码段的集中处理的优势了,它能让代码看起来更紧凑,不然,如果这里的判断多了,if就该满天飞了。
这篇文章对异常的好处做了很详细的诠释

话分两头,不使用异常也是可以的,但是观察C++的语言发展,标准会更倾向于加入并优化异常的处理。除了极端的,对速度有超高要求的场景外,我们需要引入异常,以避免程序在毫无防备的地方奔溃退出,让所有的异常都得到处理,就可以将软件的健壮性提升,如果你的代码写得很烂,那就更该引入异常处理,这样的话,奔溃时,程序至少能“死有所归”。

posted @ 2018-12-12 21:43  Lckfa  阅读(759)  评论(0编辑  收藏  举报