究竟什么时候用shared_ptr,什么时候用unique_ptr?
博主介绍:程序喵大人
- 35 - 资深C/C++/Rust/Android/iOS客户端开发
- 10年大厂工作经验
- 嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手
- 《C++20高级编程》《C++23高级编程》等多本书籍著译者
- 更多原创精品文章,首发gzh,见文末
- 👇👇记得订阅专栏,以防走丢👇👇
😉C++基础系列专栏
😃C语言基础系列专栏
🤣C++大佬养成攻略专栏
🤓C++训练营
👉🏻个人网站
最近,有同学来问我,想了解C++的三种智能指针的使用场景,在项目中应该如何选择?
首先要了解这三种智能指针的特点,std::unique_ptr
、std::shared_ptr
和std::weak_ptr
:
std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,意味着同一时间内只能有一个unique_ptr
指向一个特定的对象。
当unique_ptr
被销毁时,它所指向的对象也会被销毁。
使用场景:
- 当你需要确保一个对象只被一个指针所拥有时。
- 当你需要自动管理资源,如文件句柄或互斥锁时。
- 当你不确定用哪种智能指针时,优先选择unique_ptr就没毛病。
示例代码:
#include <iostream>
#include <memory>class Test {
public:Test() { std::cout << "Test::Test()\n"; }~Test() { std::cout << "Test::~Test()\n"; }void test() { std::cout << "Test::test()\n"; }
};int main() {std::unique_ptr<Test> ptr(new Test());ptr->test();// 当ptr离开作用域时,它指向的对象会被自动销毁return 0;
}
std::shared_ptr
std::shared_ptr
是一种共享所有权的智能指针,多个shared_ptr
可以指向同一个对象。内部使用引用计数来确保只有当最后一个指向对象的shared_ptr
被销毁时,对象才会被销毁。
使用场景:
- 当你需要在多个所有者之间共享对象时。
- 当你需要通过复制构造函数或赋值操作符来复制智能指针时。
示例代码:
#include <iostream>
#include <memory>class Test {
public:Test() { std::cout << "Test::Test()\n"; }~Test() { std::cout << "Test::~Test()\n"; }void test() { std::cout << "Test::test()\n"; }
};int main() {std::shared_ptr<Test> ptr1(new Test());std::shared_ptr<Test> ptr2 = ptr1;ptr1->test();// 当ptr1和ptr2离开作用域时,它们指向的对象会被自动销毁return 0;
}
std::weak_ptr
std::weak_ptr
是一种不拥有对象所有权的智能指针,它指向一个由std::shared_ptr
管理的对象。weak_ptr
用于解决shared_ptr
之间的循环引用问题。
是另外一种智能指针,它是对 shared_ptr 的补充,std::weak_ptr
是一种弱引用智能指针,用于观察 std::shared_ptr 指向的对象,而不影响引用计数。它主要用于解决循环引用问题,从而避免内存泄漏,另外如果需要追踪指向某个对象的第一个指针,则可以使用 weak_ptr。
可以考虑在对象本身中维护一个指向第一个 shared_ptr 的弱引用(std::weak_ptr)。当创建对象的第一个 shared_ptr 时,将这个 shared_ptr 赋值给对象的 weak_ptr 成员。这样,在需要时,可以通过检查对象的 weak_ptr 成员来获取指向对象的第一个 shared_ptr(如果仍然存在的话).
使用场景:
- 当你需要访问但不拥有由
shared_ptr
管理的对象时。 - 当你需要解决
shared_ptr
之间的循环引用问题时。 - 注意
weak_ptr
肯定要和shared_ptr
搭配使用。
示例代码:
#include <iostream>
#include <memory>class Test {
public:Test() { std::cout << "Test::Test()\n"; }~Test() { std::cout << "Test::~Test()\n"; }void test() { std::cout << "Test::test()\n"; }
};int main() {std::shared_ptr<Test> sharedPtr(new Test());std::weak_ptr<Test> weakPtr = sharedPtr;if (auto lockedSharedPtr = weakPtr.lock()) {lockedSharedPtr->test();}// 当sharedPtr离开作用域时,它指向的对象会被自动销毁return 0;
}
这三种智能指针各有其用途,选择哪一种取决于你的具体需求。
1)智能指针方面的建议:
- 尽量使用智能指针,而非裸指针来管理内存,很多时候利用
RAII
机制管理内存肯定更靠谱安全的多。 - 如果没有多个所有者共享对象的需求,建议优先使用
unique_ptr
管理内存,它相对shared_ptr
会更轻量一些。 - 在使用
shared_ptr
时,一定要注意是否有循环引用的问题,因为这会导致内存泄漏。 shared_ptr
的引用计数是安全的,但是里面的对象不是线程安全的,这点要区别开。
2)为什么std::unique_ptr
可以做到不可复制,只可移动?
因为把拷贝构造函数和赋值运算符标记为了delete,见源码:
template <typename _Tp, typename _Tp_Deleter = default_delete<_Tp> >
class unique_ptr {// Disable copy from lvalue.unique_ptr(const unique_ptr&) = delete;template<typename _Up, typename _Up_Deleter> unique_ptr(const unique_ptr<_Up, _Up_Deleter>&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;template<typename _Up, typename _Up_Deleter> unique_ptr& operator=(const unique_ptr<_Up, _Up_Deleter>&) = delete;
};
3)shared_ptr
的原理:
每个 std::shared_ptr 对象包含两个成员变量:一个指向被管理对象的原始指针,一个指向引用计数块的指针(control block pointer)。
引用计数块是一个单独的内存块,引用计数块允许多个 std::shared_ptr 对象共享相同的引用计数,从而实现共享所有权。
当创建一个新的 std::shared_ptr 时,引用计数初始化为 1,表示对象当前被一个 shared_ptr 管理。
- 拷贝 std::shared_ptr:当用一个 shared_ptr 拷贝出另一个 shared_ptr 时,需要拷贝两个成员变量(被管理对象的原始指针和引用计数块的指针),并同时将引用计数值加 1。这样,多个 shared_ptr 对象可以共享相同的引用计数。
- 析构 std::shared_ptr:当 shared_ptr 对象析构时,引用计数值减 1。然后检测引用计数是否为 0。如果引用计数为 0,说明没有其他 shared_ptr 对象指向该资源,因此需要同时删除原始对象(通过调用自定义删除器,如果有的话)。
4)智能指针的缺点
- 性能开销,需要额外的内存来存储他们的控制块,控制块包括引用计数,以及运行时的原子操作来增加或减少引用技术,这可能导致裸指针的性能下降。
- 循环引用问题,如果两个对象通过成员变量
shared_ptr
相互引用,并且没有其他指针指向这两个对象中的任何一个,那么这两个对象的内存将永远不会被释放,导致内存泄露。
#include<iostream>
#include<memory>
class B;// 前向声明class A {
public:std::shared_ptr<B> b_ptr;~A() {std::cout << "A has been destroyed."<< std::endl;}
};class B {
public:std::shared_ptr<A> a_ptr;~B() {std::cout << "B has been destroyed."<< std::endl;}
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b; // A 引用 Bb->a_ptr = a; // B 引用 A// 由于存在循环引用,A 和 B 的析构函数将不会被调用,从而导致内存泄漏return 0;
}
- 智能指针不一定适用于所有场景:有一些容器类,内部实现依赖于裸指针,另外在考虑某些性能关键场景下,使用裸指针可能更合适。但绝大多数场景,用智能指针就OK。
选型建议
-
默认选择
unique_ptr
,因为它性能最优,且语义清晰,比如局部动态对象。 -
当你发现
unique_ptr
使用受限,那大概率就是有需要共享的需求,需要多个模块或对象需共享同一资源时(如全局配置、线程间共享数据),使用shared_ptr
,但要注意循环引用的问题。 -
优先使用
make_unique
和make_shared
构造对应的智能指针,具备异常安全性。 -
避免裸指针和智能指针混用,容易出现
double free
等问题。 -
unique_ptr
放心使用,并没有额外开销。 -
shared_ptr
的引用计数可能引发原子操作开销,除非对性能有非常极致的要求,否则没必要在意这点开销。也要注意循环引用会导致内存泄漏。
码字不易,欢迎大家点赞,关注,评论,谢谢!
👉C++训练营
一个专为校招、社招3年工作经验的同学打造的 1v1 项目实战训练营,量身定制学习计划、每日代码review,简历优化,面试辅导,已帮助多名学员获得大厂offer!