文章目录
- [C++ 异常](#C++ 异常)
C++ 异常
我们在上一章了解map是说到过map::at()函数在访问不存在的key时会抛异常,官方也推荐我们使用map::at()函数,那么什么是异常?异常又有什么用?那么我们就一起来了解一下C++的异常吧。
C语言传统处理错误的方式
为什么C++要推出异常这个处理错误的方式,我们首先要了解一下C语言处理错误的方式是怎么样的:
传统的错误处理机制:
- 终止程序,如assert,缺陷: 用户难以接受。如发生内存错误,除0错误时就会终止程序。
- 返回错误码,缺陷: 需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。
cpp
#include <iostream>
#include <vector>
using namespace std;
// 传统错误码方式:层层返回,代码臃肿
int ConnectDB() { return -1; /* 模拟连接失败 */ }
int InitServer() {
int ret = ConnectDB();
if (ret < 0) return ret; // 被迫传递错误
return 0;
}
void TestErrorCode() {
if (InitServer() < 0) {
cout << "Server init failed via error code!" << endl;
}
}
// 异常方式:直接穿透调用链,代码清爽
void ConnectDB_Exc() { throw runtime_error("DB connection failed"); }
void InitServer_Exc() { ConnectDB_Exc(); /* 无需检查返回值 */ }
void TestException() {
try {
InitServer_Exc();
} catch (const exception& e) {
cout << "Server init failed via exception: " << e.what() << endl;
}
}
虽然C语言处理错误的方式比较臃肿,但是C语言真正需要处理的错误普遍还是比较少的。因为需要返回错误的地方通常是:开辟内存空间、打开文件等一些往往是手动操作的。那么为什么C++还要多此一举呢?毕竟在设计中加法未必是能到达很好的设计。
C++异常概念
俗话说:知己知彼才能百战百胜。我们要知道为什么创造C++的大佬给C++设计了异常就要清楚异常到底是为做了哪些事而设计出来的。
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误。
C++ 异常处理由三个关键字组成:
throw:当问题发生时,抛出一个异常对象。try:包裹可能抛出异常的代码(保护代码),其后必须跟随一个或多个catch块。catch:捕获并处理特定类型的异常。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。使用 try/catch 语句的语法如下所示:
cpp
try
{
// 保护的标识代码
}catch( ExceptionName e1 )
{
// catch 块
}catch( ExceptionName e2 )
{
// catch 块
}catch( ExceptionName eN )
{
// catch 块
}
异常的使用
异常的抛出和捕获
异常的抛出和匹配原则
- 匹配原则 :异常对象的类型决定了激活哪个
catch。匹配是严格类型匹配的,但允许派生类对象被基类引用/指针捕获(多态)。 - 栈展开(Stack Unwinding) :抛出异常后,程序会沿着函数调用链向上查找匹配的
catch。在查找过程中,会自动调用当前栈帧中已构造的局部对象的析构函数来释放资源。 - 兜底捕获 :
catch(...)可以捕获任意类型的未知异常。
在函数调用链中异常栈展开匹配原则
- 首先检查
throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则
调到catch的地方进行处理。 - 没有匹配的
catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。 - 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的
catch子句的过程称为栈展开 。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。 - 找到匹配的
catch子句并处理以后,会继续沿着catch子句后面继续执行。
注意:捕获基类和派生类时,基类要在前,派生类要在后。如果是先派生类那么就永远到不了基类。
cpp
catch (const DerivedException& e) { ... }
catch (const BaseException& e) { ... }

栈展开示例:
cpp
void func1() {
throw std::runtime_error("func1 error");
}
void func2() {
func1();
}
void func3() {
func2();
}
int main() {
try {
func3();
} catch (const std::exception& e) {
cout << "caught: " << e.what() << endl;
}
return 0;
}
注意:推荐抛异常时使用catch(const ExceptionName& name) 这种写法,第一这种写法使用了引用不会触发额外的拷贝构造,第二参考下面代码对基类和派生类的抛异常处理。
cpp
class BaseException {
public:
virtual ~BaseException() = default;
virtual string what() const { return "BaseException"; }
};
class DerivedException : public BaseException {
public:
string what() const override { return "DerivedException: Specific Error"; }
};
void ThrowDerived() { throw DerivedException(); }
void TestStackUnwindingAndSlicing() {
try {
ThrowDerived();
}
// 错误示范:按值捕获,发生对象切片,多态失效,只输出 "BaseException"
// catch (BaseException e) { cout << e.what() << endl; }
// 正确示范:按常量引用捕获,保留多态特性,输出 "DerivedException: Specific Error"
catch (const BaseException& e) {
cout << e.what() << endl;
}
catch (...) {
cout << "Unknown Exception Caught!" << endl;
}
}
异常的重新抛出
有时候单个 catch 不能完全处理异常,只做部分清理,然后希望交给外层继续处理。
用法:
cpp
catch (...) {
// 清理
throw; // 重新抛出当前异常
}
注意:
throw;不带表达式,表示重新抛出当前异常。throw e;会重新拷贝一份异常对象,可能切片。throw;只能在catch中,或从catch调用的函数中,表示重新抛出当前异常。
cpp
double Division(int a, int b) {
if (b == 0) {
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func() {
int* array = new int[10];
try {
int len, time;
std::cin >> len >> time;
std::cout << Division(len, time) << std::endl;
} catch (...) {
// 如果不清理就会导致内存泄漏
std::cout << "delete [] " << array << std::endl;
delete[] array;
throw; // 重新抛出,交给外层处理
}
std::cout << "delete [] " << array << std::endl;
delete[] array;
}
int main() {
try {
Func();
} catch (const char* errmsg) {
cout << errmsg << endl;
} catch (...) {
cout << "unknown exception" << endl;
}
return 0;
}
异常安全
异常安全的核心问题:抛出异常后,资源是否正确释放?对象是否处于有效状态?
- 构造函数完成对象构造和初始化,最好不要在构造函数中抛异常,否则可能导致对象不完整或没有完全初始化。
- 析构函数主要完成资源清理,最好不要在析构函数内抛异常,否则可能导致资源泄漏。
- C++ 没有垃圾回收,异常容易导致内存泄漏、死锁。
- 常用 RAII 解决资源管理问题。
那么为什么析构函数绝对不能抛出异常?
因为当程序正在处理一个异常(进行栈展开)时,会自动调用局部对象的析构函数。如果此时析构函数又抛出了一个新的异常,C++运行时机制无法同时处理两个异常,会直接调用 std::terminate() 导致程序强制崩溃。
如果构造函数中可能发生异常,解决方法很简单使用一个智能指针就可以解决,因为智能指针是RAII的,智能指针我们后面会重点了解。
异常规范
C++98 异常规范 :在函数声明后使用 throw(type1, type2) 声明可能抛出的异常类型;throw() 表示不抛异常。但这在C++11中被废弃 ,因为编译器难以在编译期严格检查,且运行时检查开销大。
C++11 noexcept :用于明确承诺函数不会抛出异常 。如果 noexcept 函数内部抛出了异常,程序会直接调用 std::terminate 终止。
cpp
// 旧式异常规范:
void fun() throw(A, B, C, D); // 可能抛 A/B/C/D
void* operator new(std::size_t size) throw(std::bad_alloc);
void* operator delete(std::size_t size, void* ptr) throw(); // 不抛
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
cpp
void noThrowFunc() noexcept {
// 不能抛异常;如果抛,会 std::terminate
}
void mayThrowFunc() {
throw std::runtime_error("error");
}
int main() {
try {
noThrowFunc();
mayThrowFunc();
} catch (const std::exception& e) {
cout << e.what() << endl;
}
return 0;
自定义异常体系
实际项目中,如果每个人随意抛异常,外层调用者无法统一处理。所以公司通常会定义一套继承体系:
- 定义一个基类
Exception。 - 所有异常继承自它。
- 外层只捕获基类引用,利用多态调用
what()。 - 派生类可以携带更多信息,比如 SQL 语句、HTTP 类型等。

cpp
// 1. 自定义异常基类
class BaseException : public exception {
public:
BaseException(const string& errmsg, int id)
: _errmsg(errmsg), _id(id) {}
// 使用 virtual 保证多态,noexcept 保证不抛异常
virtual const char* what() const noexcept override {
return _errmsg.c_str();
}
int GetID() const { return _id; }
protected:
string _errmsg;
int _id;
};
// 2. 派生类:数据库异常
class SqlException : public BaseException {
public:
SqlException(const string& errmsg, int id, const string& sql)
: BaseException(errmsg, id), _sql(sql) {}
virtual const char* what() const noexcept override {
// 这里为了演示简单拼接,实际应避免在 what 中频繁分配内存
static string fullMsg;
fullMsg = "SqlException: " + _errmsg + " | SQL: " + _sql;
return fullMsg.c_str();
}
private:
string _sql;
};
void MockDbQuery() {
// 模拟数据库查询失败
throw SqlException("权限不足", 1001, "SELECT * FROM users");
}
void TestCustomException() {
try {
MockDbQuery();
}
// 重点:必须使用引用捕获,防止对象切片,保证多态生效
catch (const BaseException& e) {
cout << "捕获业务异常: " << e.what() << " (ErrorID: " << e.GetID() << ")" << endl;
}
catch (const exception& e) {
cout << "捕获标准库异常: " << e.what() << endl;
}
catch (...) {
cout << "捕获未知异常" << endl;
}
}
C++标准库的异常体系
C++ 标准库定义了一系列异常,以父子类层次组织。
核心基类:
cpp
exception
常见标准异常:
| 异常 | 说明 | 异常 | 说明 |
|---|---|---|---|
std::exception |
所有标准 C++ 异常的父类 | std::bad_alloc |
new 失败抛出 |
std::bad_cast |
dynamic_cast 引用失败抛出 |
std::bad_exception |
处理无法预期异常时有用 |
std::bad_typeid |
typeid 抛出 |
std::logic_error |
理论上可通过读代码检测到的异常 |
std::domain_error |
无效数学域 | std::invalid_argument |
无效参数 |
std::length_error |
创建太长的 std::string 等 |
std::out_of_range |
vector::at、bitset::operator[] 等越界 |
std::runtime_error |
理论上不能通过读代码检测到的异常 | std::overflow_error |
数学上溢 |
std::range_error |
存储超出范围 | std::underflow_error |
数学下溢 |
头文件:
<exception>:std::exception<stdexcept>:logic_error、runtime_error等<new>:bad_alloc<typeinfo>:bad_cast、bad_typeid

cpp
int main() {
try {
std::vector<int> v(10, 5);
v.reserve(1000000000); // 可能抛 bad_alloc 或 length_error
v.at(10) = 100; // 抛 out_of_range
} catch (const std::exception& e) {
cout << e.what() << endl;
} catch (...) {
cout << "Unknown Exception" << endl;
}
return 0;
}
异常的优缺点
优点:
- 异常对象可以清晰准确地展示错误信息,甚至包含堆栈信息。
- 相比错误码,不需要层层返回错误,异常会直接跳到能处理的
catch。 - 很多第三方库使用异常,比如
Boost、gtest、gmock。 - 构造函数没有返回值,不方便用错误码,异常更合适。
- 运算符重载、
operator[]等不方便返回错误码,异常更自然。
缺点:
- 异常会导致执行流乱跳,调试和分析困难。
- 异常有性能开销,但现代硬件下不抛时开销很小,抛出时开销大。
- C++ 没有垃圾回收,异常容易导致内存泄漏、死锁。
- C++ 标准库异常体系设计不够好用,很多公司自定义异常体系。
- 异常规范很重要,随意抛异常会让外层捕获者痛苦。
我们再回到最初的问题:为什么异常又有什么用?创造C++的大佬又为什么要设计异常?
经过上述对异常的了解,这两个问题我们就好回答了,
- 异常有什么用呢?它让我们能快速定位到错误,不需要像C语言那样层层返回错误;
- 创造C++大佬为什么要设计异常呢?Bjarne 设计异常是为了让 C++ 在大型系统和库开发中能优雅地处理错误。
完