我们知道,在C++中没有像Java那样的自动回收垃圾机制,,系统只会清理栈上由系统管理的资源,在类中若有对堆资源的申请,不进行手动释放资源就会导致内存泄漏问题,在学习了类和类模板之后,我们发现在类中有析构函数来进行资源释放功能,并且析构函数还支持加入程序员自身的操作,那么将指针托管给对象来实现可以做到程序员手动申请资源,让系统自动回收资源的效果,Boost库就提供了这样的智能指针来实现C++的自动回收机制,引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露,二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。
auto_ptr在C++11已被摒弃,但在实际项目中仍可使用,若想实现auto_ptr的操作,最好还是使用unique_ptr,因为unique_ptr比auto_ptr更加安全,在介绍unique_ptr时会详细介绍。
伪代码如下:
template
class SmartPtr
{
public:
SmartPtr(T* ptr) :mptr(ptr){}
~SmartPtr()
{
delete mptr;
mptr = NULL;
}
SmartPtr(const SmartPtr& rhs)
{
mptr = rhs.mptr;
rhs.Release();
}
SmartPtr& operator=(const SmartPtr& rhs)
{
if (this != &rhs)
{
delete mptr;
mptr = rhs.mptr;
rhs.Release();
}
return *this;
}
T& operator*()
{
return *mptr;
}
T* operator->()
{
return mptr;
}
private:
void Release()const // const SmartPtr* const this
{
(T*)mptr = NULL;
}
T* mptr;
};
int main()
{
SmartPtr sp1 = new int;
SmartPtr sp2 = sp1;//sp1.mptr = NULL;
*sp1 = 10;//error,因为此时sp1.mptr = NULL;,无法再被赋值
return 0;
}
反映到图中如下:
当申请一个auto指针后,auto会将堆资源托管到auto对象中,auto不会共享堆资源,只要进行拷贝构造或者是赋值操作时就会对当前auto对象进行Release()操作,将原本的使用权和销毁权剥夺,所有的权限由新auto对象获得,无法再访问原auto对象,也就是会提前释放,这在使用中是很危险的,运行时稍不注意就会崩溃。
auto_ptr总结:
缺点:智能指针会被提前释放
特点:资源拥有权唯一,释放权唯一
当发生拷贝构造,赋值运算重载时,拥有权和释放权会改变
auto_ptr特殊的拷贝行为使得使用它来远距离传递动态对象变成了一件十分危险的行为,在传递的过程中,一不小心就会留下一些实际为空但程序本身却缺少这样认知的auto_ptr对象。
unique_ptr于auto_ptr相同,但是unique更加安全,所以在使用的时候尽量使用unique指针,虽然auto还可以用,unique有移动语义,如果访问已经失效的指针时,编译阶段就会报错。
1.对于unique_ptr来说,他可以放在容器中,但是auto_ptr则不能,可以说是auto的加强版,容器相关下次会谈到,这里仅作了解
vector>ST;
unique_ptr sp1(new string("abc"));
这样是可行的。
2.同时unique_ptr可以管理动态数组,在unique_ptr内部实现中重载了unique_ptr
3.unique_ptr默认的资源删除操作是delete/delete[],若需要,可以进行自定义Deleter。
综上所述,由于unique_ptr的安全性和扩充的功能,auto_ptr完败。
在C++11中引入了shared_ptr,shared_ptr是一种强类型指针,实现了堆资源的共享问题,引入了引用计数器方法,即:拥有权共享,释放权是在判断没有指针指向该资源时,由最后放弃该资源的指针进行释放,解决了auto和unique的局限性,需要额外的开销(指维护引用计数表需要的资源)
上一个实战例子
#include
//使用迭代器遍历操作,比起数组更加普适化
//使用单例模式将引用计数器表合并
class Ref_Management
{
public:
static Ref_Management* getInstance()
{
return &rm;
}
void addref(void* mptr)
{
if (mptr != NULL)
{
std::vector::iterator fit = find(mptr);
if (fit == vec.end())
{
Node node(mptr, 1);
vec.push_back(node);
}
else
{
fit->ref++;
}
}
}
void delref(void* mptr)
{
std::vector::iterator fit = find(mptr);
if(fit == vec.end())
{
throw std::exception("adder can't find! ");
}
else
{
if(fit->ref != 0)
fit->ref--;
}
}
int getref(void* mptr)
{
if(mptr != NULL)
{
std::vector::iterator fit = find(mptr);
if(fit != vec.end())
{
return fit->ref;
}
else
{
throw std::exception("adder can't find! ");
}
}
return -1;
}
private:
Ref_Management(){}
static Ref_Management rm;
Ref_Management(const Ref_Management& rhs);
class Node
{
public:
Node(void* padd = NULL, int rf = 0) :addr(padd), ref(rf){}
public:
void* addr;
int ref;
};
std::vector::iterator find(void* mptr)
{
std::vector::iterator it = vec.begin();
for (it; it != vec.end(); it++)
{
if (it->addr == mptr)
break;
}
return it;
}
std::vector vec;
};
Ref_Management Ref_Management::rm;//单例模式的Rm
template
class Shared_Ptr
{
public:
Shared_Ptr(T* ptr = NULL) :mptr(ptr)
{
AddRef();
}
Shared_Ptr(const Shared_Ptr& rhs) :mptr(rhs.mptr)
{
AddRef();
}
~Shared_Ptr()
{
DelRef();
if (GetRef() == 0)
{
delete mptr;
}
mptr = NULL;
}
Shared_Ptr& operator=(const Shared_Ptr& rhs)
{
if(this != &rhs)
{
this->~Shared_Ptr();
this->mptr = rhs.mptr;
AddRef();
}
return *this;
}
T* operator->()
{
return this->mptr;
}
T& operator*()
{
return this->mptr;
}
private:
void AddRef()
{
prm->addref(mptr);
}
void DelRef()
{
prm->delref(mptr);
}
int GetRef()
{
return prm->getref(mptr);
}
T* mptr;
static Ref_Management *prm;
};
template
Ref_Management* Shared_Ptr::prm = Ref_Management::getInstance();//智能指针的维护表
这里使用了单例模式合并了不同Shared指针的引用计数表管理,设计模式之后也会提到,单例模式只生成一个对象,拷贝构造和赋值运算符重载放到private中,要让其他类型的对象都可以访问到这个对象并使用,必然要用static方法实现单例模式,留有一个接口对单例模式的对象进行调用。
回归正题,shared指针实现了每有一个新的指针指向shared管理的资源时,引用计数+1,每当一个指针放弃资源时引用计数-1,如果此指针是最后一个放弃资源的指针,则会delete本资源。
如何实现高效的引用计数表是shared指针的关键所在。
总结:
shared缺点:强类型指针,在遇到交叉引用时会完全失效,在weak指针会有提到
特点:实现了对资源的拥有权分享和释放权的唯一且正确释放的能力。
先来看看一个情景
class B;
class A
{
public:
Shared_Ptr spa;
};
class B
{
public:
Shared_Ptr spb;
};
int main()
{
Shared_Ptr pa(new A());//生成新对象的时候给A的引用计数+1
Shared_Ptr pb(new B());//生成新对象的时候给B的引用计数+1
pa->spa = pb;//保存一个指针的时候B引用计数又+1
pb->spb = pa;//保存一个指针的时候A引用计数又+1
return 0;
//调用析构的时候,B先出作用域,B的引用计数-1,不为0,无法释放内存
//A后出,A的引用计数-1,不为0,无法释放内存
}
此时,内存无法被正确释放,引用计数不为0,在图中表示为:
对于A,B类型的交叉指向,导致无法正确将引用计数减为0,若将A,B类中的强指针换成weak弱指针,此问题就可以被解决,虽然有其他的实现方式,但都需要程序员手动管理且极易出错,所以推荐使用智能指针的方式来处理交叉引用的问题
如图所示
template
class Weak_ptr
{
public:
Weak_ptr(T* ptr = NULL) :mptr(ptr){}
Weak_ptr(const Weak_ptr& rhs)
{
mptr = rhs.mptr;
}
Weak_ptr& operator=(const Weak_ptr& rhs)
{
if (this != &rhs)
{
mptr = rhs.mptr;
}
return *this;
}
Weak_ptr& operator=(const Shared_Ptr& rhs)
{
mptr = rhs.getPtr();
return *this;
}
~Weak_ptr()
{
}
T* operator->()
{
return mptr;
}
T& operator*()
{
return *mptr;
}
private:
T* mptr;
};
weak指针被用于和shared指针共同使用,在weak指针内部没有真正的addRef和delRef操作,他只会观测shared指针内部有多少个引用,自身不会进行ref的修改,如上面的代码所示就是一个自订的weak指针的实现,无论是拷贝构造还是赋值运算符重重载。
//weak_ptr弱智能指针,虽然有引用计数,但实际上它并不增加计数,
//而是只观察对象的引用计数,weak_ptr的引用计数指的是有多少个weak_ptr在观察同一个shared_ptr。
//而shared_ptr强智能指针的引用计数是对资源的引用计数所以此时对象A的引用计数只为1,
//对象B的引用计数也只为1。
总结:
(1)创建对象的时候用shared_ptr强智能指针,
别的地方一律持有weak_ptr弱智能指针,否则析构顺序有可能出现错误。
(2)当通过弱智能指针访问对象时,需要先进行lock提升操作,
提升成功,证明对象还存在,再通过强智能指针访问对象。
1.很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定,程序运行时会异常崩溃)。
2.如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。
3.如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr也是可以的。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete,只要不调用将一个unique_ptr复制或赋值给另一个unique_ptr的算法即可(因为方法很可能会重复调用,unique很明显不支持这样的操作)