异常规范与自定义异常类的设计
大家好~ 上一篇我们聊了 C++ 异常处理的基础------try-catch-throw 的基本用法,学会了如何捕获和处理简单异常。但在实际开发中,仅用基础用法远远不够:比如多个函数抛出不同异常时,如何清晰约束抛出类型?系统自带的异常(比如字符串、int 错误码)无法携带足够的错误信息,该怎么办?
今天我们就来解决这两个问题,聊聊 异常规范 和 自定义异常类的设计,让我们的异常处理更规范、更灵活,适配复杂项目的开发需求。
话不多说,咱们直奔主题,全程搭配可直接运行的示例代码,新手也能轻松跟上~
一、先搞懂:什么是异常规范?
异常规范,简单来说,就是对函数可能抛出的异常类型进行明确声明,告诉调用者"这个函数只会抛出这些类型的异常,其他类型的异常不会从这里抛出"。
为什么需要异常规范?举个例子:如果一个函数没有任何异常声明,调用者无法提前预知它会抛出什么异常,只能用 catch (...) 万能捕获,既不规范,也不利于排查错误;而有了异常规范,调用者可以精准捕获对应类型的异常,代码的可读性和可维护性会大大提升。
1. C++ 中的异常规范写法(两种方式)
C++ 中异常规范有两种常用写法:一种是 C++98 引入的 throw 列表(目前已被弃用,但仍有老代码在使用),另一种是 C++11 引入的 noexcept 关键字(推荐使用,更简洁、更高效)。
(1)C++98 异常规范:throw 列表(弃用,了解即可)
语法格式:在函数声明/定义的末尾,加上 throw(异常类型1, 异常类型2, ...),表示该函数只会抛出括号内列出的异常类型。
cpp
#include <iostream>
#include <string>
using namespace std;
// 异常规范:该函数只会抛出 int 或 string 类型的异常
void func() throw(int, string) {
int flag = 1;
if (flag == 1) {
throw 100; // 允许抛出 int 类型
} else if (flag == 2) {
throw string("函数执行失败"); // 允许抛出 string 类型
}
// 不能抛出其他类型,比如 char*
// throw "错误"; // 编译可能报警告,运行时可能触发 terminate()
}
int main() {
try {
func();
} catch (int errCode) {
cout << "捕获到 int 异常:" << errCode << endl;
} catch (string errMsg) {
cout << "捕获到 string 异常:" << errMsg << endl;
}
return 0;
}
补充说明:
-
如果函数末尾写 throw(),表示该函数不会抛出任何异常;
-
这种写法的缺点:编译器检查不严格(比如上面抛出 char* 类型,有些编译器仅报警告,不会报错),且运行时开销较大,所以 C++11 后被弃用,不推荐在新项目中使用。
(2)C++11 推荐:noexcept 关键字(重点掌握)
noexcept 是 C++11 引入的,用来替代 throw 列表,语法更简洁,编译器检查更严格,运行时开销更小,是目前主流的异常规范方式。
核心用法分两种:
-
noexcept:表示该函数绝对不会抛出任何异常;
-
noexcept(条件):表示当条件为 true 时,函数不会抛出异常;条件为 false 时,可能抛出异常(较少用)。
cpp
#include <iostream>
#include <string>
using namespace std;
// 异常规范:该函数绝对不会抛出任何异常(推荐写法)
void func1() noexcept {
cout << "func1:不会抛出异常" << endl;
// 如果强行抛出异常,编译器会直接调用 terminate(),程序崩溃
// throw 100; // 运行时崩溃,提示 terminate called after throwing an instance of 'int'
}
// 异常规范:该函数可能抛出异常(不写 noexcept 等价于 noexcept(false))
void func2() {
throw string("func2:抛出 string 异常");
}
int main() {
try {
func1();
func2();
} catch (string errMsg) {
cout << "捕获到异常:" << errMsg << endl;
}
return 0;
}
关键注意点:
-
如果一个函数声明为 noexcept,但内部强行抛出异常,程序会直接崩溃(调用 terminate()),不会进入 catch 块;
-
如果函数没有任何异常规范(不写 throw 列表,也不写 noexcept),等价于 noexcept(false),表示"可能抛出任何类型的异常";
-
建议:明确不会抛出异常的函数,一定要加 noexcept(比如工具函数、简单的getter/setter),可以提升程序运行效率;可能抛出异常的函数,无需刻意声明(默认 noexcept(false))。
二、核心重点:自定义异常类的设计
上一篇我们用 int、string 作为异常值抛出,但在实际开发中,这些类型的异常有明显的缺点:
-
携带的信息有限:比如抛出 int 错误码,调用者需要记住每个错误码对应的含义,维护成本高;
-
无法区分异常来源:多个函数都抛出 string 类型异常,无法快速判断异常是来自哪个函数、哪个模块;
-
扩展性差:无法根据需求添加额外的异常信息(比如异常发生的行号、时间、模块名)。
此时,自定义异常类 就派上用场了。我们可以通过类的封装,让异常携带更丰富的信息,区分不同来源的异常,还能实现异常的继承和扩展,适配复杂项目的需求。
1. 自定义异常类的设计原则(必看)
设计自定义异常类时,遵循以下3个原则,能让异常处理更规范、更灵活:
-
继承自 C++ 标准异常类(std::exception):标准异常类提供了基础的异常接口(比如 what() 方法,用来返回异常描述),继承它可以让我们的自定义异常和系统异常兼容,方便统一处理;
-
提供 what() 方法:重写 std::exception 的 what() 方法,返回具体的异常描述信息(比如"文件打开失败,路径:xxx");
-
支持异常信息的自定义:通过构造函数传入异常相关信息(比如错误码、模块名、描述),让异常携带足够的调试信息。
2. 基础示例:自定义异常类(单异常类)
先从最简单的自定义异常类开始,继承 std::exception,重写 what() 方法,支持传入异常描述和错误码。
cpp
#include <iostream>
#include <string>
#include <exception> // 必须包含,std::exception 所在头文件
using namespace std;
// 自定义异常类:继承自 std::exception
class MyException : public exception {
private:
int errCode; // 错误码
string errMsg; // 异常描述
string errModule; // 异常所在模块(可选,提升实用性)
public:
// 构造函数:初始化异常信息(支持自定义错误码、模块、描述)
MyException(int code, const string& module, const string& msg)
: errCode(code), errModule(module), errMsg(msg) {}
// 重写 what() 方法:返回异常描述(必须是 const char* 类型)
virtual const char* what() const noexcept override {
// 拼接异常信息:模块 + 错误码 + 描述
static string fullMsg; // 静态变量,避免返回局部变量地址
fullMsg = "[" + errModule + "] 错误码:" + to_string(errCode) + ",描述:" + errMsg;
return fullMsg.c_str(); // 转换为 const char* 返回
}
// 可选:提供获取错误码、模块的接口,方便后续处理
int getErrCode() const noexcept {
return errCode;
}
string getErrModule() const noexcept {
return errModule;
}
};
// 测试函数:抛出自定义异常
void openFile(const string& filePath) {
// 模拟文件打开失败的场景
if (filePath.empty()) {
// 抛出自定义异常:错误码1001,模块FileOperate,描述文件路径为空
throw MyException(1001, "FileOperate", "文件路径为空,无法打开文件");
}
cout << "文件打开成功:" << filePath << endl;
}
int main() {
try {
// 调用可能抛出自定义异常的函数
openFile(""); // 传入空路径,触发异常
}
// 捕获自定义异常(匹配 MyException 类型)
catch (const MyException& e) {
cout << "捕获到自定义异常:" << e.what() << endl;
cout << "异常模块:" << e.getErrModule() << endl;
cout << "错误码:" << e.getErrCode() << endl;
// 可以根据错误码做不同的处理
if (e.getErrCode() == 1001) {
cout << "建议:请传入有效的文件路径!" << endl;
}
}
// 兜底捕获:防止其他未预料到的异常
catch (...) {
cout << "捕获到未知异常!" << endl;
}
return 0;
}
运行结果:
plaintext
捕获到自定义异常:[FileOperate] 错误码:1001,描述:文件路径为空,无法打开文件
异常模块:FileOperate
错误码:1001
建议:请传入有效的文件路径!
关键点解析:
-
必须包含 头文件,否则无法继承 std::exception;
-
what() 方法必须是 const 修饰,且 noexcept(C++11 后推荐),返回值是 const char*;
-
用静态变量拼接异常信息,避免返回局部变量的地址(局部变量生命周期结束后,地址会失效,导致乱码);
-
捕获自定义异常时,建议用 const 引用(const MyException& e),避免拷贝,提升效率。
3. 进阶示例:自定义异常类的继承(多异常场景)
在复杂项目中,不同模块可能会抛出不同类型的异常(比如文件操作异常、网络异常、数据库异常),此时可以设计一个"基类异常",再让各个模块的异常类继承它,实现异常的分层处理。
比如:基类 Exception → 子类 FileException(文件异常)、NetworkException(网络异常),这样调用者可以精准捕获某个模块的异常,也可以捕获基类异常,实现统一处理。
cpp
#include <iostream>
#include <string>
#include <exception>
using namespace std;
// 异常基类:继承自 std::exception(所有自定义异常的父类)
class BaseException : public exception {
private:
int errCode;
string errMsg;
string errModule;
public:
BaseException(int code, const string& module, const string& msg)
: errCode(code), errModule(module), errMsg(msg) {}
virtual const char* what() const noexcept override {
static string fullMsg;
fullMsg = "[" + errModule + "] 错误码:" + to_string(errCode) + ",描述:" + errMsg;
return fullMsg.c_str();
}
int getErrCode() const noexcept { return errCode; }
string getErrModule() const noexcept { return errModule; }
};
// 子类1:文件异常(继承自基类异常)
class FileException : public BaseException {
public:
// 构造函数:调用父类构造函数,固定模块为FileModule
FileException(int code, const string& msg)
: BaseException(code, "FileModule", msg) {}
};
// 子类2:网络异常(继承自基类异常)
class NetworkException : public BaseException {
public:
// 构造函数:调用父类构造函数,固定模块为NetworkModule
NetworkException(int code, const string& msg)
: BaseException(code, "NetworkModule", msg) {}
};
// 测试函数1:文件操作,抛出 FileException
void readFile(const string& filePath) {
if (filePath.find(".txt") == string::npos) {
// 抛出文件异常:错误码2001,描述不是txt文件
throw FileException(2001, "文件格式错误,仅支持.txt文件");
}
cout << "读取文件成功:" << filePath << endl;
}
// 测试函数2:网络连接,抛出 NetworkException
void connectNetwork(const string& ip) {
if (ip == "127.0.0.1") {
// 抛出网络异常:错误码3001,描述本地IP无法连接
throw NetworkException(3001, "本地IP(127.0.0.1)无法建立远程连接");
}
cout << "网络连接成功:" << ip << endl;
}
int main() {
try {
readFile("test.docx"); // 触发文件异常
connectNetwork("127.0.0.1"); // 触发网络异常(不会执行,因为上面已经抛出异常)
}
// 捕获文件异常(子类异常,优先匹配)
catch (const FileException& e) {
cout << "捕获到文件异常:" << e.what() << endl;
}
// 捕获网络异常(子类异常)
catch (const NetworkException& e) {
cout << "捕获到网络异常:" << e.what() << endl;
}
// 捕获基类异常(如果子类异常未被单独捕获,会被基类捕获,实现统一处理)
catch (const BaseException& e) {
cout << "捕获到基础异常:" << e.what() << endl;
}
catch (...) {
cout << "捕获到未知异常!" << endl;
}
return 0;
}
运行结果:
plaintext
捕获到文件异常:[FileModule] 错误码:2001,描述:文件格式错误,仅支持.txt文件
进阶要点:
-
异常子类继承基类后,无需重写 what() 方法(除非需要自定义子类的异常格式),直接复用父类的实现即可;
-
catch 块的顺序很重要:子类异常必须放在基类异常前面!如果先捕获 BaseException,子类异常会被基类捕获,子类的 catch 块永远不会执行;
-
这种分层设计的优势:可以单独处理某个模块的异常(比如文件异常做重试操作),也可以通过捕获基类异常,统一处理所有自定义异常(比如统一记录异常日志)。
三、常见避坑点(重点提醒)
自定义异常类和异常规范很容易踩坑,这部分一定要记好,避免写出无效的异常处理代码:
-
自定义异常类必须继承 std::exception,且重写 what() 方法,否则无法和系统异常兼容,也无法正常捕获;
-
what() 方法的返回值必须是 const char*,且不能返回局部变量的地址(建议用静态变量或堆内存,堆内存需注意释放,避免内存泄漏);
-
noexcept 不要滥用:可能抛出异常的函数,不要声明为 noexcept,否则强行抛出异常会导致程序崩溃;
-
catch 块顺序:子类异常在前,基类异常在后;具体类型异常在前,万能捕获在后;
-
捕获异常时,优先用 const 引用(const 异常类& e),避免拷贝异常对象,提升效率,同时避免切片问题(子类异常被基类对象捕获时,丢失子类特有信息)。
四、总结与实践建议
今天我们掌握了两个核心知识点:异常规范和自定义异常类的设计,总结一下:
-
异常规范:推荐用 C++11 的 noexcept,明确不会抛出异常的函数加 noexcept,提升效率;
-
自定义异常类:继承 std::exception,重写 what() 方法,携带足够的异常信息(错误码、模块、描述),复杂项目建议用"基类+子类"的分层设计;
-
核心目标:让异常处理更规范、更易调试、更易维护,避免滥用万能捕获,让错误处理更精准。
实践建议:
-
把今天的示例代码复制到编译器中运行,亲手修改异常信息、错误码,感受异常的捕获和处理流程;
-
尝试自己设计一个自定义异常子类(比如 DatabaseException 数据库异常),添加特有属性(比如数据库连接地址);
-
在实际项目中,给每个模块设计对应的异常类,统一异常规范,方便后续排查错误和维护。