《Effective Modern C++》学习笔记 - Item 19: 使用 std::shared_ptr 管理共享性资源(附MSVC源码解析)

本文详细介绍了C++11的智能指针std::shared_ptr,讨论了其如何结合垃圾回收的便利性和析构函数的可预期性。std::shared_ptr通过引用计数管理资源,其大小为两倍裸指针,控制块包含引用计数、weakcount和deleter。文章提到了潜在的循环引用问题和解决方案,以及std::shared_ptr与std::unique_ptr的区别。此外,还阐述了std::shared_ptr的构造、赋值、析构以及与this指针的交互,强调了避免从裸指针创建std::shared_ptr的重要性。
  • 很多编程语言中垃圾回收(GC)的确是非常便利的特性,但它们执行的时间往往不可预期;而C++98的全手动资源管理又显得过于“原始”了一点。将能够自动进行操作(GC)以及操作的时机可预期(析构函数)这两个优势结合在一起的,就是C++11的 std::shared_ptr

  • 任一个 std::shared_ptr 都不拥有它管理的对象;但它们共同确保对象不再被需要时会被销毁。其中主要的实现机制是 引用计数(reference count):一个资源被多少个 std::shared_ptr 指向的计数。std::shared_ptr 的大部分构造函数会使该值增加1(移动构造除外,左右抵消不需要增减,因此也比复制更快),析构函数会使该值减小1,复制赋值运算符同时增减(左减右加)。如果 std::shared_ptr 发现经自减后引用计数变为0,则销毁该对象。

  • 性能上来说:

    • std::shared_ptr 的大小是两倍裸指针,一个指针指向资源,一个指向引用计数(严格说是包含引用计数的 control block,见下文)。
    • 引用计数占用的内存必须是动态分配的,因为被指向的对象本身对引用计数毫不知情,因此必须由 std::shared_ptr 对引用计数进行动态分配和管理。(笔者注:C++的对象模型不像Java等有一个公共基类Object,因此如果采取将实现引用计数的责任甩给用户类编写者的设计,则会大大限制 std::shared_ptr 的使用场景,也无法支持 built-in types。)
    • 引用计数的自增和自减必须是原子操作,否则线程不安全,而原子操作相对较慢。
  • 笔者注:到这里你可能会质疑循环引用的问题——的确,当出现循环引用时引用计数永远不会归零,造成内存泄漏。C++的解决办法是引入 std::weak_ptr,见下一节 Item 20 的讲解。

  • std::shared_ptr 默认也使用 delete 作为资源释放方式,支持自定义 deleter。但与 std::unique_ptr 设计不同的是,std::shared_ptr 的 deleter 不属于其类型的一部分,这样的设计使其更加灵活,例如可以将多个持有对象种类相同而 deleter 不同的对象互相赋值,或放在同一个容器中。(注:而 std::unique_ptr 的根本设计理念是 lightweight,zero-overhead,使用默认deleter还多占一个指针的空间是不可接受的。)

std::unique_ptr<Widget, decltype(customDeleter1)> upw(new Widget, customDeleter1);
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);

std::vector<std::shared_ptr<Widget>> vpw{
   
    pw1, pw2 };
pw
`std::vector` 是 C++ 标准库中用于存储动态数组的一种容器,而 `std::shared_ptr` 则是一个智能指针,它允许你管理内存,并自动处理资源的生命周期,避免了内存泄露的问题。 当你将 `std::shared_ptr` 放入 `std::vector` 中时,可能会遇到一些异常情况: ### 1. 内存泄漏风险 如果在向 `std::vector` 添加元素的过程中,由于某种原因导致 `std::shared_ptr` 的构造函数抛出异常,则该元素不会被添加到 `std::vector` 中,但是分配给这个 `std::shared_ptr` 的内存仍然会被保留。这可能导致内存泄漏,因为你实际上无法访问或利用这部分内存。 ```cpp #include <iostream> #include <vector> #include <memory> int main() { try { std::vector<std::shared_ptr<int>> vec; std::shared_ptr<int> ptr(new int(42)); // 这假设有一个函数 create_shared_ptr() 可能会抛出异常 std::shared_ptr<int> ptr_err = create_shared_ptr(); vec.push_back(ptr); vec.push_back(ptr_err); // 抛出异常后,ptr_err 未加入到 vector 中 for (const auto& p : vec) { std::cout << *p << std::endl; } } catch(...) { std::cerr << "Caught an exception!" << std::endl; } return 0; } ``` ### 2. 异常传播 当在循环中或在其他需要遍历 `std::vector` 的操作中发生异常时,程序会立即停止运行,即使有部分数据已经成功插入。这种情况下,未处理的部分 `std::shared_ptr` 也有可能导致后续操作出现问题,尤其是涉及所有权转移的操作(如 `std::make_shared()` 或者通过 `std::weak_ptr` 访问时)。 ### 解决方案 为了避免上述问题,可以采用几种策略: 1. **使用 `std::optional`**:`std::optional` 提供了一种更安全的方式来存放可能存在的值,它可以捕获并忽略掉异常。 ```cpp #include <optional> // 使用 std::optional 替代 std::shared_ptr,在向 std::vector 插入前检查是否有效 ``` 2. **使用 `try-catch` 来捕捉异常**:在向 `std::vector` 添加元素之前尝试捕获可能出现的异常,并适当地处理它们,例如记录错误信息或跳过异常。 ```cpp try { vec.push_back(std::move(ptr)); // 将 ptr 移动至 vector 而不是拷贝 vec.push_back(std::move(ptr_err)); } catch(...) { // 处理异常,比如记录日志、跳过异常等 } ``` 3. **使用 RAII 管理资源**:确保所有资源都通过 RAII(Resource Acquisition Is Initialization)原则进行管理,即资源在离开作用域时自动释放。 ```cpp if(auto ptr = create_shared_ptr()) { vec.push_back(std::move(ptr)); } ``` ### 相关问题: 1. 如何在使用 `std::vector` 和 `std::shared_ptr` 结合时有效地管理异常? 2. 何时应该考虑使用 `std::optional` 而非 `std::shared_ptr` 存储变量? 3. 如何设计程序结构以避免因 `std::shared_ptr` 引发的内存泄漏和异常传播?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值