c++中的智能指针主要是基于RAII思想的。
不懂RAII思想的同学可以看看这篇博文-->RAII思想---利用对象生命周期来控制程序资源
- RAII思想:把资源交给这个对象管理
- 像指针一样的行为(重载operator
*
和operator->
)
template
class SmartPtr
{
public://交给对象去管理
SmartPtr(T* ptr=nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete[] _ptr;
}
//像指针一样的行为
T& operator*()//对象出了作用域还在,所以返回引用
{
return *_ptr;
}
T* operator->()
{
return _ptr;//返回原生指针
}
private:
T* _ptr;
};
C++98版本的库中提供了auto_ptr的智能指针。
想要实现一个智能指针就要实现这几个功能:RAII思想,像指针一样的行为。但是对象会有拷贝构造和赋值,auto_ptr的原理是进行了管理权的转移(管理权的转移是当把a的值给b之后,就把a置成空),这是一种带有缺陷的智能指针,会导致对象悬空。
实现的原理如下:
template
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr != nullptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
//拷贝构造ap1(ap2)---ap2拷贝构造ap1,此时把ap2置空,管理权交给ap1
//this指针是ap1,ap是ap2
auto_ptr(auto_ptr& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;//不执行此句会崩溃,同一资源释放了两次
}
//赋值 ap1=ap2
auto_ptr& operator=(auto_ptr& ap)
{
if (this != &ap)//检测是否给自己赋值
{
//释放当前对象ap的资源
if (_ptr)
delete _ptr;
//转移ap中的资源到当前对象去
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;//支持连续赋值
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
auto_ptr是一种管理权转移的思想,由于转移之后原来的对象会被置空,会导致对象的悬空,如下:
void test_auto_ptr()
{
cpp::auto_ptr ap1(new int);
cpp::auto_ptr ap2(ap1);
// *ap1 = 10;//拷贝构造后管理权转移,ap1悬空
*ap2 = 20;
cpp::auto_ptr ap3(new int);
//ap3 = ap2;//赋值把p2置空了
*ap3 = 40;
}
这里导致原来的对象悬空了,auto_ptr是带有缺陷的早期设计,因此很多公司都会禁用这个auto_ptr。
C++11中开始提供更靠谱的unique_ptr。
unique_ptr是简单粗暴的防止拷贝,这种比较简单,效率高,但是功能不全面,不支持拷贝和赋值操作。具体实现思想如下:
template
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
delete _ptr;
}
//拷贝
unique_ptr(unique_ptr& ap) = delete;
//赋值
unique_ptr& operator=(unique_ptr& ap) = delete;
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
private:
T* _ptr;
};
C++11中开始提供更靠谱的并且支持拷贝的shared_ptr。
shared_ptr它的原理是引用计数,它的功能更加全面,支持拷贝构造。但是设计复杂,会有循环引用的问题。
这里提到一个概念,引用计数,引用计数表示这块空间被几个对象引用,有一个对象析构了,引用计数就会减一,但是并不是每次析构都会释放空间,而是最后一个引用它的对象才去释放它。
我们这里的引用计数只需要一个就行了,所以很容易想到把它定义成静态的,但是其实这也不可以,因为一旦一个类中的对象管理不同的空间,就如下图,Sp1和Sp2管理一块空间,Sp3和Sp4和Sp5管理另一块空间,用同一个引用计数的话,我们期望3,4,5析构后,这块空间也被释放掉,但是由于引用计数只减为2,,没有变为1,他认为你不是最后一个引用它的,所以不会释放掉空间,就会有内存泄漏问题。
所以这里的引用计数就不能跟着对象走,而是应该跟着空间走的,有一个空间就动态开辟一个引用计数跟着它,每个智能指针都能拿到该引用计数如下图:
template
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))//初始化,动态开辟
{}
~shared_ptr()
{
if (--(*_pcount) == 0)//引用计数为0时,最后一个指向它的释放它
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pcount;
}
}
//拷贝
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
++(*_pcount);//同一个引用计数
}
//赋值
//sp1 = sp3;
shared_ptr& operator=(shared_ptr& sp)
{
//if (this != &sp)//防止自己给自己赋值
//两个对象的ptr如果相同就不进入,否则--再++是无用功
//要是相同的对象,ptr相同就不作操作
//要是不同的对象,但是指向同一块空间,ptr相同也不作操作
if (_ptr != sp._ptr)
{
//判断要赋值对象的引用计数是不是1
//如果是1,释放指针和引用计数再指向新的空间进行赋值
if (--(*_pcount) == 0)
{
delete _ptr;
delete _pcount;
}
//两个指针指向同一块空间,再++引用计数
_ptr = sp._ptr;
_pcount = sp._pcount;
++(*_pcount);
}
return *this;
}
T& operator*(){return *_ptr;}
T* operator->(){return _ptr;}
int use_count()
{
return *_pcount;
}//查看引用计数是多少
private:
T* _ptr;
int* _pcount;//引用计数
};
看到这里,很多童鞋肯定像我一样以为shared_ptr就到此结束了,其实并没有,它在用起来没有大问题,但是存在着一个问题,就是多线程的情况下的线程安全问题,来看下面这段代码:
void test_multi_thread_copy(cpp::shared_ptr& sp,size_t n)
{
for (size_t i = 0; i < n; ++i)//拷贝n次ptr
{
cpp::shared_ptr copy(sp);
}
}
void test_shared_ptr_safe()
{
cpp::shared_ptr sp(new int);
std::thread t1(test_multi_thread_copy, sp, 1000);
std::thread t2(test_multi_thread_copy, sp, 1000);
cout << sp.use_count() << endl;
t1.join();
t2.join();
}
t1和t2是我们创建的两个线程,这两个线程都去拷贝ptr1000次,此时多个线程同时对该空间进行++,--操作,那么导致结果错误,并且打印出来的引用计数也是不确定的,说明此时的引用计数已经出现了问题,导致内存没有释放,会出现内存泄漏的问题。因为这里的 ++操作和 - - 操作不是原子的操作 ,存在线程安全问题。因此多线程编程时,要给这里的引用计数进行加锁操作,基于以上我们进一步完善shared_ptr。
template
class shared_ptr
{
public:
shared_ptr(T* ptr)
:_ptr(ptr)
,_pcount(new int(1))//初始化,动态开辟
, _pmtx(new std::mutex)
{}
~shared_ptr()
{
Release();
//if (--(*_pcount) == 0)//引用计数为0时,最后一个指向它的释放它
//{
// cout << "delete:" << _ptr << endl;
// delete _ptr;
// delete _pcount;
//}
}
void AddRef()//增加引用计数
{
_pmtx->lock();
++(*_pcount);//同一个引用计数
_pmtx->unlock();
}
void Release()//释放引用计数
{
_pmtx->lock();
int flag = 0;
if (--(*_pcount) == 0)
{
cout << "delete: " << _ptr << endl;
delete _ptr;
delete _pcount;
flag = 1;
}
_pmtx->unlock();
if (flag == 1){delete _pmtx;}
}
//拷贝
shared_ptr(const shared_ptr& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
, _pmtx(sp._pmtx)
{
//++(*_pcount);//同一个引用计数
AddRef();//加锁,封装成函数
}
//赋值
//sp1 = sp3;
shared_ptr& operator=(shared_ptr& sp)
{
//if (this != &sp)//防止自己给自己赋值
if (_ptr != sp._ptr)
{
//if (--(*_pcount) == 0)
//{
// delete _ptr;
// delete _pcount;
//}
Release();
//两个指针指向同一块空间,再++引用计数
_ptr = sp._ptr;
_pcount = sp._pcount;
_pmtx = sp._pmtx;
AddRef();
/*++(*_pcount);*/
}
return *this;
}
T& operator*() {return *_ptr;}
T* operator->() {return _ptr;}
int use_count() { return *_pcount;}//查看引用计数是多少
private:
T* _ptr;
int* _pcount;//引用计数
std::mutex* _pmtx;//定义一个锁,大家都锁在该锁上,所以定义为指针
};
这样加锁之后,智能指针的引用计数的++和- - 是安全的,也支持拷贝构造,所以shared_ptr本身是安全的,智能指针的线程安全体现在引用计数的++和- -是线程安全的,但是它指向的对象不一定是线程安全的,因为智能指针指向的对象本身并不受它的管控。
struct ListNode
{
std::shared_ptr _prev;
std::shared_ptr _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
void test_shared_ptr_cycle_ref()
{
std::shared_ptr cur(new ListNode);
std::shared_ptr next(new ListNode);
cur->_next = next;
next->_prev = cur;
}
这一段代码中用shared_ptr管理了cur和next,同时也管理了cur和next里面的_prev和_next,让智能指针share_ptr指向cur和next时,两个智能指针的引用计数都是1,再让cur的_next指向了next,让next的_prev指向了cur,此时两个智能指针的引用计数都变成了2,出了test_shared_ptr_cycle_ref函数之后,cur和next应该去调析构函数,但是此时智能指针的引用计数不为1,所以两个智能指针的引用计数- -,那么此时就没有完成节点的释放,因为发生了循环引用。可以理解为相互牵制,谁也无法进行释放,画个图理解一下:
那么这种问题要怎么解决呢?解决方案就是使用weak_ptr:
struct ListNode
{
std::weak_ptr _prev;
std::weak_ptr _next;
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
因为在执行cur->_next = next;
和next->_prev = cur;
时,weak_ptr的_next和_prev不会增加cur和next的引用计数,这样就保证了正常的释放。
但是weak_ptr只能用于循环引用场景下,其他时候不能用。
在上面写的代码中,都是一次只申请了一块空间,然后让智能指针管理,智能指针释放的时候直接调用delete即可,但是不排除有这么一种情况,那就是如果一次申请多块空间呢?
struct A
{
~A()
{
cout << "~A()" << endl;
}
};
//定制删除器
void test_shared_ptr_deletor()
{
std::shared_ptr sp(new A[10]);
}
int main()
{
test_shared_ptr_deletor();
system("pause");
return 0;
}
那此时就需要用delete[]来进行释放,因此可以利用仿函数来定制删除器。
struct A
{
~A()
{
cout << "~A()" << endl;
}
};
template
struct DeleteArray
{
void operator()(T* ptr)
{
delete[] ptr;
}
};
//定制删除器
void test_shared_ptr_deletor()
{
DeleteArray del;
std::shared_ptr sp(new A[10],del);
}
int main()
{
test_shared_ptr_deletor();
system("pause");
return 0;
}