强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

C/C++ Linux 开发教程(GCC + CMake) / 异常处理

异常处理

1. try / catch / throw 基本语法

#include <iostream>
#include <stdexcept>

double divide(double a, double b) {
    if (b == 0.0) {
        throw std::invalid_argument("除数不能为零");
    }
    return a / b;
}

int main() {
    try {
        std::cout << "10 / 3 = " << divide(10, 3) << "\n";
        std::cout << "10 / 0 = " << divide(10, 0) << "\n";  // 抛出异常
        std::cout << "这行不会执行\n";
    } catch (const std::invalid_argument& e) {
        // 捕获特定异常
        std::cerr << "参数错误: " << e.what() << "\n";
    } catch (const std::exception& e) {
        // 捕获所有标准异常
        std::cerr << "标准异常: " << e.what() << "\n";
    } catch (...) {
        // 捕获所有未知异常
        std::cerr << "未知异常\n";
    }

    std::cout << "程序继续执行\n";
    return 0;
}

编译运行:

g++ -std=c++17 -o demo demo.cpp && ./demo

2. 异常类层次结构

std::exception
├── std::logic_error
│   ├── std::invalid_argument
│   ├── std::domain_error
│   ├── std::length_error
│   └── std::out_of_range
├── std::runtime_error
│   ├── std::range_error
│   ├── std::overflow_error
│   └── std::underflow_error
└── std::bad_alloc (new 失败)
    └── std::bad_array_new_length
#include <iostream>
#include <stdexcept>
#include <vector>
#include <string>

void demonstrate_exceptions() {
    // 1. 逻辑错误(程序逻辑问题)
    try {
        std::vector<int> v = {1, 2, 3};
        v.at(10);  // std::out_of_range
    } catch (const std::out_of_range& e) {
        std::cout << "越界访问: " << e.what() << "\n";
    }

    // 2. 运行时错误(运行环境问题)
    try {
        throw std::runtime_error("文件不存在");
    } catch (const std::runtime_error& e) {
        std::cout << "运行时错误: " << e.what() << "\n";
    }

    // 3. 无效参数
    try {
        throw std::invalid_argument("参数不能为空");
    } catch (const std::invalid_argument& e) {
        std::cout << "参数错误: " << e.what() << "\n";
    }

    // 4. 长度错误
    try {
        throw std::length_error("字符串超过最大长度");
    } catch (const std::length_error& e) {
        std::cout << "长度错误: " << e.what() << "\n";
    }
}

int main() {
    demonstrate_exceptions();
    return 0;
}

3. 自定义异常类

#include <iostream>
#include <stdexcept>
#include <string>
#include <format>

// 方式 1:继承 std::exception
class FileNotFoundError : public std::runtime_error {
    std::string filename_;

public:
    explicit FileNotFoundError(const std::string& filename)
        : std::runtime_error("文件未找到: " + filename)
        , filename_(filename) {}

    const std::string& filename() const { return filename_; }
};

// 方式 2:带错误码的异常
class ApiError : public std::runtime_error {
    int statusCode_;
    std::string endpoint_;

public:
    ApiError(int code, const std::string& endpoint, const std::string& message)
        : std::runtime_error(message)
        , statusCode_(code)
        , endpoint_(endpoint) {}

    int statusCode() const { return statusCode_; }
    const std::string& endpoint() const { return endpoint_; }
};

// 方式 3:异常层次
class AppError : public std::runtime_error {
    using std::runtime_error::runtime_error;
};

class DatabaseError : public AppError {
    using AppError::AppError;
};

class NetworkError : public AppError {
    using AppError::AppError;
};

void readConfig(const std::string& path) {
    if (path.empty()) {
        throw FileNotFoundError(path);
    }
    if (path.find("remote://") == 0) {
        throw NetworkError("无法连接到远程服务器");
    }
    // ... 读取逻辑
}

void queryDB() {
    throw DatabaseError("连接超时");
}

int main() {
    // 捕获自定义异常
    try {
        readConfig("");
    } catch (const FileNotFoundError& e) {
        std::cerr << e.what() << "\n";
        std::cerr << "文件名: " << e.filename() << "\n";
    }

    // 使用异常层次分层捕获
    try {
        queryDB();
    } catch (const DatabaseError& e) {
        std::cerr << "数据库错误: " << e.what() << "\n";
    } catch (const NetworkError& e) {
        std::cerr << "网络错误: " << e.what() << "\n";
    } catch (const AppError& e) {
        std::cerr << "应用错误: " << e.what() << "\n";
    }

    // API 错误
    try {
        throw ApiError(404, "/api/users", "资源未找到");
    } catch (const ApiError& e) {
        std::cerr << "API 错误 [" << e.statusCode() << "] "
                  << e.endpoint() << ": " << e.what() << "\n";
    }

    return 0;
}

4. noexcept 关键字

noexcept 承诺函数不会抛出异常,编译器可据此进行优化。

#include <iostream>
#include <vector>
#include <type_traits>

// 基本 noexcept
void safe_function() noexcept {
    // 承诺不抛异常
    // 如果内部抛出异常 → std::terminate()
}

// 条件 noexcept
template <typename T>
void swap_values(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);
}

// noexcept 运算符
class MyType {
public:
    MyType() noexcept {}                          // 不抛异常
    MyType(const MyType&) {}                       // 可能抛异常
    MyType(MyType&&) noexcept {}                   // 不抛异常
    MyType& operator=(const MyType&) {}            // 可能抛异常
    MyType& operator=(MyType&&) noexcept { return *this; } // 不抛异常
};

int main() {
    // noexcept 影响容器行为
    // vector 扩容时,如果元素的移动构造是 noexcept,才会使用移动
    // 否则回退到拷贝(更安全但更慢)

    std::cout << std::boolalpha;
    std::cout << "MyType move noexcept: "
              << std::is_nothrow_move_constructible<MyType>::value << "\n";
    std::cout << "MyType copy noexcept: "
              << std::is_nothrow_copy_constructible<MyType>::value << "\n";

    // 条件 noexcept 检查
    std::cout << "int swap noexcept: "
              << noexcept(swap_values(std::declval<int&>(), std::declval<int&>())) << "\n";

    return 0;
}
声明含义
void f() noexcept承诺不抛异常
void f() noexcept(false)可能抛异常
void f() noexcept(expr)根据 expr 结果决定
noexcept(expr)noexcept 运算符,检查表达式是否 noexcept

⚠️ 注意noexcept 承诺被违反时,程序会直接调用 std::terminate(),不进行栈展开。所以只在真正确定不会抛异常时才标记。


5. 异常安全保证

保证级别含义说明
不抛出保证 (nothrow)函数绝不抛异常noexcept,析构函数
强保证 (strong)异常发生时,状态回滚到调用前copy-and-swap
基本保证 (basic)异常发生时,对象仍处于有效状态最常见要求
无保证 (no guarantee)可能泄漏资源或损坏状态❌ 不可接受
#include <iostream>
#include <vector>
#include <algorithm>

class StrongExceptionSafe {
    std::vector<int> data_;

public:
    explicit StrongExceptionSafe(size_t n) : data_(n, 0) {}

    // 强异常安全:copy-and-swap 惯用法
    StrongExceptionSafe& operator=(StrongExceptionSafe other) {  // 按值传递 = 拷贝
        swap(*this, other);
        return *this;
    }

    friend void swap(StrongExceptionSafe& a, StrongExceptionSafe& b) noexcept {
        std::swap(a.data_, b.data_);
    }

    void push(int value) {
        // vector::push_back 提供强异常安全保证
        // 如果 reallocation 抛异常,vector 状态不变
        data_.push_back(value);
    }

    void print() const {
        for (auto v : data_) std::cout << v << " ";
        std::cout << "\n";
    }
};

// 基本异常安全示例
class BasicExceptionSafe {
    int* data_;
    size_t size_;
    size_t capacity_;

public:
    BasicExceptionSafe(size_t cap)
        : data_(new int[cap]), size_(0), capacity_(cap) {}

    ~BasicExceptionSafe() { delete[] data_; }

    // 基本保证:如果 new 抛异常,原对象仍有效
    void resize(size_t newCap) {
        int* newData = new int[newCap];  // 可能抛异常 → 原状态不变
        std::copy(data_, data_ + size_, newData);
        delete[] data_;                  // 这里之后不能抛异常
        data_ = newData;
        capacity_ = newCap;
    }
};

int main() {
    StrongExceptionSafe a(5), b(3);
    a = b;  // 强异常安全

    return 0;
}

6. RAII 与异常安全

RAII 是实现异常安全的关键技术:资源在构造时获取,在析构时释放,即使发生异常也不例外。

#include <iostream>
#include <fstream>
#include <mutex>
#include <memory>
#include <vector>

std::mutex g_mtx;

// RAII 保证异常安全
bool processFile(const std::string& inputPath, const std::string& outputPath) {
    // RAII:文件自动关闭,即使中途抛异常
    std::ifstream in(inputPath);
    if (!in.is_open()) {
        return false;
    }

    std::ofstream out(outputPath);
    if (!out.is_open()) {
        return false;
    }

    // RAII:锁自动释放
    std::lock_guard<std::mutex> lock(g_mtx);

    std::string line;
    while (std::getline(in, line)) {
        // 如果这里抛异常:
        // 1. lock_guard 析构 → 解锁
        // 2. out 析构 → 关闭文件
        // 3. in 析构 → 关闭文件
        // 所有资源正确释放
        out << line << "\n";
    }

    return true;
}

// 对比:不用 RAII 的版本(不安全)
bool processFileUnsafe(const std::string& inputPath, const std::string& outputPath) {
    FILE* in = std::fopen(inputPath.c_str(), "r");
    if (!in) return false;

    FILE* out = std::fopen(outputPath.c_str(), "w");
    if (!out) {
        std::fclose(in);  // 必须手动关闭
        return false;
    }

    g_mtx.lock();

    // 如果这里抛异常...
    char buf[1024];
    while (std::fgets(buf, sizeof(buf), in)) {
        std::fputs(buf, out);  // ...锁不会释放,文件不会关闭
    }

    g_mtx.unlock();
    std::fclose(out);
    std::fclose(in);
    return true;
}

int main() {
    // RAII 版本即使抛异常也安全
    try {
        processFile("/tmp/in.txt", "/tmp/out.txt");
    } catch (...) {
        std::cerr << "异常被捕获,资源已正确释放\n";
    }

    return 0;
}

7. 异常 vs 错误码

特性异常错误码
传播机制自动沿调用栈向上手动检查并返回
忘记处理默认终止程序容易被忽略
性能无错误时零开销;抛出时有开销恒定小开销
代码侵入性低(正常路径清晰)高(到处 if 检查)
适用场景不可恢复/罕见错误预期的、频繁的错误
线程安全线程内安全需要额外同步
#include <iostream>
#include <expected>  // C++23
#include <string>
#include <system_error>

// 方式 1:异常(适用于异常情况)
int parse_int_strict(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (const std::invalid_argument&) {
        throw std::invalid_argument("无效数字: " + s);
    } catch (const std::out_of_range&) {
        throw std::out_of_range("数字超出范围: " + s);
    }
}

// 方式 2:错误码(适用于可预期的错误)
std::pair<int, std::error_code> parse_int_safe(const std::string& s) {
    try {
        return {std::stoi(s), {}};
    } catch (const std::invalid_argument&) {
        return {0, std::make_error_code(std::errc::invalid_argument)};
    } catch (const std::out_of_range&) {
        return {0, std::make_error_code(std::errc::result_out_of_range)};
    }
}

// 方式 3:optional(适用于可能不存在的值)
#include <optional>
std::optional<int> parse_int_optional(const std::string& s) {
    try {
        return std::stoi(s);
    } catch (...) {
        return std::nullopt;
    }
}

int main() {
    // 异常方式
    try {
        std::cout << parse_int_strict("42") << "\n";
        parse_int_strict("abc");
    } catch (const std::exception& e) {
        std::cerr << "异常: " << e.what() << "\n";
    }

    // 错误码方式
    auto [val, err] = parse_int_safe("42");
    if (!err) {
        std::cout << "值: " << val << "\n";
    }

    auto [val2, err2] = parse_int_safe("abc");
    if (err2) {
        std::cerr << "错误: " << err2.message() << "\n";
    }

    // optional 方式
    if (auto val = parse_int_optional("42")) {
        std::cout << "值: " << *val << "\n";
    }

    if (!parse_int_optional("abc")) {
        std::cout << "解析失败\n";
    }

    return 0;
}

💡 提示:选择建议 — 不可预期的错误用异常,可预期的频繁错误用错误码或 std::optional


8. 异常性能影响

#include <iostream>
#include <chrono>
#include <stdexcept>

// 无异常路径 — 性能正常
int compute_no_throw(int n) noexcept {
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        sum += i * i;
    }
    return sum;
}

// 异常路径 — 无错误时几乎无开销,抛出时有开销
int compute_with_throw(int n) {
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        if (i < 0) throw std::runtime_error("不可能");  // 永远不会触发
        sum += i * i;
    }
    return sum;
}

int main() {
    constexpr int N = 100'000'000;

    auto t1 = std::chrono::steady_clock::now();
    volatile int r1 = compute_no_throw(N);
    auto t2 = std::chrono::steady_clock::now();
    volatile int r2 = compute_with_throw(N);
    auto t3 = std::chrono::steady_clock::now();

    auto d1 = std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1).count();
    auto d2 = std::chrono::duration_cast<std::chrono::microseconds>(t3 - t2).count();

    std::cout << "无异常: " << d1 << " μs\n";
    std::cout << "有异常(未触发): " << d2 << " μs\n";
    std::cout << "差异: " << (d2 - d1) << " μs(通常可忽略)\n";

    return 0;
}
场景性能影响
无异常抛出几乎零开销(零成本异常,zero-cost exception)
异常抛出有显著开销(栈展开、析构调用)
-fno-exceptions禁用异常,throwstd::terminate()
二进制大小异常表增加约 10-20% 二进制大小

💡 提示:现代编译器使用"零成本异常"模型:不抛异常时没有运行时开销,代价是异常表占用额外二进制空间。


9. 异常与构造函数/析构函数

#include <iostream>
#include <stdexcept>
#include <vector>

// 构造函数中抛异常 — 析构函数不会被调用
class ResourceHolder {
    int* data_;
    std::string name_;

public:
    ResourceHolder(const std::string& name, bool fail)
        : name_(name), data_(new int[100]) {
        std::cout << "构造: " << name_ << "\n";

        if (fail) {
            // ⚠️ data_ 已分配但不会被析构函数释放
            // 因为对象尚未完全构造,析构函数不会被调用
            delete[] data_;  // 必须手动清理
            data_ = nullptr;
            throw std::runtime_error("构造失败");
        }
    }

    ~ResourceHolder() {
        std::cout << "析构: " << name_ << "\n";
        delete[] data_;  // 只有构造成功才会调用
    }
};

// ✅ 最佳实践:使用 RAII 成员自动管理资源
class SafeResourceHolder {
    std::vector<int> data_;  // RAII:构造失败时自动析构
    std::string name_;

public:
    SafeResourceHolder(const std::string& name, bool fail)
        : name_(name), data_(100) {
        std::cout << "构造: " << name_ << "\n";

        if (fail) {
            // data_ 的析构函数会被自动调用
            throw std::runtime_error("构造失败");
        }
    }
    // 不需要手动编写析构函数
};

// 析构函数中不应该抛异常
class NoThrowDestructor {
public:
    ~NoThrowDestructor() noexcept {
        try {
            // 可能抛异常的操作放在 try-catch 中
            // throw std::runtime_error("析构中出错");
        } catch (...) {
            std::cerr << "析构函数中捕获异常(不应传播)\n";
            // 不要让异常逃出析构函数!
        }
    }
};

int main() {
    // 构造失败
    try {
        ResourceHolder r1("R1", true);
    } catch (const std::exception& e) {
        std::cerr << "捕获: " << e.what() << "\n";
    }

    // 安全版本
    try {
        SafeResourceHolder r2("R2", true);
    } catch (const std::exception& e) {
        std::cerr << "捕获: " << e.what() << "\n";
    }

    return 0;
}

⚠️ 注意

  • 构造函数抛异常时,已构造的成员会自动析构,但构造函数体内手动分配的资源需要手动清理。
  • 析构函数绝不应该抛异常(可能导致 std::terminate)。C++11 起析构函数默认 noexcept

10. 最佳实践

规则说明
按引用捕获catch (const std::exception& e) 避免对象切片
按引用抛出throw MyException() 但捕获时用 const&
使用标准异常优先继承 std::runtime_errorstd::logic_error
不要捕获后忽略catch {} 隐藏问题
RAII 管理资源确保异常发生时资源正确释放
标记 noexcept移动构造函数、swap、析构函数应标记
不要在析构函数中抛异常C++11 起析构函数默认 noexcept
使用 std::terminate无法恢复时直接终止
考虑 std::expectedC++23 可选的错误处理方案

实际场景:安全的文件处理器

#include <iostream>
#include <fstream>
#include <string>
#include <stdexcept>

class FileProcessor {
    std::string inputPath_;
    std::string outputPath_;

public:
    FileProcessor(const std::string& input, const std::string& output)
        : inputPath_(input), outputPath_(output) {}

    void process() {
        // RAII:所有资源自动管理
        std::ifstream in(inputPath_, std::ios::binary);
        if (!in) {
            throw std::runtime_error("无法打开输入文件: " + inputPath_);
        }

        std::ofstream out(outputPath_, std::ios::binary);
        if (!out) {
            throw std::runtime_error("无法创建输出文件: " + outputPath_);
        }

        // 获取文件大小
        in.seekg(0, std::ios::end);
        auto size = in.tellg();
        in.seekg(0, std::ios::beg);

        // 逐块处理
        constexpr size_t BUFFER_SIZE = 4096;
        char buffer[BUFFER_SIZE];

        size_t processed = 0;
        while (in.read(buffer, BUFFER_SIZE) || in.gcount() > 0) {
            size_t bytesRead = in.gcount();

            // 处理数据(这里简单复制)
            out.write(buffer, bytesRead);
            if (!out) {
                throw std::runtime_error("写入失败: " + outputPath_);
            }

            processed += bytesRead;

            // 模拟:如果文件过大则报错
            if (processed > 100 * 1024 * 1024) {
                throw std::length_error("文件超过 100MB 限制");
            }
        }

        std::cout << "处理完成: " << processed << " 字节\n";
    }
};

int main() {
    try {
        FileProcessor fp("/tmp/input.txt", "/tmp/output.txt");
        fp.process();
    } catch (const std::runtime_error& e) {
        std::cerr << "运行时错误: " << e.what() << "\n";
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << "\n";
        return 1;
    }

    return 0;
}

扩展阅读