C++ 智能指针 -- shared_ptr
潘忠显 / 2020-06-24
今天在看好客的代码,发现有一部分代码在实现智能指针和引用计数相关的代码。
C++11开始已经有 shared_ptr
这样的智能指针了,所以以后这部分代码大部分可以被替换掉。
在 shared_ptr指引 里边有以下几点需要注意:
-
shared_ptr
是个类模板 -
shared_ptr<T> make_shared( Args&&... args );
是会实际new对象的 -
get()
会返回指针 -
指向控制块的指针(控制块用于管理对象的指针信息)
-
make_shared会产生控制块信息
-
static_cast也可能会产生控制块?
-
use_count
和get
是是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
中的p
、lp
以及main中p
三个引用。
不可能不构造完t2
、t3
而在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 加载)