【C++ 异常处理】try-catch

C++ try-catch 异常处理完全指南


一、为什么需要异常处理

程序运行时可能遇到各种错误:文件打不开、内存分配失败、网络断开、输入的除数为零等。C++ 提供异常机制,让你把正常逻辑错误处理 分离开,避免到处写 if (error) return -1;。注意,整数除零本身不会自动变成可捕获的 C++ 异常,必须在执行除法前主动检查并抛出异常。


二、基本语法:try / throw / catch

三个关键字

关键字 作用
try 标记一段"可能出错"的代码
throw 当错误发生时,抛出一个异常对象
catch 捕获并处理被抛出的异常

最小可运行示例

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

int main() {
    try {
        int age = -1;
        if (age < 0) {
            throw std::runtime_error("age cannot be negative");
            // throw 之后的代码不会执行
            std::cout << "这行永远不会打印" << std::endl;
        }
        std::cout << "age = " << age << std::endl;
    } catch (const std::runtime_error& e) {
        // e 就是 throw 抛出的那个对象
        // e.what() 返回构造时传入的字符串
        std::cerr << "Error: " << e.what() << std::endl;
    }

    std::cout << "program continues" << std::endl;
    return 0;
}

执行流程:

  1. 进入 try
  2. age < 0 为真,执行 throw
  3. 立刻跳出 try 块(后面的 cout 不执行)
  4. 从上到下查找匹配的 catch
  5. 找到后执行 catch 块内的代码
  6. catch 结束后,从 catch之后继续执行

输出:

复制代码
Error: age cannot be negative
program continues

关键点

  • throw 可以抛出任何类型intstd::string、自定义类......但实践中几乎只抛 std::exception 的派生类
  • 捕获类类型异常时,参数通常写成 const X&,以避免复制和对象切片(object slicing);int 等简单类型可以按值捕获。
  • 如果抛出的异常最终没有任何 catch 能匹配,程序会调用 std::terminate();其默认处理通常会终止进程。

三、多个 catch 块

一个 try 后面可以跟多个 catch,按从上到下 的顺序匹配,只执行第一个匹配的那个

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

void do_something(int code) {
    if (code == 1) throw std::runtime_error("runtime problem");
    if (code == 2) throw std::logic_error("logic problem");
    if (code == 3) throw 42;              // 抛一个 int
    if (code == 4) throw std::string("oops");
}

int main() {
    for (int code : {1, 2, 3, 4, 0}) {
        try {
            do_something(code);
            std::cout << "code=" << code << " OK\n";
        } catch (const std::runtime_error& e) {
            std::cerr << "runtime_error: " << e.what() << "\n";
        } catch (const std::logic_error& e) {
            std::cerr << "logic_error: " << e.what() << "\n";
        } catch (int n) {
            std::cerr << "caught int: " << n << "\n";
        } catch (const std::string& s) {
            std::cerr << "caught string: " << s << "\n";
        }
    }
    return 0;
}

输出:

复制代码
runtime_error: runtime problem
logic_error: logic problem
caught int: 42
caught string: oops
code=0 OK

⚠️ 顺序很重要 :如果先写 catch (const std::exception& e),它会匹配所有标准异常,后面的 catch (const std::runtime_error&) 永远走不到。所以派生类放前面,基类放后面


四、catch(...):捕获一切

... 是通配符,匹配任何类型 的异常。通常放在最后一个 catch,作为兜底。

cpp 复制代码
#include <iostream>

int main() {
    try {
        throw 3.14;  // 抛一个 double,没有任何具体 catch 能接
    } catch (int n) {
        std::cout << "int: " << n << "\n";
    } catch (...) {
        // 走到这里
        std::cout << "caught something unknown\n";
    }
    return 0;
}

实际用途:

  • 在析构函数中兜底(析构函数绝不能让异常逃出去)
  • 在 C 接口边界(如 extern "C" 回调)防止异常穿越 C 栈帧
  • 记录日志后重新抛出

五、重新抛出(rethrow)

catch 块里,用 不带参数的 throw; 可以把当前异常原封不动地再抛出去。

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

void low_level() {
    throw std::runtime_error("disk full");
}

void mid_level() {
    try {
        low_level();
    } catch (const std::exception& e) {
        std::cerr << "[LOG] mid_level saw: " << e.what() << "\n";
        throw;  // ← 重新抛出,不是 throw e;
    }
}

int main() {
    try {
        mid_level();
    } catch (const std::runtime_error& e) {
        // 这里仍然能捕获到 std::runtime_error
        // 因为 throw; 保留了原始类型
        std::cerr << "[MAIN] " << e.what() << "\n";
    }
    return 0;
}

⚠️ throw; vs throw e; 的区别:

  • throw; → 重新抛出原始异常对象,保留完整类型(多态安全)
  • throw e; → 把 e 拷贝 一份再抛,如果 e 是基类引用,会切片成基类,丢失派生类信息

需要重新抛出当前异常时,应使用 throw;。如果要转换异常语义,则显式抛出一个新的异常对象。


六、常见的标准异常体系

标准库已经提供了一棵常用的异常继承树。它们分布在 <stdexcept><new><typeinfo><system_error><filesystem> 等头文件中:

复制代码
std::exception                    ← 所有标准异常的基类
├── std::logic_error              ← 程序逻辑错误(理论上可在运行前避免)
│   ├── std::invalid_argument     ← 非法参数
│   ├── std::out_of_range         ← 越界
│   ├── std::length_error         ← 长度超限
│   └── std::domain_error         ← 数学域错误
├── std::runtime_error            ← 运行时才能发现的错误
│   ├── std::overflow_error
│   ├── std::underflow_error
│   └── std::system_error         ← 包装 errno
│       └── std::filesystem::filesystem_error
├── std::bad_alloc                ← new 失败
├── std::bad_cast                 ← 引用形式的 dynamic_cast 失败
└── std::bad_exception

实际项目中用得最多的:

异常类 什么时候抛
std::runtime_error IO 失败、网络断开、文件不存在等
std::invalid_argument 调用者传了非法参数
std::out_of_range 索引越界
std::logic_error 违反函数前置条件或其他程序逻辑错误
std::bad_alloc 内存分配失败(通常只在有明确恢复策略或顶层兜底时捕获)

使用示例

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

int divide(int a, int b) {
    if (b == 0) {
        throw std::invalid_argument("divisor must not be zero");
    }
    return a / b;
}

int main() {
    try {
        int r = divide(10, 0);
        std::cout << r << "\n";
    } catch (const std::invalid_argument& e) {
        std::cerr << "Bad input: " << e.what() << "\n";
        return 1;
    }
    return 0;
}

七、自定义异常类

当标准异常不足以表达你的业务语义时,继承 std::runtime_errorstd::exception

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

// 自定义异常:继承 std::runtime_error
class NetworkError : public std::runtime_error {
    int error_code_;
public:
    NetworkError(int code, const std::string& msg)
        : std::runtime_error(msg)   // 基类负责存储 what() 字符串
        , error_code_(code)
    {}

    int code() const noexcept { return error_code_; }
};

// 更细粒度的子类
class TimeoutError : public NetworkError {
public:
    explicit TimeoutError(const std::string& msg)
        : NetworkError(110, msg)    // 示例业务错误码;不要假设系统 ETIMEDOUT 在所有平台都等于 110
    {}
};

void connect(const std::string& host) {
    if (host.empty()) {
        throw std::invalid_argument("host is empty");
    }
    // 模拟超时
    throw TimeoutError("connect to " + host + " timed out");
}

int main() {
    try {
        connect("192.168.1.1");
    } catch (const TimeoutError& e) {
        // 最具体的先捕获
        std::cerr << "Timeout (code=" << e.code() << "): " << e.what() << "\n";
    } catch (const NetworkError& e) {
        std::cerr << "Network (code=" << e.code() << "): " << e.what() << "\n";
    } catch (const std::exception& e) {
        // 兜底:捕获 invalid_argument 等
        std::cerr << "Error: " << e.what() << "\n";
    }
    return 0;
}

输出:

复制代码
Timeout (code=110): connect to 192.168.1.1 timed out

要点:

  • 继承 std::runtime_error 而不是 std::exception,因为前者已经帮你实现了 what()
  • 构造函数参数传给基类的 const std::string&
  • 额外字段(如 error_code_)用 noexcept 的 getter 暴露

八、函数 try-catch 块(Function-try-block)

就是你在问题代码里看到的写法:

cpp 复制代码
int main(int argc, char** argv) try {
    // 整个函数体
} catch (const std::exception& e) {
    std::cerr << e.what() << '\n';
    return 1;
}

它和普通 try-catch 的区别

对于 main 或普通函数,可以改写为相同处理器包住整个函数体的形式:

cpp 复制代码
int main(int argc, char** argv) {
    try {
        // 整个函数体
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
        return 1;
    }
}

只是少了一层缩进,写法更紧凑。

真正有用的场景:构造函数

构造函数体内写的 try-catch 无法 捕获初始化列表中抛出的异常。函数 try-catch 块可以:

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

class Config {
public:
    Config(const std::string& path) {
        if (path.empty()) throw std::runtime_error("empty path");
        std::cout << "Config loaded from: " << path << "\n";
    }
};

class App {
    Config config_;
    int port_;
public:
    // 函数 try-catch 块:能捕获 config_ 初始化时的异常
    App(const std::string& path, int port) try
        : config_(path)     // ← 这里抛异常,下面的 catch 能接住
        , port_(port)
    {
        std::cout << "App constructed\n";
    } catch (const std::exception& e) {
        std::cerr << "App init failed: " << e.what() << "\n";
        // ⚠️ 这里不能"阻止"构造失败
        // 函数结束时异常会自动重新抛出
        // 通常只记录不依赖对象成员的日志;此时已构造的成员和基类已被销毁
    }
};

int main() {
    try {
        App app("", 8080);  // 空路径 → Config 构造抛异常
    } catch (const std::exception& e) {
        std::cerr << "main caught: " << e.what() << "\n";
    }
    return 0;
}

输出:

复制代码
App init failed: empty path
main caught: empty path

⚠️ 构造函数的 function-try-block 中,catch 块结束时必定重新抛出 (隐式 throw;)。你无法"吞掉"异常让对象假装构造成功------因为成员没有正确初始化,对象不完整。


九、noexcept:承诺不抛异常

语法

cpp 复制代码
void safe_swap(int& a, int& b) noexcept {
    int tmp = a;
    a = b;
    b = tmp;
}

noexcept 等价于 noexcept(true),表示"我保证不抛异常"。

为什么要标 noexcept

  1. 优化与容器行为 :编译器可以利用不抛异常的契约;std::vector 扩容时也会根据移动构造是否为 noexcept 等条件决定优先移动还是复制。
  2. 契约 :当成员和基类的析构都不抛异常时,析构函数通常会隐式成为 noexcept;异常一旦逃出 noexcept 函数,就会调用 std::terminate()
  3. 接口文档:告诉调用者异常不会从该函数逃出。

noexcept 运算符(编译期检查)

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

int main() {
    std::cout << std::boolalpha;
    std::cout << noexcept(1 + 1) << "\n";          // true
    std::cout << noexcept(std::vector<int>{}) << "\n"; // true:使用默认分配器的空 vector 默认构造不分配元素存储
    return 0;
}

实际规则

场景 是否标 noexcept
析构函数 通常隐式为 noexcept,不要让异常逃出
swap 确实不会抛出时标
移动构造 / 移动赋值 确实不会抛出时应标,有助于容器选择移动
简单 getter 确实不会抛出时标
可能抛异常的函数(IO、分配内存) 不标

十、异常与 RAII:资源安全的核心

C++ 异常安全依赖 RAII(Resource Acquisition Is Initialization) :资源的生命周期绑定到对象的生命周期。当异常抛出、栈展开(stack unwinding)时,局部对象的析构函数自动调用,资源自动释放。

反面教材(C 风格,异常不安全)

cpp 复制代码
void bad() {
    FILE* f = fopen("data.txt", "r");
    if (!f) throw std::runtime_error("open failed");

    char* buf = new char[1024];
    // new 自身若抛异常,f 会泄漏;process 若抛异常,f 和 buf 都会泄漏
    process(buf);

    delete[] buf;
    fclose(f);
}

正确写法(RAII)

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

void good() {
    std::ifstream f("data.txt");
    if (!f) throw std::runtime_error("open failed");

    auto buf = std::make_unique<char[]>(1024);
    // 即使 process() 抛异常:
    //   1. buf 的析构 → delete[]
    //   2. f 的析构 → 关闭底层文件
    // 栈展开自动完成,无需手写清理
    process(buf.get());
}

核心原则: 优先用智能指针、std::fstreamstd::lock_guard 等 RAII 包装资源,让析构函数完成清理,而不是依赖 catch 中的手动释放。


十一、异常安全级别

写代码时要清楚你的函数提供哪个级别的保证:

级别 含义 例子
nothrow 不抛异常;仍可能通过返回值等方式报告失败 简单 getter、满足条件的析构函数
强保证(strong) 要么成功,要么状态不变(事务语义) std::vector::push_back 通常提供此保证,但对某些会抛出的移动类型存在例外
基本保证(basic) 不泄漏资源,对象处于合法但不确定的状态 大多数标准库操作
无保证 抛异常后状态可能混乱 你自己写的没考虑异常的代码

实际项目目标:至少基本保证,关键操作争取强保证。


十二、异常在多层调用中的传播

异常会沿调用栈向上传播 ,直到遇到匹配的 catch。中间每一层的局部对象都会被析构(栈展开)。

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

struct Guard {
    const char* name;
    Guard(const char* n) : name(n) { std::cout << "  [construct] " << name << "\n"; }
    ~Guard() { std::cout << "  [destruct]  " << name << "\n"; }
};

void level3() {
    Guard g3("g3");
    throw std::runtime_error("boom in level3");
}

void level2() {
    Guard g2("g2");
    level3();
}

void level1() {
    Guard g1("g1");
    level2();
}

int main() {
    try {
        level1();
    } catch (const std::exception& e) {
        std::cout << "caught: " << e.what() << "\n";
    }
    return 0;
}

输出:

复制代码
  [construct] g1
  [construct] g2
  [construct] g3
  [destruct]  g3    ← 栈展开,逆序析构
  [destruct]  g2
  [destruct]  g1
caught: boom in level3

这证明了:你不需要在每一层都写 try-catch。让异常自然传播到能处理它的层级即可。


十三、实际项目中的常见模式

模式 1:顶层兜底(main 或线程入口)

cpp 复制代码
int main(int argc, char** argv) try {
    // 业务逻辑...
} catch (const std::exception& e) {
    std::cerr << "Fatal: " << e.what() << "\n";
    return 1;
} catch (...) {
    std::cerr << "Fatal: unknown exception\n";
    return 1;
}

模式 2:在边界层转换异常类型

cpp 复制代码
// 底层抛 std::system_error,API 层转成业务异常
void api_read_config(const std::string& path) {
    try {
        auto data = read_file(path);  // 可能抛 std::system_error
        parse(data);
    } catch (const std::system_error& e) {
        throw ConfigError("failed to read config: " + std::string(e.what()));
    }
}

模式 3:析构函数中绝不抛

cpp 复制代码
#include <unistd.h>  // POSIX close

class Connection {
public:
    ~Connection() noexcept {
        if (fd_ >= 0 && ::close(fd_) == -1) {
            // close 通过返回值报告失败,不会抛 C++ 异常;这里可按需记录日志
        }
    }
private:
    int fd_{-1};
};

模式 4:用 std::exception_ptr 跨线程传递异常

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

int main() {
    std::exception_ptr eptr;

    std::thread t([&eptr] {
        try {
            throw std::runtime_error("error in worker thread");
        } catch (...) {
            eptr = std::current_exception();  // 捕获并保存
        }
    });
    t.join();

    if (eptr) {
        try {
            std::rethrow_exception(eptr);  // 在主线程重新抛出
        } catch (const std::exception& e) {
            std::cerr << "Main thread caught: " << e.what() << "\n";
        }
    }
    return 0;
}

模式 5:用错误码而非异常(高频路径)

对于预期内的、高频的 错误(如"键不存在"),用返回值/std::optional/std::expected(C++23) 比异常更高效:

cpp 复制代码
#include <optional>
#include <string>
#include <unordered_map>

std::optional<int> find(const std::unordered_map<std::string, int>& m,
                        const std::string& key) {
    auto it = m.find(key);
    if (it == m.end()) return std::nullopt;  // 不抛异常,正常返回
    return it->second;
}

经验法则:

  • 异常情况(不该发生、需要调用者处理)→ 抛异常
  • 正常分支(经常发生、属于流程控制)→ 返回值 / optional

十四、不要做的事

❌ 反模式 为什么
throw e;(在 catch 里) 会创建新的异常对象;按基类捕获时还会发生切片,重新抛出当前异常应使用 throw;
让异常逃出通常为 noexcept 的析构函数 调用 std::terminate();栈展开期间再次抛出尤其危险
用异常做正常流程控制 性能差,语义混乱
catch (...) 后吞掉不处理 隐藏 bug,至少记日志
noexcept 函数里抛异常 直接 std::terminate
抛裸 int / const char* 无法携带上下文,无法多态捕获
在构造函数体内 catch 初始化列表的异常 catch 不到,用 function-try-block

十五、编译与运行

完整示例在补齐其使用的头文件后可用以下命令编译;展示设计模式的片段还需要项目中的占位类型和函数定义:

bash 复制代码
g++ -std=c++17 -Wall -Wextra -o demo demo.cpp
# 或
clang++ -std=c++17 -Wall -Wextra -o demo demo.cpp

具体链接选项取决于示例和平台,例如某些平台上的线程程序需要 -pthread<stdexcept> 本身是头文件。


速查表

text 复制代码
try {
    // 可能出错的代码
    throw std::runtime_error("msg");
} catch (const DerivedException& e) {   // 派生类在前
    // 处理
    throw;                              // 重新抛出(保留原始类型)
} catch (const std::exception& e) {     // 基类在后
    std::cerr << e.what() << '\n';
} catch (...) {                         // 兜底
    // 记日志 / 转换 / 吞掉
}
相关推荐
-银雾鸢尾-1 小时前
C#中的协变和逆变
开发语言·c#
旖旎夜光1 小时前
C++(内存管理)
开发语言·c++·学习
皓月斯语1 小时前
P2858 [USACO06FEB] Treats for the Cows G/S
数据结构·c++·算法·动态规划
XS0301061 小时前
GitHub/Gitee 团队协作笔记
笔记·gitee·github
旖旎夜光1 小时前
LeetCode 397:整数替换(贪心问题) —— 题解
数据结构·c++·算法·leetcode·贪心算法
≮傷£≯√1 小时前
QT配置FFmpeg
开发语言·qt·ffmpeg
别动我齐刘海1 小时前
“三层同步审计”判定掉帧缺失
c语言·c++·人工智能·深度学习·学习·机器学习·机器人
三8441 小时前
RCE长度&字符限制绕过
开发语言·php
祁白_2 小时前
WebShell工具流量特征分析
笔记·web安全·流量特征·webshell工具