2.说一下理解的c++中的四种智能指针

要了解智能指针,首先我们需要了解使用普通指针可能存在的问题:
<1> 首先描述使用普通指针管理内存可能会造成内存泄露
在项目中我们经常会使用指针管理内存, 指针操作最重要的莫过于对分配的内存的及时回收. 有时候较难划分指针指向的对象的生命周期, 就会经常出现申请的内存没有及时释放的问题.

** <2> <3> 在面试中可以不予引申 **
<2> 引入使用类内部存储指针的方式管理指针
由于指针本身并没有一个内在机制来自动管理与释放。我就想到使用类内部存储指针,然后在析构函数中销毁该指针的方式管理资源。这就是利用类局部变量超出其作用域,就会自动调用析构函数的机制;
基于这样的想法,我们实现一个简单的智能指针类
示例参考: https://www.yuque.com/ciqujingnian-ttlsa/owutlx/xb1sxf#AS0N0

<3> 类内部管理指针的隐患
当我们使用其中一个智能指针对象作为初始化列表去初始化另一个智能指针对象?或者作为函数的实参传递,因为触发了默认拷贝构造函数,因此会对已经释放的内存重复的析构
示例参考:https://www.yuque.com/ciqujingnian-ttlsa/owutlx/xb1sxf#EPFWD


为了更容易(更安全)地管理通过指针申请的动态内存,可以使用智能指针来管理动态对象;避免我们程序中申请的空间在函数结束时忘记释放, 从而造成了内存泄漏这种情况的发生.

<4>那么智能指针具体是如何管理指针的的呢?
因为智能指针本身就是一个类(c++ primer 400页), 类的特性就是当类对象生命周期结束时, 会自动调用析构函数, 析构函数会自动释放资源.

所以智能指针的原理就是在函数结束时, 类对象生命周期结束, 自动释放内存空间, 不需要手动释放内存空间.

<5>智能指针常用接口

  1. T* get(); // 获取封装在内部的指针, 获取原生指针
  2. T& operator*();
  3. T* operator->();
  4. T& operator=(const T& val);
  5. T* release(); // 将封装在内部的指针置为 nullptr, 但并不会破坏指针所指向的内容, 函数返回的是内部指针置空之前的值
  6. void reset (T* ptr = nullptr); // 直接释放封装的内部指针所指向的内存, 若指定了 ptr, 则将内部指针初始化为该值

<4>然后描述一下四种智能指针及其各自的特点
下面我就分别描述一下有哪四种:

1> auto_ptr (c++ 98的方案, c++11已抛弃)

关键词: 所有权模式

看一个示例:

  1. auto_ptr<std::string> p1(new string("hello"));
  2. auto_ptr<std::string> p2;
  3. p2 = p1; // auto_ptr不会报错
  4. // 此时在内存中 p1 = empty p2 = auto_ptr"auto", 若程序此时访问p1会报错

上述代码运行时不会报错, 但是当执行 p2 = p1时, p2会接管原p1的对string的所有权. 原来的p1会被置为null,此时程序运行时若访问了p1会发生报错.

所以auto_ptr的缺陷是: 存在潜在的内存崩溃问题!

2> unique_ptr

关键词: 独占式拥有

auto_ptr不同, unique_ptr 能够保证同一时间内只有一个智能指针可以指向该对象, 有效避免资源泄漏

  1. unique_ptr<string> p3 (new string(auto));
  2. unique_ptr<string> p4;
  3. p4 = p3; // 编译报错

编译器会认为 p4=p3是未定义的行为,避免了p3不再指向有效数据的问题.
因此unique_ptrauto_ptr更安全.

3> shared_ptr

关键词: 共享式拥有、强引用

  • 共享式拥有: 支持多个智能指针指向相同的对象, 该对象和其相关资源会在”最后一个引用被销毁”的时候释放.
  • 强引用: 使用引用计数机制来标记资源被几个指针共享.

shared_ptr提供成员函数use_count()来查看资源的所有者个数, 当调用 reset() , 当前指针就会释放所有权, 引用计数减一. 当计数等于0, 资源会被释放.

shared_ptr是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的), 实现引用计数的机制, 提供可以共享所有权的智能指针.

4> weak_ptr

关键词: 弱引用

weak_ptr是一种不控制生命周期的智能指针, 它指向一个shared_ptr管理对象.

weak_ptr仅提供对管理对象的一个访问手段. 它的构造和析构不会引起引用计数的增加或减少.

**weak_ptr**的设计解决了什么问题?

weak_ptr 是⽤来解决 shared_ptr相互引用时的死锁问题.

如果说两个 shared_ptr相互引⽤, 那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放

所以当两个智能指针都是 shared_ptr类型的时候, 我们可以将其中一个修改为weak_ptr, 有效避免无法回收资源的问题.


unique_ptr 相关补充

2.1.0 说一下 unique_ptr 中的 release() 和 reset() 的区别?

  • release() 是 unique_ptr 对象释放其对内部指针的控制权,并将该指针作为返回值返回,并将 unique_ptr 自身置空;需要注意的是,release() 后需及时回收释放的原始指针:

    1. std::unique_ptr<ResourceClass> uptr3(new ResourceClass("uptr3", 6, 8));
    2. auto p = uptr3.release();
    3. cout << "*p:" << *p << endl; // 6/8
    4. // 调用 release 之后务必使用 delete 释放指针
    5. delete(p);
    6. p = nullptr;
  • reset() 是 unique_ptr 对象释放其指针所指向的内存;p.reset(q) 将 unique_ptr 对象 p 指向原始指针 q,接管 q 所指向的动态内存:

    1. std::unique_ptr<ResourceClass> p2(new ResourceClass("p2", 9, 11));
    2. std::unique_ptr<ResourceClass> p3(new ResourceClass("p3", 10, 12));
    3. p2.reset(p3.release()); // reset 首先释放 p2 指向的内存, 然后 p2 接管 p3 指向的内存
    4. cout << "*p2:" << *p2 << endl;

    上面的输出打印如下:

    1. construct ResourceClass: p2 <- 构造 p2
    2. construct ResourceClass: p3 <- 构造 p3
    3. destruct ResourceClass: p2 <- 调用 p2.reset() 释放了 p2 指向的对象
    4. *p2:name:p3 10/12
    5. destruct ResourceClass: p3 <- 退出作用域, 释放 p2 指向的内容

2.1.1 追问:说到了 release(), 那么我调用 release() 后不做任何处理会有什么问题吗?

release 是释放当前 unique_ptr 和其管理对象的联系, 调用完毕后 unique_ptr 内部指针丢失,我们需要手动释放 release 出来的指针的内存

  1. auto p = p1.release();
  2. delete p;

否则会造成内存泄漏

2.1.2 追问:对于 unique_ptr ,一般会如何去初始化?为什么应这样初始化呢?

参考: Difference between std::make_unique and std::unique_ptr with new
为什么使用 make_unique 有两个方面的优势:
1>

make_unique is safe for creating temporaries, whereas with explicit use of new you have to remember the rule about not using unnamed temporaries.

make_unique 可以安全地创建临时变量,而显式地使用 new 来创建时,必须记住不应使用未命名临时变量的规则
举例:

  1. foo(make_unique<T>(), make_unique<U>()); // exception safe
  2. foo(unique_ptr<T>(new T()), unique_ptr<U>(new U())); // unsafe*

使用未命名临时变量有什么风险吗?(待补充)

再举一个例子:

  1. f(make_unique<T>(), function_that_can_throw());
  2. f(unique_ptr<T>(new T), function_that_can_throw());

以上面的第二个例子为例,编译器内部的调用顺序是:

  • new T
  • function_that_can_throw()
  • unique_ptr<T>(...)

假如 function_that_can_throw()发生异常,那么new T的内存就会泄露,但是使用 make_unique 的方式,就可以避免这种情况;

参考: https://stackoverflow.com/questions/19472550/exception-safety-and-make-unique/19472607#19472607

总结就是:使用 make_unique 的方式,是可以保证异常安全的

2>

make_unique does not require redundant type usage. unique_ptr(new T()) -> make_unique()

make_unique不需要冗余类型使用

** 简单介绍下 make_unique 的初始化方式 **
make_unique 提供多个重载:参考 make_unique

  1. // make_unique<T>
  2. template <class T, class... Args>
  3. unique_ptr<T> make_unique(Args&&... args);
  4. // make_unique<T[]>
  5. template <class T>
  6. unique_ptr<T> make_unique(size_t size);
  7. // make_unique<T[N]> disallowed
  8. template <class T, class... Args>
  9. /* unspecified */ make_unique(Args&&...) = delete;

第一个重载用于初始化单个对象;第二个重载用于初始化数组;第三个重载用于阻止在 type 中指定为数组;
其实这里就涉及到使用 unique_ptr 初始化数组的问题,举例:

  1. std::unique_ptr<ResourceClass[]> ptr2 = std::make_unique<ResourceClass[]>(2);
  2. ptr2[0] = ResourceClass("new ptr2_0", 1, 3);
  3. cout << "ptr2[0]:" << *(&ptr2[0]) << endl;
  4. // std::unique_ptr<ResourceClass[4]> ptr3 = std::make_unique<ResourceClass[4]>(); // 非法

上面代码输出如下:

  1. construct default ResourceClass: default resource
  2. construct default ResourceClass: default resource <- 默认构造函数初始化数组
  3. construct ResourceClass: new ptr2_0 <- 栈上初始化 ResourceClass 类, 并浅拷贝到 ptr2[0]
  4. destruct ResourceClass: new ptr2_0 <- 表达式结束后立即析构
  5. ptr2[0]:name:new ptr2_0 1/3
  6. destruct ResourceClass: default resource <- unique_ptr 数组析构
  7. destruct ResourceClass: new ptr2_0

c++14提供 make_unique,若编译器不支持,可以使用 make_unique 初始化,其简化版本实现如下:

  1. // 注意:无法处理数组
  2. template<typename T, typename ... Ts>
  3. std::unique_ptr<T> make_unique(Ts ... args)
  4. {
  5. return std::unique_ptr<T> {new T{ std::forward<Ts>(args) ... }};
  6. }

可以看出, make_unique 其实就是将 unique_ptr 作为函数返回值返回

2.1.3 追问:我将 std::unique_ptr 放入 std::vector 中,然后通过 push_back() 的方式导入 std::unique_ptr 对象,这样做会有什么问题吗?

如果直接使用 push_back() 传递左值,会触发 unique_ptr 的拷贝操作,这样会编译报错;
但是可以通过传输右值引用的方式:

  1. std::vector<unique_ptr<ResourceClass>> vec;
  2. unique_ptr<ResourceClass> v_uptr(new ResourceClass("v_uptr", 1, 1));
  3. // vec.push_back(v_uptr); // 触发unique_ptr 的拷贝构造, 非法
  4. vec.push_back(std::move(v_uptr)); // 传递右值引用, 合法
  5. vec.push_back(unique_ptr<ResourceClass>(new ResourceClass("ptr"))); // 合法

要解释上面的现象,观察 unique_ptr内部的实现可以发现,禁用了拷贝构造以及赋值运算符:

  1. unique_ptr(const unique_ptr&) = delete;
  2. unique_ptr& operator=(const unique_ptr&) = delete;

所以传递左值会触发已经被禁用的拷贝构造函数,编译阶段会报错。

2.1.4 追问: 在堆上申请 unique_ptr 对象,这样做会有什么问题吗?

std::unique_ptr 对象可以方便地管理动态内存。前提是该对象是建立在栈上;
在堆上建立,则其行为与普通指针变得一样。

2.1.5 追问:我用同一个资源来初始化多个 std::unique_ptr 对象,这样做会有什么问题吗?

千万不要用同一个资源来初始化多个std::unique_ptr对象,举例:

  1. /* 务必避免用同一个资源来初始化多个std::unique_ptr对象 */
  2. Resource *res = new Resource;
  3. std::unique_ptr<Resource> res1(res);
  4. std::unique_ptr<Resource> res2(res);

这样会导致同一资源被释放多次造成内存错误

2.1.6 追问:我用普通指针作为初始化参数去初始化 std::unique_ptr,这样做会有什么问题吗?

不要混用普通指针与智能指针,举例:

  1. Resource *res = new Resource;
  2. std::unique_ptr<Resource> res1(res);
  3. delete res;

这样会导致 res1 的回收内存过程中对已释放内存的重复析构

2.1.7 追问:std::unique_ptr 可以使用 malloc() 和 free() 管理资源吗?

是可以的,我们为其包装一个删除器

  1. // 大部分时候没有理由这样做
  2. auto deleter = [](int* p) { free(p); };
  3. int* p = (int*)malloc(sizeof(int));
  4. *p = 2;
  5. std::unique_ptr<int, decltype(deleter)> mySmartPtr{ p, deleter };
  6. cout << *mySmartPtr << endl; // output: 2

但是一般情况下没有必要

2.1.8 追问:将 unique_ptr 对象作为函数返回值返回,这样会有什么问题吗?

是没有问题的,通常情况下, unique_ptr 不允许拷贝,但是可以拷贝即将被销毁的 unique_ptr。举例:

  1. std::unique_ptr<ResourceClass> GetResourceClass()
  2. {
  3. return std::unique_ptr<ResourceClass>(new ResourceClass("ret class"));
  4. }

编译器若直到要返回的对象将要被销毁,在该情况下,编译器执行的是一种特殊的拷贝,也就是通过移动构造函数的方式进行”移动”

2.1.8.1 追问:描述一下将 unique_ptr 作为函数返回值的中间的过程?

首先 c++ 中当函数返回一个对象时,原理是会产生一个临时变量,并导致新对象的构造和旧对象的析构; 所以c++ -编译针对该情况进行优化, 返回值优化(RVO), 返回有名字的对象叫做具名返回值优化(NRVO)。

  • 假如编译器开启了 RVO 优化, 构造返回对象时候会直接在接收返回对象的空间中构造。
  • 假如编译器未开启返回值优化, 将 unique_ptr 作为返回值, 也是可行的,因为标准运行编译器这么处理:
    • 1.如果支持 move 构造, 调用 move 构造
    • 2.如果不支持 move, 那么调用 copy 构造
    • 3.如果不支持 copy 报错

总结:编译器开启优化,将 unique_ptr 作为函数返回值返回,会直接在接收该对象的控件中构造;
编译器未开启优化,将 unique_ptr 作为函数返回值返回,会使用移动构造函数;
其实 unique_ptr 支持 move 构造, 所以 unique_ptr 对象可以以该方式被函数返回.

参考: 为什么函数可以返回unique_ptr

2.1.8.2 将 unique_ptr 对象作为函数返回值返回,触发的是拷贝构造函数吗?

unique_ptr 内部禁用了通过常量左值引用的方式的拷贝以及赋值运算符,但是支持传递右值引用的方式进行移动;
举例:

  1. unique_ptr(const unique_ptr&) = delete;
  2. unique_ptr& operator=(const unique_ptr&) = delete;
  3. unique_ptr& operator=(unique_ptr&& _Right) noexcept
  4. { ... }

所以该情况是通过移动构造函数的方式进行的传递

2.1.9 追问: 将 unique_ptr 作为函数参数类型声明, 这样有什么问题吗?

可以将 unique_ptr 作为对象传递给函数,但需要注意一点,无法通过值传递的方式,因为触发了禁用的拷贝构造函数;
举例:

  1. void uniqueptrFunc(std::unique_ptr<ResourceClass> ptr) {}
  2. int main()
  3. {
  4. std::unique_ptr<ResourceClass> uptr = std::unique_ptr<ResourceClass>(new ResourceClass("uptr", 1, 1));
  5. // uniqueptrFunc(uptr); // 触发拷贝构造,非法
  6. uniqueptrFunc(std::unique_ptr<ResourceClass>(new ResourceClass("xxx"))); // 传递临时对象
  7. uniqueptrFunc(std::move(uptr)); // 传递右值引用, 合法
  8. }

只要不会触发拷贝,就是合法操作。

以上均有参考:https://zhuanlan.zhihu.com/p/54078587

2.1.10 追问:一般在什么场景下会使用 unique_ptr ?

1> unique_ptr 非常适合作为工厂函数的返回值类型.
常用在有对象继承层级中的工厂函数,工厂函数通常是在堆上分配一个对象, 并返回一个指向该对象的指针,当不再需要该对象时,由调用者负责删除对象。若工厂函数返回 std::unique_ptr ,调用者无需关心删除对象的问题。举例如下:

  1. class Investment { .... }
  2. class Stock : public Investment { .... }
  3. std::unique_ptr<Investment> makeInvestment();

调用上述工厂函数 makeInvestment 会返回一个 Investment 类型对象, 我们不用去考虑该对象在什么时候删除。

2> unique_ptr 也适用于实现 Pimpl 机制.
Pimpl即 Private Implementation 。 其主要作用是解开类的使用解开和实现的耦合。

实现上 Pimpl 是将私有数据和函数放入一个单独的类中, 并保存在一个实现文件中,然后在头文件中对这个类进行前向声明并保存一个指向该实现类的指针。

我们将实现类的成员变量置为 unique_ptr 类型, 能够方便管理实现类的生命周期。

参考: (Effective Modern C++:04智能指针) (unique_ptr实现Impl模式时遇到的问题分析)

C++程序设计机制—-Pimpl机制


shared_ptr 相关补充

2.2.0 追问:shared_ptr 是怎么初始化的?

可以使用默认构造的方式初始化;也可以使用 make_shared 进行初始化。

1> 引入使用默认构造的方式初始化 shared_ptr 的隐患
先来举个例子,通过构造的方式初始化 shared_ptr:

  1. SharedResourceClass* res = new SharedResourceClass();
  2. std::shared_ptr<SharedResourceClass> sptr(res);
  3. cout << "sptr use count: " << sptr.use_count() << endl; // sptr use count: 1
  4. {
  5. std::shared_ptr<SharedResourceClass> sptr2(res);
  6. cout << "sptr2 use count: " << sptr2.use_count() << endl; // sptr2 use count: 1
  7. }
  8. // sptr2 析构
  9. cout << "sptr use count: " << sptr.use_count() << endl; // sptr use count: 1
  10. // 会导致程序崩溃

虽然 sptr 与 sptr2 是由同一块内存初始化的,但是这个共享却并不被两个指针所知道。这是由于两个对象是独立初始化的,它们互相之间没有通信。
深入研究 shared_ptr 的构成,发现 std::shared_ptrstd::unique_ptr内部实现机理有区别,前者内部使用两个指针,一个指针用于管理实际的指针,另外一个指针指向一个“控制块”,其中记录了哪些对象共同管理同一个指针。
所以尽管是同一块内存初始化两个 std::shared_ptr对象,但是其各自的“控制块”并没有互相记录。
就会出现上面的问题,发生内存的重复释放;

现在使用 make_shared 的方式初始化,举例:

  1. auto sptr = std::make_shared<SharedResourceClass>("make_shared_obj", 1, 1);
  2. cout << "sptr use count: " << sptr.use_count() << endl; // sptr use count: 1
  3. {
  4. auto sptr2 = sptr; // 通过拷贝构造函数使两个对象管理同一块内存
  5. std::shared_ptr<SharedResourceClass> sptr3(sptr2);
  6. cout << "sptr use count: " << sptr.use_count() << endl; // sptr use count: 3
  7. cout << "sptr2 use count: " << sptr2.use_count() << endl; // sptr2 use count: 3
  8. cout << "sptr3 use count: " << sptr3.use_count() << endl; // sptr3 use count: 3
  9. }
  10. // sptr2 与 sptr3 对象析构
  11. cout << "sptr use count: " << sptr.use_count() << endl; // sptr use count: 1

通过复制构造函数或者赋值可实现共享内存。

总结:使用 make_shared 初始化 std::shared_ptr 可以有效避免“控制块”未同步导致内存重复释放的问题,真正地使用引用计数的方式管理指针。

2.2.0.1 使用 make_shared 初始化 shared_ptr 的优势?

除了上述可以避免使用默认构造 shared_ptr 的方式引用同一块内存可能会导致的内存错误,还有以下几点:
1> 使用 make_shared 可以带来性能上的提升

  1. std::shared_ptr<Widget> spw(new Widget);

这条语句会引发两次内存分配,一次是为Widget分配内存,一次是为控制块分配内存。

如果使用make_shared,则只有一次内存分配操作,因为std::make_shared会分配单块内存既保存Widget对象,又保存与其关联的控制块。

2> 使用 make_shared 可以保证异常安全性(待补充)
例如如下代码:

  1. void processWidget(std::shared_ptr<Widget> spw, int priority);
  2. processWidget(std::shared_ptr<Widget>(new Widget), computePriority());

这段代码可能会造成内存泄漏。

原因就在于编译器生成的计算顺序可能是:new Widget,执行 computePriority,运行 std::shared_ptr 构造函数。当 computePriority发生异常时,就会发生内存泄漏。

参考: make_shared理解 effective c++

3> 使用 make_shared 能够避免某些情况下非法地访问悬空指针
举例:

  1. void process(shared_ptr<int> ptr)
  2. {
  3. }
  4. int main()
  5. {
  6. int *x = new int(1024);
  7. process(shared_ptr<int>(x)); // 该行结束会释放x指向的内存
  8. int j = *x; // x 是悬空指针, 发生未定义的行为
  9. }

通过值传递的方式将 shared_ptr(x) 临时对象传递给 process,当该表达式结束时,该临时对象即被销毁,并递减引用计数,此时引用计数值就变为0。导致 x 变为悬空指针。
我们使用 make_shared 的方式构造就可以避免这个问题:

  1. process(std::make_shared<int>(1));

表达式结束后销毁 make_shared 构造的对象,不会存在能够继续访问悬空指针的问题。

2.2.1 追问:我使用一个 shared_ptr 赋值给另一个 shared_ptr,其中会发生什么变化?

举例:

  1. auto sptr1 = std::make_shared<SharedResourceClass>("sptr1");
  2. auto copySptr1 = sptr1;
  3. auto sptr2 = std::make_shared<SharedResourceClass>("sptr2");
  4. auto copySptr2 = sptr2;
  5. cout << "pre sptr1:" << sptr1.use_count() << copySptr1.use_count() << endl; // 2 2
  6. cout << "pre sptr2:" << sptr2.use_count() << copySptr2.use_count() << endl; // 2 2
  7. sptr1 = sptr2; // 这样会增加 sptr2 的引用计数, 并减少 strp1 的引用计数
  8. cout << "after sptr1:" << sptr1.use_count() << copySptr1.use_count() << endl; // 3 1
  9. cout << "after sptr2:" << sptr2.use_count() << copySptr2.use_count() << endl; // 3 3

即赋值操作,会增加被取值的 shared_ptr 的引用计数,以及降低被赋值的 shared_ptr 的引用计数;
上述示例中若 sptr1 绑定对象的引用计数归0,会释放其管理的内存;

2.2.2 追问: shared_ptr 的 reset() 有了解吗?

reset() 有三种调用方式,分别对应不同的功能:

  • p.reset():释放 shared_ptr 的 p 对其内部指针的控制权,引用计数减一,若引用计数归零,会回收指针所指向对象;举例: ```cpp auto sptr1 = std::make_shared(“sptr1”); auto sptr2 = sptr1;

cout << “sptr1:” << sptr1.use_count() << “sptr2:” << sptr2.use_count() << endl; // 2 2 // 调用reset(): sptr1 引用计数减一, 但是sptr1 不是唯一指向其对象的 shared_ptr, // 此时 reset 不会释放此对象 sptr1.reset();
cout << “sptr1:” << sptr1.use_count() << “sptr2:” << sptr2.use_count() << endl; // 0 1

  1. - **p.reset(q):**传递可选参数 原始指针q,会令 p 指向 q
  2. ```cpp
  3. auto sptr1 = std::make_shared<SharedResourceClass>("sptr1");
  4. cout << "ex sptr1:" << sptr1.use_count() << endl; // 1
  5. SharedResourceClass* sptrx = nullptr;
  6. sptr1.reset(sptrx); // reset 释放 sptr1 对原指针的控制权, 并将 sptr1 指向 sptrx
  7. cout << "ex sptr1:" << sptr1.use_count() << endl; // 1

上述代码打印日志如下:

  1. construct ResourceClass: sptr1
  2. ex sptr1:1 <- 构造 sptr1 智能指针,仅有该对象指向一块内存
  3. destruct ResourceClass: sptr1 <- sptr1 指向的原指针的内存引用计数归零, 会释放该内存
  4. ex sptr1:1 <- 此时 sptr1 指向 sptrx,引用计数为1
  • p.reset(q, d):支持传递智能指针删除器,将会调用 d 的方式而不是 delete 来释放 q

2.2.3 追问: shared_ptr 会有线程安全的问题吗?

shared_ptr 为了支持多线程,本身规定引用计数的操作是原子操作。
什么是原子操作?
* 原子操作概念补充 *
原子操作指的是在多线程程序中”最小且不可并行化”的操作,即多个线程访问同一资源时,有且仅有一个线程能对资源进行操作。
通常情况下原子操作通过互斥的访问方式来保证。例如互斥锁(mutex)
参考:c++11原子操作
* * * * * *

所以当有多个线程维护的 shared_ptr 对同一个对象发生析构,也只会出现执行顺序的交错,不会有内存泄漏。
shared_ptr 的构造、赋值和析构是线程安全的。

但是某些情况下 shared_ptr 不是线程安全的,例如调用 get() 获取原始指针,那么其后都是对原始指针的操作,需要控制线程安全(例如 shared_ptr::reset() 和 shared_ptr::swap())

当同时读写一个对象,无法确保编译器是先操作引用计数还是先操作指针,这种情况下需要加锁,避免出现悬空指针。

shared_ptr 是线程不安全的
(待补充)

参考:https://www.jianshu.com/p/c75a0d33655c

2.2.3.1 追问:为什么多线程读写 shared_ptr 要加锁?

shared_ptr 的引用计数本身是安全且无锁的, 但是对象的读写则不是。

因为 shared_ptr 有两个数据成员(指向被管理对象的指针,和指向控制块的指针),读写操作不能原子化。

参考:shared_ptr的线程安全性

<扩展: 循环引用的示例>

先看如下示例:

  1. class A {
  2. public:
  3. shared_ptr<B>_pb;
  4. };
  5. class B {
  6. public:
  7. shared_ptr<A>_pa;
  8. };
  9. int main()
  10. {
  11. shared_ptr<A>pa = make_shared<A>();
  12. shared_ptr<B>pb = make_shared<B>();
  13. cout<<"pa count:"<<pa.use_count()<<endl; // 1
  14. cout<<"pb count:"<<pb.use_count()<<endl; // 1
  15. pa->_pb = pb;
  16. pb->_pa = pa;
  17. cout<<"pa count:"<<pa.use_count()<<endl; // 2
  18. cout<<"pb count:"<<pb.use_count()<<endl; // 2
  19. return 0;
  20. }

比如说A、B两个类,两个类里面分别定义了一个对方类的智能指针,然后在主函数里面首先定义两个类的智能指针,然后分别把两个指针分别赋予对方的成员指针里,这样就形成了循环引用。

循环引用的问题是:一旦b出作用域,引用计数减一,导致b里面的a永远不会减一,导致a智能指针空间永远释放不掉,然后a出作用域时,a引用计数减一,a最终没释放,它里面的b也就不可能释放掉,最后a b都是1无法释放。

如何用weak_ptr来解决这个问题?

我们只需要将类成员智能指针类型修改为 weak_ptr 就可以解决问题

  1. class A{
  2. public:
  3. weak_ptr<B>_pb;
  4. };
  5. class B{
  6. public:
  7. weak_ptr<A>_pa;
  8. };

2.2.4 追问:什么场景下会使用 shared_ptr ?

shared_ptr智能指针的应用场景

1> 要把指针存入标准库容器中

使用智能指针的优越性主要体现在容器使用完毕后清空容器的操作上。

如果容器中保存的是普通指针,当我们在清空某个容器时,先要释放容器中指针所指向的资源,然后才能清空这些指针本身。

如果容器中保存智能指针, 清空容器时仅需要 使用 clear() 函数清空容器中保存的 shared_ptr

2> 某一个对象的复制操作很费时

如果一个对象的复制操作很费时,同时我们又需要在函数间传递这个对象,我们往往会选择传递指向这个对象的指针来代替传递对象本身,以此来避免对象的复制操作。既然选择使用指针,那么使用shared_ptr是一个更好的选择,即起到了向函数传递对象的作用,又不用为释放对象操心.


unique_ptr 与 shared_ptr 对比

2.3.0 追问:share_ptr 和 unique_ptr 哪一个开销更大?