异常规范与自定义异常类的设计

异常规范与自定义异常类的设计

大家好~ 上一篇我们聊了 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 列表,语法更简洁,编译器检查更严格,运行时开销更小,是目前主流的异常规范方式。

核心用法分两种:

  1. noexcept:表示该函数绝对不会抛出任何异常

  2. 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个原则,能让异常处理更规范、更灵活:

  1. 继承自 C++ 标准异常类(std::exception):标准异常类提供了基础的异常接口(比如 what() 方法,用来返回异常描述),继承它可以让我们的自定义异常和系统异常兼容,方便统一处理;

  2. 提供 what() 方法:重写 std::exception 的 what() 方法,返回具体的异常描述信息(比如"文件打开失败,路径:xxx");

  3. 支持异常信息的自定义:通过构造函数传入异常相关信息(比如错误码、模块名、描述),让异常携带足够的调试信息。

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 块永远不会执行;

  • 这种分层设计的优势:可以单独处理某个模块的异常(比如文件异常做重试操作),也可以通过捕获基类异常,统一处理所有自定义异常(比如统一记录异常日志)。

三、常见避坑点(重点提醒)

自定义异常类和异常规范很容易踩坑,这部分一定要记好,避免写出无效的异常处理代码:

  1. 自定义异常类必须继承 std::exception,且重写 what() 方法,否则无法和系统异常兼容,也无法正常捕获;

  2. what() 方法的返回值必须是 const char*,且不能返回局部变量的地址(建议用静态变量或堆内存,堆内存需注意释放,避免内存泄漏);

  3. noexcept 不要滥用:可能抛出异常的函数,不要声明为 noexcept,否则强行抛出异常会导致程序崩溃;

  4. catch 块顺序:子类异常在前,基类异常在后;具体类型异常在前,万能捕获在后;

  5. 捕获异常时,优先用 const 引用(const 异常类& e),避免拷贝异常对象,提升效率,同时避免切片问题(子类异常被基类对象捕获时,丢失子类特有信息)。

四、总结与实践建议

今天我们掌握了两个核心知识点:异常规范和自定义异常类的设计,总结一下:

  • 异常规范:推荐用 C++11 的 noexcept,明确不会抛出异常的函数加 noexcept,提升效率;

  • 自定义异常类:继承 std::exception,重写 what() 方法,携带足够的异常信息(错误码、模块、描述),复杂项目建议用"基类+子类"的分层设计;

  • 核心目标:让异常处理更规范、更易调试、更易维护,避免滥用万能捕获,让错误处理更精准。

实践建议:

  1. 把今天的示例代码复制到编译器中运行,亲手修改异常信息、错误码,感受异常的捕获和处理流程;

  2. 尝试自己设计一个自定义异常子类(比如 DatabaseException 数据库异常),添加特有属性(比如数据库连接地址);

  3. 在实际项目中,给每个模块设计对应的异常类,统一异常规范,方便后续排查错误和维护。

相关推荐
xyq20241 小时前
SQL Mid() 函数详解
开发语言
小卓(friendhan2005)1 小时前
高并发内存池
c++
zlpzpl2 小时前
Linux系统下安装配置Nginx(保姆级教程)
java·linux·nginx
好家伙VCC2 小时前
# 发散创新:用Python+Pandas构建高效BI数据清洗流水线在现代数据分析领域,**BI(商业智能)工具的核心竞
java·python·数据分析·pandas
CappuccinoRose2 小时前
CSS 语法学习文档(十一)
前端·css·学习·表单控件
文艺倾年2 小时前
【源码精讲+简历包装】LeetcodeRunner—手搓调试器轮子(20W字-下)
java·开发语言·人工智能·语言模型·自然语言处理·大模型·免训练
励ℳ2 小时前
Python环境操作完全指南
开发语言·python
海兰2 小时前
Elastic Stack 9.3.0 日志探索
java·服务器·前端
汉克老师2 小时前
GESP2024年9月认证C++二级( 第三部分编程题(1) 数位之和 )
c++·循环结构·分支结构·gesp二级·gesp2级·求余数·拆数字