引言

在程序运行过程中,错误是不可避免的。内存分配失败、文件不存在、网络连接中断、除数为零……这些运行时错误如果处理不当,轻则导致程序崩溃,重则造成数据丢失甚至系统故障。

C 语言处理错误的方式相对原始,主要依赖返回值和全局变量 errno。这种方式虽然简单,但存在明显缺陷:错误处理代码与正常业务逻辑混杂在一起,降低了代码的可读性和可维护性。

C++ 引入了异常处理机制,提供了一种结构化的错误处理方式,将错误检测与错误处理分离开来,使代码更加清晰、健壮。

第一部分:C 语言的错误处理回顾

一、常见错误处理方式

1. 返回值判断

#include <stdlib.h>
#include <stdio.h>

// malloc 失败返回 NULL
int *p = (int*)malloc(sizeof(int) * 100);
if (p == NULL) {
    printf("内存分配失败\n");
    return -1;
}

2. errno 错误号

#include <errno.h>
#include <string.h>
#include <fcntl.h>

int fd = open("nonexistent.txt", O_RDONLY);
if (fd == -1) {
    printf("错误号: %d\n", errno);
    printf("错误信息: %s\n", strerror(errno));
    perror("open error");
}

3. assert 断言

#include <assert.h>

void divide(int a, int b) {
    assert(b != 0);  // 表达式为假时,发送 SIGABRT 信号终止程序
    return a / b;
}

二、C 语言方式的局限性

问题 说明
错误检测与处理耦合 业务逻辑被大量 if 判断污染
返回值二义性 有些函数的返回值既可能是有效数据,也可能是错误码
错误传播困难 深层调用的错误需要逐层返回,容易遗漏
无法处理构造函数错误 C 语言的 struct 没有构造函数

第二部分:C++ 异常处理基础

一、异常处理三要素

C++ 异常处理由三个关键字组成:

关键字 作用
throw 抛出异常
try 检测可能抛出异常的代码块
catch 捕获并处理特定类型的异常

二、基本语法

try {
    // 可能抛出异常的代码
    throw 异常对象;
}
catch (异常类型1 变量名) {
    // 处理类型1的异常
}
catch (异常类型2 变量名) {
    // 处理类型2的异常
}
catch (...) {
    // 处理所有其他异常
}

三、异常抛出与捕获流程

四、基础示例:除零保护

#include <iostream>
using namespace std;

int divide(int a, int b) {
    if (b == 0) 
        throw "除数为0";   // 抛出 const char* 类型异常
    return a / b;
}

int main() {
    int a, b;
    while (1) {
        cout << "a b: ";
        cin >> a >> b;
        if (a == 0) break;
        
        try {
            cout << divide(a, b) << endl;
        }
        catch (const char* err) {    // 捕获 const char* 异常
            cout << "error: " << err << endl;
        }
    }
    cout << "--OK--" << endl;
    return 0;
}

关键理解

  • throw 可以是任意类型:intdoubleconst char*string、类对象

  • catch 的类型必须与 throw 的类型匹配

  • 异常发生时,try 块中剩余的代码被跳过

五、多类型异常捕获

#include <iostream>
#include <string>
using namespace std;

void testerror(int a, int b, int c) {
    if (a > 10) throw a;              // 抛出 int 类型
    if (b > 5)  throw 1.25;           // 抛出 double 类型
    if (c > 2)  throw string("c值不能超出2");  // 抛出 string 类型
    cout << "a:" << a << ", b:" << b << ", c:" << c << endl;
}

int main() {
    int a, b, c;
    while (1) {
        cout << "a b c: ";
        cin >> a >> b >> c;
        if (a == 0) break;
        
        try {
            testerror(a, b, c);
        }
        catch (int e) {           // 捕获 int 异常
            cout << e << " 的值超过了10" << endl;
        }
        catch (double e) {        // 捕获 double 异常
            cout << e << " 的值超过了5" << endl;
        }
        catch (string e) {        // 捕获 string 异常
            cout << e << endl;
        }
    }
    cout << "Over" << endl;
    return 0;
}

第三部分:自定义异常类

一、基本要求

自定义异常类没有强制要求继承哪个类,但建议遵循以下规范:

建议 说明
包含错误信息成员 string errinfo 存储错误描述
提供获取方法 what() 或 error() 返回错误信息
继承自 exception 与 C++ 标准库异常体系统一
支持虚函数 便于多态处理

二、基础自定义异常类

#include <iostream>
#include <string>
using namespace std;

class Exception {
private:
    string einfo;
    
public:
    Exception(const string& info) : einfo(info) {
        cout << "构造异常对象: " << this << endl;
    }
    
    Exception(const Exception& other) {
        einfo = other.einfo;
        cout << "拷贝构造异常对象: " << this 
             << " from " << &other << endl;
    }
    
    virtual ~Exception() {
        cout << "销毁异常对象: " << this << endl;
    }
    
    virtual string what() {
        return einfo;
    }
};

三、派生异常类

// 越界异常
class OutOfRangeException : public Exception {
public:
    OutOfRangeException() : Exception("下标越界") {}
    
    // 重写 what() 提供更详细的信息
    string what() override {
        return string("严重错误: ") + Exception::what();
    }
};

// 使用示例
void check_index(int index, int size) {
    if (index < 0 || index >= size) {
        throw OutOfRangeException();
    }
}

四、异常对象生命周期

void test1() {
    // 方式1:抛出临时对象(直接存入异常栈)
    throw Exception("测试异常1");
    
    // 方式2:抛出局部对象(拷贝到异常栈)
    Exception e("测试异常1_1");
    throw e;  // 这里发生拷贝构造
}

int main() {
    try {
        test1();
    }
    catch (Exception& e) {  // 建议使用引用捕获
        cout << e.what() << endl;
    }
    return 0;
}

关键理解

建议catch 时使用引用,避免不必要的拷贝。


第四部分:C++ 标准异常体系

一、异常类层次结构

二、继承标准异常类

#include <stdexcept>
#include <string>
#include <cstring>
using namespace std;

class InvalidException : public invalid_argument {
private:
    int argV;
    
public:
    InvalidException(int argV, const string& msg) 
        : argV(argV), invalid_argument(msg) {}
    
    const char* what() const noexcept override {
        static char buff[128] = {0};
        snprintf(buff, sizeof(buff), "%d %s", 
                 argV, invalid_argument::what());
        return buff;
    }
};

// 使用
void test(int a) {
    if (a < 0 || a > 100)
        throw InvalidException(a, "不在取值范围[0, 100]中");
    cout << "a: " << a << endl;
}

int main() {
    try {
        test(20);
        test(-1);  // 抛出异常
    }
    catch (invalid_argument& e) {  // 捕获派生类
        cout << "invalid_argument: " << e.what() << endl;
    }
    catch (logic_error& e) {       // 捕获基类
        cout << "logic_error: " << e.what() << endl;
    }
    catch (exception& e) {         // 捕获更基类
        cout << "exception: " << e.what() << endl;
    }
    return 0;
}

三、异常捕获顺序规则

规则catch 块按顺序匹配,派生类应放在基类前面,否则基类会先"拦截"所有异常。


第五部分:noexcept 关键字

一、基本语法

void func() noexcept {
    // 此函数承诺不抛出异常
}

二、作用与效果

作用 说明
编译器优化 编译器知道该函数不会抛出异常,可以生成更优化的代码
代码文档 明确告知调用者该函数是安全的
强制约束 若 noexcept 函数内部抛出异常,程序直接调用 terminate()

三、noexcept 函数的后果

#include <iostream>
#include <stdexcept>
using namespace std;

void safe_function() noexcept {
    cout << "safe_function() OK" << endl;
    // 如果非要抛出异常,程序会 abort
    // throw runtime_error("error!");  // 会导致 terminate
}

int main() {
    try {
        safe_function();
    }
    catch (exception& e) {
        cout << e.what() << endl;  // 永远不会执行
    }
    return 0;
}

如果 noexcept 函数抛出异常

  1. 程序调用 std::terminate()

  2. 默认行为是调用 std::abort() 终止程序

  3. catch 块无法捕获该异常

四、建议使用 noexcept 的函数

 

函数类型 原因
构造函数 构造失败应使用异常,但成功构造后不应抛出
析构函数 C++11 起析构函数默认是 noexcept
简单 getter 只返回成员变量,不应抛出异常
移动构造函数 通常只是转移资源,不应抛出
swap 函数 应保证不抛出
class MyClass {
private:
    int value;
    
public:
    MyClass() noexcept : value(0) {}  // 构造函数
    
    int getValue() const noexcept {   // getter
        return value;
    }
    
    void setValue(int v) {            // setter 可能抛出
        if (v < 0) 
            throw invalid_argument("值不能为负");
        value = v;
    }
};

第六部分:throw 声明(已废弃)

一、历史演变

// C++98 动态异常说明(已废弃)
void func1() throw(int, double) { }   // 可能抛出 int 或 double
void func2() throw() { }              // 不抛出任何异常

// C++11 noexcept(推荐使用)
void func3() noexcept { }             // 不抛出异常
void func4() noexcept(false) { }      // 可能抛出异常

二、为什么不推荐 throw()

问题 说明
运行时检查 违反声明时调用 unexpected(),性能差
模板兼容性差 难以推断模板参数的异常类型
C++17 移除 只保留 throw() 作为 noexcept 的别名

第七部分:异常安全的三个级别

级别 说明 示例
基本保证 异常发生时,程序状态仍然有效,无资源泄漏 大多数代码
强保证 异常发生时,状态回滚到操作前的状态(原子操作) 事务操作
不抛出保证 承诺永远不抛出异常 noexcept 函数
// 基本保证示例
void append_to_file(const string& data) {
    ofstream file("data.txt", ios::app);
    file << data;  // 如果失败,file 析构函数正确关闭
}

// 强保证示例
void update_record(int id, const string& new_data) {
    string old_data = get_record(id);   // 保存旧数据
    try {
        set_record(id, new_data);
    } catch (...) {
        set_record(id, old_data);       // 回滚
        throw;                           // 重新抛出
    }
}

总结

一、C 与 C++ 错误处理对比

对比项 C 语言 C++ 异常
错误检测 返回值 + if 判断 try 块
错误报告 返回错误码、设置 errno throw
错误处理 函数内就地处理 catch 块集中处理
错误传播 逐层返回,容易遗漏 自动向上传播(栈展开)
代码清晰度 错误处理与业务逻辑混杂 正常逻辑与错误处理分离
资源管理 手动清理 RAII + 栈展开自动清理

二、核心知识点速查

知识点 说明
throw 抛出任意类型的异常
try 包裹可能抛出异常的代码
catch(T& e) 捕获类型为 T 的异常(建议引用)
catch(...) 捕获所有异常
异常栈 存储异常对象的安全区域
栈展开 异常发生时,自动销毁栈上的局部对象
noexcept 声明函数不抛出异常
exception::what() 获取异常描述信息
捕获顺序 从派生类到基类

三、最佳实践

C++ 异常处理机制是构建健壮程序的重要工具。它让错误检测与错误处理分离,使代码更加清晰;通过栈展开和 RAII 技术,确保异常发生时资源正确释放;通过异常类层次结构,实现灵活的分类处理。

学习建议

  1. 先用简单的 throw int 和 throw string 理解异常抛出与捕获流程

  2. 掌握自定义异常类的编写,特别是继承 std::exception

  3. 理解异常对象的生命周期,养成使用引用捕获的习惯

  4. 合理使用 noexcept 关键字,提升程序性能

  5. 结合 RAII 技术,编写异常安全的代码

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐