Jason Pan

C++ 智能指针 -- shared_ptr

潘忠显 / 2020-06-24


今天在看好客的代码,发现有一部分代码在实现智能指针和引用计数相关的代码。

C++11开始已经有 shared_ptr 这样的智能指针了,所以以后这部分代码大部分可以被替换掉。

shared_ptr指引 里边有以下几点需要注意:

如何将普通的指针转成shared_ptr

void* a = static_cast<void*>(new int(2));
std::shared_ptr<int> b(static_cast<int*>(a));

如果将普通指针转成shared_ptr指针之后,再去删除之前的指针,是不对的。

因为除了显式的delete之外,还有shared_ptr引用计数为0时,也会delete,导致两次释放。

示例代码的一些解释

#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
 
struct Base
{
    Base() { std::cout << "  Base::Base()\n"; }
    // 注意:此处非虚析构函数 OK
    ~Base() { std::cout << "  Base::~Base()\n"; }
};
 
struct Derived: public Base
{
    Derived() { std::cout << "  Derived::Derived()\n"; }
    ~Derived() { std::cout << "  Derived::~Derived()\n"; }
};
 
void thr(std::shared_ptr<Base> p)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::shared_ptr<Base> lp = p; // 线程安全,虽然自增共享的 use_count
    {
        static std::mutex io_mutex;
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << "local pointer in a thread:\n"
                  << "  lp.get() = " << lp.get()
                  << ", lp.use_count() = " << lp.use_count() << '\n';
    }
}
 
int main()
{
    std::shared_ptr<Base> p = std::make_shared<Derived>();
 
    std::cout << "Created a shared Derived (as a pointer to Base)\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    std::thread t1(thr, p), t2(thr, p), t3(thr, p);
    p.reset(); // 从 main 释放所有权
    std::cout << "Shared ownership between 3 threads and released\n"
              << "ownership from main:\n"
              << "  p.get() = " << p.get()
              << ", p.use_count() = " << p.use_count() << '\n';
    t1.join(); t2.join(); t3.join();
    std::cout << "All threads completed, the last one deleted Derived\n";
}

这里还给出了可能的输出

Base::Base()
  Derived::Derived()
Created a shared Derived (as a pointer to Base)
  p.get() = 0xc99028, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
  p.get() = (nil), p.use_count() = 0
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 3
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 4
local pointer in a thread:
  lp.get() = 0xc99028, lp.use_count() = 2
  Derived::~Derived()
  Base::~Base()
All threads completed, the last one deleted Derived

示例引申出的面试题

Q: 打印的第一个lp.use_count()的范围是多少到多少?

A: 范围是 {4,5,6}

Q: 为什么不能是3?

A: 打印至少要进入到一个线程中,假设另外两对象还没有开始构造,则此时会有t1中的plp以及main中p三个引用。

不可能不构造完t2t3而在main中释放所有权;

有一种情况能让打印的lp.use_count() = 2就是当[t1,t2,t3]中的两个都已经完成,main函数中的p.reset()也已经执行。因为函数中io_mutex的存在,能够保证打印顺序,所以2一定是在最后,而不可能出现在第一行。

Q: 为什么不会是7?

A: 因为子线程中有sleep,在一秒时间内,一定会调度到主线程,也就是会执行到p.reset()

Q: 为什么1秒内一定会调度到主线程?

Q: 线程对象从什么时间点开始执行?

根据构造函数描述,以下方式的构造函数,会在关联一个线程并开始执行函数内容:

template< class Function, class... Args >
explicit thread( Function&& f, Args&&... args );

本来,我以为我以为的就是我以为的,直到读到了这段:

In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load) 多线程环境下, use_count 返回的值是近似的(典型实现使用 memory_order_relaxed 加载)