【C++进阶】异常
1. 传统错误处理方式
在C++之前,C语言主要使用两种错误处理方式:
1.1 终止程序
cpp
#include <cassert>
void traditional_approach1() {
int* ptr = malloc(sizeof(int) * 100);
assert(ptr != nullptr); // 如果分配失败,直接终止程序
// 使用内存...
free(ptr);
}
缺点:用户体验差,程序突然崩溃。
1.2 返回错误码
cpp
#include <cerrno>
#include <cstring>
int traditional_approach2() {
FILE* file = fopen("nonexistent.txt", "r");
if (file == nullptr) {
printf("错误: %s\n", strerror(errno)); // 返回错误码
return -1;
}
// 处理文件...
fclose(file);
return 0;
}
缺点:需要程序员手动检查错误码,容易遗漏。
2. C++异常概念
C++引入异常机制,提供更优雅的错误处理方式:
- throw:抛出异常
- try:尝试执行可能抛出异常的代码
- catch:捕获并处理异常
cpp
#include <iostream>
#include <stdexcept>
double divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("除数不能为零!");
}
return static_cast<double>(a) / b;
}
int main() {
try {
double result = divide(10, 0);
std::cout << "结果: " << result << std::endl;
}
catch (const std::exception& e) {
std::cout << "捕获异常: " << e.what() << std::endl;
}
return 0;
}
3. 异常的使用
3.1 异常的抛出和捕获
异常匹配原则:
- 异常通过抛出对象引发,对象类型决定匹配的catch块
- 选择调用链中类型匹配且距离最近的catch块
- 抛出异常会生成对象拷贝
catch(...)可捕获任意类型异常- 可以用基类捕获派生类异常(多态)
cpp
#include <iostream>
#include <string>
void func3() {
throw std::string("在func3中发生错误!");
}
void func2() {
func3();
}
void func1() {
func2();
}
int main() {
try {
func1();
}
catch (const std::string& msg) {
std::cout << "字符串异常: " << msg << std::endl;
}
catch (const char* msg) {
std::cout << "C字符串异常: " << msg << std::endl;
}
catch (...) {
std::cout << "未知异常" << std::endl;
}
return 0;
}
3.2 异常的重新抛出
cpp
#include <iostream>
#include <memory>
void processResource() {
auto resource = std::make_unique<int>(42);
try {
// 模拟可能抛出异常的操作
throw std::runtime_error("处理资源时发生错误");
}
catch (...) {
std::cout << "清理资源并重新抛出异常" << std::endl;
// 资源会被unique_ptr自动释放
throw; // 重新抛出
}
}
int main() {
try {
processResource();
}
catch (const std::exception& e) {
std::cout << "主函数捕获: " << e.what() << std::endl;
}
return 0;
}
3.3 异常安全
重要准则:
- 构造函数:避免抛出异常,可能导致对象不完整
- 析构函数:绝对不要抛出异常,可能导致资源泄漏
- 使用RAII(资源获取即初始化)管理资源
cpp
#include <memory>
#include <vector>
class DatabaseConnection {
private:
std::unique_ptr<int> connection;
public:
DatabaseConnection() {
connection = std::make_unique<int>(1);
// 如果这里抛出异常,对象构造失败是安全的
}
~DatabaseConnection() {
// 析构函数不抛出异常
// 资源由unique_ptr自动管理
}
// 禁用拷贝,避免资源管理问题
DatabaseConnection(const DatabaseConnection&) = delete;
DatabaseConnection& operator=(const DatabaseConnection&) = delete;
};
3.4 异常规范
cpp
#include <iostream>
// C++98风格:可能抛出int或double异常
void old_style() throw(int, double) {
throw 42;
}
// C++11风格:不抛出异常
void modern_style() noexcept {
// 这个函数保证不会抛出异常
}
// 无异常规范:可能抛出任何异常
void any_exception() {
throw std::runtime_error("任何异常");
}
int main() {
try {
old_style();
}
catch (int e) {
std::cout << "捕获int异常: " << e << std::endl;
}
catch (double e) {
std::cout << "捕获double异常: " << e << std::endl;
}
return 0;
}
4. 自定义异常体系
实际项目中通常需要自定义异常体系:
cpp
#include <iostream>
#include <string>
#include <memory>
// 基础异常类
class Exception {
protected:
std::string _errmsg;
int _id;
public:
Exception(const std::string& errmsg, int id)
: _errmsg(errmsg), _id(id) {}
virtual ~Exception() = default;
virtual std::string what() const {
return _errmsg + " (错误码: " + std::to_string(_id) + ")";
}
};
// 数据库异常
class SqlException : public Exception {
private:
std::string _sql;
public:
SqlException(const std::string& errmsg, int id, const std::string& sql)
: Exception(errmsg, id), _sql(sql) {}
std::string what() const override {
return "SQL异常: " + _errmsg + " -> " + _sql;
}
};
// 缓存异常
class CacheException : public Exception {
public:
CacheException(const std::string& errmsg, int id)
: Exception(errmsg, id) {}
std::string what() const override {
return "缓存异常: " + _errmsg;
}
};
// HTTP服务器异常
class HttpServerException : public Exception {
private:
std::string _type;
public:
HttpServerException(const std::string& errmsg, int id, const std::string& type)
: Exception(errmsg, id), _type(type) {}
std::string what() const override {
return "HTTP服务器异常[" + _type + "]: " + _errmsg;
}
};
// 模拟业务逻辑
void processDatabase() {
// 模拟数据库操作可能抛出异常
throw SqlException("权限不足", 1001, "SELECT * FROM users");
}
void processCache() {
// 模拟缓存操作
throw CacheException("缓存键不存在", 2001);
}
void handleHttpRequest() {
// 模拟HTTP请求处理
throw HttpServerException("请求超时", 3001, "GET");
}
int main() {
try {
handleHttpRequest();
processCache();
processDatabase();
}
catch (const Exception& e) {
// 多态处理:一个catch块处理所有派生异常
std::cout << e.what() << std::endl;
}
catch (...) {
std::cout << "未知异常" << std::endl;
}
return 0;
}
5. C++标准库异常体系
C++标准库提供了一套完整的异常体系:
cpp
#include <iostream>
#include <stdexcept>
#include <vector>
#include <memory>
void demonstrate_standard_exceptions() {
try {
// 1. bad_alloc - 内存分配失败
std::unique_ptr<int[]> huge_array =
std::make_unique<int[]>(1000000000000LL);
}
catch (const std::bad_alloc& e) {
std::cout << "内存分配失败: " << e.what() << std::endl;
}
try {
// 2. out_of_range - 下标越界
std::vector<int> vec = {1, 2, 3};
int value = vec.at(10); // 抛出std::out_of_range
}
catch (const std::out_of_range& e) {
std::cout << "下标越界: " << e.what() << std::endl;
}
try {
// 3. invalid_argument - 无效参数
std::string str("hello");
int num = std::stoi(str); // 可能抛出std::invalid_argument
}
catch (const std::invalid_argument& e) {
std::cout << "无效参数: " << e.what() << std::endl;
}
try {
// 4. logic_error - 逻辑错误
throw std::logic_error("逻辑错误示例");
}
catch (const std::logic_error& e) {
std::cout << "逻辑错误: " << e.what() << std::endl;
}
}
int main() {
demonstrate_standard_exceptions();
return 0;
}
标准异常类层次结构:
std::exception
├── std::bad_alloc
├── std::bad_cast
├── std::bad_typeid
├── std::bad_exception
├── std::logic_error
│ ├── std::domain_error
│ ├── std::invalid_argument
│ ├── std::length_error
│ └── std::out_of_range
└── std::runtime_error
├── std::overflow_error
├── std::range_error
├── std::underflow_error
└── std::system_error
6. 异常的优缺点
6.1 优点
1. 清晰的错误信息:
cpp
// 传统方式
int connectDatabase() {
if (/* 连接失败 */) return -1;
if (/* 权限不足 */) return -2;
return 0;
}
// 异常方式
void connectDatabaseWithException() {
if (/* 连接失败 */)
throw std::runtime_error("数据库连接失败: 网络超时");
if (/* 权限不足 */)
throw std::runtime_error("数据库连接失败: 权限不足");
}
2. 调用链简化:
cpp
void deepFunction() {
throw std::runtime_error("深层错误");
}
void middleFunction() {
deepFunction(); // 不需要检查返回值
}
void topFunction() {
try {
middleFunction();
}
catch (const std::exception& e) {
std::cout << "统一处理: " << e.what() << std::endl;
}
}
3. 构造函数错误处理:
cpp
class FileHandler {
private:
std::FILE* file;
public:
FileHandler(const std::string& filename) {
file = std::fopen(filename.c_str(), "r");
if (!file) {
throw std::runtime_error("无法打开文件: " + filename);
}
}
~FileHandler() {
if (file) std::fclose(file);
}
};
6.2 缺点
1. 性能开销 :异常处理有一定性能代价
2. 代码复杂度 :可能使控制流难以跟踪
3. 资源管理:需要RAII确保异常安全
7. 推荐用法
7.1 RAII确保异常安全
cpp
#include <memory>
#include <fstream>
class SafeFile {
private:
std::unique_ptr<std::FILE, decltype(&std::fclose)> file;
public:
SafeFile(const std::string& filename, const std::string& mode)
: file(std::fopen(filename.c_str(), mode.c_str()), &std::fclose) {
if (!file) {
throw std::runtime_error("无法打开文件: " + filename);
}
}
void write(const std::string& content) {
if (std::fputs(content.c_str(), file.get()) == EOF) {
throw std::runtime_error("写入文件失败");
}
}
};
void safeFileOperation() {
SafeFile file("data.txt", "w");
file.write("安全的数据写入");
// 即使抛出异常,文件也会自动关闭
}
7.2 异常规范建议
cpp
class RobustClass {
public:
// 明确标注不抛异常的函数
void simpleOperation() noexcept {
// 简单操作,保证不抛异常
}
// 可能抛异常的函数要明确文档说明
/**
* @brief 复杂操作,可能抛出std::runtime_error
* @throws std::runtime_error 当操作失败时
*/
void complexOperation() {
if (/* 失败条件 */) {
throw std::runtime_error("操作失败");
}
}
// 析构函数绝对不抛异常
~RobustClass() noexcept {
// 清理资源,确保不抛异常
}
};
总结
C++异常处理提供了强大的错误处理机制:
- 优势:清晰的错误信息、简化的错误传播、构造函数错误处理
- 挑战:性能开销、代码复杂度、资源管理
- 推荐用法:使用RAII、自定义异常体系、明确的异常规范