当前位置: 首页 > news >正文

《C++异常处理完全指南》

✨✨小新课堂开课了,欢迎欢迎~✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++:由浅入深篇

小新的主页:编程版小新-CSDN博客

一.异常的概念和使用

异常的概念

在C++中,异常是一种处理程序运行时错误或异常情况的机制。它允许程序在遇到错误时,将控制权从错误的部分转移到可以处理该错误的部分。

异常处理使得程序更加健全,并且可以更优雅的处理错误情况,而不是直接崩溃。

C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息。

int divide(int a, int b, int* result)
{if (b == 0){errno = EINVAL;return EINVAL;//返回错误码}*result = a / b;return 0;
}
int main()
{int res;int ret = divide(10, 0, &res);if (ret != 0){perror("Division failed");return 1;}printf("%d\n", res);return 0;
}

异常处理关键字

throw:当程序出现错误时,可以用throw关键字抛出一个异常。抛出的异常可以是任意类型的对象。当throw被执行时,throw后面的语句将不再被执行。

try:用于包含可能会抛出异常的代码。try块后面通常会跟一个或者多个catch块。抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个局部对象,所以会生成一个拷贝对象,这个拷贝对象会在catch子句后销毁。

catch:用来捕获并处理异常。每个catch块指定它可以处理的异常类型。当异常被抛出时,程序会寻找匹配的且离抛出异常位置最近的那一个catch块来处理它。catch可能是同一函数中的一个局部的catch,也可能是另一个函数中的catch,控制权从throw位置转移到了catch位置。

**示例**

double Divide(int a, int b)
{try{// 当b == 0时抛出异常if (b == 0){string s("Divide by zero condition!");throw s;//抛出的是s的拷贝,s在当前局部域,出了作用域就销毁了cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;} else{return ((double)a / (double)b);}} catch(int errid){cout << errid << endl;} return 0;
} void Func()
{int len, time;cin >> len >> time;try{cout << Divide(len, time) << endl;} catch(const char* errmsg){cout << errmsg << endl;} cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
} int main()
{while (1){try{Func();} catch(const string & errmsg){cout << errmsg << endl;}} return 0;
}

**调试演示**

异常调试演示

我们第一组输入的是10和0,在Divide函数内throw抛出一个异常,这个异常是个string对象,可以注意到,当throw被执行时,throw后面的cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句没有被执行。随后catch进行捕捉,我们发现是匹配到了主函数内catch。因为与其他的catch类型不匹配。第二组输入的3和4。不存在异常,调用完Divide函数后,执行了cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;语句后进入的下一轮的循环。

**运行结果**

栈展开

抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句,首先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理,如果找到匹配的catch子句处理后,catch子句代码会继续执行。如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开。

还是以上面的代码为例。在DIvide函数中的try块内部,throw抛出一个异常。该异常是一个string对象。接下里去寻找匹配的catch语句,由于与当前函数的cach子句类型不匹配,就退出了当前函数,继续从外层的调用链中查找,来到了Func函数,依旧类型不匹配,最后来到了主函数,类型匹配,异常得到处理。

如果到达main函数,依旧没有找到匹配的catch子句,程序会调调标准库的 terminate 函数终止程序。

查找匹配的处理代码

一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近那个,但是也有一些例外。

 允许从非常量向常量的类型转换,也就是权限缩小。因为常量引用或指针只承诺不修改对象,而非常量对象完全满足这个条件。

try
{throw 45;//抛出int类型(非常量)
}
catch (const int& e)//可以用常量引用捕获
{//
}

允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针。在C++中,数组和函数在大多数表达式中会自动转换为指针。(数组退化为指向数组首元素的指针,函数退化为函数值指针,保持与C的兼容)

try
{throw"Hello";//抛出const char[6]类型,自动转化为const char*
}
catch (const char* e)
{//...
}

允许从派生类向基类类型的转换。我们通常使用继承  来建立类之间的层次关系,基类表示一般概念,派生类表示更具体的概念。在异常处理中,我们通常希望捕获基类可以处理所有派生类异常。这样我们可以用基类捕获整个类族的异常,而不用为每个派生类编写单独的catch块。

如果到main函数,异常仍旧没有被匹配就会终止程序,不是发生严重错误的情况下,我们是不期望程序终止的,所以一般main函数中最后都会使用catch(...),它可以捕获任意类型的异常,但是是不知道异常错误是什么。

**示例**

class Exception
{public :Exception(const string& errmsg, int id): _errmsg(errmsg), _id(id){}virtual string what() const //what是个虚函数,派生类可以重写{return _errmsg;} int getid() const{return _id;}
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 //对what进行重写后,给出更详细的错误信息{string str = "SqlException:";str += _errmsg;str += "->";str += _sql;return str;}
private:const string _sql;
};
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;}
};
class HttpException : public Exception
{public :HttpException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}
private:const string _type;
};
void SQLMgr()
{if (rand() % 7 == 0){throw SqlException("权限不足", 100, "select * from name = '张三'");} else{cout << "SQLMgr 调用成功" << endl;}
}void CacheMgr()
{if (rand() % 5 == 0){throw CacheException("权限不足", 100);} else if (rand() % 6 == 0){throw CacheException("数据不存在", 101);} else{cout << "CacheMgr 调用成功" << endl;} SQLMgr();
} void HttpServer()
{if (rand() % 3 == 0){throw HttpException("请求资源不存在", 100, "get");} else if (rand() % 4 == 0){throw HttpException("权限不足", 101, "post");} else{cout << "HttpServer调用成功" << endl;} CacheMgr();
} int main()
{srand(time(0));while (1){this_thread::sleep_for(chrono::seconds(1));try{HttpServer();} catch(const Exception & e) // 这里捕获基类,基类对象和派生类对象都可以被捕获{cout << e.what() << endl;} catch(...)//捕获任意类型的异常{cout << "Unkown Exception" << endl;}} return 0;
}

**运行结果**

异常的重新抛出

在异常处理中,重新抛出是指在捕获一个异常后不完全处理它,而是将其再次抛出,让更高层的调用者继续处理。

下面程序模拟展示了聊天时发送消息,发送失败捕获异常,但是可能在电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的错误,捕获后也要重新抛出。

class Exception
{
public:Exception(const string& errmsg, int id): _errmsg(errmsg), _id(id){}virtual string what() const //what是个虚函数,派生类可以重写{return _errmsg;}int getid() const{return _id;}
protected:string _errmsg;int _id;
};class HttpException : public Exception
{
public:HttpException(const string& errmsg, int id, const string& type): Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException:";str += _type;str += ":";str += _errmsg;return str;}
private:const string _type;
};void _SeedMsg(const string& s)
{if (rand() % 2 == 0){throw HttpException("网络不稳定,发送失败", 102, "put");} else if (rand() % 7 == 0){throw HttpException("你已经不是对象的好友,发送失败", 103, "put");} else{cout << "发送成功" << endl;}
} void SendMsg(const string& s)
{// 发送消息失败,则再重试3次for (size_t i = 0; i < 4; i++){try{_SeedMsg(s);break;} catch(const Exception & e){// 捕获异常,if中是102号错误,网络不稳定,则重新发送// 捕获异常,else中不是102号错误,则将异常重新抛出if (e.getid() == 102){// 重试三次以后否失败了,则说明网络太差了,重新抛出异常if (i == 3)throw;cout << "开始第" << i + 1 << "重试" << endl;} else{throw;}}}
} int main()
{srand(time(0));string str;while (cin >> str){try{SendMsg(str);} catch(const Exception & e){cout << e.what() << endl << endl;} catch(...){cout << "Unkown Exception" << endl;}} return 0;
}

**运行结果**

异常安全问题

异常抛出后,后面的代码就不再执行,前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,这里由于异常就引发了资源泄漏,产生安全性的问题。

double Divide(int a, int b)
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";} return(double)a / (double)b;
}
void Func()
{// 这⾥可以看到如果发⽣除0错误抛出异常,另外下面的array没有得到释放。// 所以这⾥捕获异常后并不处理异常,异常还是交给外层处理,这⾥捕获了再// 重新抛出去。int* array = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;} catch(...){// 捕获异常释放内存cout << "delete []" << array << endl;delete[] array;throw; // 异常重新抛出,捕获到什么抛出什么} cout << "delete []" << array << endl;delete[] array;
} 
int main()
{try{Func();} catch(const char* errmsg){cout << errmsg << endl;} catch(const exception & e){cout << e.what() << endl;} catch(...){cout << "Unkown Exception" << endl;} return 0;
}

异常规范

对于用户和编译器而言,预先知道某个程序会不会抛出异常大有裨益,知道某个函数是否会抛出异常有助于简化调用函数的代码。

C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2...)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。

// C++98
// 这⾥表⽰这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这⾥表⽰这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();

C++98的这种方式过于复杂,实践中并不好用,C++11中进行了简化,函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。

// C++11
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;

 编译器并不会在编译时检查noexcept,也就是说如果一个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是一个声明了noexcept的函数抛出了异常,程序会调用terminate 终止程序。

double Divide(int a, int b) noexcept
{// 当b == 0时抛出异常if (b == 0){throw "Division by zero condition!";} return(double)a / (double)b;
} 

 noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会,注意这里说的是可能,则返回false,不会就返回true。

**示例**

int main()
{int i = 0;cout << noexcept(Divide(1, 2)) << endl;cout << noexcept(Divide(1, 0)) << endl;cout << noexcept(++i) << endl;
}

**运行结果**

二.标准库的异常

C++标准库也定义了一套自己的一套异常继承体系库,基类是exception,所以我们日常写程序,需要在主函数捕获exception即可,要获取异常信息,就调用what函数。

**示例**

#include <exception>
#include <string>// 基础业务异常(通用场景)
class BusinessException : public std::exception //继承exception就可以
{
public:explicit BusinessException(const std::string& msg): msg_(msg) {}const char* what() const noexcept override {return msg_.c_str();}private:std::string msg_;
};// 具体领域异常(推荐继承最匹配的标准异常)
class DatabaseException : public std::runtime_error //也可以继承更具体的标准异常
{
public:explicit DatabaseException(const std::string& msg, int error_code = 0): std::runtime_error(msg),error_code_(error_code) {}int error_code() const noexcept {return error_code_;}private:int error_code_;
};

创作不易,还请各位大佬支持~

http://www.lryc.cn/news/624389.html

相关文章:

  • 如何在 Ubuntu 24.04 或 22.04 LTS 上安装 PowerShell
  • Spring Boot 实用小技巧:多级缓存(Caffeine + Redis)- 第545篇
  • 【网络安全实验报告】实验四: PGP邮件加密软件应用
  • C++STL之list详解
  • 【Linux指南】gcc/g++编译器:从源码到可执行文件的全流程解析
  • 8.18 机器学习-决策树(1)
  • goland怎么取消自动删除未使用的包
  • SWMM排水管网水力、水质建模及在海绵与水环境中的应用技术-模拟降雨和污染物质经过地面、排水管网、蓄水和处理
  • 【前端面试题】JavaScript 核心知识点解析(第一题到第三十题)
  • 2025 世界机器人大会启示录:机构学 × AI × 视频链路的融合之路
  • 从零开始部署经典开源项目管理系统最新版redmine6-Linux Debian12
  • 粉刷房子(简单多状态dp问题)
  • 场外期权的股票停牌了怎么处理?
  • 226. 翻转二叉树
  • 《Unity Shader入门精要》学习笔记二
  • IOPaint 远程修图:cpolar 内网穿透服务实现跨设备图片编辑
  • 旧物回收小程序的商业变现路径探索
  • LeetCode 刷题【45. 跳跃游戏 II】
  • nuScence数据集
  • AI应用商业化加速落地 2025智能体爆发与端侧创新成增长引擎
  • 【2025CVPR-目标检测方向】RaCFormer:通过基于查询的雷达-相机融合实现高质量的 3D 目标检测
  • 机器学习(决策树)
  • 【音视频】瑞芯微、全志芯片在运动相机和行车记录仪产品分析
  • 从决策树基础到熵与信息增益
  • 机器学习的多种算法
  • 常见的光源频闪控制方式
  • 20. 云计算-Service MeshServerless
  • 用本地代理 + ZIP 打包 + Excel 命名,优雅批量下载跨域 PDF
  • 基于 ONNX Runtime 的 YOLOv8 高性能 C++ 推理实现
  • Pomian语言处理器 研发笔记(一):使用C++的正则表达式构建词法分析器