C++:异常

异常的抛出与捕获

C++中使用try-catch块来处理异常,throw关键字用于抛出异常。

  • throw: 当问题出现时,程序会抛出一个异常,这是通过使用 throw 关键字来完成的
  • catch: 在想要处理问题的地方,通过异常处理程序捕获异常,catch关键字用于捕获异
  • trytry 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码
double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "error!!!";
    else
        return ((double)a / (double)b);
}

void Func()
{
    int a, b;
    cin >> a >> b;
    cout << Division(a, b) << endl;
}

int main()
{
    try 
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }

    return 0;
}

此处的throw关键字用于抛出异常,"error!!!"是一个const char*类型的字符串,throw可以抛出任何类型变量。

由于main调用了Func,Func调用了Division,所以Func函数是有可能间接发生异常的,此时把调用Func的语句放到try块中,说明我们要检测这个Func函数会不会发生异常。

一旦Division抛出异常,那么就是Func发生了异常,此时try就可以检测到,由于抛出的异常是const char*,我们要检测该类型的异常,所以catch的参数就是const char*了,catch (const char* errmsg)。

一旦catch匹配到了相同类型的异常,就会执行{ }中的代码。

异常匹配原则
  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码
try 
{
    Func();
}
catch (const char* errmsg)
{
    cout << "const char*" << endl;
}
catch (int errmsg)
{
    cout << "int" << endl;
}
catch (double errmsg)
{
    cout << "double" << endl;
}
catch (int* errmsg)
{
    cout << "int*" << endl;
}

此处有4个catch块,当在Func内部检测到异常时,根据被抛出的异常类型,选择执行哪一个catch来输出信息。

2.catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么

try 
{
    Func();
}
catch (const char* errmsg)
{
    cout << "const char*" << endl;
}
catch (...)
{
    cout << "unknow error" << endl;
}
3.被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个

4.抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁

由于被throw的异常,本质上也是一个变量,当往回找catch的时候,很有可能会出函数作用域,那么局部变量就会被销毁。因此catch()内部是一个传值调用,通过不断拷贝被抛出的异常,最后把异常送到catch中。

5.实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获。

// 定义基类异常
class Base
{
public:
    virtual const char* what()    
    {
        return "Base Exception";
    }
};

// 定义派生类异常
class Derived : public Base
{
public:
    const char* what() 
    {
        return "Derived Exception";
    }
};

int main() 
{
    try 
    {
        // 抛出派生类异常
        throw Derived();
    }
    catch (Base& e) 
    {
        // 捕获基类异常,但实际接收到的是派生类异常
        cout << e.what() << endl;
    }
    catch (...) 
    {
        cout << "Catch all exception" << endl;
    }

    return 0;
}

异常重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。

double Division(int a, int b)
{
    // 当b == 0时抛出异常
    if (b == 0)
        throw "error!!!";
    else
        return ((double)a / (double)b);
}

void Func()
{
    int a, b;
    cin >> a >> b;

    try
    {
        cout << Division(a, b) << endl;
    }
    catch (const char* errmsg)
    {
        cout << "第1次处理异常" << endl;
        throw errmsg;
    }
}

int main()
{
    try
    {
        Func();
    }
    catch (const char* errmsg)
    {
        cout << "第2次处理异常"  << endl;
    }
    catch (...)
    {
        cout << "unknow error" << endl;
    }

    return 0;
}

异常安全问题

构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化

class A
{
public:
    A(int n)
    {
        if (n == 0)
            throw 0;
        
        _ptr = new int[n];
    }

    ~A()
    {
        delete[] _ptr;
    }

private:
    int* _ptr;
};

以上代码中,A类的构造函数在n = 0的时候,直接抛出异常throw 0。此时会直接结束析构函数,去找catch。但是一旦出了A的作用域,A就会被销毁,调用析构函数,而析构函数要delete[]_ptr没有初始化,此时是野指针,这就导致了严重的问题,程序直接崩溃。

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等) 

析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内存泄漏、句柄未关闭等)

C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题 

先看到一个普通的异常造成的内存泄漏:

void func2()
{
    throw "error";
}

void func1()
{
    int* array = new int[10];

    func2();

    delete[] array;
}

int main()
{
    try
    {
        func1();
    }
    catch (const char* errmsg)
    {
        cout << errmsg << endl;
    }

    return 0;
}

以上代码中,func2抛出了异常,由于在调用func2之前,func1已经new了一块动态内存,此时如果直接去处理异常,那么delete就会被忽略,导致内存泄漏。

对于这种内存泄漏,我们就可以用到异常的重新抛出了,将func1写为如下形式:

void func1()
{
    int* array = new int[10];

    try 
    {
        func2();
    }
    catch (const char* errmsg)
    {
        delete[] array;
        throw;
    }

    delete[] array;
}

,C++提供了RAII(Resource Acquisition Is Initialization)机制,通过在构造函数中申请资源,在析构函数中释放资源,确保资源的安全释放。 

class SmartPtr
{
public:
    SmartPtr(int* ptr)
        : _ptr(ptr)
    {}

    ~SmartPtr()
    {
        delete[] _ptr;
    }

private:
    int* _ptr;
};

void func()
{
    SmartPtr p1 = new int[10];
    SmartPtr p2 = new int[10];
    SmartPtr p3 = new int[10];
}

我们将原先的newdelete放到了类SmartPtr中,此时就算哪个地方出现了异常,要去上层找catch,由于自定义类型出了作用域,会自动调用析构函数,此时就会调用~SmartPtr内部的delete,就完成了自动释放资源,不用我们复杂的截获,然后一个一个delete了。


异常规范

为了让函数的调用者可以更加清晰地知道一个函数有可能会抛出什么类型的异常,方便处理,C++98给出了异常规格说明

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型
  2. 函数的后面接throw(),表示函数不抛异常
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常
void* fun() throw(int, double, string)
{
    return nullptr;
}

throw(int, double, string)代表函数func只有可能抛出这三种类型的异常。 

void* fun() throw()
{
    return nullptr;
}

throw()代表函数func不会抛出任何异常。

C++11引入了noexcept关键字,用于指定函数不会抛出任何异常。如果一个noexcept函数内部抛出异常,程序会直接调用std::terminate()终止。

  • 如果一个函数有可能抛出异常,就不对该函数进行任何修饰
  • 如果一个函数一定不会抛出异常,那么用noexcept来修饰该函数

异常体系

在实际开发中,由于异常抛出的类型是不确定的,公司内部一般会约定好如何抛异常,而异常继承体系是最优秀的一套体系。

先定义一个基类:

class Exception
{
public:
    Exception(const string& errmsg, int id)
        :_errmsg(errmsg)
        , _id(id)
    {}
    virtual string what() const
    {
        return _errmsg;
    }
protected:
    string _errmsg;
    int _id;
};

某个SQL的程序员想要抛异常,于是它写出来以下派生类

class SqlException : public Exception
{
public:
    SqlException(const string& errmsg, int id, const string& sql)
        :Exception(errmsg, id)
        , _sql(sql)
    {}
    virtual string what() const
    {
        string str = "SqlException:";
        str += _errmsg;
        str += "->";
        str += _sql;
        return str;
    }
private:
    const string _sql;
};

外层捕获异常的方式:

void SQLMgr()
{
    if (true)
    {
        throw SqlException("权限不足", 100, "select * from name = '张三'");
    }
}

int main()
{
    try
    {
        SQLMgr();
    }
    catch (Exception e)
    {
        cout << e.what() << endl;
    }
}

在外层只捕获基类Exception ,此时所有程序员重写的派生类都可以被捕获到。捕获到异常后,就去调用what函数,拿到返回值并输出,此时就可以直到到底是什么问题了。 

这个地方,异常最后就会输出:

SqlException:权限不足->select * from name = '张三'

 看到这样一个异常,我们可以知道:该异常来自于SQL,原因在于权限不足,问题语句是select * from name = '张三'

这样的异常看的就让人很舒服,可以很快定位到错误,而且整个抛异常的过程也很简洁,不会发生接收不到异常,或者接收到未知类型的异常等问题。

 

 

 

 

相关推荐

  1. <span style='color:red;'>C</span>++<span style='color:red;'>异常</span>

    C++异常

    2024-07-15 20:02:02      46 阅读
  2. <span style='color:red;'>C</span>++<span style='color:red;'>异常</span>

    C++异常

    2024-07-15 20:02:02      25 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-15 20:02:02       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-15 20:02:02       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-15 20:02:02       58 阅读
  4. Python语言-面向对象

    2024-07-15 20:02:02       69 阅读

热门阅读

  1. C++的模板(十一):算法的轨迹

    2024-07-15 20:02:02       20 阅读
  2. goframe 之ORM链式封装

    2024-07-15 20:02:02       23 阅读
  3. 高通平台android的Framework开发遇到的一些问题总结

    2024-07-15 20:02:02       21 阅读
  4. 第六章 动画【Android基础学习】

    2024-07-15 20:02:02       18 阅读
  5. 【爬虫】爬虫基础

    2024-07-15 20:02:02       19 阅读
  6. CSS 技巧与案例详解:开篇介绍

    2024-07-15 20:02:02       21 阅读
  7. 力扣刷题之2732.找到矩阵中的好子集

    2024-07-15 20:02:02       21 阅读
  8. golang基础用法

    2024-07-15 20:02:02       18 阅读
  9. shell脚本传参调用http接口

    2024-07-15 20:02:02       17 阅读