讲师中心 微信公众号
AI工具推荐 视频效率加速

C++如何在智能指针中处理自定义删除器异常

阿涛小哥_7034

阿涛小哥_7034

发布时间:2025-09-06 08:53:01

|

483人浏览过

|

来源于php中文网

原创

自定义删除器不应抛出异常,因析构过程抛异常会触发std::terminate导致程序崩溃;正确做法是将删除器声明为noexcept,并在内部用try-catch捕获并处理所有异常,确保资源释放操作安全可靠。

c++如何在智能指针中处理自定义删除器异常

当我们在C++中使用智能指针,比如

std::unique_ptr
或
std::shared_ptr
,并为其提供自定义删除器时,一个非常关键但又容易被忽视的问题就是:如何处理这个删除器内部可能抛出的异常。我的核心观点是,最安全、最符合C++惯例的做法是,确保你的自定义删除器永远不会抛出任何异常。 如果删除器中涉及的操作确实可能失败并产生异常,那么这些异常必须在删除器内部被捕获并妥善处理,例如记录到日志,但绝不能让它们传播出删除器的边界。否则,你将面临未定义行为,甚至可能导致程序非正常终止。

这听起来可能有点绝对,但它背后有非常扎实的理由。C++标准对析构函数(以及智能指针删除器这种在对象生命周期结束时被调用的机制)有着明确的期待:它们应该是

noexcept
的。这意味着,如果一个析构函数,或者一个被显式声明为
noexcept
的函数(我们的删除器如果被正确设计,就应该如此),抛出了异常,C++运行时会立即调用
std::terminate
。这可不是我们乐意见到的,它会导致程序直接崩溃,而且往往因为没有正常的栈回溯而难以调试。

设想一下,你的智能指针在析构时,尝试释放一个资源,而这个释放操作因为某些外部因素(比如文件句柄已失效、网络连接已断开,或者底层API本身就设计得会抛异常)抛出了一个异常。如果这个异常没有在删除器内部被捕获,它就会从一个析构函数的调用链中逃逸出来。此时,C++运行时环境会陷入一个非常尴尬的境地:在一个对象正在被销毁的过程中又出现了新的异常,这与异常安全的基本原则是冲突的。它无法保证程序的稳定状态,所以最直接、最保守的反应就是终止程序,以避免更深层次的问题。

因此,我个人在实践中,几乎都会将自定义删除器视为一个“无条件成功”的操作。如果删除器确实需要执行一些复杂且可能失败的逻辑,那么这些逻辑必须被包装在一个

try-catch
块中。在
catch
块里,你可以选择记录错误日志,或者更新一些错误状态标志,但关键在于——你不能重新抛出异常。这是为了保证智能指针在销毁时的行为是可预测且安全的。

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

#include <iostream>
#include <memory>
#include <stdexcept>
#include <cstdio> // For FILE* operations

// 模拟一个可能抛出异常的资源释放函数
// 比如关闭一个文件,但文件句柄可能无效或操作失败
void close_file_resource(FILE* fp) {
    if (fp == nullptr) {
        // 实际场景中,nullptr通常不会导致抛异常,但为了演示可以模拟
        throw std::runtime_error("Attempted to close a null file pointer.");
    }
    std::cout << "Attempting to close file pointer: " << fp << std::endl;
    if (fclose(fp) != 0) {
        // fclose 失败会返回 EOF,但通常不会抛C++异常。
        // 这里为了演示“可能抛出异常的操作”而模拟抛出。
        throw std::runtime_error("Failed to close file resource properly.");
    }
    std::cout << "File pointer " << fp << " closed successfully." << std::endl;
}

// 错误的自定义删除器示例:会直接抛出异常
struct BadFileDeleter {
    void operator()(FILE* fp) const {
        std::cout << "BadFileDeleter: Executing..." << std::endl;
        close_file_resource(fp); // 这里可能会抛出异常
    }
};

// 正确的自定义删除器示例:内部处理异常,并声明noexcept
struct SafeFileDeleter {
    void operator()(FILE* fp) const noexcept { // 明确声明noexcept
        std::cout << "SafeFileDeleter: Executing..." << std::endl;
        try {
            close_file_resource(fp);
        } catch (const std::exception& e) {
            // 捕获并处理异常,例如记录日志
            std::cerr << "ERROR: SafeFileDeleter caught exception for file " << fp 
                      << ": " << e.what() << std::endl;
            // 关键:不要重新抛出异常
        } catch (...) {
            std::cerr << "ERROR: SafeFileDeleter caught unknown exception for file " << fp << std::endl;
        }
    }
};

// int main() {
//     std::cout << "--- Testing BadFileDeleter (may terminate) ---" << std::endl;
//     try {
//         // 模拟一个文件指针
//         FILE* f1 = fopen("test1.txt", "w");
//         if (f1) {
//             std::unique_ptr<FILE, BadFileDeleter> bad_file_ptr(f1, BadFileDeleter{});
//             // bad_file_ptr 在离开作用域时析构,如果 close_file_resource 抛异常,程序会 terminate
//             // 我们可以通过将 f1 设为 nullptr 来模拟抛异常
//             // bad_file_ptr.release(); // 避免实际关闭,让它在析构时遇到 nullptr
//         }
//         // 为了演示异常,我们故意创建一个会抛异常的场景
//         std::unique_ptr<FILE, BadFileDeleter> bad_file_ptr_null(nullptr, BadFileDeleter{});
//     } catch (const std::exception& e) {
//         std::cerr << "Caught exception in main for BadFileDeleter: " << e.what() << std::endl;
//     }
//     // 注意:如果 BadFileDeleter 真的抛出异常,程序可能在这里之前就已经终止了
//     std::cout << "--- BadFileDeleter test finished (or terminated) ---" << std::endl;
//
//     std::cout << "\n--- Testing SafeFileDeleter (should not terminate) ---" << std::endl;
//     try {
//         FILE* f2 = fopen("test2.txt", "w");
//         if (f2) {
//             std::unique_ptr<FILE, SafeFileDeleter> safe_file_ptr(f2, SafeFileDeleter{});
//             // safe_file_ptr 析构时,即使 close_file_resource 抛异常,也会被内部处理,程序继续运行
//         }
//         // 故意创建一个会抛异常的场景
//         std::unique_ptr<FILE, SafeFileDeleter> safe_file_ptr_null(nullptr, SafeFileDeleter{});
//     } catch (const std::exception& e) {
//         std::cerr << "Caught exception in main for SafeFileDeleter: " << e.what() << std::endl;
//     }
//     std::cout << "--- SafeFileDeleter test finished ---" << std::endl;
//     return 0;
// }

为什么智能指针的自定义删除器不应该抛出异常?

这个问题是理解智能指针异常安全性的基石。智能指针的自定义删除器,其核心职责是在所管理的对象生命周期结束时,执行必要的资源清理工作。从语义上讲,这与C++对象的析构函数行为非常相似。而C++标准对析构函数有一个非常重要的约定:它们通常应该是

noexcept
的。这意味着,一个析构函数如果抛出异常并允许它传播出去,会导致未定义行为,最常见的结果就是程序调用
std::terminate
并崩溃。

具体来说,当一个异常正在传播(即已经有一个异常被抛出,但尚未被捕获)时,如果此时又有另一个析构函数被调用,并且这个析构函数也抛出了异常,C++标准库会立即调用

std::terminate
。这被称为“双重异常”(double exception)问题。即使当前没有异常在传播,一个
noexcept
的函数(包括隐式
noexcept
的析构函数,或者我们明确标记为
noexcept
的删除器)如果抛出异常,同样会直接导致
std::terminate
。

所以,让删除器抛出异常,不仅违反了C++关于析构函数异常安全的基本原则,还会让你的程序变得极其脆弱和不可预测。我个人在开发中,几乎都会把删除器设计成

noexcept
的,这不仅仅是遵守规范,更是为了避免那些难以追踪的运行时崩溃。你肯定不希望你的程序在释放资源的时候,因为一个清理失败而直接崩溃,而不是优雅地记录错误并继续运行。

C++
C++

"空空如也"

下载

如果自定义删除器必须执行可能抛出异常的操作,我们应该如何处理?

现实世界并非总是理想的,有时你确实需要在一个删除器中调用一个第三方库函数,或者一个你无法控制的API,而这些函数就是被设计成可能抛出异常的。在这种“必须”执行可能抛出异常操作的情况下,我们的策略就变成了“内部消化”异常。

最直接的方法就是使用

try-catch
块将所有可能抛出异常的代码包裹起来。在
catch
块中,你可以选择记录下错误信息(这是最常见的做法),或者更新一些内部的错误状态标志。关键是,你不能在
catch
块中再次抛出异常。

// 示例:一个处理异常的删除器
struct NetworkConnectionDeleter {
    void operator()(void* connection_handle) const noexcept {
        try {
            // 假设这个函数可能因为网络问题而抛出异常
            disconnect_network_resource(connection_handle); 
        } catch (const std::exception& e) {
            // 记录错误日志,但不要重新抛出
            std::cerr << "Error disconnecting network resource " << connection_handle 
                      << ": " << e.what() << std::endl;
        } catch (...) {
            // 捕获所有其他未知异常
            std::cerr << "Unknown error disconnecting network resource " 
                      << connection_handle << std::endl;
        }
    }

    // 假设的外部函数,可能抛出异常
    void disconnect_network_resource(void* handle) const {
        if (handle == nullptr) {
            throw std::runtime_error("Invalid network handle.");
        }
        // 模拟网络断开操作,可能失败
        bool success = (reinterpret_cast<long>(handle) % 2 == 0); // 随机失败
        if (!success) {
            throw std::runtime_error("Failed to disconnect due to network issue.");
        }
        std::cout << "Network connection " << handle << " disconnected." << std::endl;
    }
};

// 使用示例
// int main() {
//     void* conn_handle1 = (void*)0x100; // 模拟一个有效的句柄
//     std::unique_ptr<void, NetworkConnectionDeleter> conn_ptr1(conn_handle1, NetworkConnectionDeleter{});
//     // conn_ptr1 析构时,如果 disconnect_network_resource 抛异常,会被内部处理
//
//     void* conn_handle2 = (void*)0x101; // 模拟另一个句柄,可能导致失败
//     std::unique_ptr<void, NetworkConnectionDeleter> conn_ptr2(conn_handle2, NetworkConnectionDeleter{});
//     return 0;
// }

除了内部

try-catch
,你还可以考虑在智能指针的生命周期结束 之前,就主动地、显式地执行那些可能抛出异常的清理操作。例如,如果你有一个文件句柄,并且关闭文件可能失败,你可以在智能指针即将被销毁之前,调用一个独立的
close_file_safely()
函数,该函数负责处理异常,并确保文件被关闭。如果这个函数成功,你再将智能指针的原始指针
release()
掉,这样删除器就什么都不用做了。但这会增加代码的复杂性,并且失去了智能指针自动管理的便利。我发现很多时候,如果删除器变得复杂到需要考虑异常,那往往意味着我们把不该放进去的逻辑塞进去了。删除器就该是“终结者”,而不是“错误报告员”。

如何设计一个健壮的自定义删除器来规避异常问题?

设计一个健壮的自定义删除器,其核心思想是“防御性编程”和“最小化职责”。

  1. 明确声明

    noexcept
    : 这是第一步,也是最重要的一步。通过在删除器函数签名后加上
    noexcept
    关键字,你是在向编译器和读者保证,这个函数不会抛出异常。如果它真的抛出了,编译器会直接调用
    std::terminate
    ,这虽然是崩溃,但至少是可预测的崩溃,避免了更隐蔽的未定义行为。

  2. 保持删除器逻辑的简单性: 删除器的主要任务就是释放资源,它不应该包含复杂的业务逻辑、状态管理或者与其他组件的交互。越简单,出错的可能性越小,需要处理异常的情况也就越少。例如,一个负责

    delete
    内存的删除器,或者一个负责
    fclose
    文件句柄的删除器,通常都非常简单。

  3. 内部消化所有潜在异常: 如果确实无法避免调用可能抛出异常的函数,那么所有这些调用都必须被包裹在

    try-catch
    块中。在
    catch
    块里,记录错误是最好的做法。你可以使用日志系统、或者
    std::cerr
    输出错误信息。

  4. 考虑资源本身的RAII封装: 有时候,与其在智能指针的删除器里处理复杂逻辑,不如将资源本身封装在一个更小的RAII(Resource Acquisition Is Initialization)类中。这个小RAII类负责资源的构造、销毁以及所有可能抛出异常的中间操作。这样,智能指针的删除器就只需要简单地

    delete
    这个小RAII类的实例,而所有异常处理的复杂性都被封装在那个RAII类内部了。

    // 示例:一个更健壮的资源封装
    class FileHandle {
    private:
        FILE* fp_;
    public:
        explicit FileHandle(const char* filename, const char* mode) : fp_(nullptr) {
            fp_ = fopen(filename, mode);
            if (!fp_) {
                throw std::runtime_error("Failed to open file: " + std::string(filename));
            }
            std::cout << "File " << filename << " opened. Handle: " << fp_ << std::endl;
        }
    
        // 析构函数处理关闭,内部捕获异常
        ~FileHandle() noexcept {
            if (fp_) {
                std::cout << "FileHandle destructor:

热门AI工具

更多
DeepSeek

DeepSeek是一款面向对话、写作、编程和推理场景的AI大模型工具。

Loomy
Loomy Hot

一款AI工具,主要用于科大讯飞发布的桌面级 AI 助理,比 OpenClaw 更易用、更安全!,适合需要提升相关任务效率的用户。

豆包大模型

豆包大模型是一款由字节跳动推出的企业级大语言模型服务平台。

UP简历
UP简历 Hot

一款AI办公效率工具,主要用于基于AI技术的免费在线简历制作工具,适合需要提升相关任务效率的用户。

SkildArt
SkildArt Hot

SkildArt是一款AI文本写作工具,一站式 AI 视觉创作平台。

墨刀AI
墨刀AI Hot

一款AI图像与设计工具,主要用于产品经理的专属智能体,适合需要提升相关任务效率的用户。

WorkBuddy

一款AI办公效率工具,主要用于腾讯云推出的AI原生桌面智能体工作台,适合需要提升相关任务效率的用户。

讯飞绘文

讯飞绘文是一款由科大讯飞推出的一站式 AIGC 内容运营平台。

AionClaw
AionClaw Hot

AionClaw是一款面向办公、创作和编程任务的AI桌面智能体。

相关专题

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

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

320

2023.12.20

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

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

822

2023.11.30

c++怎么把double转成int
c++怎么把double转成int

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

3268

2025.08.29

C++中int、float和double的区别
C++中int、float和double的区别

本专题整合了c++中int和double的区别,阅读专题下面的文章了解更多详细内容。

604

2025.10.23

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

299

2026.04.23

C++ 智能指针与现代内存管理
C++ 智能指针与现代内存管理

深入讲解 C++ 现代内存管理的核心工具——智能指针,涵盖 unique_ptr 独占所有权语义、shared_ptr 引用计数机制与循环引用问题、weak_ptr 弱引用的应用场景、make_unique/make_shared 工厂函数的性能优势、自定义删除器的编写、RAII 资源管理思想的实践,以及从裸指针迁移到智能指针的重构策略,帮助开发者编写安全无泄漏的现代 C++ 代码。

299

2026.04.23

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

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

4687

2023.07.18

堆和栈区别
堆和栈区别

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

2128

2023.08.10

Buffalo框架数据库开发全教程
Buffalo框架数据库开发全教程

本专题围绕Buffalo框架数据库开发,讲解database.yml多环境配置、soda与fizz迁移生成回滚、模型结构体标签、增删改查与条件查询、一对多与多对多关联、数据校验、回调钩子、事务处理及原生SQL执行能力。

80

2026.09.23

热门下载

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

精品课程

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

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn