C++ 异常处理:搞定程序崩溃与异常安全的核心指南
从try-catch-throw基础到标准异常(std::exception)、自定义异常,配以通俗示例。文章剖析异常安全与RAII,展示智能指针和资源管理如何防泄漏。探讨性能开销、noexcept、多线程与异步异常处理,分享模板和异常层次设计。调试用GDB、ASan,C++11到20的演进也一并覆盖。提供最佳实践,如返回值与异常混合、集中日志处理。常见错误如忘catch、析构抛异常有详细教训。适
引言
异常处理是C++里绕不过的一环。C++不像Java强制要求处理异常,但用好了能让程序更健壮,用不好就是一堆未定义行为(UB)。我这些年从手动检查返回值到拥抱try-catch,再到异常安全的代码设计,算是趟出了一条路。异常处理的核心是“出错时优雅退出”,而不是让程序炸掉。好了,废话不多说,开始正题!
第一部分:异常处理基础
1.1 什么是异常?
异常是程序运行时发生的意外情况,比如文件打不开、内存分配失败、除零错误。C++用异常机制让你捕获和处理这些问题,而不是直接崩溃。
C++的异常处理基于三关键字:try、catch、throw。
- try:包裹可能抛异常的代码。
- catch:捕获并处理异常。
- throw:抛出异常对象。
异常可以是任何类型(int、string、自定义类),但建议用std::exception或其派生类,因为标准且好用。
为什么用异常? 比起返回值检查,异常能跳出调用栈,直接到能处理的地方,代码更简洁。早年我用返回值检查文件操作,结果代码嵌套七八层,维护简直噩梦。
1.2 基本语法
最简单的异常处理长这样:
#include <iostream>
using namespace std;
int main() {
try {
throw 42; // 抛个整数异常
} catch(int e) {
cout << "Caught int: " << e << endl;
}
return 0;
}
输出:Caught int: 42
try里抛异常,catch捕获对应类型。如果不捕获,程序调用std::terminate(),直接退出。
我第一次用异常是在读文件时,忘了catch,程序崩了。后来才明白,异常要么处理,要么传上去。
1.3 异常的传播
异常会沿着调用栈向上找catch,没找到就terminate。可以用多级catch捕获不同类型。
示例:多级catch。
#include <iostream>
#include <string>
using namespace std;
void func() {
throw string("Error in func!");
}
int main() {
try {
func();
} catch(int e) {
cout << "Caught int: " << e << endl;
} catch(const string& s) {
cout << "Caught string: " << s << endl;
} catch(...) { // 捕获所有
cout << "Caught unknown" << endl;
}
return 0;
}
输出:Caught string: Error in func!
经验:总是放个catch(...)兜底,避免漏掉异常。我在服务器项目里加了这个,救了不少崩溃。
第二部分:标准异常与自定义异常
2.1 标准异常
C++标准库提供了std::exception(在<stdexcept>),常用派生类:
- std::logic_error:逻辑错误,如invalid_argument、out_of_range。
- std::runtime_error:运行时错误,如overflow_error、bad_alloc。
示例:用标准异常。
#include <iostream>
#include <stdexcept>
using namespace std;
double divide(double a, double b) {
if(b == 0) throw runtime_error("Divide by zero!");
return a / b;
}
int main() {
try {
cout << divide(10, 0) << endl;
} catch(const runtime_error& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}
输出:Error: Divide by zero!
标准异常的what()返回错误描述。bad_alloc常用于new失败。
我用runtime_error处理网络超时,logic_error检查参数。
2.2 自定义异常
复杂项目需要自定义异常,继承std::exception。
示例:自定义文件异常。
#include <iostream>
#include <stdexcept>
using namespace std;
class FileError : public runtime_error {
public:
FileError(const string& msg) : runtime_error(msg) {}
};
void openFile(const string& filename) {
if(filename.empty()) throw FileError("Empty filename");
// 模拟文件操作
throw FileError("File not found: " + filename);
}
int main() {
try {
openFile("");
} catch(const FileError& e) {
cout << "File error: " << e.what() << endl;
} catch(const exception& e) {
cout << "Standard error: " << e.what() << endl;
} catch(...) {
cout << "Unknown error" << endl;
}
return 0;
}
输出:File error: Empty filename
自定义异常清晰,我在数据库模块用过,区分不同错误类型。
第三部分:异常安全与RAII
3.1 异常安全保证
异常可能导致资源泄漏,如new了内存但没delete。异常安全有三级别:
- 基本保证:异常后程序状态合法,可能丢数据。
- 强保证:异常后状态不变(回滚)。
- 不抛异常:函数不抛异常(noexcept)。
示例:资源泄漏问题。
void badFunc() {
int* p = new int[100];
throw runtime_error("Oops"); // p没delete
delete[] p;
}
修复用RAII(资源获取即初始化)。
3.2 RAII与异常安全
RAII用对象生命周期管理资源,如智能指针、文件句柄。异常发生,析构自动清理。
示例:用unique_ptr。
#include <iostream>
#include <memory>
using namespace std;
void safeFunc() {
unique_ptr<int[]> p(new int[100]);
throw runtime_error("Safe throw"); // p自动delete
}
int main() {
try {
safeFunc();
} catch(const exception& e) {
cout << e.what() << endl;
}
return 0;
}
输出:Safe throw
unique_ptr自动释放。我在多线程服务器用RAII管理锁,异常也不漏。
3.3 自定义RAII类
封装资源,如文件。
示例:
#include <iostream>
#include <fstream>
using namespace std;
class FileHandle {
FILE* fp;
public:
FileHandle(const char* name) : fp(fopen(name, "r")) {
if(!fp) throw runtime_error("Open failed");
}
~FileHandle() {
if(fp) fclose(fp);
cout << "File closed" << endl;
}
// 禁用拷贝
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
};
int main() {
try {
FileHandle fh("test.txt");
throw runtime_error("Error after open");
} catch(const exception& e) {
cout << e.what() << endl;
}
return 0;
}
输出(假设文件不存在):Error: Open failed 或(文件存在):Error after open File closed
RAII让我少写一堆清理代码。
第四部分:异常的性能与注意事项
4.1 性能开销
异常的抛出和捕获比返回值检查慢,因为涉及栈展开、对象拷贝。
测试:
#include <iostream>
#include <chrono>
using namespace std;
void throwFunc(int n) {
if(n < 0) throw runtime_error("Negative");
}
int returnFunc(int n) {
return n < 0 ? -1 : 0;
}
int main() {
auto start = chrono::high_resolution_clock::now();
for(int i=0; i<1000000; ++i) {
try {
throwFunc(i);
} catch(...) {}
}
auto end = chrono::high_resolution_clock::now();
cout << "Exception: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
start = chrono::high_resolution_clock::now();
for(int i=0; i<1000000; ++i) {
returnFunc(i);
}
end = chrono::high_resolution_clock::now();
cout << "Return: " << chrono::duration_cast<chrono::milliseconds>(end - start).count() << "ms" << endl;
return 0;
}
输出(因机器而异):Exception: 200ms Return: 10ms
异常适合“异常”情况,别用作控制流。
我曾用异常做循环退出,结果性能崩了,改回if-else。
4.2 注意事项
- 异常规格(C++11前):throw(Type)限制抛出类型,C++11弃用,改用noexcept。
- noexcept:声明不抛异常,优化性能。
void noThrow() noexcept {
// 编译器优化
}
- 栈展开:异常抛出析构局部对象,小心析构再抛异常。
- 异常对象拷贝:抛对象可能拷贝,建议throw by value, catch by const ref。
- 多线程:线程抛异常只能本线程catch。
我在线程池用noexcept函数,减少开销。
第五部分:异常处理的最佳实践
5.1 异常 vs 返回值
- 异常:罕见错误,跨多层调用,如文件丢失。
- 返回值:常见情况,如用户输入错误。
示例:混合使用。
#include <iostream>
#include <stdexcept>
using namespace std;
int parseNumber(const string& s) {
try {
return stoi(s); // 抛invalid_argument
} catch(const invalid_argument&) {
return -1; // 转为返回值
}
}
int main() {
cout << parseNumber("123") << endl; // 123
cout << parseNumber("abc") << endl; // -1
return 0;
}
5.2 异常安全的函数设计
- 强保证:用RAII,临时对象。
- 基本保证:检查状态后操作。
示例:异常安全的swap。
#include <iostream>
#include <memory>
using namespace std;
class SafeArray {
unique_ptr<int[]> data;
size_t size;
public:
SafeArray(size_t n) : data(new int[n]), size(n) {}
void swap(SafeArray& other) noexcept {
data.swap(other.data);
std::swap(size, other.size);
}
};
int main() {
SafeArray a(5), b(10);
a.swap(b); // 异常安全
return 0;
}
5.3 集中异常处理
顶层catch,记录日志。
示例:
#include <iostream>
#include <fstream>
using namespace std;
void process() {
throw runtime_error("Processing failed");
}
int main() {
ofstream log("error.log");
try {
process();
} catch(const exception& e) {
log << "Error: " << e.what() << endl;
cout << "Logged error" << endl;
}
return 0;
}
输出:Logged error
我在服务器用这种方式统一记录。
第六部分:高级主题
6.1 异常与模板
模板函数可能抛未知异常,用noexcept(false)。
示例:
template<typename T>
T process(T x) noexcept(false) {
if(x < 0) throw runtime_error("Negative");
return x;
}
6.2 异常与多线程
线程抛异常需本线程catch,主线程无法捕获。
示例:
#include <iostream>
#include <thread>
using namespace std;
void worker() {
try {
throw runtime_error("Thread error");
} catch(const exception& e) {
cout << "Caught in thread: " << e.what() << endl;
}
}
int main() {
thread t(worker);
t.join();
return 0;
}
输出:Caught in thread: Thread error
C++20 jthread自动join,简化。
6.3 异常与异步
std::future捕获异常。
示例:
#include <iostream>
#include <future>
using namespace std;
int asyncTask() {
throw runtime_error("Async error");
}
int main() {
auto fut = async(launch::async, asyncTask);
try {
fut.get();
} catch(const exception& e) {
cout << e.what() << endl;
}
return 0;
}
输出:Async error
6.4 自定义异常层次
复杂系统用异常继承体系。
示例:
class DatabaseError : public runtime_error {
public:
DatabaseError(const string& msg) : runtime_error(msg) {}
};
class ConnectionError : public DatabaseError {
public:
ConnectionError(const string& msg) : DatabaseError(msg) {}
};
class QueryError : public DatabaseError {
public:
QueryError(const string& msg) : DatabaseError(msg) {}
};
分层处理不同错误。
第七部分:调试与工具
7.1 GDB
用gdb捕获异常点,break std::exception::what。
7.2 ASan
AddressSanitizer查内存问题,可能暴露异常引发的泄漏。
7.3 日志
记录异常栈,C++20有source_location。
示例:
#include <iostream>
#include <source_location>
using namespace std;
void logError(const string& msg, const source_location loc = source_location::current()) {
cout << loc.function_name() << ":" << loc.line() << " - " << msg << endl;
}
int main() {
try {
throw runtime_error("Test error");
} catch(const exception& e) {
logError(e.what());
}
return 0;
}
输出类似:main:12 - Test error
第八部分:C++标准演进
- C++98:基本try-catch,std::exception。
- C++11:noexcept,exception_ptr。
- C++17:std::uncaught_exceptions()。
- C++20:source_location。
我从C++98到20,异常越来越好用。
常见错误与教训
- 忘catch,程序terminate。
- catch(...)不记录,查不出原因。
- 析构抛异常,触发terminate。
- 异常对象拷贝开销大,throw by value。
- 多线程异常未捕获。
教训:多用RAII,少裸new,顶层catch。
结语
C++异常处理是健壮程序的基石。我从踩坑到熟练,靠的是多练和总结。异常不是万能药,但用对地方能救命。建议新手从标准异常练起,老手关注异常安全和性能。欢迎评论分享你的坑!
更多推荐



所有评论(0)