如果这篇博客帮助到你,可以请我喝一杯咖啡~
CC BY 4.0 (除特别声明或转载文章外)
一 起因:资源管理
对资源(内存,文件描述符,网络套接字,互斥量)进行管理首先要明确的就是 所有权!
对管理资源的类进行浅拷贝要小心谨慎,这可能会造成所有权的混乱!因此一般有几种解决办法:
- 总是深复制。但是随之而来的开销也是巨大的。
- 单一所有权。 禁止复制,禁止编译器自动生成
copy construct/operator=
,如 将上述两种函数声明为=delete
,或生命在private
访问限定符内。unique_ptr
采用这种方式实现。 - 共享所有权。 通过引用计数实现,
shared_ptr
就是采用这种方式实现。 - 不拥有所有权。只读访问。比如
string_view
。weak_ptr
就是采用这种方式实现。
二 手法:RAII
RAII(Resource Acquisition Is Initialization)是一种 利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
避免资源泄露,不需要显式地释放资源。
避免悬空指针,采用这种方式,对象所需的资源在其生命期内始终保持有效。
通过在类内重载 operator*
和 operator->
让智能指针对象可以像指针一样使用。
1.auto_ptr
两个 auto_ptr
不能指向同一个对象。auto_ptr
之间的赋值实际上是 =
右边的 auto_ptr
失去了资源的所有权。
对拷贝的处理方式是:隐式转移所有权
class auto_ptr{
auto_ptr& operator=(auto_ptr tmp) noexcept {
// copy and swap技术,这里不展开了
// 注意当拷贝构造函数构造tmp时,会发生所有权的转移
tmp.swap(*this);
return *this;
}
// 拷贝构造,被复制时释放原来指针的所有权,交给复制方
auto_ptr(auto_ptr &other) noexcept {
ptr_ = other.release();
}
// 原来的指针释放所有权
T *release() noexcept {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
};
缺陷
auto_ptr
这种不能复制的特点不适用于STL
一些算法和容器。不能托管数组。
auto_ptr<int[]>
是不支持的。这个拷贝是非常致命的,也就是发生拷贝时,
old auto_ptr
的所有权转交到new auto_ptr
手上。导致old auto_ptr
悬空了。auto_ptr<int> ptr1{new int}; auto_ptr<int> ptr2{ptr1}; // ptr1变空指针了
2. unique_ptr
原理与实现
对拷贝的处理方式是:不允许隐式转移所有权,必须显式移动语句才能转移所有权。
为实现上述功能有两点需要注意:
- 拷贝构造函数和赋值运算符重载函数必须禁止。
- 需要支持移动构造函数,移动赋值运算符重载函数。
移动拷贝构造和移动赋值运算符重载会抑制编译器生成默认的拷贝构造和赋值运算符重载函数。
class unique_ptr{
public:
unique_ptr(unique_ptr &&other) noexcept { // 所有权转移
ptr_ = other.release();
}
// copy and swap
// 在构造参数tmp时,只能调用移动构造函数
unique_ptr &operator=(unique_ptr tmp) noexcept {
tmp.swap(*this);
return *this;
}
// 原来的指针释放所有权
T *release() noexcept {
T *ptr = ptr_;
ptr_ = nullptr;
return ptr;
}
void swap(unique_ptr &rhs) noexcept {
using std::swap;
swap(ptr_, rhs.ptr_); // 转移指针所有权
}
};
优势
不支持复制语义,
auto_ptr
的替代品可以用于不支持复制语义的
STL
容器与算法std::vector<std::unique_ptr<int>> v; std::unique_ptr<int> pi = std::make_unique<int>(); *pi = 3; // v.push_back(pi); error! v.emplace_back(std::make_unique<int>(3)); v.push_back(std::move(pi));
时间和空间上对比
raw
指针没有太大额外开销我们知道
unique_ptr
可以指定deletor
。deletor
作为一个模板参数传入,类内应该有一个deletor
类型的成员才对。为什么没有额外的开销?实际上我的gcc
上unique_ptr
底层使用tuple
实现的。应该是继承了 包含deletor d
成员的类,由于这个类是不含成员空类,编译器做了EBO
,空基类优化!可以定制
Deletor
。std::unique_ptr<T, Deletor>
对数组类型有一个特化版本
std::unique_ptr<T[]>
3 .shared_ptr
- 引用计数
control block
线程安全,资源不保证安全。- 定制
deletor
使用
void* operator new(size_t sz)
{
cout << "need space size: " << sz << endl;
return malloc(sz);
}
// 两次内存申请
shared_ptr<int> sp(new int(10));
// 一次内存申请
shared_ptr<int> sp2 = make_shared<int>(10);
标准输出:
need space size: 4
need space size: 24
need space size: 24
定制 Deletor
template<typename T>
class Deleter
{
public:
void operator()(T* ptr)
{
++Deleter::count;
delete ptr;
}
void getInfo()
{
std::string typeID{typeid(T).name()};
size_t sz = Deleter::count * sizeof(T);
cout << "Delete " << Deleter::count << " objects of " << typeID << endl;
cout << "Free size in bytes: " << sz << endl;
}
private:
static int count;
};
template<typename T>
int Deleter<T>::count = 0;
typedef Deleter<int> IntDeleter;
int main()
{
std::shared_ptr<int> sp1(new int(1000), IntDeleter());
auto del = std::get_deleter<IntDeleter>(sp1);
del->getInfo();
sp1.reset();
del->getInfo();
}
4. weak_ptr
1) 语义和接口
- 不持有资源
- 从
shared_ptr
获取资源 - 不能直接访问资源
- 可以创建一个持有资源的
shared_ptr
weak_ptr
并不增加 ref count
(会增加 weak count
),可以用来破除 shared_ptr
的循环引用带来的内存泄漏问题。
int main()
{
cout << std::boolalpha;
std::shared_ptr<int> sp1 = std::make_shared<int>(1000);
std::weak_ptr<int> wp1 = sp1;
cout << "sp1 ref count: " << sp1.use_count() << endl;
cout << "wp1 ref count: " << wp1.use_count() << endl;
cout << "wp1 expired? " << wp1.expired() << endl;
if( std::shared_ptr<int> sp2 = wp1.lock() )
{
cout << "*sp2 = " << *sp2 << endl;
cout << "sp2 ref count: " << sp2.use_count() << endl;
}
else
cout << "wp1 expired! Don't use that resourse" << endl;
wp1.reset();
if( std::shared_ptr<int> sp2 = wp1.lock() )
{
cout << "*sp2 = " << *sp2 << endl;
cout << "sp2 ref count: " << sp2.use_count() << endl;
}
else
cout << "wp1 expired! Don't use that resourse" << endl;
}
输出:
[global operator new] size = 24
sp1 ref count: 1
wp1 ref count: 1
wp1 expired? false
*sp2 = 1000
sp2 ref count: 2
wp1 expired! Don't use that resourse
[global operator delete] ptr = 0x197390018e0
2)weak_ptr
破除循环引用!
class Son;
class Daughter;
class Mother
{
public:
~Mother() { cout << "Mother gone" << endl; }
void setSon(std::shared_ptr<Son> s) { son = s; }
void setDaughter(std::shared_ptr<Daughter> d) { daughter = d; }
private:
std::shared_ptr<Son> son;
std::weak_ptr<Daughter> daughter;
};
class Son
{
public:
Son(std::shared_ptr<Mother> m) : mother(m) {}
~Son() { cout << "Son gone" << endl; }
private:
std::shared_ptr<Mother> mother;
};
class Daughter
{
public:
Daughter(std::shared_ptr<Mother> m) : mother(m) {}
~Daughter() { cout << "Daughter gone" << endl; }
private:
std::shared_ptr<Mother> mother;
};
int main()
{
std::shared_ptr<Mother> mother = std::make_shared<Mother>();
std::shared_ptr<Son> son = std::make_shared<Son>(mother);
std::shared_ptr<Daughter> daughter = std::make_shared<Daughter>(mother);
mother->setSon(son);
mother->setDaughter(daughter);
}
输出:
[global operator new] size = 48
[global operator new] size = 32
[global operator new] size = 32
Daughter gone
当我们将 Mother
中堆 Son
的引用变为 weak_ptr
后,Mother
和 Son
之间的循环引用就被打破
class Mother
{
public:
~Mother() { cout << "Mother gone" << endl; }
void setSon(std::shared_ptr<Son> s) { son = s; }
void setDaughter(std::shared_ptr<Daughter> d) { daughter = d; }
private:
std::weak_ptr<Son> son;
std::weak_ptr<Daughter> daughter;
};
输出:
[global operator new] size = 48
[global operator new] size = 32
[global operator new] size = 32
Daughter gone
Son gone
Mother gone
[global operator delete] ptr = 0x1cdcd0f1a90
[global operator delete] ptr = 0x1cdcd0f1a60
[global operator delete] ptr = 0x1cdcd0f18e0
3)使用场景
std::weak_ptr
的正确使用场景是那些资源如果可能就使用,如果不可使用则不用的场景,它不参与资源的生命周期管理。
例如,网络分层结构中,Session 对象(会话对象)利用 Connection 对象(连接对象)提供的服务工作,但是 Session 对象不管理 Connection 对象的生命周期,Session 管理 Connection 的生命周期是不合理的,因为网络底层出错会导致 Connection 对象被销毁,此时 Session 对象如果强行持有 Connection 对象与事实矛盾。
std::weak_ptr
的应用场景,经典的例子是订阅者模式或者观察者模式中。这里以订阅者为例来说明,消息发布器只有在某个订阅者存在的情况下才会向其发布消息,而不能管理订阅者的生命周期。
class Subscriber
{
};
class SubscribeManager
{
public:
void publish()
{
for (const auto& iter : m_subscribers)
{
if (!iter.expired())
{
//TODO:给订阅者发送消息
}
}
}
private:
std::vector<std::weak_ptr<Subscriber>> m_subscribers;
};
4)对象模型
当 refcount = 0
时,weak refcount != 0
,control block
不会被释放,整块内存不会归还给系统,可能会因为存在 weak_ptr
而导致内存泄漏。如果程序中存在大量,weak_ptr
可以考虑自己手写 make_shared
避免该情况。
三 unique_ptr
与 shared_ptr
的空基类优化
unique_ptr
class unique_ptr
{
__uniq_ptr_data<_Tp, _Dp> _M_t;
};
struct __uniq_ptr_data : __uniq_ptr_impl<_Tp, _Dp> {};
class __uniq_ptr_impl { tuple<pointer, _Dp> _M_t; }
shared_ptr
template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>
{
class _Impl : _Sp_ebo_helper<0, _Deleter>, _Sp_ebo_helper<1, _Alloc>
{
typedef _Sp_ebo_helper<0, _Deleter> _Del_base;
typedef _Sp_ebo_helper<1, _Alloc> _Alloc_base;
public:
_Impl(_Ptr __p, _Deleter __d, const _Alloc& __a) noexcept
: _Del_base(std::move(__d)), _Alloc_base(__a), _M_ptr(__p)
{ }
_Deleter& _M_del() noexcept { return _Del_base::_S_get(*this); }
_Alloc& _M_alloc() noexcept { return _Alloc_base::_S_get(*this); }
_Ptr _M_ptr;
};
private:
_Impl _M_impl;
};
_Sp_ebo_helper
// 如果 Tp 不是 final 并且是空,采用 EBO
template<int _Nm, typename _Tp,
bool __use_ebo = !__is_final(_Tp) && __is_empty(_Tp)>
struct _Sp_ebo_helper;
/// Specialization using EBO.
template<int _Nm, typename _Tp>
struct _Sp_ebo_helper<_Nm, _Tp, true> : private _Tp
{
explicit _Sp_ebo_helper(const _Tp& __tp) : _Tp(__tp) { }
explicit _Sp_ebo_helper(_Tp&& __tp) : _Tp(std::move(__tp)) { }
static _Tp&
_S_get(_Sp_ebo_helper& __eboh) { return static_cast<_Tp&>(__eboh); }
};
/// Specialization not using EBO.
template<int _Nm, typename _Tp>
struct _Sp_ebo_helper<_Nm, _Tp, false>
{
explicit _Sp_ebo_helper(const _Tp& __tp) : _M_tp(__tp) { }
explicit _Sp_ebo_helper(_Tp&& __tp) : _M_tp(std::move(__tp)) { }
static _Tp&
_S_get(_Sp_ebo_helper& __eboh)
{ return __eboh._M_tp; }
private:
_Tp _M_tp;
};
四 使用智能指针注意事项
使用智能指针托管的对象,尽量不要在再使用原生指针
对同一个对象会混用智能指针和原生指针,导致程序异常,所以
get()
方法要少用。void incorrect_smart_pointer1() { A *a= new A(); std::unique_ptr<A> unique_ptr_a(a); // 此处将导致对象的二次释放 delete a; }
不要把一个原生指针交给多个智能指针管理
如果将一个原生指针交个多个智能指针,这些智能指针释放对象时会产生对象的多次销毁。智能指针可不会在构造时,判断你有没有交给其他智能指针。
void incorrect_smart_pointer2() { A *a= new A(); std::unique_ptr<A> unique_ptr_a1(a); std::unique_ptr<A> unique_ptr_a2(a);// 此处将导致对象的二次释放 }
通过值传递,返回。
shared_ptr<int> share_func(shared_ptr<int> ptr); // 所有权转移 unique_ptr<int> unique_func(unique_ptr<int> ptr);// 所有权共享
五 shared_ptr
实现原理
aliasing constructor
template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
别名使用构造函数:构造 shared_ptr ,与 r 的初始值共享所有权信息,但保有无关且不管理的指针 ptr 。若此 shared_ptr 是离开作用域的组中的最后者,则它将调用最初 r 所管理对象的析构函数。然而,在此 shared_ptr 上调用 get() 将始终返回 ptr 的副本。程序员负责确保只要此 shared_ptr 存在,此 ptr 就保持合法,例如在典型使用情况中,其中 ptr 是 r 所管理对象的成员,或是 r.get() 的别名(例如向下转型)
通过 get()
结构获取到的是智能指针栈上指向资源块的指针。
使用 set()
修改的也是这个指针。
一开始这个指针指向智能指针托管的对象的地址,但是如果通过 set
修改了该指针,该指针会指向别处。真正管理的资源存储在控制块内。
手写注意事项
use_count
和 weak_count
的增减时机
use_count
何时改变:
shared_ptr
构造时,+1;shared_ptr
拷贝赋值时,+1;weak_ptr.lock()
,发生weak
到shared
的晋级,+1;shared_ptr
析构时,-1;
weak_count
何时改变:
weak_ptr
构造时,+1;weak_ptr
拷贝赋值时,+1;weak_ptr
析构时,-1;- 当
use_count
减为 0 时,weak_count
-1;
我们会发现 weak_count
在初始化时会多加一个1,所以当 weak_count
减为 0 时,weak_count
再 -1。这样就平衡了。之所以加 1 是为了保持接口的一致性。
我们知道一个智能指针的实现最起码包括:指针(指向资源)、强引用、弱引用这三个数据成员。
所以我们需要释放两块内存,分别是数据块和控制块。
- 由于弱引用是不控制数据块资源的,所以是否释放数据块仅需观察强引用
use_count
,当use_count
时,释放数据块; - 至于控制块的释放,自然是要等
use_count == 0
且use_count == 0
时,还记得吗,在分析引用计数何时改变时,介绍到weak_count
初始化会多加 1,当use_count == 0
时,再减去 1。因此当weak_count == 0
时,一定有use_count == 0
,所以只需判断weak_count == 0
即可。
通过上面的分析,得出一个注意事项,那就是 weak_ptr
在使用前很可能 use_count == 0
,导致数据块已经被释放了,因此 weak_ptr
并不控制数据块的生命周期。
六 make_shared
优点:
异常安全:
void F(const std::shared_ptr<Lhs> &lhs, const std::shared_ptr<Rhs> &rhs) { /* ... */ } F(std::shared_ptr<Lhs>(new Lhs("foo")), std::shared_ptr<Rhs>(new Rhs("bar")));
由于 C++ 允许子表达式以任意顺序执行,一个可能的顺序为:
new Lhs("foo"))
new Rhs("bar"))
std::shared_ptr<Lhs>
std::shared_ptr<Rhs>
假设当执行到第二步发生了异常,我们的第一步申请的内存就被泄露了。根本原因在于 原生指针没有及时的被
shared_ptr
托管解决方法1:写在一行,避免乱序执行
auto lhs = std::shared_ptr<Lhs>(new Lhs("foo")); auto rhs = std::shared_ptr<Rhs>(new Rhs("bar")); F(lhs, rhs);
更好的方式是使用
make_shared
F(std::make_shared<Lhs>("foo"), std::make_shared<Rhs>("bar"));
只需要一次 new 提高效率,还能节省内存。
缺点:
由于 make_shared
对于 管理的对象 和 控制块 只有一次堆内存申请,没有办法单独的释放 管理的对象 和 控制块。
![](https://upload-images.jianshu.io/upload_images/14368201-179a5e7efec34eb2.png?imageMogr2/auto-orient/strip | imageView2/2/w/480/format/webp) |
![](https://upload-images.jianshu.io/upload_images/14368201-344a9f77a40e7d44.png?imageMogr2/auto-orient/strip | imageView2/2/w/480/format/webp) |
make_shared
的优化
class A
{
public:
A() { cout << "A()" << endl; }
A(A&& a) { cout << "A(&&)" << endl; }
void* showMeNumber(void*) { cout << rand() << endl; return nullptr; }
int i;
char ch;
double d;
};
void* operator new(size_t sz)
{
cout << "need space size: " << sz << endl;
return malloc(sz);
}
// 两次内存申请
shared_ptr<A> sp(new A());
// 一次内存申请
shared_ptr<A> sp2 = make_shared<A>();
输出:
need space size: 16
A()
need space size: 24
need space size: 32
A()
第一次申请了 16 + 24 = 40 字节
第二次只申请了 32 字节,少了 8 字节?刚好是一个指针的大小。
make_shared
施展了 “我知道你在哪” 的优化:
七 shared_from_this
引入:
#include <iostream>
#include <memory>
class Widget{
public:
Widget(){
std::cout << "Widget constructor run" << std::endl;
}
~Widget(){
std::cout << "Widget destructor run" << std::endl;
}
std::shared_ptr<Widget> GetSharedObject(){
return std::shared_ptr<Widget>(this);
}
};
int main()
{
std::shared_ptr<Widget> p(new Widget());
std::shared_ptr<Widget> q = p->GetSharedObject();
std::cout << p.use_count() << std::endl;
std::cout << q.use_count() << std::endl;
return 0;
}
GetSharedObject
让同一个资源被 所有权不共享 的多个 shared_ptr
托管!
从输出我们可以看到,调用了一次构造函数,却调用了两次析构函数,很明显这是不正确的。而 std::enable_shared_from_this
正是为了解决这个问题而存在。
Widget constructor run
1
1
Widget destructor run
Widget destructor run
22:06:45: 程序异常结束。
通过让类继承 std::enable_shared_from_this
,获取该类中的 shared_from_this
方法,完成 this
的获取。
#include <iostream>
#include <memory>
class Widget : public std::enable_shared_from_this<Widget>{
public:
Widget(){
std::cout << "Widget constructor run" << std::endl;
}
~Widget(){
std::cout << "Widget destructor run" << std::endl;
}
std::shared_ptr<Widget> GetSharedObject(){
return shared_from_this();
}
};
int main()
{
std::shared_ptr<Widget> p(new Widget());
std::shared_ptr<Widget> q = p->GetSharedObject();
std::cout << p.use_count() << std::endl;
std::cout << q.use_count() << std::endl;
return 0;
}
enable_shared_from_this
类内部有一个 weak_ptr
,当需要 this
时,通过 shared_from_this
完成 weak_ptr
到 shared_ptr
的提升。
template<class _Tp>
class enable_shared_from_this
{
mutable weak_ptr<_Tp> __weak_this_;
protected:
enable_shared_from_this() _NOEXCEPT {}
enable_shared_from_this(enable_shared_from_this const&) _NOEXCEPT {}
enable_shared_from_this& operator=(enable_shared_from_this const&) _NOEXCEPT
{return *this;}
~enable_shared_from_this() {}
public:
shared_ptr<_Tp> shared_from_this()
{return shared_ptr<_Tp>(__weak_this_);}
shared_ptr<_Tp const> shared_from_this() const
{return shared_ptr<const _Tp>(__weak_this_);}
weak_ptr<_Tp> weak_from_this() _NOEXCEPT
{ return __weak_this_; }
weak_ptr<const _Tp> weak_from_this() const _NOEXCEPT
{ return __weak_this_; }
template <class _Up> friend class shared_ptr;
};
看完不禁有一个问题,weak_ptr
到底是什么时候被初始化的?
答案存在于 shared_ptr
的构造函数中:
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr
: public __shared_ptr_access<_Tp, _Lp>
{
template<typename _Yp, typename = _SafeConv<_Yp>>
explicit
__shared_ptr(_Yp* __p)
: _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type())
{
_M_enable_shared_from_this_with(__p);
}
template<typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
typename enable_if<__has_esft_base<_Yp2>::value>::type
_M_enable_shared_from_this_with(_Yp* __p) noexcept
{
if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
__base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
}
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
}
__enable_shared_from_this_base
和 _M_weak_assign
具体的实现看不到,我的理解是:
- 我们有继承了
enable_shared_from_this
的类对象的指针__p
。 __p.weak_from_this
获取_weak_this
指针- 通过
__p
和_M_refcount
来构造weak_ptr
的控制块即可。
enable_shared_from_this 中的 _weak_this 如何初始化?
CRTP
奇异递归模板方法
子类继承父类,父类的模板参数是子类对象。
https://zhuanlan.zhihu.com/p/54945314