C++_错误处理

C++ 中错误处理的核心知识点: 包括错误处理的不同方式、异常机制的原理与使用、异常安全、以及实际开发中的最佳实践等 ------ 这是 C++ 编程中保障程序健壮性的核心内容,我会从基础到进阶、结合实际场景全面讲解。

一、C++ 错误处理的核心方式

C++ 提供了多种错误处理手段,不同手段适用于不同场景,先对比理解它们的定位:

处理方式 核心用法 适用场景 优点 缺点
返回码 / 错误码 int func() { return -1; } 简单函数、底层系统调用 轻量、无运行时开销 易忽略、无法自动中断流程
全局错误码 errno/ 自定义全局变量 传统 C 风格代码 跨函数传递 线程不安全、无法关联上下文
断言(assert) assert(条件); 调试阶段、逻辑断言(必真条件) 快速定位逻辑错误 发布版本可被禁用、直接终止程序
异常机制 try-catch-throw 运行时错误、复杂业务逻辑 类型安全、自动中断流程 有轻微运行时开销、需注意安全

1. 传统错误处理:返回码 / 全局错误码

这是 C 语言继承的方式,也是 C++ 早期常用的错误处理手段。

示例(返回码):

cpp 复制代码
#include <iostream>
// 模拟文件打开,返回0成功,-1失败
int open_file(const std::string& path) {
    if (path.empty()) {
        return -1; // 错误码:路径为空
    }
    std::cout << "文件打开成功" << std::endl;
    return 0;
}

int main() {
    int ret = open_file("");
    if (ret != 0) { // 必须手动检查返回码
        std::cerr << "文件打开失败,错误码:" << ret << std::endl;
    }
    return 0;
}

示例(全局错误码,线程不安全):

cpp 复制代码
#include <iostream>
#include <cerrno> // C标准错误码头文件
#include <cstring>

// 模拟内存分配,失败时设置errno
void* alloc_mem(size_t size) {
    if (size == 0) {
        errno = EINVAL; // 全局错误码:无效参数
        return nullptr;
    }
    return malloc(size);
}

int main() {
    void* ptr = alloc_mem(0);
    if (ptr == nullptr) {
        // 解析全局错误码
        std::cerr << "分配内存失败:" << strerror(errno) << std::endl;
    }
    return 0;
}

2. 断言(assert):调试阶段的 "硬检查"

assert是宏(定义在<cassert>),用于检查程序运行时必须为真的条件 ,若条件为假,会打印错误信息并调用abort()终止程序。

核心特点:

  • 默认仅在调试模式 (未定义NDEBUG)生效,发布模式下会被完全移除;
  • 用于 "逻辑断言"(比如 "指针非空""数组索引合法"),而非处理运行时错误(比如 "文件不存在")。

示例:

cpp 复制代码
#include <cassert>
#include <iostream>

void process_data(int* data, int size) {
    // 断言:data不能为nullptr,size必须>0(逻辑上必真)
    assert(data != nullptr && "data pointer is null");
    assert(size > 0 && "size must be positive");
    
    std::cout << "处理数据,大小:" << size << std::endl;
}

int main() {
    int arr[] = {1,2,3};
    // 正常调用:断言通过
    process_data(arr, 3);
    
    // 错误调用:断言触发(调试模式下)
    process_data(nullptr, 0); // 程序终止,打印:assertion failed: data != nullptr && "data pointer is null"
    return 0;
}

3. 异常机制:C++ 的核心错误处理方案

异常是 C++ 专为 "运行时错误" 设计的类型安全机制,核心是将错误检测与错误处理分离,且能自动中断当前流程、向上传播错误。

核心优势:

  • 错误不会被忽略(除非故意不捕获);
  • 可携带丰富的错误上下文(自定义异常类);
  • 适配复杂的函数调用链(比如多层嵌套函数);
  • void返回值的函数(如之前的submit),是唯一优雅的错误通知方式。

二、异常机制的核心原理与语法

1. 基本语法:throw → try → catch

异常的生命周期:抛出(throw)→ 捕获(catch)→ 处理,中间会发生 "栈展开(Stack Unwinding)"。

cpp 复制代码
#include <iostream>
#include <stdexcept>

// 抛出异常的函数
double divide(int a, int b) {
    if (b == 0) {
        // 抛出标准异常:运行时错误
        throw std::runtime_error("division by zero");
    }
    return static_cast<double>(a) / b;
}

int main() {
    // try块:包裹可能抛出异常的代码
    try {
        std::cout << divide(10, 2) << std::endl; // 正常执行:5
        std::cout << divide(10, 0) << std::endl; // 抛出异常
    } 
    // catch块:捕获指定类型的异常(按类型匹配)
    catch (const std::runtime_error& e) {
        // 处理异常:打印错误信息
        std::cerr << "错误:" << e.what() << std::endl;
    }
    // 万能catch:捕获所有类型的异常(不推荐,无法区分错误类型)
    catch (...) {
        std::cerr << "未知错误" << std::endl;
    }

    std::cout << "程序继续执行" << std::endl; // 异常处理后,程序不会终止
    return 0;
}

e 是什么?

e 只是一个变量名 (你可以换成任何合法名字,比如 errex),它代表的是被捕获到的异常对象

举个类比:

  • 你用 catch (const std::runtime_error& e) 捕获异常,就像 "警察(catch)抓住了一个小偷(异常对象),并给这个小偷起了个临时名字叫 e";
  • 这个 "小偷(e)" 不是普通变量,而是一个继承自 std::exception 的异常类对象 (比如 std::runtime_errorstd::out_of_range,或你自定义的异常类对象)。

简单示例(替换变量名):

cpp 复制代码
try {
    throw std::runtime_error("除数不能为0");
}
// 把e换成err,完全不影响功能
catch (const std::runtime_error& err) {
    // 这里用err.what(),效果和e.what()一样
    std::cerr << "错误:" << err.what() << std::endl;
}

what() 是什么?

what() 是 C++ 标准异常基类 std::exception 中定义的一个虚成员函数,作用是:

  • 返回一个 const char* 类型的字符串,描述当前异常的具体错误信息;
  • 所有标准异常类(如 std::runtime_errorstd::out_of_range)都会重写这个函数,返回对应的错误描述;
  • 你自定义的异常类也可以重写它,返回自定义的错误信息。

std::exception 类的核心结构(简化版):

cpp 复制代码
class exception {
public:
    // 虚函数:返回异常描述信息
    virtual const char* what() const noexcept;
    virtual ~exception() = default;
};

// 标准异常类继承并重写what()
class runtime_error : public exception {
private:
    std::string msg; // 存储错误信息
public:
    explicit runtime_error(const std::string& message) : msg(message) {}
    
    // 重写what(),返回传入的错误信息
    const char* what() const noexcept override {
        return msg.c_str();
    }
};

完整示例:拆解 e.what() 的使用

cpp 复制代码
#include <iostream>
#include <stdexcept> // 包含标准异常类

int main() {
    try {
        // 1. 抛出一个std::runtime_error类型的异常对象,携带信息"division by zero"
        throw std::runtime_error("division by zero");
    } 
    // 2. 捕获这个异常对象,给它起个名字叫e(const引用避免拷贝)
    catch (const std::runtime_error& e) {
        // 3. 调用e的what()函数,获取异常的描述信息并打印
        std::cerr << "异常信息:" << e.what() << std::endl;
        // 输出:异常信息:division by zero
    }

    // 自定义异常类的what()
    try {
        // 自定义异常(继承std::exception)
        class MyException : public std::exception {
        public:
            // 重写what(),返回自定义信息
            const char* what() const noexcept override {
                return "这是我自定义的异常信息";
            }
        };
        throw MyException();
    }
    catch (const std::exception& e) { // 基类捕获
        std::cerr << "自定义异常信息:" << e.what() << std::endl;
        // 输出:自定义异常信息:这是我自定义的异常信息
    }

    return 0;
}

关键注意点

  • what() 返回的字符串生命周期 :标准异常类的 what() 返回的字符串由异常对象管理,只要异常对象 e 还在(catch 块内),这个字符串就有效;
  • noexcept 特性:what() 被声明为 noexcept,意味着它本身不会抛出异常(保证获取错误信息时不会再出问题);

2. C++ 标准异常体系

C++ 标准库提供了一套异常继承体系(均继承自std::exception),定义在<stdexcept>/<exception>中,推荐优先使用:

异常类 头文件 适用场景
std::exception <exception> 所有标准异常的基类
std::runtime_error <stdexcept> 运行时错误(可自定义消息)
std::logic_error <stdexcept> 逻辑错误(比如参数非法、越界)
std::out_of_range <stdexcept> 索引越界(如 vector::at ())
std::bad_alloc <new> 内存分配失败(new 失败)
std::bad_cast <typeinfo> 动态类型转换失败(dynamic_cast)

示例:使用标准异常派生类

cpp 复制代码
#include <stdexcept>
#include <vector>

void access_vector(const std::vector<int>& vec, int idx) {
    if (idx < 0 || idx >= vec.size()) {
        // 抛出索引越界异常
        throw std::out_of_range("index " + std::to_string(idx) + " out of range, size: " + std::to_string(vec.size()));
    }
    std::cout << "元素值:" << vec[idx] << std::endl;
}

int main() {
    std::vector<int> vec = {1,2,3};
    try {
        access_vector(vec, 5);
    }
    catch (const std::out_of_range& e) {
        std::cerr << "越界错误:" << e.what() << std::endl; // 输出:index 5 out of range, size: 3
    }
    return 0;
}

3. 异常的匹配规则

  • catch 块按从上到下 的顺序匹配,因此派生类异常必须放在基类异常前面
  • 异常捕获遵循 "类型兼容":可以用基类捕获派生类(比如std::exception捕获所有标准异常);
  • 推荐捕获const 引用const T&):避免拷贝、支持多态、防止切片。

错误示例(基类 catch 在前):

cpp 复制代码
try {
    throw std::runtime_error("test");
}
// 基类catch在前,派生类catch永远不会被匹配
catch (const std::exception& e) { 
    std::cerr << "基类异常:" << e.what() << std::endl;
}
catch (const std::runtime_error& e) { // 无效!
    std::cerr << "运行时异常:" << e.what() << std::endl;
}

正确示例:

cpp 复制代码
try {
    throw std::runtime_error("test");
}
catch (const std::runtime_error& e) { // 派生类在前
    std::cerr << "运行时异常:" << e.what() << std::endl;
}
catch (const std::exception& e) { // 基类在后
    std::cerr << "基类异常:" << e.what() << std::endl;
}

4. 异常的传播(栈展开)

当异常被抛出且当前函数没有匹配的 catch 块时,异常会向上传播

  1. 终止当前函数的执行;
  2. 销毁当前函数的局部对象(包括栈上的所有变量,遵循 RAII);
  3. 回到调用该函数的上层函数,继续寻找 catch 块;
  4. 若传播到main()函数仍未捕获,触发std::terminate(),程序崩溃。

示例(栈展开):

cpp 复制代码
#include <stdexcept>
#include <iostream>

void func3() {
    throw std::runtime_error("error in func3");
}

void func2() {
    int x = 10; // 局部变量,栈展开时会被销毁
    func3(); // 调用func3,抛出异常
    std::cout << "func2继续执行" << std::endl; // 不会执行
}

void func1() {
    func2();
    std::cout << "func1继续执行" << std::endl; // 不会执行
}

int main() {
    try {
        func1();
    }
    catch (const std::runtime_error& e) {
        std::cerr << "捕获异常:" << e.what() << std::endl;
    }
    return 0;
}

5. noexcept 说明符(C++11+)

noexcept用于声明函数 "不会抛出异常"(或 "在指定条件下不抛出"),是对编译器和调用者的承诺:

  • void func() noexcept;:函数绝对不抛出异常;
  • void func() noexcept(条件);:条件为 true 时不抛出;
  • 若声明noexcept的函数实际抛出了异常,程序会直接调用std::terminate(),不会触发栈展开。

用途:

  • 优化:编译器对noexcept函数可做更多优化(比如移动构造函数标记noexcept,容器会优先使用移动而非拷贝);
  • 明确接口:告诉调用者 "无需为该函数写 catch 块";
  • 示例:
cpp 复制代码
// 声明:该函数不会抛出异常
int add(int a, int b) noexcept {
    return a + b;
}

// 条件noexcept:当T的移动构造函数不抛出时,本函数也不抛出
template <typename T>
void swap(T& a, T& b) noexcept(std::is_nothrow_move_constructible_v<T>) {
    T temp = std::move(a);
    a = std::move(b);
    b = std::move(temp);
}

三、异常安全(进阶核心)

异常安全是指 "程序抛出异常时,仍能保证:

  1. 资源不泄漏(内存、文件句柄、锁等);
  2. 数据结构不变质(不会出现半修改的状态)。"

1. 异常安全的四个级别(从低到高)

级别 含义
不保证(No guarantee) 异常可能导致资源泄漏、数据损坏
基本保证(Basic guarantee) 异常后,资源不泄漏,数据处于合法状态
强保证(Strong guarantee) 异常后,程序状态回滚到异常前的状态
不抛出保证(Nothrow guarantee) 函数永远不抛出异常(noexcept)

2. RAII:异常安全的基石

RAII(资源获取即初始化)是 C++ 的核心惯用法,通过栈对象的析构函数自动释放资源------ 即使发生异常,栈展开时局部对象的析构函数一定会被调用,从而保证资源不泄漏。

示例(RAII 保证异常安全):

cpp 复制代码
#include <stdexcept>
#include <fstream>
#include <mutex>

// 错误示例:手动管理资源,异常导致泄漏
void bad_func() {
    std::ofstream* file = new std::ofstream("test.txt"); // 动态分配
    std::mutex* mtx = new std::mutex();
    mtx->lock();

    throw std::runtime_error("error"); // 抛出异常

    // 以下代码不会执行,资源泄漏
    mtx->unlock();
    delete mtx;
    delete file;
}

// 正确示例:RAII管理资源
void good_func() {
    std::ofstream file("test.txt"); // 栈对象,析构时自动关闭
    std::lock_guard<std::mutex> lock(std::mutex()); // RAII锁,析构时自动解锁

    throw std::runtime_error("error"); // 抛出异常
    // 栈展开时,lock和file的析构函数会被调用,资源自动释放
}

四、自定义异常类

实际开发中,推荐继承std::exception(或其派生类)实现自定义异常,便于统一处理:

cpp 复制代码
#include <stdexcept>
#include <string>

// 自定义线程池异常类
class ThreadPoolException : public std::runtime_error {
public:
    // 构造函数1:仅传递错误信息,错误码默认0
    explicit ThreadPoolException(const std::string& msg) 
        : std::runtime_error(msg), err_code_(0) {}
    
    // 构造函数2:传递错误信息 + 自定义错误码
    ThreadPoolException(const std::string& msg, int err_code)
        : std::runtime_error(msg), err_code_(err_code) {}
    
    // 自定义接口:获取错误码
    int error_code() const noexcept {
        return err_code_;
    }

private:
    int err_code_; // 自定义错误码
};

// 使用自定义异常
#include <iostream>
void submit_task() {
    throw ThreadPoolException("submit to stopped thread pool", 1001);
}

int main() {
    try {
        submit_task();
    }
    catch (const ThreadPoolException& e) {
        std::cerr << "错误信息:" << e.what() << std::endl;
        std::cerr << "错误码:" << e.error_code() << std::endl;
    }
    // 基类捕获:兼容所有标准异常
    catch (const std::exception& e) {
        std::cerr << "标准异常:" << e.what() << std::endl;
    }
    return 0;
}

五、实际开发中的最佳实践

  1. 异常只用于 "异常情况":不要用异常处理正常流程(比如 "用户输入为空" 是正常情况,用返回值;"文件无法写入" 是异常情况,用异常);
  2. 优先使用标准异常 :自定义异常继承std::exception,便于统一捕获和处理;
  3. 捕获异常用 const 引用:避免拷贝、支持多态、防止切片;
  4. RAII 管理所有资源:内存、锁、文件句柄等,杜绝异常导致的泄漏;
  5. 避免 "空 catch"catch (...) {}会吞掉所有异常,至少记录日志;
  6. 接口明确异常约定:在函数注释中说明 "可能抛出哪些异常";
  7. 析构函数不抛出异常 :若析构函数必须处理错误,应内部捕获,避免向外抛出(否则栈展开时会触发std::terminate())。

六、常见误区

  1. 过度使用异常:比如简单的数学运算、参数检查用异常,导致性能开销和代码冗余;
  2. 忽略异常安全:手动管理资源,未用 RAII,导致异常时泄漏;
  3. 捕获异常后不处理:仅打印日志却不修复状态,导致程序后续逻辑出错;
  4. throw 值而非引用throw std::runtime_error("msg")会拷贝,推荐捕获引用但抛出值(或直接抛引用);
  5. 在 noexcept 函数中抛异常:直接导致程序崩溃,违背接口承诺。

总结

  1. C++ 错误处理有返回码、assert、异常三种核心方式,异常是运行时错误的首选,assert 仅用于调试阶段的逻辑检查;
  2. 异常机制的核心是throw-catch+ 栈展开,捕获时需遵循 "派生类在前、基类在后",优先捕获const引用
  3. 异常安全的核心是RAII,通过栈对象析构自动释放资源,避免泄漏;
  4. noexcept用于声明函数的异常行为,自定义异常应继承std::exception,且析构函数绝对不能抛出异常。
相关推荐
xu_yule1 小时前
网络和Linux网络-15(IO多路转接)reactor编程-服务器
linux·运维·服务器·c++
近津薪荼2 小时前
优选算法——滑动窗口3(子数组)
c++·学习·算法
方便面不加香菜2 小时前
c++入门基础
c++
雍凉明月夜2 小时前
瑞芯微RV1106G3板端部署
c++·人工智能·深度学习
bubiyoushang8882 小时前
基于MATLAB的局部特征尺度分解(LCD)实现与优化
开发语言·matlab
hgz07102 小时前
堆内存分区
java·开发语言·jvm
索荣荣2 小时前
SpringBoot Starter终极指南:从入门到精通
java·开发语言·springboot
Smart-Space2 小时前
cpphtmlbuilder-c++灵活构造html
c++·html
会叫的恐龙2 小时前
C++ 核心知识点汇总(第四日)(循环结构)
c++·算法·循环结构