阅读导航
- 引言
- 一、C++异常的概念
- 二、异常的使用
- 1. 异常的抛出和捕获
- (1)throw
- (2)try-catch
- (3)catch(. . .)
- (4)异常的抛出和匹配原则
- (5)在函数调用链中异常栈展开匹配原则
- 2. 异常的重新抛出
- 3. 异常安全
- 4. 异常规范
- 三、自定义异常体系
- 1. 创建基类异常
- 2. 创建派生异常
- 3. 抛出异常
- 4. 捕获和处理异常
- 5. 自定义异常体系示例
- 四、C++标准库的异常体系
- 五、异常的优缺点
- 1. C++异常的优点
- 2. C++异常的缺点
- 3. 总结
- 温馨提示
引言
在C++编程中,异常处理是一项重要的技术,它允许我们更好地应对程序运行过程中可能出现的错误和异常情况。本文将介绍C++中异常处理的基本概念、语法和最佳实践。我们将深入探讨try-catch块的使用方式,以及如何抛出和捕获不同类型的异常。
希望本文能够帮助您更好地理解和运用C++异常处理机制,并为您的编程工作带来便利和效益。祝您阅读愉快!
一、C++异常的概念
C++异常是一种用于处理程序运行时错误和异常情况的机制。当程序在执行过程中遇到无法正常处理的错误时,可以通过抛出异常来中断当前的执行流程,并将控制权转交给异常处理器。异常处理器可以捕获并处理异常,从而使程序能够优雅地处理错误情况,避免崩溃或数据丢失。异常可以表示各种不同类型的错误,例如除以零、访问不存在的内存地址、文件打开失败等。
二、异常的使用
如果有一个块抛出一个异常,捕获异常的方法会使用 try
和 catch
关键字。try
块中放置可能抛出异常的代码,try
块中的代码被称为保护代码。
1. 异常的抛出和捕获
(1)throw
throw
关键字用于抛出异常。当程序在执行过程中遇到无法正常处理的错误时,可以通过throw
语句抛出一个异常,让异常处理器来捕获并处理这个异常。例如:
if (count == 0) {
throw std::runtime_error("Divide by zero exception");
}
在上面的例子中,如果count等于0,则会抛出一个std::runtime_error异常,并附带异常信息“Divide by zero exception”。
(2)try-catch
try-catch
块用于捕获和处理异常。try
块用于包含可能引发异常的代码,catch
块则用于捕获和处理特定类型的异常。例如:
try {
// 可能引发异常的代码
} catch (std::exception& e) {
// 捕获std::exception及其派生类的异常
std::cerr << "Exception caught: " << e.what() << std::endl;
} catch (...) {
// 捕获其他类型的异常
std::cerr << "Unknown exception caught" << std::endl;
}
在上面的例子中,try
块包含可能引发异常的代码,catch
块捕获std::exception
及其派生类的异常,并输出异常信息。如果没有合适的catch
块来处理异常,则会跳转到最近的catch(...)
块。
(3)catch(. . .)
catch(...)
块用于捕获任何类型的异常。如果程序抛出了一个未被其他catch块捕获的异常,则会跳转到最近的catch(...)
块,并执行相关的异常处理代码。
(4)异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个
catch
的处理代码。 - 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)
- catch(…)可以捕获任意类型的异常,问题是不知道异常错误是什么。
- 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用。
(5)在函数调用链中异常栈展开匹配原则
- 首先检查
throw
本身是否在try
块内部,如果是再查找匹配的catch
语句。如果有匹配的,则调到catch的地方进行处理。 - 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。
- 如果到达main函数的栈,依旧没有匹配的,则终止程序。
- 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
🚨🚨注意:上述这个沿着调用链查找匹配的catch
子句的过程称为栈展开。所以实际中我们最后都要加一个catch(…)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
2. 异常的重新抛出
在C++中,异常的重新抛出是指在一个catch块中捕获到一个异常后,再次将它抛出,以便由更高层次的异常处理器来处理。这种重新抛出异常的操作可以在catch
块中使用throw
语句来实现。
⭕重新抛出异常的语法如下:
try {
// 可能引发异常的代码
} catch (ExceptionType1& e) {
// 异常处理代码
throw; // 重新抛出异常
} catch (ExceptionType2& e) {
// 异常处理代码
throw; // 重新抛出异常
}
在上面的代码中,当在catch
块中捕获到ExceptionType1类型的异常时,我们可以选择重新抛出该异常,让更高层的异常处理器来处理。通过throw
语句,异常会被重新抛出并继续向上寻找匹配的catch块。
重新抛出异常的好处在于,在某些情况下,当前的catch块可能无法完全处理异常,并希望由更上层的异常处理器来处理。这样可以将异常传递给更高层的代码进行处理,而不是简单地在当前的catch块中忽略掉异常。
🚨注意:在重新抛出异常时,可以选择不指定异常类型,即使用throw
;来重新抛出原始的异常。这样做的好处是可以保持异常的类型和信息不变,使得更高层的异常处理器可以正常地捕获和处理异常。
3. 异常安全
在C++中,异常安全是指程序在抛出异常时仍然能够保证程序的正确性和资源的释放。异常安全是一个重要的编程概念,因为在实际开发中,程序运行过程中难免会遇到各种异常情况,如内存不足、IO错误等,而这些异常可能会导致程序崩溃或者产生未知的错误,给程序的稳定性和可维护性带来很大的挑战。
为了提高程序的异常安全性,我们需要采取一些措施,主要包括以下几个方面:
🍁 使用RAII技术
RAII(Resource Acquisition Is Initialization)是一种资源获取即初始化的技术,通过C++对象的构造函数获取资源,利用析构函数自动释放资源。这样可以确保资源的正确释放,即使在抛出异常的情况下也能够保证资源的正确释放。比如使用std::unique_ptr智能指针管理内存,std::lock_guard管理锁等。(关于RAII我们智能指针这篇文章有所讲解)
🍁不要泄漏资源
在程序执行过程中,如果没有正确地释放资源,就会导致资源泄漏,进而影响程序的正确性和稳定性。因此,在编写程序时,要确保所有获取的资源都要正确释放,以避免资源泄漏。
🍁异常处理
在程序中,如果遇到异常情况,则需要正确地处理这些异常。一般来说,我们可以使用try-catch语句块来捕获并处理异常,确保程序在抛出异常时仍然能够保证程序的正确性和资源的释放。
4. 异常规范
异常规范(Exception Specification)是一种在C++函数声明中指定函数可能抛出的异常类型的方法。异常规范可以告诉调用者该函数可能会抛出哪些异常,以便调用者能够适当地处理这些异常。在C++中,有两种类型的异常规范:动态异常规范和静态异常规范。
🍪动态异常规范(Dynamic Exception Specification):
动态异常规范使用throw
关键字在函数声明中列出函数可能抛出的异常类型。例如:
void functionName() throw(ExceptionType1, ExceptionType2);
上述代码表示函数functionName
可能会抛出ExceptionType1
和ExceptionType2
类型的异常。如果函数抛出了未在异常规范中列出的异常,就会调用std::unexpected()
函数,该函数默认会调用std::terminate()
函数终止程序。
🍪静态异常规范(Static Exception Specification):
静态异常规范使用noexcept关键字指示函数不会抛出任何异常。例如:
void functionName() noexcept;
上述代码表示函数functionName不会抛出任何异常。这样的函数被称为不抛出异常的函数,编译器可以对其进行更多的优化。
在C++11之后,推荐使用noexcept
来指示函数不会抛出异常,而避免使用动态异常规范。同时,C++11引入了std::nothrow
关键字,用于在发生异常时返回一个null
指针,而不是抛出异常。
🚨注意:如果函数声明了静态异常规范,但实际上抛出了异常,程序会调用std::terminate()
函数终止程序。
三、自定义异常体系
✅在C++中,可以通过自定义异常类来创建自己的异常体系。自定义异常体系可以用于对不同类型的异常进行分类和处理。在实际的程序中都会定义一套继承的规范体系。这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。
⭕下面是创建自定义异常体系的步骤
1. 创建基类异常
创建一个基类异常,作为自定义异常体系的根。这个基类异常可以是std::exception的子类或者直接继承自std::exception。例如:
class MyException : public std::exception {
public:
virtual const char* what() const throw() {
return "My exception occurred";
}
};
在基类异常中,通常会重写what()
函数,该函数返回异常的描述信息。
2. 创建派生异常
根据需要,可以创建多个派生异常类,用于表示不同类型的异常。这些派生异常类可以继承自基类异常或者其他已有的异常类。例如:
class OutOfRangeException : public MyException {
public:
virtual const char* what() const throw() {
return "Out of range exception";
}
};
class NullPointerException : public MyException {
public:
virtual const char* what() const throw() {
return "Null pointer exception";
}
};
在派生异常类中,也可以重写what()
函数,以提供特定异常的描述信息。
3. 抛出异常
在程序中遇到异常情况时,可以使用throw
语句抛出自定义的异常对象。例如:
void myFunction() {
throw OutOfRangeException();
}
以上代码示例在myFunction()
函数中抛出了一个OutOfRangeException
异常对象。
4. 捕获和处理异常
在调用可能引发异常的代码时,可以使用try-catch
语句块捕获并处理异常。例如:
try {
myFunction();
} catch (MyException& ex) {
std::cout << "Caught exception: " << ex.what() << std::endl;
}
以上代码示例在调用myFunction()
时捕获并处理了MyException
及其派生异常的对象。
自定义异常体系可以根据具体需求进行扩展和细化,以提供更好的异常分类和处理能力。通过创建不同类型的异常类,可以更好地组织和处理程序中的异常情况。
5. 自定义异常体系示例
#include <iostream>
#include <string>
#include <ctime>
#include <chrono>
#include <thread>
using namespace std;
// 自定义异常基类
class Exception
{
public:
// 构造函数,接收错误信息和错误码作为参数
Exception(const string& errmsg, int id)
: _errmsg(errmsg)
, _id(id)
{}
// 返回异常信息的函数
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg; // 异常信息
int _id; // 异常码
};
// 派生自Exception的SqlException异常类
class SqlException : public Exception
{
public:
// 构造函数,接收错误信息、错误码和SQL语句作为参数
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; // SQL语句
};
// 派生自Exception的CacheException异常类
class CacheException : public Exception
{
public:
// 构造函数,接收错误信息和错误码作为参数
CacheException(const string& errmsg, int id)
: Exception(errmsg, id)
{}
// 返回异常信息的函数
virtual string what() const
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
// 派生自Exception的HttpServerException异常类
class HttpServerException : public Exception
{
public:
// 构造函数,接收错误信息、错误码和请求类型作为参数
HttpServerException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id)
, _type(type)
{}
// 返回异常信息的函数
virtual string what() const
{
string str = "HttpServerException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
const string _type; // 请求类型
};
// SQL管理函数
void SQLMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
// 如果随机数满足条件,抛出SqlException异常
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
}
// 缓存管理函数
void CacheMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
// 如果随机数满足条件,抛出CacheException异常
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
// 如果随机数满足条件,抛出CacheException异常
throw CacheException("数据不存在", 101);
}
SQLMgr();
}
// HTTP服务器函数
void HttpServer()
{
// ...
srand(time(0));
if (rand() % 3 == 0)
{
// 如果随机数满足条件,抛出HttpServerException异常
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
// 如果随机数满足条件,抛出HttpServerException异常
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try {
// 调用HttpServer函数可能会抛出异常
HttpServer();
}
catch (const Exception& e) {
// 捕获Exception及其派生类的异常对象
cout << e.what() << endl; // 输出异常信息
}
catch (...) {
// 捕获其他类型的异常
cout << "Unkown Exception" << endl;
}
}
return 0;
}
这是一个基于C++的自定义异常体系。在该代码中,自定义了三个异常类,分别是SqlException
、CacheException
和HttpServerException
,这些异常类都派生自基类Exception
。通过自定义异常类,可以更准确地捕获异常,同时也可以对不同类型的异常进行不同的处理。
在代码中,SqlException
用于处理SQL语句相关的异常,CacheException
用于处理缓存相关的异常,HttpServerException
用于处理HTTP服务器相关的异常。每个异常类都有一个构造函数,用于初始化异常信息和错误码等参数。在捕获异常时,可以根据异常类型来选择相应的处理方式,比如输出错误信息、记录日志、重试操作等等。
四、C++标准库的异常体系
⭕C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下图所示:
下表是对上面层次结构中出现的每个异常的说明:
异常类型 | 描述 |
---|---|
std::exception | 所有异常类的基类 |
std::logic_error | 程序逻辑错误 |
std::invalid_argument | 无效参数 |
std::domain_error | 域错误 |
std::length_error | 长度错误 |
std::out_of_range | 超出范围 |
std::future_error | 异步执行错误 |
std::runtime_error | 运行时错误 |
std::range_error | 范围错误 |
std::overflow_error | 溢出错误 |
std::underflow_error | 下溢错误 |
std::regex_error | 正则表达式错误 |
std::bad_alloc | 内存分配错误 |
std::bad_cast | 类型转换错误 |
std::bad_exception | 未捕获的异常 |
std::bad_function_call | 错误的函数调用 |
std::bad_typeid | typeid操作错误 |
std::bad_weak_ptr | weak_ptr错误 |
std::ios_base::failure | I/O流错误 |
std::system_error | 系统调用错误 |
std::filesystem::filesystem_error | 文件系统错误 |
std::experimental::filesystem::filesystem_error | 实验性文件系统错误 |
五、异常的优缺点
异常是一种处理程序运行时错误的机制,它提供了一种在程序中处理异常情况的方式。以下是异常的优点和缺点:
1. C++异常的优点
- 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
- 异常可以提高代码的可读性和可维护性,将错误处理代码与正常逻辑分离开来,使代码更加清晰易懂。
- 异常可以通过层级结构进行处理,允许异常在不同的层次上被捕获和处理,从而提高了程序的灵活性和扩展性。
2. C++异常的缺点
- 异常可能会导致性能问题,因为在发生异常时需要执行一些额外的操作,如调用析构函数、回收内存等,这些操作会增加程序的开销。
- 异常可能会影响代码的可移植性,因为异常处理机制在不同的编译器和操作系统上可能有所不同,需要针对不同的平台进行调整。
- 异常可能会导致程序的安全性问题,因为异常可以破坏程序的状态,可能会被恶意利用。此外,异常也可能会隐藏错误,使得程序的调试和测试变得更加困难。
- 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会导致我们跟踪调试时以及分析程序时,比较困难。
3. 总结
综上所述,异常总体而言,利大于弊,所以工程中我们还是鼓励使用异常的。在使用异常时,我们应该权衡利弊,根据实际情况选择合适的处理方式,保证程序的健壮性和安全性。
温馨提示
感谢您对博主文章的关注与支持!另外,我计划在未来的更新中持续探讨与本文相关的内容,会为您带来更多关于C++以及编程技术问题的深入解析、应用案例和趣味玩法等。请继续关注博主的更新,不要错过任何精彩内容!
再次感谢您的支持和关注。期待与您建立更紧密的互动,共同探索C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!