【C++ 】异常

异常

传统错误处理机制是assert断言和errno错误码。两种方式都有很大的局限性:

错误处理机制局限性
断言强制终止程序,用户难以接受
错误码返回值传递错误码,占用函数返回位置;无法直接展示信息,需查错误码表

1. 异常的概念

异常是面向对象语言对错误的处理机制。更加灵活和全面。

当一个函数发现自己无法处理错误时就可以抛出异常,让函数的调用者处理这个错误。

关键字含义
throw使用throw可以抛出一个异常
catch在处理问题的地方,使用catch用来捕获并处理异常。
trytry块中的代码将被激活的特定异常,它后面通常跟着一个或多个catch块。

异常机制可以帮助我们将异常检测和处理机制解耦,这样异常检测部分可以不必知道异常如何处理。

int Division(int a, int b)
{
	if (b == 0)
		throw "Division by zero condition";
	else
		return a / b;
}

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

int main() 
{
	try 
    {
		Func();
	}
	catch (const char* errmsg) 
    {
		cerr << errmsg << endl;
	}
	catch (...) 
    {
		cerr << "Unkown Exception" << endl;
	}
    return 0;
}    

 

2. 异常的使用

2.1抛出和捕获规则

  1. 异常时通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。
  2. 被选中的处理代码的调用链是,找到于该类型匹配且离抛出异常位置最近的那一个catch。
  3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象。
  4. catch(...)可以捕获任意类型的对象,主要是用来捕获没有显示捕获类型的异常,因为如果没有匹配的catch会终止程序。相当于条件判断中的else。问题是不知道异常错误是什么。
  5. 实际中抛出和捕获的类型不一定类型完全匹配,可以抛出派生类对象,使用基类来捕获,这个在实际生活中很实用。主要原因是:派生类可以赋值给基类。

实际工程中也都是用自定义异常类,用父类对象捕获子类对象,这非常实用。

try
{
    throw memory_exception; // 子类
}
catch (const exception& e)  // 父类
{
}
函数调用链展开异常匹配原则
  1. 首先检查throw本身是否在try块内部,如果是,再在当前函数栈中查找匹配catch语句。如果有匹配的直接跳到catch的地方执行。
  2. 如果没有匹配的catch块,则退出当前函数栈,在调用函数的栈中查找匹配的catch
  3. 如果到达main函数的栈,都没有匹配的catch,就会终止程序。上述沿着调用链查找匹配的catch块的过程叫栈展开。所以实际要最后要加一个catch(...)来捕获任意类型的异常,防止程序终止。
  4. 找到匹配的catch会直接跳到catch语句执行,执行完后,会继续沿着catch语句后面执行。

在这里插入图片描述

异常的重新抛出

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

一般在大型项目中都是在最外层统一进行处理异常,但资源的释放需要在当前层完成,为防止异常跳过资源释放,可以在当前层捕获并释放再抛出,再在最外层重新捕获并处理。

void Func()
{
    try {
        some();
    }
    catch (...) {
        delete resource; // 释放资源
      	throw; // 重新抛出
    }
}
int main()
{
    try {
        Func();
    }
    catch(...) {
        HandlerException(); // 处理异常
    }
}

2.2 异常安全和规范

C++ 中经常会出现异常导致资源泄漏的问题。比如:

newdelete中间抛出异常,可能导致内存泄漏。在lockunlock中间抛出异常,可能导致死锁。

异常规范

针对异常,C++标准也有规范,

  • 如果函数不会抛异常可以在其声明后加上关键字noexcept

加上noexcept的效果:
加上noexcept 异常不起作用编译器报错

  • 在函数声明后加上throw(...)表明该函数可能抛出的异常类型。
// 表示函数可能抛出的异常有指定类型
void Func() throw(int, char, string, vector); 

// 表示函数只可能会抛出std::bad_alloc一种异常
void New(size_t size) throw (std::bad_alloc); 

// 表示函数不会抛出异常
void Delete(size_t size, void* ptr) throw(); 

编译器不对这两条声明作检查。

异常安全

异常处理在 C++ 中是一项重要的任务,但它可能导致一些异常安全问题。这些问题可能影响程序的正确性和资源管理。以下是一些常见的异常安全问题和解决方法:

  1. 在构造函数中抛出异常
    构造函数负责对象的构造和初始化。如果在构造函数中抛出异常,可能导致对象状态不完整或者没有完全初始化。这会引发对象管理和状态不一致的问题。

解决方法:

尽量避免在构造函数中抛出异常。
如果必须在构造函数中处理可能引发异常的操作,考虑使用初始化列表、工厂模式或者智能指针等方式来确保异常安全。

  1. 在析构函数中抛出异常

析构函数主要负责资源的清理工作。如果在析构函数中抛出异常,可能导致资源无法正确释放,进而引发内存泄漏或者其他资源泄漏问题。

解决方法:

尽量避免在析构函数中抛出异常。
如果需要在析构函数中进行可能引发异常的操作,确保在异常处理过程中不会引发更严重的问题,比如使用安全的资源管理技术。

  1. 资源泄漏问题

C++ 异常经常会导致资源泄漏问题,比如在 new 和 delete 中抛出异常导致内存泄漏,或者在 lock 和 unlock 中抛出异常导致死锁。

解决方法:

使用异常捕获,在捕获异常后释放资源并重新抛出异常,以确保资源得到释放。
使用 RAII(资源获取即初始化)的思想解决资源管理问题。定义一个资源管理类,在其析构函数中释放资源,利用栈对象的生命周期来管理资源的获取和释放。
异常安全问题是 C++ 中需要特别注意的一点,合理的异常处理策略可以提高程序的稳定性和可靠性。

总结:

  • 不要在构造析构函数内抛出异常,以防对象初始化或释放不全。
  • 对于异常导致的资源泄漏、死锁等问题,通常使用锁守卫和智能指针来初始化和析构。

 

3. 自定义异常体系

实际项目中不可随便抛任意类型的异常,这样会增加处理难度。很多公司都会定义自己的异常类体系进行规范的异常管理。

实际工程中也都是自定义异常类体系,用父类对象捕获子类对象,这非常实用。

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

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

class CacheException : public Exception  
{
public:
    CacheException(const string& errmsg, int id)
        : Exception(errmsg, id)
    {}
    virtual string What() const { 
        return "CacheException: " + _errmsg;
    }
};

class HttpException : public Exception   
{
public:
    HttpException(const string& errmsg, int id, const string& type)
        : Exception(errmsg, id), _type(type)
    {}
    virtual string What() const {
        return "HttpException: " + _type + " : " + _errmsg;
    }
private:
    string _type;
};

模拟网络服务体系:

void SqlMgr()
{
    //...
    if (rand() % 7 == 0)
        throw SqlException("sql sytanx wrong!", 100, "some error sql");
    if (rand() % 8 == 0)
        throw SqlException("permission denied!", 101, "select * from secret");
    //...
    throw "Unkown Exception";
}

void CacheMgr()
{
    //...
    if (rand() % 5 == 0)
        throw CacheException("insufficient space!", 100);
    if (rand() % 6 == 0)
        throw CacheException("permission denied!", 101);
    //...
    SqlMgr();
}

void HttpMgr()
{
    //...
    if (rand() % 3 == 0)
        throw HttpException("resource is not exist!", 100, "post");
    if (rand() % 4 == 0)
        throw HttpException("permission denied!", 101, "post");
    //...
    CacheMgr();
}

void ServerStart()
{
    srand((unsigned int)time(nullptr));
    while (1)
    {
        this_thread::sleep_for(chrono::seconds(1));
        try {
            HttpMgr();
        }
        catch (const Exception& e) {
            cout << e.What() << endl;
        }
        catch (...) {
            cout << "Unkown Exception" << endl;
        }
    }
}

int main()
{
    ServerStart();
    return 0;
}

 

4. 标准库的异常体系

C++标准库也提供了标准异常类std::exception

继承体系结构图如下:


异常描述
std::exception该异常是所有标准异常的父类
std::bad_allocnew出现异常,内存不足
std::bad_castdynamic_cast出现异常
std::bad_exception出现无法预期的异常
std::bad_typeidtypeid出现异常
std::logic_error逻辑错误
std::domain_error无效的数学域异常
std::invalid_argument无效的参数异常
std::length_errortd::string出现异常,字符串太长
std::out_of_range越界访问
std::runtime_error运行时异常
std::overflow_error溢出异常
std::range_error存储超出范围异常
std::underflow_error数学下溢异常

 

5. 异常的优缺点

异常的优点

  1. 清晰准确地展示出各种错误信息: 异常对象可以清晰地描述错误,包括错误的详细信息和堆栈跟踪,这有助于更精确地定位和解决问题。
  2. 不需要层层返回,一次性到达异常处理的地方: 异常处理机制允许我们在错误发生时直接跳转到异常处理代码,而不需要在每个函数调用中返回错误代码,简化了代码结构。
  3. 不占用函数的返回位置: 对于那些不方便返回错误码的函数,异常提供了一个很好的替代方案,不影响函数的返回值。

异常的缺点

  1. 异常可能导致执行流乱跳,易混乱: 异常会改变正常的程序执行路径,导致代码的执行流变得难以预测和跟踪,增加了代码的复杂性。
  2. 异常容易导致资源泄漏,需要智能指针管理资源: 由于C++没有垃圾回收,异常可能导致资源未被释放,如内存泄漏或未关闭的文件和锁。使用智能指针(如std::unique_ptrstd::shared_ptr)可以自动管理资源,增加异常安全性。
  3. 标准库的异常体系定义得不好,导致没有规范的异常体系: C++标准库的异常体系可能不完善,这导致开发者需要自己定义和管理异常类型,增加了开发的复杂性。

对比表格:

异常的优点异常的缺点
清晰准确地展示出各种错误信息异常可能导致执行流乱跳,易混乱
不需要层层返回,一次性到达异常处理的地方异常容易导致资源泄漏,需要智能指针管理资源
不占用函数的返回位置标准库的异常体系定义得不好,导致没有规范的异常体系。
  • 26
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

SuhyOvO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值