第一次接触C++异常处理时,我也被那些try-catch块搞得晕头转向。明明代码逻辑很清晰,一加上异常处理就像天书。后来在调试一个多线程文件解析器时,因为没处理好异常导致内存泄漏,我才真正明白异常处理的价值。
异常处理本质上是一种非本地控制流转移机制。当函数执行遇到无法继续的情况时,它可以抛出一个异常对象,这个对象会沿着调用栈向上传递,直到找到匹配的catch块。这个过程涉及栈展开(stack unwinding),会自动调用局部对象的析构函数,这正是它比传统错误码优越的地方。
关键认知:异常处理不是错误处理的替代品,而是处理那些"罕见但必须处理"的特殊情况。比如内存分配失败、文件损坏、网络中断等。
新手常见的困惑点在于:
标准异常处理包含三个核心语法元素:
cpp复制try {
// 可能抛出异常的代码
if (error_occurred) {
throw std::runtime_error("Something went wrong");
}
} catch (const std::exception& e) {
// 处理标准异常
std::cerr << "Caught exception: " << e.what() << std::endl;
} catch (...) {
// 捕获所有其他异常
std::cerr << "Unknown exception caught" << std::endl;
}
throw语句会立即终止当前函数执行,开始栈展开过程。编译器会按照catch块的出现顺序进行匹配,所以应该先捕获派生类异常,再捕获基类。
这是最容易出错的地方之一。考虑以下代码:
cpp复制class MyException {
public:
MyException() { std::cout << "Constructed\n"; }
~MyException() { std::cout << "Destructed\n"; }
MyException(const MyException&) { std::cout << "Copied\n"; }
};
void foo() {
throw MyException(); // 会发生什么?
}
int main() {
try { foo(); }
catch (const MyException& e) { /*...*/ }
}
输出顺序是:
code复制Constructed
Copied
Destructed
Copied
Destructed
Destructed
因为:
最佳实践:异常类应该轻量且可复制。如果必须包含复杂数据,使用智能指针管理资源。
这是最低要求:无论是否发生异常,程序都处于有效状态,不会资源泄漏。例如:
cpp复制class Database {
Connection* conn;
public:
void updateRecord(int id, const std::string& value) {
Connection* newConn = openNewConnection(); // 可能抛出
delete conn; // 如果这里抛出,资源泄漏!
conn = newConn;
}
};
改进版本:
cpp复制void updateRecord(int id, const std::string& value) {
std::unique_ptr<Connection> newConn(openNewConnection());
delete conn; // 现在即使抛出异常也无妨
conn = newConn.release();
}
操作要么完全成功,要么保持操作前的状态。常用"copy-and-swap"惯用法:
cpp复制class Config {
std::map<std::string, std::string> settings;
public:
void setOption(const std::string& key, const std::string& value) {
auto copy = settings; // 复制
copy[key] = value; // 修改副本
settings.swap(copy); // 交换(不抛出)
}
};
最严格的保证,承诺操作绝不会抛出异常。所有内存释放函数(operator delete)和析构函数默认都应该满足:
cpp复制~MyClass() noexcept { // C++11后析构函数默认noexcept
cleanup(); // 必须确保cleanup不会抛出
}
C++11引入的noexcept有两重作用:
cpp复制void safeFunction() noexcept { // 违反会导致std::terminate
// 保证不抛出的代码
}
template<typename T>
void swap(T& a, T& b) noexcept(noexcept(a.swap(b))) {
a.swap(b); // 条件性noexcept
}
跨线程传递异常的神器:
cpp复制std::exception_ptr eptr;
void worker() {
try { /* 可能抛出 */ }
catch (...) {
eptr = std::current_exception();
}
}
int main() {
std::thread t(worker);
t.join();
if (eptr) {
try { std::rethrow_exception(eptr); }
catch (const std::exception& e) {
std::cerr << "Thread threw: " << e.what();
}
}
}
好的异常类应该:
cpp复制class NetworkError : public std::runtime_error {
std::string url;
int status_code;
public:
NetworkError(const std::string& msg, const std::string& u, int code)
: std::runtime_error(msg), url(u), status_code(code) {}
const char* what() const noexcept override {
static std::string msg;
msg = std::runtime_error::what();
msg += "\nURL: " + url;
msg += "\nStatus: " + std::to_string(status_code);
return msg.c_str();
}
};
假设我们要实现一个CSV文件解析器,会遇到哪些异常场景?
cpp复制class CSVReader {
std::ifstream file;
size_t expectedColumns;
void validateLine(const std::string& line) {
size_t count = std::count(line.begin(), line.end(), ',') + 1;
if (count != expectedColumns) {
throw std::runtime_error("Column count mismatch");
}
}
public:
CSVReader(const std::string& path, size_t cols)
: expectedColumns(cols) {
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
file.open(path);
} catch (const std::ios_base::failure&) {
throw FileOpenError("Failed to open file", path, errno);
}
}
std::vector<std::string> readNext() {
std::string line;
try {
if (!std::getline(file, line)) return {};
validateLine(line);
return split(line, ',');
} catch (const std::bad_alloc&) {
throw_with_nested(
MemoryError("Out of memory while processing file"));
} catch (const std::exception& e) {
auto pos = file.tellg();
throw ParseError(e.what(), pos);
}
}
};
异常处理常被诟病性能问题,实际情况如何?
cpp复制// 不推荐
try {
return vec.at(index); // 可能抛出
} catch (const std::out_of_range&) {
return defaultValue;
}
// 推荐
if (index >= vec.size()) { // 先检查
return defaultValue;
}
return vec[index]; // 不会抛出
该用异常的情况:
不该用异常的情况:
设计原则:
throw;保留原始异常与错误码的配合:
cpp复制// 双重接口示例
class Parser {
public:
// 异常版本
Document parse(const std::string& input) {
// ...
if (error) throw ParseError(...);
return doc;
}
// 错误码版本
bool tryParse(const std::string& input,
Document& out,
std::string& errorMsg) noexcept {
try {
out = parse(input);
return true;
} catch (const std::exception& e) {
errorMsg = e.what();
return false;
}
}
};
最常见的错误是意外吞噬异常:
cpp复制try {
// 可能抛出
} catch (...) {
// 什么都没做!
}
至少应该记录日志:
cpp复制catch (...) {
logError("Unknown exception occurred");
throw; // 重新抛出
}
构造函数抛出异常时,析构函数不会被调用。必须确保已分配的资源被清理:
cpp复制class ResourceHolder {
int* resource1;
FILE* resource2;
public:
ResourceHolder() : resource1(new int[100]) {
resource2 = fopen("file.txt", "r");
if (!resource2) {
delete[] resource1; // 必须手动清理
throw std::runtime_error("File open failed");
}
}
~ResourceHolder() {
delete[] resource1;
if (resource2) fclose(resource2);
}
};
更好的做法是用智能指针管理资源:
cpp复制class ResourceHolder {
std::unique_ptr<int[]> resource1;
std::unique_ptr<FILE, decltype(&fclose)> resource2;
public:
ResourceHolder()
: resource1(new int[100]),
resource2(nullptr, &fclose) {
FILE* f = fopen("file.txt", "r");
if (!f) throw std::runtime_error("File open failed");
resource2.reset(f);
}
// 不再需要显式析构函数
};
std::set_terminate设置未捕获异常处理器catch throw命令捕获异常抛出点std::rethrow_if_nested展开异常链cpp复制void printException(const std::exception& e, int level = 0) {
std::cerr << std::string(level, ' ') << e.what() << '\n';
try {
std::rethrow_if_nested(e);
} catch (const std::exception& nested) {
printException(nested, level + 1);
}
}