C++智能指针
1. 智能指针
所谓智能指针其实是一些模板类,它们负责自动管理一个指针的内存,免去了手动 new/delete 的麻烦,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
智能指针利用了RAII(资源获取即初始化)的技术,对普通指针进行封装,使得其行为像一个指针,但其实是一个对象。把资源放进对象内,用资源来管理对象,便是 C++ 编程中最重要的编程技法之一,即 RAII ,它是 "Resource Acquisition Is Initialization" 的首字母缩写。
智能指针的作用:方便堆内存管理
- 忘记delete 内存:会导致内存泄漏问题,且除非是内存耗尽否则很难检测到这种错误。
- 使用已经释放掉的对象:如果能够记得在释放掉内存后将指针置空并在下次使用前判空,尚可避免这种错误。
- 同一块内存释放两次:如果有两个指针指向相同的动态分配对象,则很容易发生这种错误。
- 发生异常时的内存泄漏:若在new 和 delete 之间发生异常,则会导致内存泄漏。
野指针(wild pointer)就是没有被初始化过的指针
2. 三种智能指针
C++11中所新增的智能指针包括shared_ptr, unique_ptr, weak_ptr,在C++11之前还存在auto_ptr(C++17废弃)。三种类型都定义在头文件memory中。auto_ptr可能导致对同一块堆空间进行多次delete,即当两个智能指针都指向同一个堆空间时,每个智能指针都会delete一下这个堆空间,这会导致未定义行为。
让所有的智能指针都有名字
智能指针为解决资源泄漏、编写异常安全代码提供了一种解决方案,那么他是万能的良药吗?使用智能指针,就不会再有资源泄漏了吗?请看下面的代码:
上面的函数调用,看起来是安全的,但在现实世界中,其实不然:由于C++并未定义一个表达式的求值顺序,因此上述函数调用除了func在最后得到调用之外是可以确定,其他的执行序列则很可能被拆分成如下步骤:
- 1、分配内存给T1
- 2、分配内存给T2
- 3、构造T1对象
- 4、构造T2对象
- 5、构造T1的智能指针对象
- 6、构造T2的智能指针对象
- 7、调用func
此时,如果程序在第3步失败,那T1和T2对象所分配内存必然泄漏。而解决这个问题的方案也很简单,就是不要在函数实参中创建shared_ptr,抛弃临时对象,让所有的智能指针都有名字,就可以避免此类问题的发生。比如以下代码:
优先选用make_shared/make_unique而非直接使用new。简单说来,相比于直接使用new表达式,make系列函数有三个优点:消除了重复代码、改进了异常安全性和生成的目标代码尺寸更小速度更快
shared_ptr
- shared_ptr多个指针指向相同的对象,使用引用计数来完成自动析构的功能。
- shared_ptr的引用计数是线程安全的,但是其对象的写操作在多线程环境下需要加锁实现。
- 不要用同一个指针初始化多个shared_ptr,这样可能会造成二次释放。
std::unique_ptr<int> ptr0 = std::make_unique<int>(); //C++14 以及之后才可以
std::shared_ptr<int> ptr1 = std::make_shared<int>(); //C++11可以
//or
std::unique_ptr<int> ptr0(new int);
std::unique_ptr<int> ptr1(new int);
尽量使用第一种方法,这也是C++官方推荐的。
#include<iostream>
#include<memory>
using namespace std;
int main() {
int a = 10;
shared_ptr<int> ptra = make_shared<int>(a);
shared_ptr<int> ptra2(ptra); //拷贝构造函数
cout << ptra.use_count() << endl; //2
int b = 20;
int *pb = &b;
shared_ptr<int> ptrb = make_shared<int>(b);
ptra2 = ptrb;
pb = ptrb.get(); //获取指针
cout << ptra.use_count() << endl; //1
cout << ptrb.use_count() << endl; //2
return 0;
}
shared_ptr 的问题
- shared_ptr的尺寸是裸指针的两倍:因为内部既包含一个指向该资源的裸指针,也包含一个指向该资源的引用计数的裸指针。
- 引用计数的内存必须动态分配
- 引用计数的递增和递减必须是原子操作:原子操作一般比非原子操作慢。我们的实现版本里为了简单起见没有实现原子操作。
- shared_ptr 的循环引用问题:shared_ptr 意味着你的引用和原对象是一个强联系。你的引用不解开,原对象就不能销毁。滥用强联系,这在一个运行时间长、规模比较大,或者是资源较为紧缺的系统中,极易造成隐性的内存泄漏,这会成为一个灾难性的问题。更糟的是,滥用强联系可能造成循环引用的灾难。即:B持有指向A内成员的一个shared_ptr,A也持有指向B内成员的一个 shared_ptr,此时A和B的生命周期互相由对方决定,事实上都无法从内存中销毁。 更进一步,循环引用不只是两方的情况,只要引用链成环都会出现问题。weak_ptr 的用处 就是用来辅助解决循环引用
weak_ptr
- weak_ptr是为了配合shared_ptr而引入的一种智能指针,它不具备普通指针的行为,没有重载operator*和->。其最大的作用是协助shared_ptr工作,像旁观者那样检测资源的使用情况,解决shared_ptr相互引用时的死锁问题。
- weak_ptr可以从一个shared_ptr或另一个weak_ptr对象构造,获得资源的观测权。
- weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。使用weak_ptr的成员函数
use_count()
可以观测资源的引用计数。 - 另一个成员函数
expired()
等价于判断use_count()==0
。 - weak_ptr和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
#if 1
#include<iostream>
#include<memory>
using namespace std;
class B;
class A
{
public:
shared_ptr<B> pb_;
~A()
{
cout<<"A delete."<<endl;
}
};
class B
{
public:
shared_ptr<A> pa_;
~B()
{
cout<<"B delete."<<endl;
}
};
void fun()
{
shared_ptr<B> pb(new B());
shared_ptr<A> pa(new A());
pb->pa_ = pa;
pa->pb_ = pb;
cout<<pb.use_count()<<endl;
cout<<pa.use_count()<<endl;
}
int main()
{
fun();
return 0;
}
#endif
运行结果
分析
可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),
如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr<B> pb_; 改为weak_ptr<B> pb_;
这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。
shared_ptr的实现
shared_ptr
必须管理一个计数器指针,在初始化和删除时需要修改这个指针的内容。所以它必须有2个成员对象:计数器的指针和数据指针: T*
(that is returned by operator->
and dereferenced in operator*
) and a aux*
where aux
is a inner abstract class that contains:
- a counter (incremented / decremented upon copy-assign / destroy)
- whatever is needed to make increment / decrement atomic (not needed if specific platform atomic INC/DEC is available)
- an abstract
virtual destroy()=0;
- a virtual destructor.
所以通过shared_ptr 初始化一个变量,它所占的内存为机器内存地址的2倍,64位的系统为16bit。
Such aux
class (the actual name depends on the implementation) is derived by a family of templatized classes (parametrized on the type given by the explicit constructor, say U
derived from T
), that add:
- a pointer to the object (same as
T*
, but with the actual type: this is needed to properly manage all the cases ofT
being a base for whateverU
having multipleT
in the derivation hierarchy) - a copy of the
deletor
object given as deletion policy to the explicit constructor (or the defaultdeletor
just doing deletep
, wherep
is theU*
above) - the override of the destroy method, calling the deleter functor.
一个骨架实现:
template<class T>
class shared_ptr
{
struct aux
{
unsigned count;
aux() :count(1) {}
virtual void destroy()=0;
virtual ~aux() {} //must be polymorphic
};
template<class U, class Deleter>
struct auximpl: public aux
{
U* p;
Deleter d;
auximpl(U* pu, Deleter x) :p(pu), d(x) {}
virtual void destroy() { d(p); }
};
template<class U>
struct default_deleter
{
void operator()(U* p) const { delete p; }
};
aux* pa;
T* pt;
void inc() { if(pa) interlocked_inc(pa->count); }
void dec()
{
if(pa && !interlocked_dec(pa->count))
{ pa->destroy(); delete pa; }
}
public:
shared_ptr() :pa(), pt() {}
template<class U, class Deleter>
shared_ptr(U* pu, Deleter d) :pa(new auximpl<U,Deleter>(pu,d)), pt(pu) {}
template<class U>
explicit shared_ptr(U* pu) :pa(new auximpl<U,default_deleter<U> >(pu,default_deleter<U>())), pt(pu) {}
shared_ptr(const shared_ptr& s) :pa(s.pa), pt(s.pt) { inc(); }
template<class U>
shared_ptr(const shared_ptr<U>& s) :pa(s.pa), pt(s.pt) { inc(); }
~shared_ptr() { dec(); }
shared_ptr& operator=(const shared_ptr& s)
{
if(this!=&s)
{
dec();
pa = s.pa; pt=s.pt;
inc();
}
return *this;
}
T* operator->() const { return pt; }
T& operator*() const { return *pt; }
};
Where weak_ptr
interoperability is required a second counter (weak_count
) is required in aux
(will be incremented / decremented by weak_ptr
), and delete pa
must happen only when both the counters reach zero.
unique_ptr
- unique_ptr 唯一拥有所指对象,同一时刻只能有一个unique_ptr指向给定对象,这是通过禁止拷贝语义、只允许移动语义来实现的。
- unique_ptr指针本身的生命周期是从创建开始,直到离开作用域。在智能指针生命周期内,可以改变指针所指向对象,如创建智能指针时使用构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
-
只有一个智能指针能真正指向一个特定的对象,也只有该指针能析构这个对象所占用的空间,直到把这个指针赋给另一个指针,后一个指针才能真正指向这个对象,而前一个指针就不再起作用了,从而避免了两次delete而导致的未定义行为。
#include<iostream>
#include<memory>
using namespace std;
int main() {
{
unique_ptr<int> uptr(new int(10)); //绑定动态对象
unique_ptr<int> uptr2 = uptr; //不能赋值
unique_ptr<int> uptr2(uptr); //不能拷贝
unique_ptr<int> uptr2 = std::move(uptr); //转换所有权
uptr2.release(); //释放所有权
}
//超出uptr作用域,内存释放
}
3. 智能指针的实现
总体来说,实现智能指针的主要任务在于实现引用计数的维护。
首先不应该直接在智能指针类中维护引用计数,这是因为如果有多个智能指针对象指向同一对象,若改变引用计数的值,需要找到这个对象的所有智能指针对象,从而改变所有的引用计数。
所以,有两个思路可以考虑:一个是使用辅助类来维护引用计数,另一个构造句柄类。
其中,辅助类采用继承的方式实现,这种方式的缺点是实现比较复杂,需要所有使用智能指针的对象都先继承自辅助类。
句柄类的方式,是在智能指针类中维护一个指向引用计数的指针,这个引用计数是一个单独的内存空间中。
3.1 使用辅助类维护引用计数
定义一个具体类(U_Ptr)来封装引用计数和指针。在创建智能指针类之前,这个类的所有成员皆为私有类型,因为它不被普通用户所使用。为了只为智能指针使用,还需要把智能指针类声明为该类的友元。类中包含含两个数据成员:计数count与之前HasPtr类中的指针( int *ptr;)。图示:
#ifndef _UPtr_H
#define _UPtr_H
#include <iostream>
using namespace std;
template <typename T>
class HasPtr;
template <typename T>
class U_Ptr {
private:
friend class HasPtr<T>;
U_Ptr(T *p);
~U_Ptr();
T *m_ip;
int m_useCount;
};
template <typename T>
U_Ptr<T>::U_Ptr(T *p)
:m_ip(p)
,m_useCount(1)
{
}
template <typename T>
U_Ptr<T>::~U_Ptr()
{
cout<<"U_Ptr destruct"<<endl;
if (NULL != m_ip) {
delete m_ip;
m_ip = NULL;
}
}
#endif
#ifndef _HasPtr_H
#define _HasPtr_H
#include "uPtr.h"
#include <iostream>
using namespace std;
template <class T>
class HasPtr {
public:
HasPtr(T *p, int i);
HasPtr(const HasPtr &ptr);
HasPtr& operator=(const HasPtr &rhs);
~HasPtr();
T *get_ptr() const;
void set_ptr(T *p);
int get_int() const;
void set_int(int i);
T get_ptr_val() const;
void set_ptr_val(T val);
private:
U_Ptr<T> *m_uptr;
int m_val;
};
template <typename T>
HasPtr<T>::HasPtr(T *p, int i)
:m_uptr(new U_Ptr<T>(p))
,m_val(i)
{
cout<<"HasPtr constructor"<<endl;
}
template <typename T>
HasPtr<T>::HasPtr(const HasPtr &ptr)
:m_uptr(ptr.m_uptr)
,m_val(ptr.m_val)
{
++m_uptr->m_useCount;
cout<<"HasPtr copy constructor m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
}
template <typename T>
HasPtr<T>& HasPtr<T>::operator=(const HasPtr &rhs)
{
bool uptrIsSame = this->m_uptr == rhs.m_uptr;
cout<<"HasPtr assignment rhs.m_uptr->m_useCount = "<<rhs.m_uptr->m_useCount<<endl;
cout<<"HasPtr assignment m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
if (!uptrIsSame) {
++rhs.m_uptr->m_useCount;
if (--m_uptr->m_useCount == 0) {
delete m_uptr;
m_uptr = NULL;
}
m_uptr = rhs.m_uptr;
m_val = rhs.m_val;
}
return *this;
}
template <typename T>
T *HasPtr<T>::get_ptr() const {
return m_uptr->m_ip;
}
template <typename T>
void HasPtr<T>::set_ptr(T *p) {
m_uptr->m_ip = p;
}
template <typename T>
int HasPtr<T>::get_int() const {
return m_val;
}
template <typename T>
void HasPtr<T>::set_int(int i) {
m_val = i;
}
template <typename T>
T HasPtr<T>::get_ptr_val() const {
return *m_uptr->m_ip;
}
template <typename T>
void HasPtr<T>::set_ptr_val(T val) {
*m_uptr->m_ip = val;
}
template <typename T>
HasPtr<T>::~HasPtr() {
cout<<"HasPtr destruct m_uptr->m_useCount = "<<m_uptr->m_useCount<<endl;
if (--m_uptr->m_useCount == 0) {
delete m_uptr;
m_uptr = NULL;
}
}
#endif
int main() {
int *p = new int(12);
HasPtr<int> ptr(p, 20);
HasPtr<int> ptr1(ptr);
HasPtr<int> ptr2(ptr1);
{
HasPtr<int> ptr3 = ptr1;
ptr3 = ptr;
}
return 0;
}
3.2 智能指针类中维护引用计数(句柄类)
#include <iostream>
#include <memory>
template<typename T>
class SmartPointer {
private:
T* _ptr;
size_t* _count;
public:
SmartPointer(T* ptr = nullptr) :
_ptr(ptr) {
if (_ptr) { //需要判读,否则引用一个空指针会有问题
_count = new size_t(1);
} else {
_count = new size_t(0);
}
}
SmartPointer(const SmartPointer& ptr) {
if (this != &ptr) {
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
}
}
SmartPointer& operator=(const SmartPointer& ptr) {
if (this->_ptr == ptr._ptr) {
return *this;
}
if (this->_ptr) {
(*this->_count)--;
if (this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
this->_ptr = ptr._ptr;
this->_count = ptr._count;
(*this->_count)++;
return *this;
}
T& operator*() {
assert(this->_ptr == nullptr);
return *(this->_ptr);
}
T* operator->() {
assert(this->_ptr == nullptr);
return this->_ptr;
}
~SmartPointer() {
(*this->_count)--;
if (*this->_count == 0) {
delete this->_ptr;
delete this->_count;
}
}
size_t use_count(){
return *this->_count;
}
};
int main() {
{
SmartPointer<int> sp(new int(10));
SmartPointer<int> sp2(sp);
SmartPointer<int> sp3(new int(20));
sp2 = sp3;
std::cout << sp.use_count() << std::endl;
std::cout << sp3.use_count() << std::endl;
}
//delete operator
}
4. 实战
- 使用
std::tr1::enable_shared_from_this
作为基类。比如:
class A : public std::tr1::enable_shared_from_this<A>
{
public:
std::tr1::shared_ptr<A> getSharedPtr() {
return shared_from_this();
}
};
当使用了 shared_ptr 的时候,我们可能需要在所有的地方都使用它,否则就不容易达到管理生存期的目的了。但有的时候,我们手头上只有对象的原始指针,比如在对象的函数内部,我们只有 this。这就迫切的需要一个功能:如何从对象的裸指针中,生成我们需要的 shared_ptr。
有人可能会觉得这个简单,shared_ptr a(this); 不就行了么?很遗憾的告诉你,这样不行,会出问题。为什么呢?因为这里的 a,手中对 this 的引用计数只有 1,它无法知道其他地方智能指针对 this 这个指针(就是这个对象)的引用情况,因此当 a 的生命周期结束(比如函数返回)的时候,this 就会被它毫不留情的释放掉,其他地方的相关智能指针,手中拿着的该对象指针已经变成非法。因此,我们需要使用std::tr1::enable_shared_from_this
作为基类将类的this指针的引用导出来.