尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【C++进阶】异常

【C++进阶】异常 目录1 为什么需要异常C 语言错误处理的痛点2 throw try catch 基础语法throw抛出异常try受监控代码块catch捕获处理异常3 栈展开stack‑unwind核心原理4 catch 异常匹配规则5 工程实践自定义异常继承体系6 异常重新抛出 throw;7 异常安全与资源泄漏问题示例内存泄漏场景8 noexcept 异常说明C11C98 旧异常说明已经废弃C11 noexcept9 C 标准库异常继承体系10 面试高频问答总结Q1什么叫栈展开Q2捕获异常为什么要用引用传值捕获会发生什么Q3重新抛出异常throw;和throw e;区别Q4异常带来什么安全问题怎么解决Q5noexcept 是做什么的编译器会检查函数会不会抛异常吗Q6catch (...) 作用Q7 C 语言错误码和 C 异常对比1 为什么需要异常C 语言错误处理的痛点C 语言处理运行时错误依靠返回错误码。// C语言风格 int Div(int a, int b, int* ret) { if(b 0) { return -1; // 返回错误码 } *ret a / b; return 0; }错误码模式的缺点每调用一个函数都必须手动检查返回值代码充斥大量if判断错误码只是数字需要查表才能知道具体错误信息携带信息有限多层函数调用时每一层都要向上转发错误码业务逻辑和错误处理代码耦合在一起很容易忘记判断返回值错误被直接忽略程序悄悄出错。C 引入异常机制核心思想错误检测逻辑和错误处理逻辑解耦。下层函数检测到错误throw抛出异常对象对象可以携带完整错误信息上层使用try‑catch捕获并处理异常下层不需要关心上层如何处理错误不需要层层转发错误码。注意异常不是用来处理普通逻辑错误专门用于运行时不可预期的错误除零、内存分配失败、网络请求失败、数据库执行失败等。2 throw try catch 基础语法throw抛出异常throw后面跟一个任意类型对象可以是内置类型、字符串、自定义类对象。执行throw语句之后throw 后面的代码不会再执行抛出的时候会生成异常对象的拷贝。因为抛出的可能是局部对象局部对象函数结束就销毁拷贝一份异常对象存放在特殊区域供 catch 捕获使用抛出之后程序不再顺序执行进入栈展开流程。try受监控代码块把有可能抛出异常的代码放在try{}大括号内。 如果 try 块内部代码抛出异常就会去匹配后面的catch 如果没有异常所有 catch 会直接跳过。catch捕获处理异常catch(类型 对象)匹配对应类型的异常对象做错误处理。示例除法除零异常演示#includeiostream #includestring using namespace std; double Divide(int a, int b) { if(b 0) { string err Divide by zero condition!; throw err; // 抛出string类型异常对象 } return (double)a / b; } int main() { int a, b; while(cin a b) { try { double ret Divide(a,b); cout 结果 ret endl; } catch(const string e) { cout 捕获异常 e endl; } } return 0; }3 栈展开stack‑unwind核心原理栈展开是 C 异常最核心的底层机制也是笔试面试高频考点。函数调用链示例main() → Func3() → Func2() → Func1()在最内层Func1()抛出异常首先看throw是否处于当前函数的try块里面如果是尝试匹配本函数后面的catch如果没有匹配的 catch退出当前函数当前函数栈上所有局部对象自动调用析构函数销毁回到上层调用函数继续查找try‑catch重复上面的流程一层一层向外回溯如果回溯到main函数仍然找不到任何匹配的 catch程序调用库函数std::terminate()直接终止整个进程。关键点抛出异常向外回溯的时候每退出一层函数该函数栈上的局部对象全部析构。 只有堆上 new 出来的对象不会自动释放这就是异常安全资源泄漏问题的来源。流程图文字描述Func1 throw异常→ 本函数无匹配 catch销毁 Func1 局部对象回到 Func2 → Func2 无匹配 catch销毁 Func2 局部对象回到 Func3 → Func3 无匹配 catch销毁 Func3 局部对象回到 main → main 如果依然没有匹配 catch → terminate () 程序崩溃。4 catch 异常匹配规则优先找距离 throw 抛出点最近、类型匹配的 catch 分支允许的隐式转换只有下面几种允许不会做任意类型转换非 const 对象 → const 引用权限收缩数组 → 数组元素指针函数 → 函数指针派生类对象 → 基类引用大型项目自定义异常体系依靠这条规则catch(...)捕获任意所有类型异常无法拿到异常对象内容一般写在所有 catch 最后做兜底防止程序直接 terminate 崩溃。注意值捕获异常对象会发生对象切片工程中一律使用引用捕获catch(const BaseException e)。错误示范值捕获派生类信息被切片丢失// 不推荐切片 catch(Exception e) { e.what(); }推荐写法引用捕获保留派生类完整信息支持多态catch(const Exception e) { e.what(); }5 工程实践自定义异常继承体系大型后端项目会设计一套统一的异常继承体系一个异常基类提供虚函数what()返回错误描述getid()获取错误码各个业务模块派生子类数据库异常SqlException、缓存异常CacheException、网络异常HttpException上层捕获基类引用利用多态自动调用子类重写的 what ()统一打印日志。完整可运行示例#includeiostream #includestring #includecstdlib #includectime #includethread #includechrono using namespace std; // 异常基类 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; }; // 数据库异常 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 SQL语句:; 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 override { string str CacheException:; str _errmsg; return str; } }; //http网络异常 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: const string _type; }; 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(); } void HttpServer() { if(rand()%3 0) { throw HttpException(请求资源不存在,100,get); } else if(rand()%4 0) { throw HttpException(权限不足,101,post); } cout HttpServer调用成功 endl; CacheMgr(); } int main() { srand((unsigned int)time(nullptr)); while(true) { this_thread::sleep_for(chrono::seconds(1)); try { HttpServer(); } //捕获基类引用多态所有派生异常全部可以捕获 catch(const Exception e) { cout e.what() endl; } catch(...) { cout Unknown Exception endl; } } return 0; }6 异常重新抛出throw;业务场景本层只能处理一部分异常剩下异常自己处理不了需要交给上层调用者。⚠重点语法catch 块里面单独写throw;不要写对象裸 throw。 ❌禁止写throw e;会发生对象切片丢失派生类的子类信息。 ✅裸throw;原样重新抛出原来捕获的异常对象。示例场景发送网络消息102 错误代表网络波动本层可以重试 3 次其他错误不能重试直接重新抛给上层。void _SendMsg(const string s) { if(rand()%2 0) { throw HttpException(网络不稳定发送失败,102,put); } else if(rand()%7 0) { throw HttpException(你已经不是好友发送失败,103,put); } cout 发送成功 endl; } void SendMsg(const string s) { //最多重试3次 for(size_t i 0; i 4; i) { try { _SendMsg(s); break; //发送成功跳出循环 } catch(const Exception e) { //网络波动错误可以重试 if(e.getid() 102) { if(i 3) { //重试耗尽重新抛出给外层 throw; } cout 第 i1 次重试 endl; } else { //其他错误本层处理不了直接重抛 throw; } } } } int main() { srand((unsigned int)time(nullptr)); string str; while(cin str) { try { SendMsg(str); } catch(const Exception e) { cout e.what() \n; } catch(...) { cout Unknown Exception\n; } } return 0; }7 异常安全与资源泄漏问题异常会打断代码顺序执行流如果函数中间抛出异常函数提前退出。栈对象栈展开自动析构不会泄漏堆 new 出来内存、锁、文件句柄不会自动释放会资源泄漏。示例内存泄漏场景void Func() { int* arr new int[10]; //堆内存 int a,b; cin a b; double ret Divide(a,b); //这里可能抛异常 cout ret endl; delete[] arr; // 如果抛异常这行永远不会执行内存泄漏 }两种解决方案catch 捕获异常释放资源之后再裸 throw; 重新抛出异常交给上层。void Func() { int* arr new int[10]; int a,b; cin a b; try { double ret Divide(a,b); cout ret endl; } catch(...) { delete[] arr; throw; //释放完资源继续把异常向外抛 } delete[] arr; }工程推荐方案RAII资源获取即初始化智能指针。用对象管理资源栈展开的时候对象析构自动释放内存、锁。不需要手动在 catch 写释放逻辑。重要编码规范不要让异常逃离析构函数Effective C 条款 8 如果析构函数内部抛出异常此时程序已经处于栈展开流程会直接触发terminate()程序直接崩溃。析构函数内部捕获处理全部异常不要向外抛。8 noexcept 异常说明C11C98 旧异常说明已经废弃// 只会抛出bad_alloc异常 void* operator new(size_t size) throw(std::bad_alloc); // 不会抛出任何异常 void* operator delete(size_t size,void* ptr) throw();这套语法实践中实用性很差编译器不强制校验C11 被废弃。C11 noexceptvoid func() noexcept;声明这个函数承诺不会抛出异常⚠只是编译期声明编译器不会强制检查函数内部代码如果noexcept修饰的函数内部真的抛出异常程序直接调用terminate()终止。noexcept(表达式)运算符编译期判断表达式是否可能抛出异常返回 bool。#includeiostream using namespace std; double Divide(int a,int b) noexcept { if(b 0) { throw 除零错误; // 声明noexcept还抛异常运行直接terminate } return (double)a/b; } int main() { cout noexcept(Divide(1,2)) endl; //0该表达式可能抛异常 cout noexcept(12) endl; //1不会抛异常 return 0; }STL 容器大量接口使用noexcept修饰比如size() begin() end()。9 C 标准库异常继承体系C 标准库所有异常全部继承基类std::exception。 虚函数virtual const char* what() const noexcept;返回异常信息字符串。两大分支std::logic_error逻辑错误属于程序 bug理论写代码阶段可以避免。invalid_argument无效参数length_error容器长度超限out_of_range下标越界vector.at () string.at () 抛出std::runtime_error真正运行时才发生无法提前预判。overflow_error溢出、range_error范围错误独立异常子类std::bad_allocnew 分配内存失败抛出std::bad_castdynamic_cast 向下转换引用失败std::bad_typeidstd::bad_exception开发中捕获标准库异常写法catch(const std::exception e) { cout e.what() endl; }10 面试高频问答总结Q1什么叫栈展开throw 抛出异常之后停止顺序执行逐层退出函数调用栈每一层函数栈局部对象析构一层层向外寻找匹配的 catch。直到 main 函数还找不到匹配 catch调用 terminate 终止程序。Q2捕获异常为什么要用引用传值捕获会发生什么如果传值捕获会发生对象切片派生类独有的成员被切掉多态失效丢失真实异常信息。引用捕获保留原始异常对象完整信息支持多态。Q3重新抛出异常throw;和throw e;区别throw;裸抛原样把捕获到的原始异常重新抛出不会切片。throw e;会拷贝生成局部对象发生切片丢失派生类信息业务禁止这么写。Q4异常带来什么安全问题怎么解决异常打断执行流如果中间有堆内存、锁没有释放会资源泄漏。 方案 1catch 捕获释放资源再 throw 重抛 方案 2优先 RAII对象生命周期管理资源栈展开自动调用析构释放。 析构函数不要向外抛出异常否则会直接 terminate 崩溃。Q5noexcept 是做什么的编译器会检查函数会不会抛异常吗noexcept 用来声明函数承诺不抛出异常编译器编译阶段不强制检查如果 noexcept 函数内部抛出异常程序直接 terminate。Q6catch (...) 作用捕获任意类型的异常但是拿不到异常对象信息一般放在所有 catch 最后做兜底保护防止程序直接终止。Q7 C 语言错误码和 C 异常对比错误码每处调用都要判断返回值错误信息携带弱容易被忽略 异常错误检测与处理分离异常对象可以携带完整错误信息但是会有运行时代价不能滥用只用于运行时错误。补充异常不适合嵌入式、对运行时间极度敏感场景。拓展思考异常开销抛出异常路径会生成异常表正常无异常的路径几乎没有性能损耗只有真正抛出异常的时候才有开销。
返回列表