首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++高级主题】异常处理(一)

【C++高级主题】异常处理(一)

作者头像
byte轻骑兵
发布2026-01-20 17:22:35
发布2026-01-20 17:22:35
1100
举报

C++ 异常处理机制为程序提供了一种处理运行时错误的结构化方式,使代码的错误处理逻辑与正常业务逻辑分离。通过抛出(throw)和捕获(catch)异常,程序能够在错误发生时进行栈展开(Stack Unwinding),并将控制权转移到合适的异常处理代码中。

一、异常处理基础

1.1 异常的基本概念

异常是程序在执行期间发生的特殊情况,如内存不足、文件打开失败、除零错误等。C++ 异常处理机制允许程序在错误发生时跳转到特定的错误处理代码,而不是通过返回错误码层层传递错误信息。

异常处理的三个关键步骤:

  1. 抛出异常:使用throw语句在错误发生点抛出异常对象
  2. 捕获异常:使用try-catch块捕获并处理异常
  3. 栈展开:异常抛出后,程序沿调用栈向上寻找匹配的 catch 块,期间自动销毁局部对象

1.2 异常处理的语法

代码语言:javascript
复制
try {
    // 可能抛出异常的代码
    if (error_condition) {
        throw ExceptionType("Error message"); // 抛出异常对象
    }
    // 正常代码
} catch (const ExceptionType1& e) {
    // 处理ExceptionType1类型的异常
} catch (const ExceptionType2& e) {
    // 处理ExceptionType2类型的异常
} catch (...) {
    // 捕获所有异常
    // 通常用于资源清理或记录日志
}

1.3 异常类的设计

C++ 标准库提供了一套异常类层次结构,基类为std::exception,位于<exception>头文件中。常见的标准异常类包括:

  • std::logic_error:逻辑错误(如无效参数)
  • std::runtime_error:运行时错误(如资源耗尽)
  • std::bad_alloc:内存分配失败
  • std::out_of_range:越界访问

用户可以通过继承这些标准异常类来创建自定义异常:

代码语言:javascript
复制
#include <stdexcept>
#include <string>

class MyException : public std::runtime_error {
public:
    MyException(const std::string& message) 
        : std::runtime_error(message) {}
};

二、函数抛出类类型的异常

2.1 抛出内置类型异常

C++ 允许抛出任何类型的异常,但推荐使用类类型异常,因为它们可以携带更多信息并支持继承层次结构。不过,为了演示,我们先看一个抛出内置类型异常的例子:

代码语言:javascript
复制
#include <iostream>

void divide(int a, int b) {
    if (b == 0) {
        throw 0; // 抛出int类型异常
    }
    std::cout << "Result: " << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0);
    } catch (int e) {
        std::cout << "Error: Division by zero!" << std::endl;
    }
    return 0;
}

2.2 抛出类类型异常

更常见的做法是抛出类类型异常,尤其是从标准异常类派生的自定义异常:

代码语言:javascript
复制
#include <iostream>
#include <stdexcept>

class DivideByZeroException : public std::runtime_error {
public:
    DivideByZeroException() : std::runtime_error("Division by zero") {}
};

double safeDivide(double numerator, double denominator) {
    if (denominator == 0) {
        throw DivideByZeroException(); // 抛出类类型异常
    }
    return numerator / denominator;
}

int main() {
    try {
        double result = safeDivide(10.0, 0.0);
        std::cout << "Result: " << result << std::endl;
    } catch (const DivideByZeroException& e) {
        std::cout << "Exception caught: " << e.what() << std::endl;
    } catch (const std::exception& e) {
        std::cout << "General exception: " << e.what() << std::endl;
    }
    return 0;
}

2.3 异常对象的拷贝语义

当抛出异常时,异常对象会被复制到一个特殊的内存区域(异常对象副本),即使原对象在栈展开过程中被销毁,该副本仍然存在。意味着:

  • 异常对象必须是可拷贝的(或可移动的)
  • 捕获异常时,通常通过引用捕获以避免二次拷贝
  • 异常对象的生命周期持续到最后一个匹配的 catch 块结束
代码语言:javascript
复制
class MyException {
public:
    MyException() { std::cout << "Constructor" << std::endl; }
    MyException(const MyException&) { std::cout << "Copy Constructor" << std::endl; }
    ~MyException() { std::cout << "Destructor" << std::endl; }
};

void func() {
    MyException e;
    std::cout << "Throwing exception..." << std::endl;
    throw e; // 拷贝构造异常对象副本
}

int main() {
    try {
        func();
    } catch (const MyException& e) {
        std::cout << "Exception caught" << std::endl;
    }
    return 0;
}

三、栈展开(Stack Unwinding)

3.1 栈展开的过程

当抛出异常时,程序会暂停当前函数的执行,开始沿调用栈向上寻找匹配的 catch 块。这个过程称为栈展开,具体步骤如下:

  1. 抛出异常后,当前函数的执行立即停止
  2. 局部对象按构造的相反顺序析构
  3. 控制权转移到最近的匹配 catch 块
  4. 如果当前函数没有匹配的 catch 块,继续向上一层调用函数展开
  5. 重复步骤 2-4,直到找到匹配的 catch 块或到达 main 函数

3.2 栈展开的示例

代码语言:javascript
复制
#include <iostream>
#include <stdexcept>
#include <string>

class Resource {
public:
    Resource(const std::string& name) : name(name) {
        std::cout << "Resource " << name << " acquired" << std::endl;
    }
    ~Resource() {
        std::cout << "Resource " << name << " released" << std::endl;
    }
private:
    std::string name;
};

void func3() {
    Resource r3("r3");
    std::cout << "In func3, throwing exception..." << std::endl;
    throw std::runtime_error("Exception from func3");
}

void func2() {
    Resource r2("r2");
    std::cout << "In func2, calling func3..." << std::endl;
    func3();
}

void func1() {
    Resource r1("r1");
    std::cout << "In func1, calling func2..." << std::endl;
    try {
        func2();
    } catch (const std::exception& e) {
        std::cout << "Caught exception in func1: " << e.what() << std::endl;
    }
}

int main() {
    std::cout << "Starting program..." << std::endl;
    func1();
    std::cout << "Program completed." << std::endl;
    return 0;
}

3.3 栈展开与资源管理

栈展开机制确保了局部对象的析构函数会被正确调用,这为 RAII(资源获取即初始化)技术提供了坚实的基础。通过将资源封装在对象中,可以确保在异常发生时资源被正确释放。

代码语言:javascript
复制
class File {
public:
    File(const std::string& filename) : file_ptr(fopen(filename.c_str(), "r")) {
        if (!file_ptr) {
            throw std::runtime_error("Failed to open file");
        }
    }
    
    ~File() {
        if (file_ptr) {
            fclose(file_ptr);
        }
    }
    
    // 禁用拷贝构造和赋值
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
private:
    FILE* file_ptr;
};

void processFile(const std::string& filename) {
    File file(filename); // 资源获取即初始化
    // 处理文件...
    // 无论是否抛出异常,File析构函数都会关闭文件
}

四、捕获异常

4.1 异常捕获的语法

异常通过 catch 块捕获,语法如下:

代码语言:javascript
复制
catch (ExceptionType1 e) { /* 按值捕获 */ }
catch (ExceptionType2& e) { /* 按引用捕获 */ }
catch (const ExceptionType3& e) { /* 按const引用捕获(推荐) */ }
catch (...) { /* 捕获所有异常 */ }

4.2 按值、引用和指针捕获的区别

①按值捕获(catch (ExceptionType e)

  • 异常对象被拷贝到 catch 参数中
  • 可能发生切片(slicing)问题(如果捕获基类但异常是派生类)
  • 效率较低(需要额外拷贝)

②按引用捕获(catch (ExceptionType& e)

  • 直接引用异常对象副本
  • 避免拷贝开销
  • 可以修改异常对象(不推荐)

③按 const 引用捕获(catch (const ExceptionType& e)

  • 推荐做法
  • 避免拷贝开销
  • 防止意外修改异常对象
  • 可以正确处理多态(通过基类引用捕获派生类对象)

④按指针捕获(catch (ExceptionType* e)

  • 必须抛出指针(throw &e;),不推荐(容易导致悬空指针)
  • 异常对象生命周期管理复杂
  • 很少使用

4.3 捕获顺序与多态

catch 块按顺序匹配,一旦找到匹配的 catch 块,后续的 catch 块将被忽略。因此,捕获派生类异常的 catch 块应放在捕获基类异常的 catch 块之前。

代码语言:javascript
复制
class BaseException : public std::exception {};
class DerivedException : public BaseException {};

void func() {
    throw DerivedException();
}

int main() {
    try {
        func();
    } catch (const DerivedException& e) {
        std::cout << "Caught DerivedException" << std::endl;
    } catch (const BaseException& e) {
        std::cout << "Caught BaseException" << std::endl;
    } catch (const std::exception& e) {
        std::cout << "Caught std::exception" << std::endl;
    } catch (...) {
        std::cout << "Caught unknown exception" << std::endl;
    }
    return 0;
}

4.4 捕获所有异常(catch-all)

使用catch (...)可以捕获所有类型的异常,通常用于:

  1. 记录致命错误
  2. 执行必要的资源清理
  3. 重新抛出异常(见下文)
代码语言:javascript
复制
try {
    // 可能抛出异常的代码
} catch (...) {
    std::cerr << "Unknown exception caught!" << std::endl;
    // 资源清理
    throw; // 重新抛出异常
}

五、重新抛出异常

5.1 什么是重新抛出异常?

在 catch 块中,可以使用throw;语句将当前捕获的异常重新抛出,让上层调用者处理。重新抛出的异常就是原来的异常对象,不会创建新的异常对象。

代码语言:javascript
复制
try {
    // 可能抛出异常的代码
} catch (const std::exception& e) {
    std::cerr << "Logging exception: " << e.what() << std::endl;
    throw; // 重新抛出,让上层处理
}

5.2 重新抛出的用途

  1. 分层处理异常:底层函数捕获并记录异常,然后重新抛出让高层函数处理
  2. 异常转换:捕获一种异常,转换为另一种异常抛出
  3. 资源清理:在重新抛出前执行必要的资源清理

5.3 异常转换示例

代码语言:javascript
复制
class DatabaseException : public std::runtime_error {
public:
    DatabaseException(const std::string& msg) : std::runtime_error(msg) {}
};

class NetworkException : public std::runtime_error {
public:
    NetworkException(const std::string& msg) : std::runtime_error(msg) {}
};

void connectToDatabase() {
    // 尝试连接数据库
    if (connection_failed) {
        throw NetworkException("Database connection failed");
    }
}

void processData() {
    try {
        connectToDatabase();
        // 处理数据
    } catch (const NetworkException& e) {
        // 记录网络异常
        std::cerr << "Network error: " << e.what() << std::endl;
        // 转换为数据库异常并重新抛出
        throw DatabaseException("Database operation failed");
    }
}

六、函数测试块与构造函数异常

6.1 函数测试块(Function Try Block)

函数测试块允许对整个函数体进行异常捕获,语法如下:

代码语言:javascript
复制
ReturnType FunctionName(Parameters)
try {
    // 函数体
} catch (const ExceptionType& e) {
    // 异常处理
}

函数测试块特别适用于构造函数,因为构造函数没有返回值,无法通过返回错误码表示失败。

6.2 构造函数异常

构造函数可以抛出异常,但需要注意以下几点:

  1. 如果构造函数抛出异常,对象的析构函数不会被调用(因为对象尚未完全构造)
  2. 基类构造函数抛出异常时,派生类构造函数中已构造的成员会被正确析构
  3. 可以使用函数测试块捕获构造函数中的异常
代码语言:javascript
复制
class MyClass {
private:
    Resource* res;
    int* data;

public:
    MyClass() try : res(new Resource), data(new int[100]) {
        // 可能抛出异常的初始化代码
        if (initialization_failed) {
            throw std::runtime_error("Initialization failed");
        }
    } catch (const std::exception& e) {
        // 清理已分配的资源
        delete res;
        delete[] data;
        throw; // 重新抛出异常
    }
    
    ~MyClass() {
        delete res;
        delete[] data;
    }
};

6.3 使用智能指针简化构造函数异常处理

使用智能指针(如std::unique_ptrstd::shared_ptr)可以自动管理资源,避免手动清理:

代码语言:javascript
复制
#include <memory>

class MyClass {
private:
    std::unique_ptr<Resource> res;
    std::unique_ptr<int[]> data;

public:
    MyClass() : res(std::make_unique<Resource>()), data(std::make_unique<int[]>(100)) {
        // 如果构造过程中抛出异常,智能指针会自动释放已分配的资源
        if (initialization_failed) {
            throw std::runtime_error("Initialization failed");
        }
    }
};

七、异常规范(Exception Specification)

7.1 已弃用的异常规范

在 C++98/03 中,可以使用异常规范声明函数可能抛出的异常:

代码语言:javascript
复制
void func() throw(std::exception); // 可能抛出std::exception或其子类
void func2() throw(); // 不抛出任何异常

但这种异常规范在实践中被证明是有问题的,因此在 C++11 中被弃用,并在 C++17 中移除。

7.2 noexcept 说明符

C++11 引入了noexcept说明符,用于声明函数是否会抛出异常:

代码语言:javascript
复制
void func() noexcept; // 承诺不会抛出任何异常
void func2() noexcept(true); // 同上
void func3() noexcept(false); // 可能抛出异常

noexcept有两个主要用途:

  1. 性能优化:编译器可以针对不抛出异常的函数进行优化
  2. 移动语义:容器在重新分配内存时,会优先使用noexcept的移动构造函数

7.3 noexcept 与析构函数

析构函数默认是noexcept(true),即不会抛出异常。如果需要析构函数可能抛出异常,必须显式声明为noexcept(false)

代码语言:javascript
复制
class MyClass {
public:
    ~MyClass() noexcept(false) {
        // 可能抛出异常的析构代码
    }
};

八、异常处理的最佳实践

8.1 异常安全保证

C++ 提供了三种异常安全级别:

  1. 基本保证:操作失败后,对象仍处于有效状态,没有资源泄漏
  2. 强保证:操作要么成功,要么对对象没有任何影响(原子性)
  3. 不抛异常保证:操作绝对不会抛出异常

设计函数时应明确提供哪种异常安全保证,并在文档中说明。

8.2 异常处理的常见误区

①捕获异常后不处理也不重新抛出

代码语言:javascript
复制
try {
    // 代码
} catch (...) {
    // 忽略异常,不处理也不重新抛出
}

②抛出字符串而非异常类:

代码语言:javascript
复制
throw "Error occurred"; // 不推荐
throw std::runtime_error("Error occurred"); // 推荐

③在析构函数中抛出异常

代码语言:javascript
复制
~MyClass() {
    // 可能抛出异常的操作(危险!)
}

8.3 异常处理的推荐做法

  1. 使用 RAII 管理资源:确保资源在异常发生时自动释放
  2. 按 const 引用捕获异常:避免拷贝开销和切片问题
  3. 保持异常层次结构:从标准异常类派生自定义异常
  4. 明确函数的异常规范:使用noexcept说明函数是否会抛出异常
  5. 避免在关键路径上使用异常:异常处理比条件检查慢
  6. 记录异常信息:在捕获异常时记录足够的上下文信息

九、完整示例:异常处理综合应用

代码语言:javascript
复制
#include <iostream>
#include <stdexcept>
#include <memory>
#include <vector>
#include <cstdio>  // 提供FILE*和fopen/fclose/fgets等函数

// 自定义异常类
class FileOpenException : public std::runtime_error {
public:
    FileOpenException(const std::string& filename) 
        : std::runtime_error("Failed to open file: " + filename) {}
};

class DatabaseConnectionException : public std::runtime_error {
public:
    DatabaseConnectionException(const std::string& msg) 
        : std::runtime_error("Database error: " + msg) {}
};

// 资源管理类(RAII)
class File {
public:
    File(const std::string& filename) : file_ptr(fopen(filename.c_str(), "r")) {
        if (!file_ptr) {
            throw FileOpenException(filename);
        }
    }
    
    ~File() {
        if (file_ptr) {
            fclose(file_ptr);
        }
    }
    
    // 禁用拷贝,支持移动
    File(const File&) = delete;
    File& operator=(const File&) = delete;
    
    File(File&& other) noexcept : file_ptr(other.file_ptr) {
        other.file_ptr = nullptr;
    }
    
    File& operator=(File&& other) noexcept {
        if (this != &other) {
            if (file_ptr) {
                fclose(file_ptr);
            }
            file_ptr = other.file_ptr;
            other.file_ptr = nullptr;
        }
        return *this;
    }
    
    bool readLine(char* buffer, size_t size) {
        return fgets(buffer, size, file_ptr) != nullptr;
    }
    
private:
    FILE* file_ptr;
};

// 数据库连接类
class Database {
public:
    Database(const std::string& connectionString) 
        : connected(false), connectionString(connectionString) {
        connect();
    }
    
    ~Database() {
        disconnect();
    }
    
    void connect() {
        // 模拟连接数据库
        if (connectionString.empty()) {
            throw DatabaseConnectionException("Invalid connection string");
        }
        
        // 模拟连接成功
        connected = true;
        std::cout << "Connected to database" << std::endl;
    }
    
    void disconnect() noexcept {
        if (connected) {
            // 模拟断开连接
            std::cout << "Disconnected from database" << std::endl;
            connected = false;
        }
    }
    
    void executeQuery(const std::string& query) {
        if (!connected) {
            throw DatabaseConnectionException("Not connected to database");
        }
        
        // 模拟查询执行
        std::cout << "Executing query: " << query << std::endl;
        
        // 模拟查询失败
        if (query.empty()) {
            throw std::invalid_argument("Empty query");
        }
    }
    
private:
    bool connected;
    std::string connectionString;
};

// 业务逻辑类
class DataProcessor {
public:
    DataProcessor(const std::string& filename, const std::string& dbConnection) 
        : file(filename), db(dbConnection) {}
    
    void processData() {
        try {
            char buffer[1024];
            while (file.readLine(buffer, sizeof(buffer))) {
                // 处理每一行数据
                std::string query = "INSERT INTO table VALUES ('" + std::string(buffer) + "')";
                db.executeQuery(query);
            }
        } catch (const std::invalid_argument& e) {
            std::cerr << "Invalid query: " << e.what() << std::endl;
            // 可以选择继续处理其他数据或终止
            throw; // 重新抛出,让上层处理
        }
    }
    
private:
    File file;
    Database db;
};

int main() {
    try {
        DataProcessor processor("data.txt", "localhost:3306/mydb");
        processor.processData();
    } catch (const FileOpenException& e) {
        std::cerr << "File error: " << e.what() << std::endl;
        return 1;
    } catch (const DatabaseConnectionException& e) {
        std::cerr << "Database error: " << e.what() << std::endl;
        return 2;
    } catch (const std::exception& e) {
        std::cerr << "General error: " << e.what() << std::endl;
        return 3;
    } catch (...) {
        std::cerr << "Unknown error occurred" << std::endl;
        return 4;
    }
    
    std::cout << "Data processing completed successfully" << std::endl;
    return 0;
}

十、总结

C++ 异常处理机制为程序提供了一种强大而灵活的错误处理方式,通过抛出异常、栈展开和捕获异常,可以将错误处理逻辑与正常业务逻辑分离,提高代码的可读性和可维护性。

关键要点:

  1. 异常基础:使用throw抛出异常,try-catch捕获异常
  2. 栈展开:异常抛出后,程序沿调用栈向上寻找匹配的 catch 块,自动销毁局部对象
  3. 异常类设计:从标准异常类派生自定义异常,便于分类处理
  4. 捕获异常:按 const 引用捕获异常,注意捕获顺序
  5. 重新抛出:在 catch 块中使用throw;重新抛出当前异常
  6. 构造函数异常:构造函数可抛出异常,但需注意资源清理
  7. 异常安全:提供明确的异常安全保证(基本、强、不抛异常)
  8. 最佳实践:使用 RAII 管理资源,避免常见误区

合理使用异常处理机制,可以使代码更加健壮、可靠,同时保持良好的性能。但需注意,异常不应替代正常的错误检查,而应仅用于处理真正的异常情况。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-05-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、异常处理基础
    • 1.1 异常的基本概念
    • 1.2 异常处理的语法
    • 1.3 异常类的设计
  • 二、函数抛出类类型的异常
    • 2.1 抛出内置类型异常
    • 2.2 抛出类类型异常
    • 2.3 异常对象的拷贝语义
  • 三、栈展开(Stack Unwinding)
    • 3.1 栈展开的过程
    • 3.2 栈展开的示例
    • 3.3 栈展开与资源管理
  • 四、捕获异常
    • 4.1 异常捕获的语法
    • 4.2 按值、引用和指针捕获的区别
    • 4.3 捕获顺序与多态
    • 4.4 捕获所有异常(catch-all)
  • 五、重新抛出异常
    • 5.1 什么是重新抛出异常?
    • 5.2 重新抛出的用途
    • 5.3 异常转换示例
  • 六、函数测试块与构造函数异常
    • 6.1 函数测试块(Function Try Block)
    • 6.2 构造函数异常
    • 6.3 使用智能指针简化构造函数异常处理
  • 七、异常规范(Exception Specification)
    • 7.1 已弃用的异常规范
    • 7.2 noexcept 说明符
    • 7.3 noexcept 与析构函数
  • 八、异常处理的最佳实践
    • 8.1 异常安全保证
    • 8.2 异常处理的常见误区
    • 8.3 异常处理的推荐做法
  • 九、完整示例:异常处理综合应用
  • 十、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档