Skip to content

17 异常处理

问题引出

程序错误分类

  • 编译 / 静态分析错误:语法错误(无法编译)、逻辑错误与不良习惯(new-delete 不配对、使用前未初始化等)
  • 运行错误
    • 运行异常:可以检测(内存分配失败、文件读写错误、除零、指针越界);难以预测(段错误、栈溢出)
    • 逻辑错误:程序正常运行,但未得到期望结果

C语言错误处理

C语言一般通过返回值和全局变量表示错误:

  • 函数返回 int 或指针时,0 / 非空表示成功,非零 / 空指针表示失败
  • 使用全局变量与函数:
    • errno<error.h>):错误码
    • char *strerror(int errnum)<string.h>):将错误码转为描述字符串
    • void perror(const char *s)<stdio.h>):打印错误描述
FILE *pf = fopen("unexist.txt", "rb");
if (pf == NULL) {
    int errnum = errno;  // 防止被其他错误覆盖
    fprintf(stderr, "error num: %d\n", errno);
    perror("open(unexist.txt)");
    fprintf(stderr, "open file error: %s\n", strerror(errnum));
} else {
    fclose(pf);
}

C语言错误处理的优势:简单、高效。缺点

  • 应检必检,必须检测各种错误可能,对编程能力与经验要求极高
  • 正常代码与错误处理代码混在一起,难以阅读与维护

健壮性

健壮性(Robustness) 是指程序在执行过程中处理错误,以及算法在遭遇输入、运算等异常时保持正常运行的能力。异常处理机制的目的就是提升程序的健壮性。

异常处理概述

异常处理(Exception Handling)采用结构化方法对程序的运行时错误进行显式管理:

  • 将异常种类定义为树状结构
  • 异常检测异常处理分离:检测到异常时抛出异常对象,由特定位置集中捕获并处理
  • 将正常处理代码与异常处理代码显式区别开来,提高可读性

异常用于给出错误信号,不必立即处理。抛出异常的部分与处理异常的部分可以独立开发,通过异常对象进行通信。

异常定义与抛出

throw 关键字

throw 表达式;   // 抛出异常对象
throw;          // 重抛当前异常(仅用于 catch 块内)

throw 表达式; 的基本行为:

  1. 复制被抛出的对象为临时对象,作为异常对象
  2. 如果在当前函数 try 块中,中止 try 执行,执行 catch 语句捕获
  3. 如异常对象未在当前函数被捕获,则中止函数执行,异常传播给调用者

表达式可以是任意类型的对象,既可以是基本类型(如 int),也可以是类对象。

标准库异常基类 exception

定义在头文件 <exception> 中,是所有标准异常的基类:

class exception {
public:
    exception() noexcept;
    exception(const exception&) noexcept;
    exception& operator=(const exception&) noexcept;
    virtual ~exception();
    virtual const char* what() const noexcept;  // 返回异常描述
};

what() 是虚函数,子类可重写以提供具体错误信息。noexcept 表示该函数不会抛出异常。

标准库异常树

头文件 异常类 说明
<exception> exception 所有异常的基类
<stdexcept> logic_error 逻辑类异常的基类
<stdexcept> runtime_error 运行时异常的基类
<new> bad_alloc 内存分配失败(new 失败时抛出)
<typeinfo> bad_cast dynamic_cast 引用转换失败
<typeinfo> bad_typeid typeid 对空指针解引用
<stdexcept> out_of_range 下标越界(如 vector::at
<stdexcept> invalid_argument 无效参数
<stdexcept> length_error 试图创建超过最大长度的对象
<stdexcept> domain_error 定义域错误
<stdexcept> range_error 范围错误
<stdexcept> overflow_error / underflow_error 算术溢出

logic_error 及其子类通常可通过修改程序逻辑避免;runtime_error 及其子类表示运行时才能发现的错误。二者构造函数都用 explicit 修饰,防止隐式转换。

使用标准异常

bad_alloc 示例:

#include <iostream>
#include <new>

int main() {
    try {
        int* myarray = new int[10000];
    } catch (std::bad_alloc& ba) {
        std::cerr << "bad_alloc caught: " << ba.what() << '\n';
    }
    return 0;
}

out_of_range 示例(vector::at 越界时自动抛出):

std::vector<int> myvector(10);
try {
    myvector.at(20) = 100;  // at 会进行越界检查
} catch (const std::out_of_range& oor) {
    std::cerr << "Out of Range error: " << oor.what() << '\n';
}

invalid_argument 示例(主动抛出):

double mySqrt(double dnum) {
    if (dnum < 0)
        throw std::invalid_argument("argument dnum must be great than zero");
    return std::sqrt(dnum);
}

int main() {
    try {
        mySqrt(-1);
    } catch (std::exception& e) {  // 用基类引用捕获
        std::cout << e.what() << '\n';
    }
    return 0;
}

异常的捕获及处理

try-catch 语法

try {
    // 程序的正常处理逻辑
    throw exception-object;
} catch (exception-declaration) {
    // 异常处理代码
} catch (exception-declaration) {
    // 异常处理代码
} catch (...) {
    // 捕获其他所有类型的异常(必须放在最后)
}

执行机制

  • 若保护段执行期间没有引发异常,跳过所有 catch 子句,继续执行最后一个 catch 后的语句
  • 若有异常,catch 子句按出现顺序被检查,类型匹配的 catch 捕获并处理
  • 如果找不到匹配的处理代码,则自动调用 std::terminate,默认调用 abort() 终止程序

捕获匹配规则

  • 按 catch 顺序与异常定义匹配,异常对象类型是定义类型或其子类即匹配
  • 匹配成功后:若接收变量是值,则复制异常对象;若是引用或指针,则创建引用或指针
  • 子类必须优先于基类 catchcatch(...) 必须放在最后
  • 用基类引用类型捕获异常,且虚函数才能产生多态

捕获顺序与多态

class EBase {
public:
    void what() { cout << "base" << endl; }       // 非虚函数
};
class EDrived : public EBase {
public:
    void what() { cout << "EDrived" << endl; }    // 隐藏基类
};

int main() {
    try {
        throw EDrived();
    }
    // 注意:捕获次序,子类必须在前面
    catch (EBase& e) { e.what(); }    // 先匹配基类 → 输出 "base"
    catch (EDrived& e) { e.what(); }  // 永远不会执行到
    return 0;
}

what() 声明为 virtual,则用基类引用捕获时仍会调用派生类版本,输出 "EDrived"。这就是异常多态的基础。

异常传播

从异常抛出到控制转移给合适的异常处理语句的过程叫异常传播(propagation)

  • 由里层向外层执行每一个包围抛出点的 try 语句
  • 当前函数未捕获的异常,传播给调用者
  • 若调用链中均无处理,则调用 std::terminate 终止程序
void f3(int x) {
    switch (x) {
        case 1: throw 3.4;          // double
        case 2: throw 2.5f;         // float
        case 3: throw 1;            // int
        case 5: throw exception();  // 标准异常
    }
}

void f2(int x) {
    try { f3(x); }
    catch (int) { cout << "int --from f2" << endl; }
    catch (float) { cout << "float --from f2" << endl; }
    // double 和 exception 未捕获,传播给 f1
}

void f1(int x) {
    try { f2(x); }
    catch (int) { cout << "int --from f1" << endl; }
    catch (float) { cout << "float --from f1" << endl; }
    catch (double) { cout << "double --from f1" << endl; }
    // exception 未捕获 → terminate
}

异常说明与 noexcept

C++11 之前可用异常说明限定函数可抛出的异常类型(C++11 起废弃,C++17 移除):

void f3(int x) throw(double, float);  // 只允许抛出 double、float

C++11 引入 noexcept 关键字,说明函数不会抛出异常

void f2(int x) noexcept;  // 承诺不抛出异常

noexcept 函数内部有异常未处理,则直接调用 std::abort() 终止程序。noexcept 主要用于优化与接口契约。

异常重抛出

在 catch 块内使用 throw;(无表达式)可以重抛当前异常,交由上层函数处理:

catch (MyException& e) {
    e.setMessage("new message");
    throw;   // 重抛,上层能见到修改后的 message
}

用引用捕获时,修改会传递到上层;用值捕获时,修改的只是局部副本,重抛的对象不发生改变。

异常编程与实践

用户自定义异常

强烈建议从 <stdexcept> 中的异常类继承,常用基类有 logic_errorruntime_error。优势:

  • 代码更简洁,易于阅读
  • 更利于分类处理异常
  • 减少自定义虚函数(多数情况标准库都做好了)
class Divided_By_Zero : public runtime_error {
public:
    explicit Divided_By_Zero(const string& s = "Divide By Zero")
        : runtime_error(s) {}
    explicit Divided_By_Zero(const char* s)
        : runtime_error(string(s)) {}
};

int main() {
    double a = 10, b = 0, res;
    try {
        if (b == 0) throw Divided_By_Zero();
        res = a / b;
    } catch (const exception& e) {
        cout << e.what() << endl;  // 输出 "Divide By Zero"
    }
    return 0;
}

对象构造异常

构造函数中抛出异常时:

  • 构造失败的对象不会分配空间,也不会析构
  • 但 try 块中在该对象之前已正常构造的局部对象会正常析构
class Dummy {
public:
    ~Dummy() { std::cout << "Dummy destructed\n"; }
};

class Triangle {
public:
    Triangle(int a = 3, int b = 4, int c = 5) {
        if (a < 0) { throw a; }   // 构造中抛出异常
    }
    ~Triangle() { std::cout << "Triangle destructed\n"; }
};

int main() {
    try {
        Dummy dum;
        Triangle tri(-1);   // 抛出异常,tri 未完成构造
        std::cout << "Triangle created\n";
    } catch (...) {
        std::cout << "Exception catched\n";
    }
}
// 输出:
// Dummy destructed
// Exception catched

这说明异常发生时,已构造的自动对象会被正确析构,但构造未完成的对象不会析构。若构造过程中已 new 了资源,这些资源会泄漏——这正是 RAII 要解决的问题。

RAII 技术

RAII(Resource Acquisition Is Initialization),资源获取即初始化,即使用自动对象的生命周期来管理资源(内存、文件等)。

RAII 技术的四个部分:

  1. 设计一个类封装资源
  2. 在构造函数中申请资源
  3. 在析构函数中释放资源
  4. 使用时声明该类的局部对象
// 传统方法:需手动关闭
void old_fct(const char* s) {
    FILE* f = fopen(s, "r");
    // use f
    fclose(f);  // 必须手动关闭,异常时易泄漏
}

// RAII 方法:析构自动关闭
void fct(string s) {
    File_handle f(s, "r");  // 构造时打开
    // use f
}  // 离开作用域,析构自动关闭

文件管理类的 RAII 实现:

class FileManager {
public:
    explicit FileManager(const std::string& filename, std::ios_base::openmode mode)
        : file_(filename, mode) {
        if (!file_.is_open())
            throw std::runtime_error("Unable to open file.");
    }
    ~FileManager() { file_.close(); }   // 析构自动关闭
    void Write(const std::string& data) { file_ << data << std::endl; }
private:
    std::ofstream file_;
};

智能指针

智能指针是 RAII 在指针管理上的应用,用对象包装指向资源的指针,析构时自动释放资源。

智能指针 说明
auto_ptr C++98 已废弃,C++17 移除。存在拷贝时所有权转移的缺陷
unique_ptr 独占所有权,不允许拷贝,只能移动。一个原始指针只能有一个所有者
shared_ptr 共享所有权,采用引用计数。多个 shared_ptr 可指向同一资源,最后一个所有者销毁时释放
weak_ptr 配合 shared_ptr 使用的弱引用,不增加引用计数,用于打破循环引用
#include <memory>

class MyResource {
public:
    MyResource(int size) : data_(std::make_unique<int[]>(size)) {
        std::cout << "MyResource created with size " << size << std::endl;
    }
    // 禁用拷贝
    MyResource(const MyResource&) = delete;
    MyResource& operator=(const MyResource&) = delete;
    // 支持移动
    MyResource(MyResource&& other) noexcept : data_(std::move(other.data_)) {}
private:
    std::unique_ptr<int[]> data_;
};

int main() {
    std::vector<std::unique_ptr<MyResource>> resources;
    for (int i = 0; i < 5; ++i) {
        resources.emplace_back(std::make_unique<MyResource>(i + 1));
    }
    // vector 销毁时,unique_ptr 自动释放所指向对象
    return 0;
}