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 | 禁用异常,throw → std::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_error 或 std::logic_error |
| 不要捕获后忽略 | 空 catch {} 隐藏问题 |
| RAII 管理资源 | 确保异常发生时资源正确释放 |
标记 noexcept | 移动构造函数、swap、析构函数应标记 |
| 不要在析构函数中抛异常 | C++11 起析构函数默认 noexcept |
使用 std::terminate | 无法恢复时直接终止 |
考虑 std::expected | C++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;
}
扩展阅读
- cppreference — Exception handling
- C++ Core Guidelines — E: Error handling
- Effective C++ 条款 29-33(异常安全)
- C++ Coding Standards 条款 71-77
- Herbceptions — Herb Sutter’s exception proposal