在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
失效或被销毁时,计数器减一。只有当引用计数归零时,资源才会被释放。它的设计理念是“大家一起管理,直到最后一个管理者离开”。
何时选择它们?
这其实是一个关于资源语义的问题,我通常是这样思考的:
-
首选
std::unique_ptr
。 如果你能明确地知道某个资源只有一个“老板”,或者说它的生命周期完全由一个特定的对象或作用域来控制,那么unique_ptr
几乎总是最佳选择。-
场景示例:
- 局部变量: 函数内部创建的对象,只在该函数生命周期内有效。
- 类成员: 一个类拥有一个独占的子组件,这个子组件的生命周期与父类绑定。
-
工厂函数返回对象:
std::unique_ptr
非常适合作为工厂函数的返回值,它清晰地表明工厂创建了一个新对象,并将所有权转移给调用者。 -
性能敏感的场景:
unique_ptr
没有引用计数的开销,通常比shared_ptr
更快。
-
场景示例:
-
当需要共享所有权时才使用
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的引用计数都不会降到零(因为它们互相引用),导致这两个对象都无法被销毁。

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


#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++循环与算法优化提高程序执行效率
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。