一、智能指针的使用及原理
1、RAII
RAII是一种利用对象生命周期来控制程序资源(内存、文件句柄、网络连接、互斥量等等)的简单技术;
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象,有两大好处:
- 不需要显示的访问;
- 采用这种方式,对象所需要的资源在其生命期内始终有效;
2、std::auto_ptr
C++98版本库中提供的auto_ptr的智能指针;
class Date {
public:
Date() {
cout << "Date()" << endl;
}
~Date() {
cout << "~Date()" << endl;
}
int _year;
int _mouth;
int _day;
};
void Test() {
auto_ptr<Date> ap(new Date);
auto_ptr<Date> copy(ap);
//ap->_year = 2018;
}
auto_ptr的实现原理:管理权限转移,下面简化模拟实现了一份 AutoPtr 来了解它的原理;
template<class T>
class AutoPtr {
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~AutoPtr() {
if (_ptr) {
delete _ptr;
}
}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T> operator=(AutoPtr<T>& ap) {
if (this != &ap) {
delete _ptr;
if (_ptr) {
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
T* _ptr;
};
一旦发生拷贝,就将 ap 中的资源转移到当前对象中,然后另 ap 与其所管理的资源断开联系;
3、std::unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝!!
template<class T>
class UniquePtr {
public:
UniquePtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~UniquePtr() {
if (_ptr) {
delete _ptr;
}
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
private:
//C++98,只(私有)声明不实现
UniquePtr(UniquePtr<T> const&);
UniquePtr& operator=(UniquePtr<T> const&);
//C++11,防拷贝 delete
UniquePtr(UniquePtr<T> const&) = delete;
UniquePtr& operator=(UniquePtr<T> const&) = delete;
private:
T* _ptr;
};
4、std::shared_ptr
C++11推出的更靠谱且支持拷贝的智能指针;
1、shared_ptr的原理:是通过引用计数的方式来实现多个 shared_ptr 对象之间共享资源;
- shared_ptr 在其内部,给每个资源都会维护着一份计数,用来记录该份资源被几个对象共享;
- 在对象被销毁时(调用析构函数的时候),就说明自己不使用该资源了,对象引用计数减一;
- 如果引用计数时0,就是说明自己时最后一个使用该资源的对象,必须释放该资源;
- 如果不是0了,就说明除了自己还有其他的对象在使用该份资源,不能释放资源,否则其他的对象就成了野指针;
template<class T>
class SharedPtr {
public:
SharedPtr(T* ptr = nullptr)
:_ptr(ptr)
,_pRefCount(new int(1))
,_pMutex(new mutex)
{}
~SharedPtr() {
Release();
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
,_pRefCount(sp._pRefCount)
,_pMutex(sp._pMutex)
{
AddRefCount();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp) {
if (_ptr != sp._ptr) {
Release();
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
_pMutex = sp._pMutex;
AddRefCount();
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
int UseCount() {
return *_pRefCount;
}
T* Get() {
return _ptr;
}
void AddRefCount() {
//加锁或者使用加1原子操作
_pMutex->lock();
++(*_pRefCount);
_pMutex->unlock();
}
private:
void Release() {
bool deleteflag = false;
//引用计数-1,如果减到0,则释放资源
_pMutex->lock();
if (--(*_pRefCount) == 0) {
delete _ptr;
delete _pRefCount;
deleteflag = true;
}
_pMutex->unlock();
if (deleteflag == true) {
delete _pMutex;
}
}
private:
T* _ptr;
int* _pRefCount; //引用计数
mutex* _pMutex; //互斥锁
};
引入一段测试函数,试一试:
void Test() {
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp2 = 20;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
SharedPtr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
sp1 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
}
大致的执行过程如下所示:
- 申请了一块资源 A,由 sp1 指向该资源,此时 A 资源的引用计数是1;
- 拷贝构造了sp2,也是指向资源 A,所以此时的引用计数需要加1,此时 A 资源的引用计数是2;
- 申请了另一块资源B,由sp3指向该资源,此时 B 资源的引用计数是1;
- 执行 sp2 = sp3,此时sp2 和 sp3 指针都指向 B资源,所以此时的引用计数是2,而A资源,现在只有sp1,所以计数为1;
- 执行 sp1 = sp3,此时 sp1 、 sp2 、sp3都指向B资源,所以此时的引用计数是3;
2、std::shared_ptr的线程安全问题:
- 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或者--,这个操作不是原子的,引用计数原来是1,++了两次,也可能还是2,这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题,所以只能在指针中引用计数++、--是需要加锁的,也就说引用计数的操作是线程安全的;
- 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题;
3、std::shared_ptr的循环引用:
struct ListNode {
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
void Test() {
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
}
- node1和node2两个智能指针对象指向两个节点,引用计数变为1,不需要手动delete;
- node1的_next 指向 node2,node2 的_prev指向 node1,引用计数变成2;
- node1 和 node2 析构,引用计数减到1,但是_next还指向下一个节点,_prev还指向上一个节点;
- _next 属于node 的成员,node1 释放了,_next才会析构,而node1 由_prev管理,_prev 属于 node2成员,构成循环引用,谁也不会释放;
为了解决循环引用问题,引入了 weak_ptr
struct ListNode {
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode() {
cout << "~ListNode()" << endl;
}
};
void Test() {
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
}
weak_ptr的作用就是:node1->_next = node2;和 node2->_prev = node1 时 weak_ptr 的 next 和prev 不会增加node1 和node2的引用计数;
4、对于不是new 出来的对象(删除器)
template<class T>
struct FreeFunc {
void operator()(T* ptr) {
cout << "free:" << ptr << endl;
free(ptr);
}
};
template<class T>
struct DeleteArrayFunc {
void operator()(T* ptr) {
cout << "delete[]" << ptr << endl;
delete[] ptr;
}
};
void Test() {
FreeFunc<int> freeFunc;
shared_ptr<int> sp1((int*)malloc(4),freeFunc);
DeleteArrayFunc<int> deleteArrayFunc;
shared_ptr<int> sp2((int*)malloc(4), deleteArrayFunc);
}