0

0

C++如何在智能指针中实现RAII模式

P粉602998670

P粉602998670

发布时间:2025-09-03 10:22:02

|

464人浏览过

|

来源于php中文网

原创

智能指针通过将资源生命周期与对象生命周期绑定,在构造时获取资源、析构时自动释放,实现RAII模式;其核心机制包括资源封装、构造函数获取、析构函数释放、所有权语义和操作符重载;std::unique_ptr和std::shared_ptr分别提供独占和共享所有权,支持异常安全;通过自定义删除器可扩展至文件、锁等非内存资源管理。

c++如何在智能指针中实现raii模式

智能指针在C++中实现RAII(Resource Acquisition Is Initialization,资源获取即初始化)模式的核心机制在于,它们将资源的生命周期与对象的生命周期绑定在一起。当智能指针对象被创建时(通常在构造函数中),它会获取或接管一个资源(比如动态分配的内存);而当智能指针对象超出其作用域被销毁时(在析构函数中),它会自动释放所管理的资源。这种设计确保了资源无论在何种情况下(正常执行、函数返回、甚至异常抛出)都能被及时、正确地释放,从而有效避免了资源泄漏。

解决方案

要深入理解智能指针如何实现RAII,我们需要从几个关键点着手。RAII模式本身是一种C++惯用法,其精髓在于将资源的生命周期管理(获取与释放)封装在类的构造函数和析构函数中。智能指针正是这一模式的完美实践者,它们将原始指针(代表资源)进行封装,并通过自身的生命周期来自动化管理这些资源。

具体来说,智能指针的实现方式通常包括:

  1. 资源封装: 智能指针内部持有一个原始指针,指向它所管理的资源。这个原始指针对外通常是不可直接访问的,或者只能通过受限的方式访问(例如
    get()
    方法)。
  2. 构造函数中的资源获取: 智能指针的构造函数负责接收或创建资源。例如,
    std::unique_ptr
    可以直接从一个
    new
    表达式返回的原始指针构造,或者通过
    std::make_unique
    函数创建并初始化。这是RAII中的“资源获取”部分。
  3. 析构函数中的资源释放: 这是RAII模式的关键所在。当智能指针对象超出其作用域时,其析构函数会被自动调用。在这个析构函数中,智能指针会执行相应的资源释放操作,例如调用
    delete
    来释放堆内存,或者调用自定义的释放函数(如
    fclose
    对于文件句柄)。这种自动释放机制是其强大之处,因为它不依赖于程序员手动记住在每个可能的退出路径上释放资源。
  4. 所有权语义: 不同的智能指针(如
    std::unique_ptr
    std::shared_ptr
    )通过不同的所有权语义来管理资源。
    unique_ptr
    实现独占所有权,资源只能有一个所有者,通过移动语义转移所有权。
    shared_ptr
    实现共享所有权,通过引用计数来管理资源的生命周期,当最后一个
    shared_ptr
    离开作用域时,资源才会被释放。这种所有权机制是RAII在复杂场景下正确运作的基石。
  5. 操作符重载: 智能指针通常会重载
    operator*
    operator->
    ,使其行为类似于原始指针,这样用户代码可以无缝地访问底层资源。

通过这些机制,智能指针将资源管理从业务逻辑中解耦出来,使得开发者可以更专注于核心功能,而不用担心繁琐且易出错的资源管理问题。

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

智能指针的RAII如何确保C++程序的异常安全性?

在我看来,RAII模式在智能指针中的应用,对C++程序的异常安全性贡献巨大,甚至可以说是现代C++异常安全编程的基石。过去,使用原始指针管理资源时,一个常见的陷阱就是异常。想象一下,你分配了一块内存,然后执行了一些操作,接着可能抛出异常,最后才计划释放内存:

void old_style_function() {
    int* data = new int[10]; // 资源获取
    // ... 执行一些可能抛出异常的操作 ...
    delete[] data; // 资源释放,如果上面抛异常,这里就执行不到了
}

如果

// ... 执行一些可能抛出异常的操作 ...
这部分代码抛出了异常,那么
delete[] data;
这行代码将永远不会被执行到,导致内存泄漏。为了解决这个问题,你可能需要引入
try-catch
块,但这样代码会变得冗长且容易出错,尤其是在有多个资源需要管理时。

RAII通过智能指针巧妙地解决了这个问题。当一个智能指针对象被创建并拥有资源后,无论函数是正常返回,还是因为异常而提前退出(即栈展开),智能指针的析构函数都保证会被调用。在智能指针的析构函数中,它会执行资源释放操作。

例如,使用

std::unique_ptr

#include 
#include 

void modern_function() {
    std::unique_ptr data = std::make_unique(10); // 资源获取
    std::cout << "Resource acquired." << std::endl;
    // ... 执行一些可能抛出异常的操作 ...
    if (true) { // 模拟一个条件,可能抛出异常
        throw std::runtime_error("Something went wrong!");
    }
    // delete[] data; // 不需要手动释放,智能指针会处理
    std::cout << "This line will not be reached if an exception is thrown." << std::endl;
}

int main() {
    try {
        modern_function();
    } catch (const std::runtime_error& e) {
        std::cerr << "Caught exception: " << e.what() << std::endl;
    }
    std::cout << "Program finished." << std::endl;
    // 即使抛出异常,data指向的内存也已经被unique_ptr的析构函数释放了
    return 0;
}

在这个例子中,即使

modern_function
内部抛出异常,
std::unique_ptr data
的析构函数也会在栈展开时被调用,从而安全地释放
data
指向的内存。这确保了资源管理的“强异常安全保证”——如果操作失败,程序的状态将回滚到操作之前的状态,并且不会有资源泄漏。这对于编写健壮、可靠的C++代码至关重要。

std::unique_ptr和std::shared_ptr在RAII实践中的选择与权衡

在RAII的实践中,

std::unique_ptr
std::shared_ptr
是C++标准库提供的两种主要智能指针,它们都实现了RAII模式,但在所有权语义和适用场景上有着显著区别。理解这些差异,对于做出正确的选择和权衡至关重要。

std::unique_ptr:独占所有权,轻量高效

  • 核心特点:

    unique_ptr
    体现的是独占所有权。一个
    unique_ptr
    实例是它所管理资源的唯一所有者。这意味着资源在任何给定时间点都只能被一个
    unique_ptr
    拥有。

  • 所有权转移:

    unique_ptr
    不支持复制,但支持通过移动语义(
    std::move
    )转移所有权。一旦所有权被转移,原
    unique_ptr
    将不再拥有资源,变为“空”状态。

  • 性能考量: 它非常轻量,几乎没有运行时开销,与原始指针的开销相当。它不涉及引用计数,因此没有额外的内存分配或原子操作的开销。

  • 适用场景:

    闻君电脑报价系统
    闻君电脑报价系统

    一个实用于电脑系列产品报价的网站内容管理系统,傻瓜式地安装后,就有了一个类似于中关村 基本特点有: a).安装简便,傻瓜式的安装。 b).有一定的智能化,管理员管理发布信息都极其方便。 c).功能比较强大,该有的功能都有了,且有一些独特实用的功能,没有的功能,只要您提出合理,都会改进,现在还在改进中... d).后台相当完善,决不亚于任一个CMS系统。 e).定制性强,采用模板制,会有大

    下载
    • 当你明确知道资源只有一个所有者时,
      unique_ptr
      是首选。
    • 作为函数返回值,将新创建的资源所有权转移给调用者。
    • 作为类成员,管理该类独有的资源。
    • 在容器中存储指向动态分配对象的指针,如
      std::vector>
    #include 
    #include 
    
    class MyResource {
    public:
        MyResource() { std::cout << "MyResource constructed." << std::endl; }
        ~MyResource() { std::cout << "MyResource destroyed." << std::endl; }
        void doSomething() { std::cout << "Doing something with MyResource." << std::endl; }
    };
    
    std::unique_ptr createResource() {
        return std::make_unique(); // 返回unique_ptr,所有权被移动
    }
    
    void processResource(std::unique_ptr res) { // 接收unique_ptr,所有权被移动
        res->doSomething();
    } // res超出作用域,资源被释放

std::shared_ptr:共享所有权,引用计数

  • 核心特点:

    shared_ptr
    体现的是共享所有权。多个
    shared_ptr
    实例可以共同拥有同一个资源。资源只有当所有指向它的
    shared_ptr
    都销毁或不再拥有它时才会被释放。

  • 所有权管理: 它通过引用计数来管理资源的生命周期。每当一个

    shared_ptr
    被复制时,引用计数会增加;每当一个
    shared_ptr
    被销毁或重置时,引用计数会减少。当引用计数降为零时,资源被释放。

  • 性能考量:

    shared_ptr
    unique_ptr
    有更高的开销。它需要额外的内存来存储引用计数(通常是一个控制块),并且在复制、赋值、销毁时涉及原子操作来维护引用计数的线程安全,这会带来一定的性能成本。

  • 适用场景:

    • 当资源需要被多个不相关的部分共享,并且它们的生命周期相互独立时。
    • 实现对象工厂,返回共享所有权的实例。
    • 在回调函数或事件处理器中捕获对象,确保对象在回调执行期间仍然存活。
    #include 
    #include 
    
    // MyResource类同上
    
    std::shared_ptr globalResource;
    
    void consumerFunction(std::shared_ptr res) { // 接收shared_ptr,引用计数增加
        res->doSomething();
    } // res超出作用域,引用计数减少
    
    void setupGlobalResource() {
        globalResource = std::make_shared(); // 创建并共享
    }
    
    // main函数中
    // setupGlobalResource();
    // consumerFunction(globalResource); // 传递拷贝,引用计数增加
    // // globalResource超出作用域时,如果还有其他shared_ptr持有,资源不会立即释放

选择与权衡:

  • 优先
    unique_ptr
    除非你确实需要共享所有权,否则总是优先使用
    std::unique_ptr
    。它更轻量、高效,并且清晰地表达了资源的独占所有权语义,有助于避免循环引用等复杂问题。
  • shared_ptr
    的必要性:
    仅当资源确实需要被多个不相关的代码片段共享,并且其生命周期由这些共享者共同决定时,才使用
    std::shared_ptr
  • weak_ptr
    应对循环引用:
    使用
    shared_ptr
    时,要特别警惕循环引用问题(A持有B的
    shared_ptr
    ,B也持有A的
    shared_ptr
    ),这会导致引用计数永远无法降到零,造成内存泄漏。此时,
    std::weak_ptr
    是一个解决方案,它提供对
    shared_ptr
    管理资源的非拥有性引用,不会增加引用计数,可以用于打破循环。

简而言之,

unique_ptr
是默认选择,代表了C++中清晰、高效的RAII实践;而
shared_ptr
则在需要复杂共享所有权场景下提供了便利,但需要注意其带来的开销和潜在的循环引用问题。

超越内存:智能指针如何利用自定义删除器实现通用RAII?

智能指针的RAII能力远不止于管理动态分配的堆内存。通过引入“自定义删除器”(Custom Deleter),它们可以扩展到管理几乎任何类型的资源,只要这些资源需要明确的获取和释放操作。这使得智能指针成为C++中实现通用RAII模式的强大工具

默认情况下,

std::unique_ptr
std::shared_ptr
在析构时会调用
delete
(对于单个对象)或
delete[]
(对于数组)来释放它们所管理的内存。但许多资源并非通过
new
分配,也无法通过
delete
释放。例如:

  • 文件句柄:
    FILE*
    通过
    fopen
    获取,需要
    fclose
    释放。
  • 互斥锁:
    std::mutex
    lock()
    操作需要
    unlock()
    来释放。
  • 网络套接字: 通过系统API获取,需要特定的
    close
    shutdown
    函数释放。
  • Windows API句柄:
    HANDLE
    ,需要
    CloseHandle
    释放。

在这种情况下,自定义删除器就派上用场了。自定义删除器是一个可调用对象(函数指针、lambda表达式、函数对象),它会在智能指针析构时被调用,负责执行特定的资源释放逻辑。

std::unique_ptr
与自定义删除器:

unique_ptr
的自定义删除器是其类型的一部分。这意味着,带有不同删除器的
unique_ptr
实例是不同类型的。这在编译时提供了类型安全,但可能会使函数签名变得复杂。

#include 
#include  // For FILE, fopen, fclose
#include 

// 1. 使用函数指针作为删除器
void closeFile(FILE* f) {
    if (f) {
        std::cout << "Closing file via function pointer." << std::endl;
        fclose(f);
    }
}

// 2. 使用lambda表达式作为删除器(更常见和灵活)
auto lambdaCloseFile = [](FILE* f) {
    if (f) {
        std::cout << "Closing file via lambda." << std::endl;
        fclose(f);
    }
};

void demoUniquePtrCustomDeleter() {
    // 使用函数指针
    std::unique_ptr file1(fopen("test1.txt", "w"), &closeFile);
    if (file1) {
        fprintf(file1.get(), "Hello from file1!\n");
    }

    // 使用lambda表达式
    std::unique_ptr file2(fopen("test2.txt", "w"), lambdaCloseFile);
    if (file2) {
        fprintf(file2.get(), "Hello from file2!\n");
    }

    // 直接在构造时定义lambda
    std::unique_ptr file3(fopen("test3.txt", "w"), [](FILE* f) {
        if (f) {
            std::cout << "Closing file via inline lambda." << std::endl;
            fclose(f);
        }
    });
    if (file3) {
        fprintf(file3.get(), "Hello from file3!\n");
    }
    // 当这些unique_ptr超出作用域时,它们各自的删除器会被调用
}

std::shared_ptr
与自定义删除器:

shared_ptr
的自定义删除器不是其类型的一部分。这意味着,即使使用不同的删除器,
shared_ptr
的类型也保持不变。这是因为
shared_ptr
将删除器存储在内部的控制块中,以类型擦除的方式。这提供了更大的灵活性,但会带来轻微的运行时开销。

#include 
#include 
#include  // For std::mutex

// 模拟一个需要手动锁定和解锁的资源
class MyLock {
    std::mutex& mtx_;
public:
    MyLock(std::mutex& m) : mtx_(m) {
        mtx_.lock();
        std::cout << "Lock acquired." << std::endl;
    }
    ~MyLock() {
        mtx_.unlock();
        std::cout << "Lock

相关专题

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

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

147

2023.12.20

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

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

325

2023.11.30

string转int
string转int

在编程中,我们经常会遇到需要将字符串(str)转换为整数(int)的情况。这可能是因为我们需要对字符串进行数值计算,或者需要将用户输入的字符串转换为整数进行处理。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

315

2023.08.02

int占多少字节
int占多少字节

int占4个字节,意味着一个int变量可以存储范围在-2,147,483,648到2,147,483,647之间的整数值,在某些情况下也可能是2个字节或8个字节,int是一种常用的数据类型,用于表示整数,需要根据具体情况选择合适的数据类型,以确保程序的正确性和性能。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

534

2024.08.29

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

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

51

2025.08.29

C++中int的含义
C++中int的含义

本专题整合了C++中int相关内容,阅读专题下面的文章了解更多详细内容。

194

2025.08.29

lambda表达式
lambda表达式

Lambda表达式是一种匿名函数的简洁表示方式,它可以在需要函数作为参数的地方使用,并提供了一种更简洁、更灵活的编码方式,其语法为“lambda 参数列表: 表达式”,参数列表是函数的参数,可以包含一个或多个参数,用逗号分隔,表达式是函数的执行体,用于定义函数的具体操作。本专题为大家提供lambda表达式相关的文章、下载、课程内容,供大家免费下载体验。

202

2023.09.15

python lambda函数
python lambda函数

本专题整合了python lambda函数用法详解,阅读专题下面的文章了解更多详细内容。

190

2025.11.08

c++主流开发框架汇总
c++主流开发框架汇总

本专题整合了c++开发框架推荐,阅读专题下面的文章了解更多详细内容。

80

2026.01.09

热门下载

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

精品课程

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

共48课时 | 7万人学习

Excel 教程
Excel 教程

共162课时 | 11.4万人学习

PHP基础入门课程
PHP基础入门课程

共33课时 | 1.9万人学习

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

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