C++如何使用std::unique_ptr和std::shared_ptr管理资源(如何使用.资源.管理.unique_ptr.std...)

wufei123 发布于 2025-09-11 阅读(1)
std::unique_ptr和std::shared_ptr是C++中用于自动管理资源生命周期的智能指针,遵循RAII原则,前者实现独占所有权,后者通过引用计数实现共享所有权,能有效防止内存泄漏和悬空指针。

c++如何使用std::unique_ptr和std::shared_ptr管理资源

在C++中,

std::unique_ptr
std::shared_ptr
是现代C++(C++11及更高版本)提供的智能指针,它们的核心作用是自动化内存及其他资源的生命周期管理,从而有效避免内存泄漏和悬空指针等常见问题。简单来说,它们通过RAII(Resource Acquisition Is Initialization)原则,在对象超出作用域时自动释放所持有的资源,大大简化了资源管理的代码,让开发者能更专注于业务逻辑。
unique_ptr
强调独占所有权,而
shared_ptr
则支持共享所有权,两者各司其职,覆盖了C++中大部分的资源管理场景。 解决方案

谈到智能指针,我个人觉得这简直是C++迈向“更安全、更现代”的关键一步。以前手动

new
delete
的日子,总感觉像是在走钢丝,生怕一个不小心就漏了、错了。现在有了智能指针,那种心安理得的感觉,真是太棒了。

使用

std::unique_ptr
管理独占资源

std::unique_ptr
,顾名思义,它对所指向的资源拥有“独占”所有权。这意味着在任何时候,只有一个
unique_ptr
实例能够管理特定的资源。一旦这个
unique_ptr
被销毁(比如超出作用域),它所管理的资源也会被自动释放。这种独占性让它非常适合那些不希望被多个地方同时引用的资源,例如,一个函数内部创建的对象,或者作为类成员的某个独有组件。

它的一个显著特点是不能被复制,但可以被“移动”。移动语义在这里发挥了关键作用,它允许所有权从一个

unique_ptr
转移到另一个,而不会导致资源被双重释放。
#include <iostream>
#include <memory>
#include <vector>

// 假设有一个资源类
class MyResource {
public:
    MyResource(int id) : id_(id) {
        std::cout << "MyResource " << id_ << " created." << std::endl;
    }
    ~MyResource() {
        std::cout << "MyResource " << id_ << " destroyed." << std::endl;
    }
    void doSomething() {
        std::cout << "MyResource " << id_ << " doing something." << std::endl;
    }
private:
    int id_;
};

void processUniqueResource(std::unique_ptr<MyResource> res) {
    if (res) { // 检查是否有效
        res->doSomething();
    }
    // res 在这里超出作用域,MyResource 会被自动销毁
}

int main() {
    // 1. 创建 unique_ptr
    // 推荐使用 std::make_unique,它更安全,效率更高
    auto ptr1 = std::make_unique<MyResource>(1);
    ptr1->doSomething();

    // 2. 移动所有权
    // ptr1 的所有权转移给 ptr2,ptr1 变为 nullptr
    auto ptr2 = std::move(ptr1);
    if (ptr1) {
        std::cout << "This should not print." << std::endl;
    }
    if (ptr2) {
        ptr2->doSomething();
    }

    // 3. 将 unique_ptr 作为函数参数传递(通过移动)
    std::cout << "Calling processUniqueResource..." << std::endl;
    processUniqueResource(std::move(ptr2)); // ptr2 再次变为 nullptr
    std::cout << "processUniqueResource returned." << std::endl;

    // 4. unique_ptr 数组
    std::vector<std::unique_ptr<MyResource>> resources;
    resources.push_back(std::make_unique<MyResource>(3));
    resources.push_back(std::make_unique<MyResource>(4));
    // 离开作用域时,vector 中的所有 MyResource 都会被销毁

    // main 函数结束时,所有剩余的 unique_ptr 也会自动释放资源
    return 0;
}

使用

std::shared_ptr
管理共享资源

当多个对象需要共享同一个资源的所有权时,

std::shared_ptr
就派上用场了。它通过引用计数机制来工作:每当有一个
shared_ptr
指向资源时,引用计数就会增加;当一个
shared_ptr
被销毁或重新指向其他资源时,引用计数就会减少。只有当引用计数降到零时,资源才会被真正释放。

这对于实现一些复杂的对象图,或者当资源的生命周期不确定,需要由多个不相关的部分共同管理时,非常有用。

#include <iostream>
#include <memory>
#include <vector>

// 假设有一个资源类
class SharedResource {
public:
    SharedResource(int id) : id_(id) {
        std::cout << "SharedResource " << id_ << " created." << std::endl;
    }
    ~SharedResource() {
        std::cout << "SharedResource " << id_ << " destroyed." << std::endl;
    }
    void showId() {
        std::cout << "SharedResource ID: " << id_ << std::endl;
    }
private:
    int id_;
};

void observeSharedResource(std::shared_ptr<SharedResource> res) {
    std::cout << "  Inside observeSharedResource, ref count: " << res.use_count() << std::endl;
    res->showId();
    // res 在这里超出作用域,引用计数减少
}

int main() {
    // 1. 创建 shared_ptr
    // 推荐使用 std::make_shared,它更安全,效率更高
    auto s_ptr1 = std::make_shared<SharedResource>(10);
    std::cout << "Initial ref count: " << s_ptr1.use_count() << std::endl; // 1

    // 2. 复制 shared_ptr,共享所有权
    auto s_ptr2 = s_ptr1; // 复制,引用计数增加
    std::cout << "After copy to s_ptr2, ref count: " << s_ptr1.use_count() << std::endl; // 2

    // 3. 传递 shared_ptr 作为函数参数(通过值传递或 const 引用)
    observeSharedResource(s_ptr1); // 传递时引用计数会增加1,函数返回时减少1
    std::cout << "After observeSharedResource, ref count: " << s_ptr1.use_count() << std::endl; // 2

    // 4. 创建另一个 shared_ptr
    std::shared_ptr<SharedResource> s_ptr3;
    {
        auto temp_ptr = std::make_shared<SharedResource>(20);
        s_ptr3 = temp_ptr; // 复制,引用计数增加
        std::cout << "Inside block, temp_ptr ref count: " << temp_ptr.use_count() << std::endl; // 2
    } // temp_ptr 销毁,引用计数减少到 1
    std::cout << "After block, s_ptr3 ref count: " << s_ptr3.use_count() << std::endl; // 1

    // main 函数结束时,所有 shared_ptr 销毁,引用计数归零,SharedResource 会被销毁
    return 0;
}
std::unique_ptr
std::shared_ptr
的核心区别是什么?何时选择它们?

这真的是一个非常基础但又极其重要的问题,很多时候,选错了智能指针,可能会导致不必要的性能开销,甚至引入难以调试的逻辑错误。

核心区别:所有权模型

  • std::unique_ptr
    :独占所有权。 顾名思义,它独占所管理的对象。这意味着在任何给定时间,只有一个
    unique_ptr
    实例可以指向特定的资源。它的设计哲学是“要么拥有,要么不拥有”。这种独占性确保了资源生命周期的清晰性,资源一旦被
    unique_ptr
    拥有,就由它负责销毁。它的拷贝构造函数是被禁用的,但支持移动构造和移动赋值,这意味着所有权可以从一个
    unique_ptr
    转移到另一个,原
    unique_ptr
    会变为空。
  • std::shared_ptr
    :共享所有权。 多个
    shared_ptr
    实例可以共同管理同一个资源。它内部维护了一个引用计数器,每当有新的
    shared_ptr
    指向该资源时,计数器加一;当一个
    shared_ptr
    失效或被销毁时,计数器减一。只有当引用计数归零时,资源才会被释放。它的设计理念是“大家一起管理,直到最后一个管理者离开”。

何时选择它们?

这其实是一个关于资源语义的问题,我通常是这样思考的:

  1. 首选

    std::unique_ptr
    。 如果你能明确地知道某个资源只有一个“老板”,或者说它的生命周期完全由一个特定的对象或作用域来控制,那么
    unique_ptr
    几乎总是最佳选择。
    • 场景示例:
      • 局部变量: 函数内部创建的对象,只在该函数生命周期内有效。
      • 类成员: 一个类拥有一个独占的子组件,这个子组件的生命周期与父类绑定。
      • 工厂函数返回对象:
        std::unique_ptr
        非常适合作为工厂函数的返回值,它清晰地表明工厂创建了一个新对象,并将所有权转移给调用者。
      • 性能敏感的场景:
        unique_ptr
        没有引用计数的开销,通常比
        shared_ptr
        更快。
  2. 当需要共享所有权时才使用

    std::shared_ptr
    。 如果一个资源的生命周期需要被多个不相关的对象共同管理,而且这些对象都没有一个明确的“主导者”,那么
    shared_ptr
    就是不二之选。
    • 场景示例:
      • 缓存系统: 多个客户端可能需要访问同一个缓存项,只要有客户端还在使用,缓存项就不能被销毁。
      • 图形场景中的对象: 一个模型可能被多个场景节点引用,只有当所有引用都消失时,模型才会被卸载。
      • 树形或图状数据结构: 节点之间可能互相引用,但没有一个明确的父子关系来决定生命周期。
      • 观察者模式: 观察者和被观察者都持有资源的
        shared_ptr
        ,当所有观察者都解注册后,资源才释放。

我个人在写代码时,总是先尝试用

unique_ptr
,如果发现逻辑上确实需要多个地方共享资源,并且没有一个明确的单点来管理其生命周期,才会转向
shared_ptr
。这种“默认
unique_ptr
,按需
shared_ptr
”的策略,能帮助我们写出更高效、更清晰的代码。 如何避免
std::shared_ptr
导致的循环引用问题?
std::weak_ptr
在其中扮演什么角色?

啊,

shared_ptr
的循环引用,这简直是智能指针使用中的一个经典陷阱,也是我最初接触时最容易犯错的地方。它会悄无声息地导致内存泄漏,因为引用计数永远不会降到零,资源也就永远不会被释放。

循环引用是如何发生的?

简单来说,就是两个或更多个

shared_ptr
实例互相持有对方的
shared_ptr
。想象一下两个对象A和B:A持有一个指向B的
shared_ptr
,同时B也持有一个指向A的
shared_ptr
。当它们各自的外部引用都消失后,A和B的引用计数都不会降到零(因为它们互相引用),导致这两个对象都无法被销毁。 PIA PIA

全面的AI聚合平台,一站式访问所有顶级AI模型

PIA226 查看详情 PIA
#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A constructed" << std::endl; }
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::shared_ptr<A> a_ptr;
    B() { std::cout << "B constructed" << std::endl; }
    ~B() { std::cout << "B destroyed" << std::endl; }
};

int main() {
    std::cout << "Starting main..." << std::endl;
    {
        auto a = std::make_shared<A>(); // a.use_count() == 1
        auto b = std::make_shared<B>(); // b.use_count() == 1

        a->b_ptr = b; // b.use_count() == 2 (A持有B)
        b->a_ptr = a; // a.use_count() == 2 (B持有A)

        std::cout << "a's ref count: " << a.use_count() << std::endl;
        std::cout << "b's ref count: " << b.use_count() << std::endl;
    } // a 和 b 在这里超出作用域

    std::cout << "Exiting main." << std::endl;
    // 预期 A 和 B 都不会被销毁,因为它们的引用计数都停留在 1
    // A 的 b_ptr 引用 B,B 的 a_ptr 引用 A
    return 0;
}

运行上面的代码,你会发现"A destroyed"和"B destroyed"都没有打印出来,这就是循环引用导致的内存泄漏。

std::weak_ptr
的角色:打破循环

std::weak_ptr
正是为了解决
shared_ptr
的循环引用问题而设计的。它是一种“弱引用”智能指针,它指向由
shared_ptr
管理的对象,但不增加对象的引用计数。你可以把它想象成一个观察者,它知道资源在哪里,但并不参与资源的生命周期管理。

weak_ptr
所观察的资源(被
shared_ptr
管理的对象)仍然存在时,你可以通过
weak_ptr::lock()
方法获得一个
shared_ptr
,从而安全地访问资源。如果资源已经被销毁(所有
shared_ptr
都已失效),
lock()
会返回一个空的
shared_ptr

如何使用

std::weak_ptr
避免循环引用?

核心思想是:在可能形成循环引用的关系中,将其中一方的

shared_ptr
替换为
weak_ptr
。通常,我们会让“子”或“观察者”持有对“父”或“被观察者”的
weak_ptr

以上面的A和B为例,如果我们将B对A的引用改为

weak_ptr
#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A constructed" << std::endl; }
    ~A() { std::cout << "A destroyed" << std::endl; }
};

class B {
public:
    std::weak_ptr<A> a_ptr; // 这里改为 weak_ptr
    B() { std::cout << "B constructed" << std::endl; }
    ~B() { std::cout << "B destroyed" << std::endl; }
    void accessA() {
        if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr
            std::cout << "B is accessing A." << std::endl;
            // 可以在这里使用 sharedA
        } else {
            std::cout << "A has been destroyed." << std::endl;
        }
    }
};

int main() {
    std::cout << "Starting main..." << std::endl;
    {
        auto a = std::make_shared<A>(); // a.use_count() == 1
        auto b = std::make_shared<B>(); // b.use_count() == 1

        a->b_ptr = b; // b.use_count() == 2 (A持有B)
        b->a_ptr = a; // a_ptr 是 weak_ptr,不增加 A 的引用计数,a.use_count() 仍然是 1

        std::cout << "a's ref count: " << a.use_count() << std::endl; // 1
        std::cout << "b's ref count: " << b.use_count() << std::endl; // 2

        b->accessA(); // B 仍然可以访问 A
    } // a 和 b 在这里超出作用域

    std::cout << "Exiting main." << std::endl;
    // 此时,A 和 B 都将正常销毁
    return 0;
}

这次运行,你会看到"A destroyed"和"B destroyed"都正常打印了。当外部

shared_ptr a
失效时,
A
的引用计数降为0,
A
被销毁。
A
被销毁后,其内部的
b_ptr
也会失效,导致
B
的引用计数降为1。当外部
shared_ptr b
失效时,
B
的引用计数降为0,
B
被销毁。循环引用被成功打破。

选择使用

weak_ptr
的关键在于识别出那些“非拥有”但需要“观察”资源的关系。这种模式在父子关系(子节点弱引用父节点)、观察者模式(观察者弱引用被观察者)等场景中非常常见。 除了内存,智能指针还能管理哪些类型的资源?如何实现自定义资源管理?

智能指针的威力远不止于内存管理,这是它们设计理念中非常优雅和强大的一面。

std::unique_ptr
std::shared_ptr
都可以通过自定义删除器(Custom Deleter)来管理任何需要显式释放的资源,只要这些资源能被一个指针或句柄来表示。

这完全符合RAII(Resource Acquisition Is Initialization)的精髓:资源在构造时获取,在析构时释放。智能指针就是这个“析构时释放”的完美载体。

智能指针可以管理的非内存资源示例:

  • 文件句柄:
    FILE*
    (C风格文件操作)、
    HANDLE
    (Windows文件句柄)。
  • 网络套接字:
    SOCKET
    (Winsock)、文件描述符(Linux/Unix)。
  • 互斥锁/信号量:
    pthread_mutex_t*
    HANDLE
    (Windows Mutex)。
  • 数据库连接: 数据库API返回的连接对象指针。
  • 动态库句柄:
    dlopen
    返回的句柄。
  • 图形资源: OpenGL纹理ID、Vulkan句柄等。

如何实现自定义资源管理(自定义删除器)?

自定义删除器是一个可调用对象(函数、函数对象、Lambda表达式),它接收一个指向资源的原始指针作为参数,并负责释放该资源。

对于

std::unique_ptr

自定义删除器是其模板参数的一部分。这意味着删除器的类型会影响

unique_ptr
的类型。
#include <iostream>
#include <memory>
#include <cstdio> // For FILE* and fclose

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

// 2. 使用 Lambda 表达式作为删除器(更常见和灵活)
// Lambda 通常是无状态的,或者捕获少量变量
auto customFileDeleter = [](FILE* filePtr) {
    if (filePtr) {
        std::cout << "Closing file using lambda deleter." << std::endl;
        fclose(filePtr);
    }
};

int main() {
    // 示例1: 管理文件句柄,使用函数作为删除器
    // 注意 unique_ptr 的第二个模板参数是删除器的类型
    std::unique_ptr<FILE, decltype(&closeFile)> file1(fopen("test1.txt", "w"), &closeFile);
    if (file1) {
        fprintf(file1.get(), "Hello from file1!\n");
        std::cout << "File1 opened successfully." << std::endl;
    } else {
        std::cerr << "Failed to open test1.txt" << std::endl;
    }
    // file1 超出作用域时,closeFile 会被调用

    // 示例2: 管理文件句柄,使用 Lambda 作为删除器
    // decltype(customFileDeleter) 会自动推导出 Lambda 的类型
    std::unique_ptr<FILE, decltype(customFileDeleter)> file2(fopen("test2.txt", "w"), customFileDeleter);
    if (file2) {
        fprintf(file2.get(), "Hello from file2!\n");
        std::cout << "File2 opened successfully." << std::endl;
    } else {

以上就是C++如何使用std::unique_ptr和std::shared_ptr管理资源的详细内容,更多请关注知识资源分享宝库其它相关文章!

相关标签: c++ linux windows access ai ios win 区别 常见问题 作用域 red Resource 父类 构造函数 局部变量 循环 Lambda 指针 数据结构 空指针 delete 对象 作用域 windows 数据库 linux 自动化 unix 大家都在看: C++如何使用模板实现迭代器类 C++如何处理复合对象中的嵌套元素 C++内存模型与编译器优化理解 C++如何使用ofstream和ifstream组合操作文件 C++循环与算法优化提高程序执行效率

标签:  如何使用 资源 管理 

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。