std::shared_ptr通过引用计数实现共享所有权,自动管理对象生命周期,避免内存泄漏和悬空指针;使用std::make_shared可提升性能与异常安全;需警惕循环引用,可用std::weak_ptr打破;其引用计数线程安全,但被管理对象的并发访问仍需额外同步机制。

C++的
std::shared_ptr
,在我看来,是现代C++处理动态内存和资源共享时的一把利器,它通过引入引用计数机制,巧妙地解决了多个所有者共同管理同一块内存的复杂性,避免了传统裸指针可能导致的内存泄漏和悬空指针问题,让资源管理变得更加自动化和安全。它本质上就是一种智能指针,能够确保被它管理的对象在不再被任何
shared_ptr
引用时,能够被正确、及时地销毁。
解决方案
std::shared_ptr
的核心思想是“共享所有权”。当你创建一个
shared_ptr
来管理一个对象时,它会内部维护一个引用计数。每当这个
shared_ptr
被复制(无论是通过拷贝构造函数、拷贝赋值操作符,还是作为函数参数传递),引用计数就会增加。这意味着有更多的
shared_ptr
实例正在“关注”这个对象。反之,当一个
shared_ptr
实例被销毁(例如,超出作用域、被
reset()
,或者被赋值为另一个
shared_ptr
),引用计数就会减少。一旦引用计数归零,就意味着没有任何
shared_ptr
实例再关心这个对象了,此时,
shared_ptr
会自动调用对象的析构函数并释放其所占用的内存。
这种机制的强大之处在于,它将资源的生命周期管理从程序员手中解放出来,自动化地处理了许多原本容易出错的场景。比如,你可以在不同的数据结构中存储指向同一个对象的
shared_ptr
,而无需担心谁应该负责
delete
这个对象。只要有一个
shared_ptr
仍然存活,对象就会一直存在。
创建
shared_ptr
通常推荐使用
std::make_shared
:
立即学习“C++免费学习笔记(深入)”;
#include #include #include class MyResource {public: std::string name; MyResource(const std::string& n) : name(n) { std::cout << "MyResource " << name << " created." << std::endl; } ~MyResource() { std::cout << "MyResource " << name << " destroyed." << std::endl; }};void processResource(std::shared_ptr res) { std::cout << "Processing: " <name << ", current count: " << res.use_count() << std::endl;} // res goes out of scope, ref count might decreaseint main() { // 推荐使用 std::make_shared std::shared_ptr res1 = std::make_shared("Data A"); std::cout << "Initial count for Data A: " << res1.use_count() << std::endl; { std::shared_ptr res2 = res1; // 拷贝,引用计数增加 std::cout << "Count after copy: " << res1.use_count() << std::endl; processResource(res2); // 传递拷贝,函数内部又增加一次,然后减少 std::cout << "Count after function call: " << res1.use_count() << std::endl; } // res2 goes out of scope, ref count decreases std::cout << "Count before main scope ends: " << res1.use_count() << std::endl; // main 结束时,res1 销毁,引用计数归零,MyResource "Data A" 被销毁 return 0;}
这段代码清晰地展示了
shared_ptr
如何通过引用计数管理
MyResource
对象的生命周期。
std::shared_ptr
std::shared_ptr
循环引用:一个隐蔽的内存泄漏陷阱?
没错,
shared_ptr
虽然强大,但它有一个著名的“阿喀琉斯之踵”——循环引用。这听起来有点抽象,但实际场景中并不少见。想象一下,如果对象A持有一个指向对象B的
shared_ptr
,同时对象B也持有一个指向对象A的
shared_ptr
,会发生什么?
A -> shared_ptr
在这种情况下,当A和B的外部所有
shared_ptr
都消失后,A的引用计数永远不会降到1(因为B还持有一个),B的引用计数也永远不会降到1(因为A还持有一个)。它们互相持有对方的“所有权”,导致引用计数永远无法归零,从而谁也无法被销毁。这就是一个典型的内存泄漏,而且是那种非常隐蔽、难以调试的泄漏。
解决这个问题的关键在于引入
std::weak_ptr
。
weak_ptr
是一种不拥有所有权的智能指针。它观察一个由
shared_ptr
管理的对象,但不会增加对象的引用计数。你可以把它看作是一个“旁观者”或者“观察者”。当
shared_ptr
管理的对象被销毁时,所有关联的
weak_ptr
都会自动失效。
要访问
weak_ptr
所指向的对象,你需要先将其转换为
shared_ptr
,通过调用
weak_ptr::lock()
方法。如果对象仍然存活,
lock()
会返回一个有效的
shared_ptr
;如果对象已经被销毁,
lock()
则返回一个空的
shared_ptr
。
#include #include #include class B; // 前向声明class A {public: std::shared_ptr b_ptr; std::string name; A(const std::string& n) : name(n) { std::cout << "A " << name << " created." << std::endl; } ~A() { std::cout << "A " << name << " destroyed." << std::endl; }};class B {public: std::weak_ptr a_ptr; // 使用 weak_ptr 解决循环引用 std::string name; B(const std::string& n) : name(n) { std::cout << "B " << name << " created." << std::endl; } ~B() { std::cout << "B " << name << " destroyed." << std::endl; } void print_a_name() { if (auto sharedA = a_ptr.lock()) { // 尝试获取 shared_ptr std::cout << "B " << name << " accesses A: " <name << std::endl; } else { std::cout << "A is no longer available for B " << name << std::endl; } }};int main() { std::shared_ptr myA = std::make_shared("Instance A"); std::shared_ptr myB = std::make_shared("Instance B"); // 建立连接 myA->b_ptr = myB; myB->a_ptr = myA; // 这里是 weak_ptr,不会增加 A 的引用计数 std::cout << "A's ref count: " << myA.use_count() << std::endl; // 应该是 1 (myA) std::cout << "B's ref count: " << myB.use_count() <print_a_name(); // B 可以安全地访问 A // 当 myA 和 myB 超出作用域时,它们会被正确销毁 // A 的引用计数降为 0,A 销毁。 // B 的引用计数降为 0,B 销毁。 return 0;}
通过将其中一方的
shared_ptr
替换为
weak_ptr
,我们打破了循环,确保了对象能够被正确销毁。
std::make_shared
std::make_shared
vs
new
:性能与异常安全的考量
在C++中创建
shared_ptr
时,你可能会看到两种常见的写法:
std::shared_ptr p(new T(...));
std::shared_ptr p = std::make_shared(...);
从表面上看,它们都实现了同样的目的,但
std::make_shared
在性能和异常安全性上有着显著的优势,我个人总是推荐使用它。
性能方面:
std::shared_ptr
内部需要维护一个控制块(control block),这个控制块包含了引用计数、
weak_ptr
计数以及可能的自定义删除器等信息。
当使用
new T()
然后传递给
shared_ptr
构造函数时,会发生两次独立的内存分配:一次是为
T
对象本身分配内存,另一次是为
shared_ptr
的控制块分配内存。这两次分配可能会导致内存碎片,并且由于是两次系统调用,效率通常较低。
std::make_shared
则非常聪明,它会尝试进行单次内存分配。它在一个连续的内存块中同时为
T
对象和
shared_ptr
的控制块分配空间。这不仅减少了内存分配的次数,提高了效率,还有助于改善缓存局部性(cache locality),因为对象和其管理信息存储在一起,CPU访问时效率更高。
异常安全性方面:考虑一个表达式,比如
func(std::shared_ptr(new A()), std::shared_ptr(new B()));
在C++11/14标准中,编译器可能会以任意顺序执行子表达式。一个可能的执行顺序是:
new A()
new B()
std::shared_ptr(ptr_A)
std::shared_ptr(ptr_B)
如果
new A()
成功,但紧接着
new B()
抛出了异常,那么
ptr_A
指向的内存将永远不会被
接管,从而导致
A
对象的内存泄漏。这种情况下,
shared_ptr
的构造函数还没来得及执行,它就无法管理这块内存了。
而使用
std::make_shared
则不会有这个问题:
func(std::make_shared(), std::make_shared());
如果
std::make_shared()
成功,但
std::make_shared()
抛出异常,那么
std::make_shared()
返回的
shared_ptr
会立即被销毁,其内部的
A
对象也会随之被正确释放。这是因为
make_shared
的整个操作是原子的,要么全部成功,要么在失败时能保证已分配资源的正确清理。
因此,除非你需要自定义删除器,或者需要从一个已经存在的裸指针来创建
shared_ptr
(例如,从一个C风格API返回的指针),否则
std::make_shared
几乎总是更优的选择。
std::shared_ptr
std::shared_ptr
在多线程环境下的安全边界
std::shared_ptr
在多线程环境下的行为是一个经常被误解的话题。我见过不少开发者认为只要用了
shared_ptr
,所有关于线程安全的问题就都解决了,这其实是个危险的误区。理解
shared_ptr
的线程安全边界至关重要。
shared_ptr
自身的线程安全:
std::shared_ptr
的引用计数是线程安全的。这意味着,多个线程可以同时对同一个
shared_ptr
对象进行拷贝、赋值、销毁操作(这会导致引用计数的增减),这些操作都是原子性的。标准库保证了这些引用计数的修改是正确的,不会出现竞态条件导致引用计数混乱。例如:
std::shared_ptr global_res = std::make_shared("Shared Data");void thread_func() { std::shared_ptr local_res = global_res; // 引用计数安全地增加 // ... 使用 local_res ...} // local_res 销毁,引用计数安全地减少
在这种情况下,
global_res
的引用计数在多个线程中被安全地操作。
被管理对象的线程安全:然而,
std::shared_ptr
不保证它所管理的对象的线程安全。如果多个线程通过不同的
shared_ptr
实例同时访问或修改同一个被管理的对象,你仍然需要自己实现同步机制(例如互斥锁
std::mutex
)。
shared_ptr
只负责对象的生命周期管理,而对对象内部数据的并发访问控制,则完全是另一回事。
举个例子:
#include #include #include #include #include class Counter {public: int value = 0; std::mutex mtx; // 用于保护 value void increment() { std::lock_guard lock(mtx); value++; }};std::shared_ptr shared_counter = std::make_shared();void worker_thread() { for (int i = 0; i increment(); // 访问被 shared_ptr 管理的对象 }}int main() { std::vector threads; for (int i = 0; i < 10; ++i) { threads.emplace_back(worker_thread); } for (auto& t : threads) { t.join(); } std::cout << "Final counter value: " <value << std::endl; return 0;}
在这个例子中,
shared_ptr
确保了
Counter
对象的生命周期,但
Counter
内部的
value
成员变量的并发访问仍然需要
std::mutex
来保护。如果没有
mtx
,
value
的最终结果将是不确定的。
shared_ptr
本身的并发访问:如果你在多个线程中对同一个
shared_ptr
实例(而不是它所指向的对象)进行读写操作,比如一个线程把
shared_ptr
赋值给另一个
shared_ptr
,另一个线程同时又给这个
shared_ptr
赋了新值,那么
shared_ptr
本身也需要保护。标准库提供了
std::atomic_load
、
std::atomic_store
等函数模板来原子地操作
shared_ptr
,但通常情况下,我们更倾向于通过互斥锁来保护对
shared_ptr
实例的并发修改,以避免复杂性。
总结来说,
shared_ptr
的引用计数是线程安全的,这解决了对象的生命周期管理问题。但当你通过
shared_ptr
访问其内部的对象数据时,如果这些数据可能被多个线程并发修改,你仍然需要传统的同步机制来保证数据的一致性和正确性。将
shared_ptr
视为一个智能的生命周期管理器,而不是一个万能的线程安全工具,这一点非常重要。
以上就是C++shared_ptr共享资源管理方法解析的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1474141.html
微信扫一扫
支付宝扫一扫