
目录
[1 为什么需要异常?C 语言错误处理的痛点](#1 为什么需要异常?C 语言错误处理的痛点)
[2 throw try catch 基础语法](#2 throw try catch 基础语法)
[3 栈展开(stack‑unwind)核心原理](#3 栈展开(stack‑unwind)核心原理)
[4 catch 异常匹配规则](#4 catch 异常匹配规则)
[5 工程实践:自定义异常继承体系](#5 工程实践:自定义异常继承体系)
[6 异常重新抛出 throw;](#6 异常重新抛出 throw;)
[7 异常安全与资源泄漏问题](#7 异常安全与资源泄漏问题)
[8 noexcept 异常说明(C++11)](#8 noexcept 异常说明(C++11))
[C++98 旧异常说明(已经废弃)](#C++98 旧异常说明(已经废弃))
[C++11 noexcept](#C++11 noexcept)
[9 C++ 标准库异常继承体系](#9 C++ 标准库异常继承体系)
[10 面试高频问答总结](#10 面试高频问答总结)
[Q3:重新抛出异常throw;和throw e;区别?](#Q3:重新抛出异常throw;和throw e;区别?)
[Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?](#Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?)
[Q6:catch (...) 作用?](#Q6:catch (...) 作用?)
[Q7 C 语言错误码和 C++ 异常对比](#Q7 C 语言错误码和 C++ 异常对比)
1 为什么需要异常?C 语言错误处理的痛点
C 语言处理运行时错误,依靠返回错误码。
cpp
// 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(类型 对象),匹配对应类型的异常对象,做错误处理。
示例:除法除零异常演示
cpp
#include<iostream>
#include<string>
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)。
错误示范:值捕获,派生类信息被切片丢失
cpp
// 不推荐,切片!
catch(Exception e)
{
e.what();
}
推荐写法,引用捕获,保留派生类完整信息,支持多态:
cpp
catch(const Exception& e)
{
e.what();
}
5 工程实践:自定义异常继承体系
大型后端项目,会设计一套统一的异常继承体系:
- 一个异常基类 ,提供虚函数
what()返回错误描述,getid()获取错误码; - 各个业务模块派生子类:数据库异常
SqlException、缓存异常CacheException、网络异常HttpException; - 上层捕获基类引用,利用多态自动调用子类重写的 what (),统一打印日志。
完整可运行示例:
cpp
#include<iostream>
#include<string>
#include<cstdlib>
#include<ctime>
#include<thread>
#include<chrono>
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 次;其他错误不能重试,直接重新抛给上层。
cpp
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 << "第" << i+1 << "次重试" << 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; 重新抛出异常交给上层。
cpp
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 异常说明(C++11)
C++98 旧异常说明(已经废弃)
cpp
// 只会抛出bad_alloc异常
void* operator new(size_t size) throw(std::bad_alloc);
// 不会抛出任何异常
void* operator delete(size_t size,void* ptr) throw();
这套语法实践中实用性很差,编译器不强制校验,C++11 被废弃。
C++11 noexcept
void func() noexcept;- 声明这个函数承诺不会抛出异常;
- ⚠只是编译期声明,编译器不会强制检查函数内部代码;
- 如果
noexcept修饰的函数内部真的抛出异常,程序直接调用terminate()终止。
noexcept(表达式)运算符:编译期判断表达式是否可能抛出异常,返回 bool。
cpp
#include<iostream>
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(1+2) << endl; //1,不会抛异常
return 0;
}
STL 容器大量接口使用
noexcept修饰,比如size() begin() end()。
9 C++ 标准库异常继承体系

C++ 标准库所有异常全部继承基类 std::exception。 虚函数:virtual const char* what() const noexcept; 返回异常信息字符串。
两大分支:
std::logic_error:逻辑错误,属于程序 bug,理论写代码阶段可以避免。std::runtime_error:真正运行时才发生,无法提前预判。overflow_error溢出、range_error范围错误
独立异常子类:
std::bad_alloc:new 分配内存失败抛出std::bad_cast:dynamic_cast 向下转换引用失败std::bad_typeidstd::bad_exception
开发中捕获标准库异常,写法:
cpp
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:异常带来什么安全问题?怎么解决?
异常打断执行流,如果中间有堆内存、锁没有释放,会资源泄漏。 方案 1:catch 捕获,释放资源,再 throw 重抛; 方案 2:优先 RAII,对象生命周期管理资源,栈展开自动调用析构释放。 析构函数不要向外抛出异常,否则会直接 terminate 崩溃。
Q5:noexcept 是做什么的,编译器会检查函数会不会抛异常吗?
noexcept 用来声明函数承诺不抛出异常;编译器编译阶段不强制检查;如果 noexcept 函数内部抛出异常,程序直接 terminate。
Q6:catch (...) 作用?
捕获任意类型的异常,但是拿不到异常对象信息;一般放在所有 catch 最后,做兜底保护,防止程序直接终止。
Q7 C 语言错误码和 C++ 异常对比
错误码:每处调用都要判断返回值,错误信息携带弱,容易被忽略; 异常:错误检测与处理分离,异常对象可以携带完整错误信息;但是会有运行时代价,不能滥用,只用于运行时错误。
补充:异常不适合嵌入式、对运行时间极度敏感场景。
拓展思考:异常开销:抛出异常路径会生成异常表;正常无异常的路径几乎没有性能损耗,只有真正抛出异常的时候才有开销。
