0

0

c++如何进行异常处理_c++异常处理try-catch机制详解

冰火之心

冰火之心

发布时间:2025-09-21 08:07:01

|

732人浏览过

|

来源于php中文网

原创

C++异常处理通过try-catch-throw实现,将错误处理与正常逻辑分离,避免资源泄露并提升代码可读性;结合RAII机制,在构造函数中获取资源、析构函数中释放,确保异常发生时能自动清理,保障程序状态安全。

c++如何进行异常处理_c++异常处理try-catch机制详解

C++的异常处理机制,尤其是我们常说的

try-catch
,它提供了一种相当优雅且健壮的方式来管理程序运行时的错误。简单来说,就是当程序在执行过程中遇到一些“意料之外”但又需要特殊处理的情况时,我们不再依赖传统的错误码返回,而是通过抛出(throw)一个异常对象,让调用上层合适的捕获(catch)机制来接住它,从而避免程序崩溃,并进行相应的错误恢复或报告。这就像是程序里装了个“安全气囊”,关键时刻能弹出来保护系统。

解决方案

C++的

try-catch
机制核心在于三个关键词:
try
throw
catch

try
块用来包裹那些可能会抛出异常的代码。你觉得哪段代码运行起来可能不那么“顺利”,就把它放到
try
里。

#include 
#include 
#include  // 包含标准异常类

double divide(double numerator, double denominator) {
    if (denominator == 0) {
        // 当分母为0时,这是一个异常情况,我们选择抛出异常
        throw std::runtime_error("Error: Division by zero is not allowed.");
    }
    return numerator / denominator;
}

int main() {
    try {
        // 尝试执行可能抛出异常的代码
        double result1 = divide(10.0, 2.0);
        std::cout << "10 / 2 = " << result1 << std::endl;

        double result2 = divide(5.0, 0.0); // 这行代码会抛出异常
        std::cout << "5 / 0 = " << result2 << std::endl; // 这行将不会被执行
    }
    catch (const std::runtime_error& e) {
        // 捕获特定类型的异常
        std::cerr << "Caught an exception: " << e.what() << std::endl;
    }
    catch (const std::exception& e) {
        // 捕获所有标准异常的基类,更通用
        std::cerr << "Caught a general standard exception: " << e.what() << std::endl;
    }
    catch (...) {
        // 捕获任何类型的异常(包括非标准异常),通常作为最后的防线
        std::cerr << "Caught an unknown exception!" << std::endl;
    }

    std::cout << "Program continues after exception handling." << std::endl;
    return 0;
}

divide(5.0, 0.0)
被调用时,
denominator == 0
条件成立,
throw std::runtime_error(...)
语句就会执行。这会创建一个
std::runtime_error
类型的异常对象,并终止当前函数的执行,程序控制权会沿着调用栈向上寻找匹配的
catch
块。一旦找到,相应的
catch
块就会被执行,处理完后,程序会跳过
try
块中剩余的代码,从
catch
块之后继续执行。如果找不到匹配的
catch
块,程序通常会终止(调用
std::terminate
)。

立即学习C++免费学习笔记(深入)”;

catch
块可以有多个,它们会按照声明的顺序尝试匹配抛出的异常类型。通常,我们会把更具体的异常类型放在前面,更通用的(比如
std::exception
...
)放在后面,以确保最精确的异常能被优先处理。

为什么在C++中推荐使用异常处理,它比错误码有什么优势?

说实话,以前我也习惯用错误码,函数返回个负数或者特定的枚举值来表示失败。这种方式看似直接,但用久了就会发现一堆问题,尤其是在大型项目或者深层函数调用链里。

首先,错误码很容易被“视而不见”。你调用一个函数,它返回个错误码,但如果你忘了检查,或者检查了却没做任何处理,程序就可能带着一个错误状态继续跑,直到在某个意想不到的地方彻底崩掉,那时候排查起来简直是噩梦。异常处理则不同,

throw
出去的异常如果没有被捕获,程序会直接终止,这反倒是一种“强制提醒”,让你不得不去面对和处理问题。

其次,错误码会污染你的正常业务逻辑代码。每一步操作后都得加一个

if (error_code != SUCCESS)
,导致代码里充斥着大量的错误检查,把真正想做的事情淹没了。而异常处理,它把“正常流程”和“异常处理流程”清晰地分开了。
try
块里是你的主线任务,
catch
块里才是应对突发状况的策略,这样代码的可读性会大大提升。

再者,错误码在函数调用链中传递是个麻烦事。一个底层函数出错了,它的错误码要一层一层地往上传,每个中间函数都得负责接收、判断、再返回。这不仅增加了大量冗余代码,也使得错误信息的传递效率低下。异常则能自动地“跳过”中间层,直接传递到最近的、能处理该类型异常的

catch
块,极大地简化了错误传播的机制。

最后,异常处理结合C++的RAII(Resource Acquisition Is Initialization)机制,在资源管理上有着天然的优势。当异常发生时,程序会进行“栈展开”(stack unwinding),这过程中,所有在栈上创建的对象都会被正确地析构。这意味着即使在错误发生时,像文件句柄、内存、锁等资源也能被自动释放,有效避免了资源泄露,这是错误码很难做到的。

绘蛙AI修图
绘蛙AI修图

绘蛙平台AI修图工具,支持手脚修复、商品重绘、AI扩图、AI换色

下载

在C++中,哪些情况适合用异常,哪些又该避免?

这个问题挺关键的,用不好异常反倒会带来新的问题。我的经验是,异常应该用来处理那些真正“异常”的、非预期的、程序无法继续正常执行的情况。

适合使用异常的场景:

  • 资源分配失败: 比如
    new
    操作失败(虽然现代C++中
    new
    默认抛出
    std::bad_alloc
    ),或者打开文件失败、网络连接失败等。这些情况通常意味着程序无法继续完成其核心功能。
  • 不可恢复的错误: 比如数据库连接中断、核心配置加载失败、严重的数据损坏等。这些错误通常需要程序中止或者进入一个特殊的错误恢复模式。
  • 违反函数契约: 当函数的输入参数严重违反了其设计时所做的假设时。例如,一个计算平方根的函数接收到一个负数,这可能就不是简单返回错误码能解决的问题,因为它改变了函数的根本行为。
    std::out_of_range
    std::invalid_argument
    等标准异常就常用于此。
  • 构造函数失败: 构造函数没有返回值,所以抛出异常是它报告失败的唯一标准方式。

应该避免使用异常的场景:

  • 预期内、可恢复的错误: 比如用户输入格式不正确、文件达到末尾(EOF)、查找某个元素未找到等。这些情况在业务逻辑中是常态,可以通过返回特殊值(如
    nullptr
    std::optional
    std::expected
    )或者错误码来优雅处理,而不是频繁地抛出异常。频繁抛出异常会有性能开销,并且可能打乱程序的控制流,让代码变得难以理解和调试。
  • 控制流: 不要把异常当做普通的控制流工具。比如,用异常来跳出多层循环,或者作为一种条件判断。这会使代码逻辑变得非常混乱,且难以维护。异常的目的是处理错误,而不是替代
    if-else
    for
    循环。
  • 性能敏感的代码: 异常的抛出和捕获涉及到栈展开等操作,是有一定性能开销的。在对性能要求极高的代码路径中,如果能用错误码或其他方式处理,就尽量避免使用异常。

总的来说,异常是处理“意外事件”的,而不是“日常小插曲”。

如何编写健壮的异常安全代码?RAII原则在此扮演什么角色?

编写异常安全的代码,目标是在异常发生时,程序的状态依然是有效的,并且所有已获取的资源都能被正确管理,不会发生泄露。这里有几个层次的“异常安全保证”:

  1. 基本保证 (Basic Guarantee): 如果异常发生,程序的所有不变量都保持完好,没有资源泄露。但程序的数据可能被修改,处于一个有效但未知的状态。
  2. 强保证 (Strong Guarantee): 如果异常发生,程序状态不变,就像操作从未发生过一样(事务性语义)。这通常通过“先修改副本,成功后再交换”的策略来实现。
  3. 不抛出保证 (Nothrow Guarantee): 函数保证永远不会抛出异常。这通常用于析构函数、移动操作等关键部分。

要实现这些保证,尤其是避免资源泄露,RAII(Resource Acquisition Is Initialization)原则扮演着至关重要的角色。RAII的核心思想是:将资源的生命周期与对象的生命周期绑定。当对象被创建时(通常在构造函数中),它获取资源;当对象被销毁时(在析构函数中),它释放资源。

#include 
#include 
#include  // for std::unique_ptr
#include   // for std::lock_guard

// 示例1: 传统的资源管理(容易泄露)
void process_file_old(const std::string& filename) {
    std::FILE* file = std::fopen(filename.c_str(), "r");
    if (!file) {
        throw std::runtime_error("Could not open file.");
    }
    // 假设这里有一段代码可能会抛出异常
    // 如果抛出异常,fclose(file) 将不会被执行,导致文件句柄泄露
    // ...
    std::fclose(file); // 如果前面有异常,这行代码可能永远不会执行
}

// 示例2: 使用RAII管理文件句柄
class FileHandle {
public:
    FileHandle(const std::string& filename, const char* mode) {
        file_ptr_ = std::fopen(filename.c_str(), mode);
        if (!file_ptr_) {
            throw std::runtime_error("Failed to open file: " + filename);
        }
        std::cout << "File '" << filename << "' opened." << std::endl;
    }

    ~FileHandle() {
        if (file_ptr_) {
            std::fclose(file_ptr_);
            std::cout << "File closed." << std::endl;
        }
    }

    // 禁止拷贝,避免双重释放
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    std::FILE* get() const { return file_ptr_; }

private:
    std::FILE* file_ptr_;
};

void process_file_raii(const std::string& filename) {
    FileHandle file(filename, "r"); // 资源在构造时获取
    // 假设这里有一段代码可能会抛出异常
    // 无论是否抛出异常,当file对象离开作用域时,其析构函数都会被调用
    // 从而保证文件句柄被正确关闭。
    std::cout << "Processing file content..." << std::endl;
    // ...
    // file对象离开作用域,析构函数自动调用,文件关闭
}

// 示例3: 使用标准库的RAII工具,如std::lock_guard
std::mutex my_mutex;
void guarded_operation() {
    std::lock_guard lock(my_mutex); // 构造时加锁
    // 临界区代码,可能抛出异常
    std::cout << "Critical section entered." << std::endl;
    // ...
    // 无论如何,lock对象离开作用域时,析构函数会自动解锁
    std::cout << "Critical section exited." << std::endl;
}

int main() {
    try {
        // process_file_old("non_existent.txt"); // 演示传统方式的风险
        process_file_raii("example.txt"); // 假设example.txt存在
        guarded_operation();
    } catch (const std::exception& e) {
        std::cerr << "Main caught exception: " << e.what() << std::endl;
    }
    return 0;
}

在上面的

process_file_raii
函数中,即使在
FileHandle file(filename, "r");
之后有代码抛出异常,
file
对象也会在其作用域结束时被正确析构,从而调用
std::fclose
释放文件句柄。
std::unique_ptr
std::shared_ptr
管理内存,
std::lock_guard
std::unique_lock
管理互斥锁,它们都是RAII的典范。

编写异常安全代码时,还需要注意:

  • 避免在析构函数中抛出异常: 析构函数应该具有不抛出保证。如果在析构函数中抛出异常,并且这个析构函数是在栈展开过程中被调用的(因为另一个异常正在传播),那么程序会因为两个异常同时活跃而直接终止(调用
    std::terminate
    )。
  • 考虑“拷贝并交换”惯用法: 对于提供强异常保证的函数,可以先在一个临时对象上执行所有可能抛出异常的操作,如果所有操作都成功,再用
    std::swap
    将临时对象的状态与原对象交换。这样,如果中间有异常,原对象的状态保持不变。
  • 使用
    noexcept
    明确标记那些保证不抛出异常的函数(尤其是移动构造函数和移动赋值运算符),这有助于编译器优化,也让使用者更清楚函数的行为。

理解并应用RAII是构建健壮、异常安全的C++代码库的基石。它让资源管理变得自动化和可靠,大大减轻了程序员的心智负担。

相关专题

更多
resource是什么文件
resource是什么文件

Resource文件是一种特殊类型的文件,它通常用于存储应用程序或操作系统中的各种资源信息。它们在应用程序开发中起着关键作用,并在跨平台开发和国际化方面提供支持。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

140

2023.12.20

java基础知识汇总
java基础知识汇总

java基础知识有Java的历史和特点、Java的开发环境、Java的基本数据类型、变量和常量、运算符和表达式、控制语句、数组和字符串等等知识点。想要知道更多关于java基础知识的朋友,请阅读本专题下面的的有关文章,欢迎大家来php中文网学习。

1435

2023.10.24

Go语言中的运算符有哪些
Go语言中的运算符有哪些

Go语言中的运算符有:1、加法运算符;2、减法运算符;3、乘法运算符;4、除法运算符;5、取余运算符;6、比较运算符;7、位运算符;8、按位与运算符;9、按位或运算符;10、按位异或运算符等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

222

2024.02.23

php三元运算符用法
php三元运算符用法

本专题整合了php三元运算符相关教程,阅读专题下面的文章了解更多详细内容。

84

2025.10.17

if什么意思
if什么意思

if的意思是“如果”的条件。它是一个用于引导条件语句的关键词,用于根据特定条件的真假情况来执行不同的代码块。本专题提供if什么意思的相关文章,供大家免费阅读。

711

2023.08.22

fclose函数的用法
fclose函数的用法

fclose是一个C语言和C++中的标准库函数,用于关闭一个已经打开的文件,是文件操作中非常重要的一个函数,用于将文件流与底层文件系统分离,释放相关的资源。更多关于fclose函数的相关问题,详情请看本专题下面的文章。php中文网欢迎大家前来学习。

321

2023.11.30

堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

366

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

559

2023.08.10

vlookup函数使用大全
vlookup函数使用大全

本专题整合了vlookup函数相关 教程,阅读专题下面的文章了解更多详细内容。

26

2025.12.30

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.1万人学习

Pandas 教程
Pandas 教程

共15课时 | 0.9万人学习

ASP 教程
ASP 教程

共34课时 | 3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号