文章目录
-
- C++异常机制详解(一):异常基础与核心机制
- 一、异常的概念与起源
-
- [1.1 什么是异常](#1.1 什么是异常)
- [1.2 C语言的错误处理方式](#1.2 C语言的错误处理方式)
- [1.3 C++异常的优势](#1.3 C++异常的优势)
- 二、异常的抛出与捕获
-
- [2.1 抛出异常:throw](#2.1 抛出异常:throw)
- [2.2 捕获异常:try-catch](#2.2 捕获异常:try-catch)
- [2.3 异常对象的生命周期](#2.3 异常对象的生命周期)
- 三、栈展开机制
-
- [3.1 什么是栈展开](#3.1 什么是栈展开)
- [3.2 栈展开过程演示](#3.2 栈展开过程演示)
- [3.3 栈展开与对象析构](#3.3 栈展开与对象析构)
- 四、异常的匹配规则
-
- [4.1 精确匹配](#4.1 精确匹配)
- [4.2 允许的类型转换](#4.2 允许的类型转换)
- [4.3 多个catch的匹配顺序](#4.3 多个catch的匹配顺序)
- [4.4 catch(...) 万能捕获](#4.4 catch(...) 万能捕获)
- 五、实际应用:设计异常继承体系
-
- [5.1 为什么需要异常继承体系](#5.1 为什么需要异常继承体系)
- [5.2 设计服务器异常体系](#5.2 设计服务器异常体系)
- [5.3 各模块的实现](#5.3 各模块的实现)
- [5.4 统一捕获处理](#5.4 统一捕获处理)
- 六、总结与展望
-
- [6.1 本文要点回顾](#6.1 本文要点回顾)
- [6.2 下一篇预告](#6.2 下一篇预告)
C++异常机制详解(一):异常基础与核心机制
💬 欢迎讨论:异常处理是C++中重要的错误处理机制,它让程序能够优雅地处理运行时错误。如果你在学习过程中有任何疑问,欢迎在评论区留言交流!
👍 点赞、收藏与分享:这是C++异常机制系列的第一篇,建议收藏后系统学习。如果觉得有帮助,请分享给更多的朋友!
🚀 系列导航:本文将介绍异常的基本概念、抛出与捕获机制、栈展开过程以及实际应用案例。
一、异常的概念与起源
1.1 什么是异常
在程序运行过程中,我们经常会遇到各种错误情况:
- 除零错误
- 内存分配失败
- 文件打开失败
- 网络连接中断
- 数组越界访问
这些错误如果不加以处理,程序可能会崩溃或产生未定义的行为。异常处理机制就是C++提供的一种结构化的错误处理方式。
异常处理的核心思想
异常机制允许我们将问题的检测 和问题的处理分离开来:
- 检测部分:发现问题时抛出异常
- 处理部分:在调用链的适当位置捕获并处理异常
- 分离优势:检测代码无需知道处理细节,处理代码也无需知道检测细节
1.2 C语言的错误处理方式
在学习C++异常之前,我们先回顾一下C语言是如何处理错误的。
错误码方式
C语言主要通过返回错误码来处理错误:
cpp
// C语言风格的错误处理
int divide(int a, int b, double* result)
{
if (b == 0)
{
return -1; // 错误码:除零错误
}
*result = (double)a / b;
return 0; // 成功
}
int main()
{
double result;
int ret = divide(10, 0, &result);
if (ret == -1)
{
printf("除零错误\n");
}
else
{
printf("结果: %f\n", result);
}
return 0;
}
C语言错误处理的问题
- 错误码语义不明确 :看到
-1需要查文档才知道是什么错误 - 容易被忽略:调用者可能忘记检查返回值
- 传递困难:错误码需要层层返回,代码冗长
- 信息有限:只能返回一个整数,无法携带详细信息
- 混淆返回值:正常返回值和错误码混在一起
cpp
// 多层调用时的麻烦
int funcC()
{
// ...
if (error)
return -1;
return 0;
}
int funcB()
{
int ret = funcC();
if (ret != 0)
return ret; // 层层传递错误码
return 0;
}
int funcA()
{
int ret = funcB();
if (ret != 0)
return ret; // 继续传递
return 0;
}
1.3 C++异常的优势
C++的异常机制解决了C语言错误处理的诸多问题:
优势一:信息丰富
异常抛出的是一个对象,可以包含丰富的错误信息:
cpp
class DivideException
{
public:
DivideException(const string& msg, int line)
: _msg(msg)
, _line(line)
{}
string what() const { return _msg; }
int line() const { return _line; }
private:
string _msg;
int _line;
};
优势二:自动传播
异常会自动沿着调用链向上传播,无需手动层层返回:
cpp
void funcC()
{
throw runtime_error("error in funcC");
}
void funcB()
{
funcC(); // 不需要检查返回值,异常会自动向上传播
}
void funcA()
{
try
{
funcB();
}
catch (const exception& e)
{
cout << e.what() << endl; // 在这里统一处理
}
}
优势三:强制处理
未处理的异常会导致程序终止,这迫使程序员必须考虑错误处理。
优势四:不影响正常逻辑
正常的业务逻辑和错误处理逻辑分离,代码更清晰:
cpp
// 正常逻辑清晰
try
{
step1();
step2();
step3();
}
catch (...) // 错误处理集中在这里
{
handleError();
}
二、异常的抛出与捕获
2.1 抛出异常:throw
使用throw关键字抛出异常对象:
基本语法
cpp
throw 异常对象;
抛出不同类型的异常
cpp
// 抛出整数
void func1()
{
throw 404;
}
// 抛出字符串
void func2()
{
throw "File not found";
}
// 抛出自定义对象
void func3()
{
throw DivideException("除零错误", 42);
}
// 抛出标准库异常
void func4()
{
throw runtime_error("运行时错误");
}
throw的执行语义
当throw执行时会发生什么:
- 立即终止当前函数:throw后面的语句不再执行
- 创建异常对象的副本:即使抛出的是局部对象,也会创建一个副本
- 开始栈展开:沿着调用链向上查找匹配的catch
cpp
void testThrow()
{
cout << "throw之前" << endl;
throw 1;
cout << "throw之后" << endl; // 这行不会执行
}
int main()
{
try
{
testThrow();
}
catch (int e)
{
cout << "捕获异常: " << e << endl;
}
return 0;
}
输出
bash
throw之前
捕获异常: 1
2.2 捕获异常:try-catch
使用try-catch块来捕获和处理异常。
基本语法
cpp
try
{
// 可能抛出异常的代码
}
catch (异常类型1 参数)
{
// 处理类型1的异常
}
catch (异常类型2 参数)
{
// 处理类型2的异常
}
catch (...)
{
// 捕获所有类型的异常
}
完整示例
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Division by zero!";
}
return (double)a / b;
}
int main()
{
try
{
cout << Divide(10, 2) << endl; // 正常执行
cout << Divide(10, 0) << endl; // 抛出异常
cout << "这行不会执行" << endl;
}
catch (const char* msg)
{
cout << "捕获异常: " << msg << endl;
}
cout << "程序继续执行" << endl;
return 0;
}
输出
bash
5
捕获异常: Division by zero!
程序继续执行
2.3 异常对象的生命周期
这是一个重要但容易被忽视的细节。
异常对象的拷贝
抛出异常时,会创建异常对象的一个副本:
cpp
class MyException
{
public:
MyException(const string& msg) : _msg(msg)
{
cout << "MyException构造" << endl;
}
MyException(const MyException& e) : _msg(e._msg)
{
cout << "MyException拷贝构造" << endl;
}
~MyException()
{
cout << "MyException析构" << endl;
}
string what() const { return _msg; }
private:
string _msg;
};
void func()
{
MyException e("test error");
throw e;
}
int main()
{
try
{
func();
}
catch (const MyException& ex)
{
cout << "捕获: " << ex.what() << endl;
}
return 0;
}
输出分析
bash
MyException构造
MyException拷贝构造
MyException析构
捕获: test error
MyException析构
可以看到:
- 在func中构造了原始异常对象
- throw时进行了拷贝构造(创建副本)
- 原始对象在func结束时析构
- catch捕获的是副本
- catch块结束后副本析构
为什么要拷贝?
因为抛出的异常对象可能是局部对象,当函数退出时局部对象会被销毁。为了让外层能够捕获到异常信息,必须创建一个副本。
三、栈展开机制
3.1 什么是栈展开
当异常被抛出后,程序会执行一个叫做栈展开(Stack Unwinding)的过程。
栈展开的步骤
- 首先检查throw本身是否在try块内
- 如果在,查找匹配的catch子句
- 如果找到匹配的catch,跳转到catch块执行
- 如果没找到,退出当前函数,在上层调用函数中继续查找
- 重复步骤2-4,直到找到匹配的catch或到达main函数
- 如果到达main还没找到,调用
terminate()终止程序
3.2 栈展开过程演示
cpp
void funcD()
{
cout << "funcD() 开始" << endl;
throw runtime_error("error in funcD");
cout << "funcD() 结束" << endl; // 不会执行
}
void funcC()
{
cout << "funcC() 开始" << endl;
funcD();
cout << "funcC() 结束" << endl; // 不会执行
}
void funcB()
{
cout << "funcB() 开始" << endl;
try
{
funcC();
}
catch (const logic_error& e) // 类型不匹配
{
cout << "funcB捕获logic_error" << endl;
}
cout << "funcB() 结束" << endl; // 不会执行
}
void funcA()
{
cout << "funcA() 开始" << endl;
try
{
funcB();
}
catch (const runtime_error& e) // 类型匹配!
{
cout << "funcA捕获: " << e.what() << endl;
}
cout << "funcA() 结束" << endl; // 会执行
}
int main()
{
funcA();
cout << "main结束" << endl;
return 0;
}
输出
bash
funcA() 开始
funcB() 开始
funcC() 开始
funcD() 开始
funcA捕获: error in funcD
funcA() 结束
main结束
栈展开路径分析
- funcD抛出异常,检查自己没有try-catch
- 退出funcD,在funcC中查找(funcC也没有try-catch)
- 退出funcC,在funcB中查找(funcB有try-catch但类型不匹配)
- 退出funcB,在funcA中查找(funcA有匹配的catch!)
- 跳转到funcA的catch块执行
- catch块执行完,funcA继续执行后续代码
3.3 栈展开与对象析构
栈展开过程中,所有已构造的局部对象都会被正确析构:
cpp
class Resource
{
public:
Resource(const string& name) : _name(name)
{
cout << _name << " 构造" << endl;
}
~Resource()
{
cout << _name << " 析构" << endl;
}
private:
string _name;
};
void funcInner()
{
Resource r3("r3");
throw runtime_error("error");
Resource r4("r4"); // 不会构造
}
void funcOuter()
{
Resource r1("r1");
Resource r2("r2");
funcInner();
Resource r5("r5"); // 不会构造
}
int main()
{
try
{
funcOuter();
}
catch (const exception& e)
{
cout << "捕获: " << e.what() << endl;
}
return 0;
}
输出
bash
r1 构造
r2 构造
r3 构造
r3 析构
r2 析构
r1 析构
捕获: error
重要观察
- 抛出异常前构造的对象(r1, r2, r3)都被正确析构
- 抛出异常后的对象(r4, r5)不会被构造
- 析构顺序与构造顺序相反(栈的特性)
这个特性非常重要,它保证了即使发生异常,资源也能被正确释放。
四、异常的匹配规则
4.1 精确匹配
异常类型必须与catch的参数类型匹配:
cpp
try
{
throw 10;
}
catch (int e) // 匹配
{
cout << "捕获int: " << e << endl;
}
catch (double e) // 不匹配
{
cout << "捕获double" << endl;
}
4.2 允许的类型转换
虽然要求类型匹配,但C++允许以下几种类型转换:
转换1:非const向const转换(权限缩小)
cpp
try
{
throw 10;
}
catch (const int& e) // OK,int可以转换为const int
{
cout << e << endl;
}
转换2:数组/函数转指针
cpp
try
{
throw "error"; // const char[6]
}
catch (const char* msg) // OK,数组转指针
{
cout << msg << endl;
}
转换3:派生类向基类转换(重要!)
cpp
class Base {};
class Derived : public Base {};
try
{
throw Derived();
}
catch (const Base& e) // OK,派生类可以转换为基类
{
cout << "捕获Base" << endl;
}
这个特性非常重要,我们在设计异常体系时会大量使用。
4.3 多个catch的匹配顺序
当有多个catch时,按照从上到下的顺序匹配,第一个匹配的catch会被执行:
cpp
try
{
throw 10;
}
catch (double e)
{
cout << "捕获double" << endl;
}
catch (int e)
{
cout << "捕获int" << endl;
}
catch (...)
{
cout << "捕获所有" << endl;
}
输出 :捕获int
重要原则:将更具体的异常类型放在前面,更一般的类型放在后面。
cpp
class Base {};
class Derived : public Base {};
try
{
throw Derived();
}
catch (const Derived& e) // 先捕获派生类
{
cout << "捕获Derived" << endl;
}
catch (const Base& e) // 再捕获基类
{
cout << "捕获Base" << endl;
}
4.4 catch(...) 万能捕获
catch(...)可以捕获任意类型的异常:
cpp
try
{
// 可能抛出各种异常
}
catch (...)
{
cout << "捕获到未知异常" << endl;
}
使用场景
- main函数的最后防线:确保程序不会因未捕获异常而崩溃
cpp
int main()
{
try
{
// 程序主逻辑
}
catch (const exception& e)
{
cout << "标准异常: " << e.what() << endl;
}
catch (...)
{
cout << "未知异常,程序即将退出" << endl;
}
return 0;
}
- 析构函数中:确保析构过程不抛出异常
cpp
~MyClass()
{
try
{
// 清理资源
}
catch (...)
{
// 吞掉所有异常,不让异常逃离析构函数
}
}
五、实际应用:设计异常继承体系
5.1 为什么需要异常继承体系
在大型项目中,不同的模块会抛出不同类型的异常。如果每个模块都定义自己独立的异常类型,main函数就需要catch很多种类型,非常不便。
通过继承体系,我们可以:
- 统一在基类捕获所有异常
- 每个模块添加自己特有的信息
- 保持代码的扩展性和维护性
5.2 设计服务器异常体系
假设我们要实现一个Web服务器,包含HTTP服务、SQL数据库、缓存等模块。
基类设计
cpp
class Exception
{
public:
Exception(const string& errmsg, int id)
: _errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
int getid() const
{
return _id;
}
protected:
string _errmsg;
int _id;
};
SQL模块异常
cpp
class SqlException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& sql)
: Exception(errmsg, id)
, _sql(sql)
{}
virtual string what() const override
{
string str = "SqlException:";
str += _errmsg;
str += "->";
str += _sql;
return str;
}
private:
string _sql; // SQL语句
};
缓存模块异常
cpp
class CacheException : public Exception
{
public:
CacheException(const string& errmsg, int id)
: Exception(errmsg, id)
{}
virtual string what() const override
{
string str = "CacheException:";
str += _errmsg;
return str;
}
};
HTTP模块异常
cpp
class HttpException : public Exception
{
public:
HttpException(const string& errmsg, int id, const string& type)
: Exception(errmsg, id)
, _type(type)
{}
virtual string what() const override
{
string str = "HttpException:";
str += _type;
str += ":";
str += _errmsg;
return str;
}
private:
string _type; // HTTP方法类型:GET/POST等
};
5.3 各模块的实现
cpp
#include <thread>
#include <chrono>
// SQL管理模块
void SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
cout << "SQLMgr 调用成功" << endl;
}
// 缓存管理模块
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
cout << "CacheMgr 调用成功" << endl;
SQLMgr();
}
// HTTP服务器模块
void HttpServer()
{
if (rand() % 3 == 0)
{
throw HttpException("请求资源不存在", 100, "GET");
}
else if (rand() % 4 == 0)
{
throw HttpException("权限不足", 101, "POST");
}
cout << "HttpServer 调用成功" << endl;
CacheMgr();
}
5.4 统一捕获处理
cpp
int main()
{
srand(time(0));
while (1)
{
// 模拟服务器持续运行
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer();
}
catch (const Exception& e) // 捕获基类,所有派生类异常都能捕获
{
cout << e.what() << endl;
cout << "错误ID: " << e.getid() << endl;
}
catch (...)
{
cout << "Unknown Exception" << endl;
}
}
return 0;
}
运行效果
bash
HttpServer 调用成功
CacheMgr 调用成功
SQLMgr 调用成功
HttpException:GET:请求资源不存在
错误ID: 100
HttpServer 调用成功
CacheException:数据不存在
错误ID: 101
SqlException:权限不足->select * from name = '张三'
错误ID: 100
...
设计优势
- 统一接口:main函数只需捕获Exception基类
- 扩展性强:添加新模块只需继承Exception
- 信息完整:每个模块可以添加自己特有的信息
- 多态机制:通过虚函数what()实现不同的错误描述
六、总结与展望
6.1 本文要点回顾
异常的概念
- 异常是C++提供的结构化错误处理机制
- 相比C语言的错误码,异常能携带更丰富的信息
- 异常将问题检测和处理分离,代码更清晰
异常的抛出与捕获
- 使用
throw抛出异常对象 - 使用
try-catch捕获并处理异常 - 抛出异常时会创建对象的副本
栈展开机制
- 异常沿着调用链向上查找匹配的catch
- 展开过程中,已构造的对象会被正确析构
- 未捕获的异常会导致程序终止
异常匹配规则
- 要求类型精确匹配(允许少数转换)
- 派生类可以转换为基类(重要特性)
- 多个catch按顺序匹配,第一个匹配的被执行
catch(...)可以捕获任意类型
实际应用
- 通过继承设计异常体系
- 基类统一接口,派生类添加特有信息
- 实现灵活的错误分类和处理
6.2 下一篇预告
在下一篇文章中,我们将学习异常的高级特性:
- 异常重新抛出:捕获后重新抛出的技巧
- 异常安全:如何避免异常导致的资源泄漏
- 异常规范:noexcept的使用
- 标准库异常:C++标准库的异常体系
- 最佳实践:异常使用的注意事项
通过本文的学习,我们掌握了C++异常机制的基础知识和核心概念。异常是现代C++中不可或缺的特性,正确使用异常能让程序更加健壮和易于维护!
以上就是C++异常机制基础的全部内容,期待在下一篇文章中与你继续探讨异常的高级特性!如有疑问,欢迎在评论区交流讨论!❤️
