Skip to content

异常处理

异常处理

alt text 异常处理的前情提要:很多人不喜欢使用异常处理,认为它麻烦,应对可能出现的错误要写那么多代码,会非常麻烦。 但实际上不是这样的,我们只需要在一些开发人员难以控制,比较容易出错的地方对异常进行处理就可以了,需要进行异常处理的地方并不多。 举几个例子。

(1)接收传递过来的被除数,我们难以判断被除数是否为0,此时异常处理就很有意义了。

cpp
int divide(int divisior, int dividend) {
    if(!dividend) {
        throw std::string("dividend is zero");
    }
    return divisior / dividend;
}

void clientInputNum(const std::string& str, int& num) {
    std::cout << str << std::endl;
    while(std::cin >num, !std::cin.eof()) {
        if(std::cin.bad()) {
            throw std::runtime_error("cin is corrupted");
        }
        if(std::cin.fail()) {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
            std::cout << "number format error, please input again" << std::endl;
            continue;
        }
        break;
    }
}

int main() {
    int divisior = 0;
    int dividend = 0;
    clientInputNum("please input divisior", divisior);
    clientInputNum("please input dividend", dividend);
    try {
        std::cout << divide(divisior, dividend) << std::endl;
    }catch(const std::string& str) {
        std::cout << "exception occers, exception is : " << str << std::endl;
    }catch(...) {
        std::cout << "exception occers, exception is : unknown exception" << std::endl;
    }
    return 0;
}

(2)接收文件名,如果文件不存在,我们可以按照之前的写法要求重发一遍,也可以直接报异常,异常就是文件不存在。

cpp
void outputFileContent(const std::string& fileName) {
    std::ifstream ifs(fileName);
    std::string fileLineContent;
    if(ifs.open()) {
        while (ifs >> fileLineContent) {
            std::cout << fileLineContent << std::endl;
        }
        if(ifs.bad()) {
            throw std::runtime_error("ifs is corrupted");
        }
        ifs.close();
    }else {
        if(!ifs.bad()) {
            throw std::runtime_error("ifs is corrupted");
        }
        if(ifs.fail()) {
            throw std::string("file not exist");
        }
    }
}

int main() {
    std::string str;
    std::cin >> string;
    try {
        outPutFileContent(str);
    }catch(const std::string& str) {
        std::cout << "exception occers, exception is : " << str << std::endl;
    }catch(...) {
        std::cout << "exception occers, exception is : unknown exception" << std::endl;
    }
}

(3)我们在动态分配内存时,经常出现内存不足的情况(在大型程序中,这是非常常见)。比如我们需要动态分配一个未知大小的数组,数组大小等待传入。使用new操作符会直接抛出bad_alloc的异常。 对new的处理非常重要,大家如果做专业的C++开发,会经常用到。 此外使用智能指针时如果内存分配不够也会抛出bad_alloc的异常

cpp
int main() {
    try
    {
        while(1) {
            int* pi = new int[100000]();
        }
    }
    catch(const std::bad_alloc& except) {
        std::cout << except.what() << std::endl;
    }
    catch(...) {
        //std::cout << "unknown exception" << std::endl;
        throw;
    }
    // 不过通常建议不要捕获自己的代码所无法处理的异常
    // 否则只是悄悄藏起了这个异常 可能会导致更严重的后果
}
cpp
#include <memory>
#include <vector>

int main() {
    std::vector<std::shared_ptr<int>> shared_int_vec;
    try{
        while(1) {
            std::unique_ptr<int> uniqueI(new int[1000000]());
            shared_int_vec.push_back(std::move(uniqueI));
        }
    }catch(const std::bad_alloc& except) {
        std::cout << except.what() << std::endl;
    }catch(...) {
        std::cout << "unknown exception" << std::endl;
    }
}

(4)有个vector,我们需要接受一个参数,然后取出参数对应的数组元素。此时就经常出现数组的越界问题。

cpp
int main() {
    std::vector<int> ivec{1,2,3,4};
    try{
        ivec.at(10);
    }catch(const std::out_of_range& except) {
        std::cout << except.what() << std::endl;
    }catch(...) {
        std::cout << "unknown exception" << std::endl;
    }
}
cpp
#include <vector>
template<typaname T>
class Test{
public:
    T& operator[](unsigned count)const {
        if(count >= data.size()) {
            throw std::out_of_range("Test data is out of range");
        }
    }
private:
    std::vector<T> data;
}

最常用的基本就这些例子了,剩下的也都和这些类似。 异常处理这一章东西不多,一会儿把这些例子演示一下就可以了。

异常处理的介绍:

异常是程序在执行期间产生的问题(编译期出现的错误在写代码时开发环境就有提示)。C++的异常是指程序运行时发生的特殊情况。

异常提供了一种转移程序控制权的方式。C++的异常处理涉及到三个关键字:try,catch,throw。 (1) throw:当问题出现时,程序会抛出一个异常。这是通过throw关键字来完成的。 (2) catch:在你想要处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常。 (3) try:try块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个catch块。

如果有一个块抛出一个异常,捕获异常的方法会使用try和catch关键字。try块中放可能抛出异常的代码,try块中的代码被称为保护代码。常见的异常处理格式如下所示。

cpp
try {
    //保护代码
}catch( ExceptionName e1) {
    //catch块
}catch( ExceptionName e2) {
    //catch块
}catch( ExceptionName eN) {
    //catch块
}

抛出异常

throw语句可以在代码块的任何地方抛出异常,throw抛出的表达式的结果决定了抛出的异常的类型。

标准异常

C++提供了一系列标准的异常,定义在头文件exception中,它们是以父子层次结构组织起来的,如下图所示。

异常描述
std::exception该异常是所有标准 C++ 异常的父类。
std::bad_alloc该异常可以通过 new 抛出。
std::bad_cast该异常可以通过 dynamic_cast 抛出。
std::bad_exception这在处理 C++程序中无法预期的异常时非常有用。
std::bad_typeid该异常可以通过 typeid 抛出。
std::logic_error理论上可以通过读取代码来检测到的异常。
std::domain_error当使用了一个无效的数学域时,会抛出该异常
std::invalid_argument当使用了无效的参数时,会抛出该异常。
std::length_error当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>:operator[]()
std::runtime_error理论上不可以通过读取代码来检测到的异常,
std::overflow_error当发生数学上溢时,会抛出该异常。
std::range_error当尝试存储超出范围的值时,会抛出该异常,
std::underflow_error当发生数学下溢时,会抛出该异常。

常用异常

别看图很复杂,异常种类有很多,但经常使用的其实就几个。 (1) bad_alloc错误,使用new分配内存失败就会抛出bad_alloc错误。 (2) out_of_range错误,在使用at时,容器越界就会抛出这个错误,这也是at[]更加优秀的原因。

(3) runtime_error错误,运行时错误,只有在程序运行时才能检测到的错误。这是一个相对的概念,和logic_error形成对比。logic_error可以读代码读出来,runtime_error就不行。 我们也经常将一些读代码无法判断的异常标识为runtime_error。

(4) ... 错误,可以接受任何错误,我们一般都会在catch最后加上...,这样就可以接受所有类型的异常了。

自定义异常类型

其实需要自定义异常类型的情况真的非常少

cpp
class my_exception : public exception {
private:
    unsigned int error_code;
    std::string reason;
public:
    my_exception(const string& message, const int& code):
        reason(message),error_code(code) {
        return reason.c_str();
    };
    
    virtual const char* what() const noexcept override {
        return reason.c_str();
    };
}

int main() {
    try {
        throw my_exception("custom exception", 1000);
    }
    catch (const my_exception& e) {
        std::cout<<e.what()<<std::endl;
    }
    return 0;
}

资源释放问题 RAII

在异常处理过程中的资源释放问题.

对于自动创建的对象 例如我们在函数中定义的类对象 在程序抛出异常后 他们都能被自动释放 例如这段代码中创建的MyResource对象

cpp
class MyResource {
public:
    MyResource(const char* name) : mName(name) {
        std::cout<<"MyResource Construct"<<mName<<std::endl;
    }
    virtual ~MyResource() {
        std::cout<<"MyResource Destruct"<<mName<<std::endl;
    }
};

void doSomething() {
    MyResource res("[doSomething]");
    std::cout<<"doSomething()"<<std::endl;
    throw runtime_error("do something error");
}

int main() {
    try {
        MyResource res("[main]");
        doSomething();
    }
    catch(const runtime_error& e) {
        std::cout<<e.what()<<std::endl;
    }
}

但是如果是动态分配的对象 则需要采取措施。实际情况比较复杂。

例如下面这段代码里doSomething()方法里调用了几个其他的方法。这几个方法都有可能抛出异常。 当他们任何一个方法抛出异常后, delete语句就无法执行了, 就会造成内存泄漏。

因此我们经常使用类对这些资源进行封装。当自动创建的类对象被自动释放时会调用类的析构函数, 在析构函数中可以释放动态调用的内存或关闭打开的文件

cpp
float doSomething() {
    MyResource* pRes = new MyResource("[doSomething]");
    functionA(pRes);
    functionB(pRes);
    float result = functionC(pRes);
    delete pRes;
    return result;
}

对于动态分配的内存 我们可以使用智能指针进行封装。例如像这样

cpp
float doSomething() {
    std::shared_ptr<MyResource> pRes(new MyResource("[doSomething]"));
    functionA(pRes);
    functionB(pRes);
    float result = functionC(pRes);
    delete pRes;
    return result;
}

在任何一个地方抛出异常后, pRes会被自动释放, 从而释放他所封装的指针。

像上述这种管理资源的方式叫做

RAII资源获取即初始化(Resource Acquisition Is Initialization)