首页 > 后端开发 > C++ > 正文

C++如何在智能指针中实现自定义资源释放

P粉602998670
发布: 2025-09-11 11:08:01
原创
187人浏览过
C++智能指针通过自定义删除器实现资源释放,unique_ptr在模板中指定删除器类型,适用于独占资源管理;shared_ptr将删除器作为构造参数,支持共享资源的不同释放策略,二者均扩展了RAII的应用范围。

c++如何在智能指针中实现自定义资源释放

在C++中,智能指针实现自定义资源释放的核心在于为其提供一个“删除器”(Deleter),这个删除器是一个可调用对象,负责在智能指针所管理的资源不再被引用时,执行特定的清理操作,而非默认的

delete
登录后复制
操作符。这极大地扩展了智能指针的应用范围,使其不仅限于管理堆内存,还能优雅地处理文件句柄、网络套接字、数据库连接或其他任何需要特定关闭流程的资源。

解决方案

要为C++智能指针实现自定义资源释放,你需要根据所使用的智能指针类型(

std::unique_ptr
登录后复制
std::shared_ptr
登录后复制
)来提供一个自定义删除器。

对于

std::unique_ptr
登录后复制
std::unique_ptr
登录后复制
允许你在模板参数中指定删除器的类型,并在构造时传入一个删除器实例。这通常通过一个函数对象(functor)、lambda表达式或一个普通函数指针来完成。

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

// 1. 使用函数对象作为删除器
struct FileCloser {
    void operator()(FILE* fp) const {
        if (fp) {
            std::cout << "Closing file via functor..." << std::endl;
            fclose(fp);
        }
    }
};

// 2. 使用普通函数作为删除器
void custom_file_deleter(FILE* fp) {
    if (fp) {
        std::cout << "Closing file via function pointer..." << std::endl;
        fclose(fp);
    }
}

int main() {
    // 使用函数对象作为删除器
    std::unique_ptr<FILE, FileCloser> file1(fopen("test1.txt", "w"));
    if (file1) {
        fprintf(file1.get(), "Hello from file1!\n");
    }

    // 使用lambda表达式作为删除器 (更常见和推荐)
    auto lambda_file_closer = [](FILE* fp) {
        if (fp) {
            std::cout << "Closing file via lambda..." << std::endl;
            fclose(fp);
        }
    };
    std::unique_ptr<FILE, decltype(lambda_file_closer)> file2(fopen("test2.txt", "w"), lambda_file_closer);
    if (file2) {
        fprintf(file2.get(), "Hello from file2!\n");
    }

    // 使用函数指针作为删除器
    std::unique_ptr<FILE, decltype(&custom_file_deleter)> file3(fopen("test3.txt", "w"), &custom_file_deleter);
    if (file3) {
        fprintf(file3.get(), "Hello from file3!\n");
    }

    std::cout << "Unique pointers will now go out of scope and release resources." << std::endl;
    return 0;
}
登录后复制

对于

std::shared_ptr
登录后复制
std::shared_ptr
登录后复制
的自定义删除器不需要作为模板参数,而是直接作为构造函数的第二个参数传入。这意味着所有使用相同类型资源但不同删除器的
std::shared_ptr
登录后复制
实例,其类型是完全相同的,这在容器存储或函数参数传递时非常方便。

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

#include <iostream>
#include <memory>
#include <string>

// 假设我们有一个自定义的资源类型,需要特定的释放逻辑
struct CustomResource {
    std::string name;
    CustomResource(const std::string& n) : name(n) {
        std::cout << "CustomResource " << name << " acquired." << std::endl;
    }
    void do_something() {
        std::cout << "CustomResource " << name << " doing something." << std::endl;
    }
};

// 自定义删除器函数
void custom_resource_deleter(CustomResource* res) {
    if (res) {
        std::cout << "Releasing CustomResource " << res->name << " via custom deleter." << std::endl;
        delete res; // 仍然需要释放内存,但可能在释放前做其他事情
    }
}

int main() {
    // 使用lambda表达式作为删除器
    std::shared_ptr<CustomResource> res1(new CustomResource("DB_Connection"), [](CustomResource* r) {
        std::cout << "Closing DB_Connection " << r->name << " via lambda." << std::endl;
        delete r;
    });
    res1->do_something();

    // 使用函数指针作为删除器
    std::shared_ptr<CustomResource> res2(new CustomResource("Network_Socket"), custom_resource_deleter);
    res2->do_something();

    // 即使是同一个类型,也可以有不同的删除器,但 shared_ptr 的类型保持不变
    std::shared_ptr<CustomResource> res3(new CustomResource("File_Handle"), [](CustomResource* r) {
        std::cout << "Flushing and closing File_Handle " << r->name << " via another lambda." << std::endl;
        delete r;
    });
    res3->do_something();

    std::cout << "Shared pointers will now go out of scope and release resources." << std::endl;
    return 0;
}
登录后复制

std::unique_ptr 如何优雅地处理非堆内存资源?

std::unique_ptr
登录后复制
的设计哲学是独占所有权,它天生就适合管理那些需要明确的、单一责任方来清理的资源。当资源并非传统的堆内存(即不是通过
new
登录后复制
分配的),而是像文件句柄、互斥锁、图形上下文或系统API返回的句柄时,
unique_ptr
登录后复制
配合自定义删除器就能展现出其强大的RAII(Resource Acquisition Is Initialization)能力。

我个人认为,

unique_ptr
登录后复制
在处理这类资源时,其优势在于编译期类型安全和零运行时开销(对于空删除器)。它的模板参数要求删除器类型,这意味着如果你的删除器类型不匹配,编译器会立即报错。这在大型项目中,能有效避免运行时错误。

举个例子,操作系统的许多API都返回需要特定函数(而非

delete
登录后复制
)来释放的句柄。Windows API中的
HANDLE
登录后复制
,Linux/POSIX中的
FILE*
登录后复制
DIR*
登录后复制
pthread_mutex_t
登录后复制
等都是典型。如果直接使用裸指针,我们很容易忘记调用对应的
CloseHandle
登录后复制
fclose
登录后复制
closedir
登录后复制
pthread_mutex_destroy
登录后复制
。而
unique_ptr
登录后复制
通过自定义删除器,能将这些清理逻辑封装起来,确保资源在
unique_ptr
登录后复制
超出作用域时自动、正确地释放。

#include <iostream>
#include <memory>
#include <windows.h> // 假设在Windows环境

// 定义一个用于关闭HANDLE的删除器
struct HandleCloser {
    void operator()(HANDLE h) const {
        if (h != INVALID_HANDLE_VALUE && h != nullptr) {
            std::cout << "Closing Windows HANDLE..." << std::endl;
            CloseHandle(h);
        }
    }
};

int main() {
    // 假设我们打开一个事件对象
    // std::unique_ptr<HANDLE, HandleCloser> event_handle(CreateEvent(NULL, TRUE, FALSE, NULL));
    // 注意:CreateEvent返回HANDLE* 而不是HANDLE,且 unique_ptr 默认期望 T*
    // 更正确的做法是 unique_ptr<void, HandleCloser> 或直接 unique_ptr<HANDLE, HandleCloser>
    // 但这里为了简化,我们假设 CreateEvent 返回一个可以直接被 HandleCloser 处理的类型
    // 实际使用时,可能需要对返回类型进行适配,或者直接用 HANDLE 类型
    // 这里用一个简单的 int* 模拟一个需要自定义释放的资源
    auto custom_int_deleter = [](int* p) {
        std::cout << "Deleting custom int array." << std::endl;
        delete[] p;
    };
    std::unique_ptr<int[], decltype(custom_int_deleter)> my_array(new int[10], custom_int_deleter);
    my_array[0] = 100;
    std::cout << "my_array[0]: " << my_array[0] << std::endl;

    // 真正的HANDLE例子可能更像这样:
    // std::unique_ptr<void, HandleCloser> hFile(
    //     CreateFile("example.txt", GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL),
    //     HandleCloser{}
    // );
    // if (hFile.get() == INVALID_HANDLE_VALUE) {
    //     std::cerr << "Failed to open file." << std::endl;
    // } else {
    //     std::cout << "File handle acquired successfully." << std::endl;
    // }

    std::cout << "Resources will be released now." << std::endl;
    return 0;
}
登录后复制

可以看到,

unique_ptr
登录后复制
配合自定义删除器,不仅让代码更安全,也让意图更清晰。它的类型会包含删除器的信息,这在一定程度上是好事,因为它在编译期就明确了资源如何被释放,但也意味着如果你有多种删除器,那么
unique_ptr
登录后复制
的完整类型也会不同。

std::shared_ptr 的自定义删除器如何应对复杂资源共享场景?

std::shared_ptr
登录后复制
的核心在于共享所有权和引用计数。它的自定义删除器与
unique_ptr
登录后复制
有着显著的区别
shared_ptr
登录后复制
的删除器不作为其模板参数的一部分。这意味着无论你传入什么样的自定义删除器,
std::shared_ptr<T>
登录后复制
的类型始终是
std::shared_ptr<T>
登录后复制
。这个特性在处理需要共享但清理方式各异的资源时,显得尤为灵活。

考虑一个场景:一个数据库连接池。当一个连接从池中取出时,它被包装在一个

shared_ptr
登录后复制
中。当这个
shared_ptr
登录后复制
的引用计数降为零时,我们不希望连接被真正关闭,而是希望它被“归还”到连接池中,以便重用。这就是
shared_ptr
登录后复制
自定义删除器的完美用武之地。删除器不会
delete
登录后复制
原始指针,而是调用连接池的
returnConnection
登录后复制
方法。

稿定PPT
稿定PPT

海量PPT模版资源库

稿定PPT111
查看详情 稿定PPT
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <mutex> // for thread safety

// 模拟一个数据库连接
struct DBConnection {
    std::string id;
    bool is_open = true;
    DBConnection(const std::string& _id) : id(_id) {
        std::cout << "DBConnection " << id << " opened." << std::endl;
    }
    void query(const std::string& sql) {
        if (is_open) {
            std::cout << "Connection " << id << " executing: " << sql << std::endl;
        } else {
            std::cout << "Connection " << id << " is closed, cannot query." << std::endl;
        }
    }
    void close() {
        if (is_open) {
            std::cout << "DBConnection " << id << " truly closed." << std::endl;
            is_open = false;
        }
    }
    ~DBConnection() {
        std::cout << "DBConnection " << id << " destructor called." << std::endl;
    }
};

// 模拟一个连接池
class ConnectionPool {
private:
    std::vector<std::shared_ptr<DBConnection>> available_connections;
    std::mutex mtx;
    int next_id = 0;

public:
    ConnectionPool() {
        // 预创建一些连接
        for (int i = 0; i < 3; ++i) {
            available_connections.push_back(std::make_shared<DBConnection>("conn_" + std::to_string(next_id++)));
        }
        std::cout << "ConnectionPool initialized with " << available_connections.size() << " connections." << std::endl;
    }

    // 获取一个连接
    std::shared_ptr<DBConnection> getConnection() {
        std::lock_guard<std::mutex> lock(mtx);
        if (available_connections.empty()) {
            std::cout << "Pool empty, creating new connection." << std::endl;
            // 正常情况下,这里会阻塞等待或抛异常
            // 简化处理,直接创建一个新的并加入池中
            available_connections.push_back(std::make_shared<DBConnection>("conn_" + std::to_string(next_id++)));
        }
        std::shared_ptr<DBConnection> conn = available_connections.back();
        available_connections.pop_back();

        // 为这个连接设置一个自定义删除器,当引用计数为0时,将连接归还到池中
        // 注意:这里需要捕获 this 指针,确保删除器能访问到 ConnectionPool 实例
        // 且删除器本身不能是 ConnectionPool 的成员函数,因为 shared_ptr 期望一个裸指针作为参数
        return std::shared_ptr<DBConnection>(conn.get(), [this](DBConnection* c) {
            std::lock_guard<std::mutex> lock_deleter(mtx);
            std::cout << "Returning DBConnection " << c->id << " to pool." << std::endl;
            // 确保归还的连接是打开的,如果业务逻辑中可能关闭,这里需要重新打开或检查状态
            // c->is_open = true; // 假设归还时总是可用的
            available_connections.push_back(std::shared_ptr<DBConnection>(c)); // 重新包装成shared_ptr
        });
    }

    ~ConnectionPool() {
        std::cout << "ConnectionPool shutting down. Truly closing all " << available_connections.size() << " connections." << std::endl;
        for (auto& conn : available_connections) {
            conn->close();
        }
    }
};

int main() {
    ConnectionPool pool;
    {
        std::shared_ptr<DBConnection> conn1 = pool.getConnection();
        conn1->query("SELECT * FROM users;");

        std::shared_ptr<DBConnection> conn2 = pool.getConnection();
        conn2->query("INSERT INTO products VALUES (...);");

        // conn1 和 conn2 在这里超出作用域,它们的自定义删除器会被调用,将连接归还到池中
    } // conn1, conn2 out of scope here

    std::cout << "\nAfter connections returned to pool." << std::endl;
    {
        std::shared_ptr<DBConnection> conn3 = pool.getConnection();
        conn3->query("UPDATE orders SET status='shipped';");
    } // conn3 out of scope

    std::cout << "\nMain function ending." << std::endl;
    return 0;
}
登录后复制

在这个例子中,

shared_ptr
登录后复制
的自定义删除器允许我们实现一个资源池,这在高性能服务器应用中非常常见。删除器捕获了
this
登录后复制
指针,使得它可以在资源被释放时,将资源安全地归还给创建它的
ConnectionPool
登录后复制
实例。这比手动管理资源生命周期要安全和优雅得多。

自定义删除器在实际项目中的高级应用与潜在挑战

自定义删除器在实际项目中确实是解决复杂资源管理问题的利器,但它也伴随着一些需要注意的挑战和高级应用场景。

高级应用场景:

  1. C-style API的RAII封装: 这可能是最常见的应用。例如,

    libcurl
    登录后复制
    sqlite3
    登录后复制
    OpenSSL
    登录后复制
    等库提供了大量的C风格API,它们通常通过
    xxx_init()
    登录后复制
    获取资源,通过
    xxx_cleanup()
    登录后复制
    xxx_free()
    登录后复制
    释放。自定义删除器可以完美地将这些资源封装进智能指针,确保即使在异常发生时也能正确释放。

    // 假设有一个简单的C库函数
    void* create_my_context() { std::cout << "Context created." << std::endl; return new int; }
    void destroy_my_context(void* ctx) { std::cout << "Context destroyed." << std::endl; delete (int*)ctx; }
    
    std::unique_ptr<void, decltype(&destroy_my_context)> ctx_ptr(create_my_context(), &destroy_my_context);
    登录后复制
  2. 跨DLL/SO的内存管理: 在Windows上,如果一个DLL使用

    new
    登录后复制
    分配内存,另一个DLL使用
    delete
    登录后复制
    释放,可能会导致堆损坏。同样,在Linux上,如果共享库和主程序使用不同的C运行时库,也可能出现问题。自定义删除器可以确保资源总是由分配它的模块的相应函数释放。

    // DLL_A.h
    extern "C" __declspec(dllexport) MyObject* create_object();
    extern "C" __declspec(dllexport) void destroy_object(MyObject* obj);
    
    // main.cpp
    std::unique_ptr<MyObject, decltype(&destroy_object)> my_obj_ptr(create_object(), &destroy_object);
    登录后复制
  3. 自定义内存分配器集成: 如果你的项目使用了自定义的内存分配器(例如,一个高性能的池式分配器),你可以为智能指针提供一个删除器,让它调用你的分配器的

    deallocate
    登录后复制
    方法,而不是全局的
    delete
    登录后复制

    template<typename T>
    struct MyAllocatorDeleter {
        MyCustomAllocator* allocator_ptr; // 存储分配器实例的指针
        MyAllocatorDeleter(MyCustomAllocator* alloc) : allocator_ptr(alloc) {}
        void operator()(T* p) const {
            if (p && allocator_ptr) {
                allocator_ptr->deallocate(p);
            }
        }
    };
    // 使用时
    MyCustomAllocator my_alloc;
    std::unique_ptr<MyData, MyAllocatorDeleter<MyData>> data_ptr(
        my_alloc.allocate<MyData>(), MyAllocatorDeleter<MyData>(&my_alloc)
    );
    登录后复制
  4. 资源归还而非销毁: 像前面数据库连接池的例子,删除器不销毁资源,而是将其归还到某个池中。这对于任何可复用的昂贵资源都非常有用。

潜在挑战:

  1. 状态ful Deleter的管理(
    unique_ptr
    登录后复制
    ):
    unique_ptr
    登录后复制
    的删除器类型是其模板参数的一部分。如果删除器是有状态的(例如,需要捕获一个分配器实例),那么每个
    unique_ptr
    登录后复制
    实例的类型都会不同。这可能导致在需要将它们存储在容器中或作为函数参数传递时遇到麻烦。通常,
    std::function
    登录后复制
    可以用来抹除删除器类型,但会引入额外的运行时开销。
    // 假设我们有多个不同的自定义分配器实例
    MyCustomAllocator alloc1, alloc2;
    // unique_
    登录后复制

以上就是C++如何在智能指针中实现自定义资源释放的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

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