C++程序设计中,使用堆内存是很繁琐的操作——堆内存的申请和释放都需要程序员自己去管理。虽然说程序员自己管理内存可以提高程序的效率,但是整体来说程序员手动管理内存是比较麻烦的,而且容易出现内存泄漏,异常安全(如果在malloc和free之间如果存在抛异常,那么还是有内存泄漏)等问题。而在C++11中引入了智能指针的概念来管理堆内存。
智能指针的实现采用了一种RAII(利用对象生命周期来控制程序资源)的技术对普通的指针进行封装,使得智能指针实际是一个对象,其行为表现的却像一个指针。也就是说,在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。那么我们就不需要显式地去释放资源,且对象所需要的资源在生命周期内始终有效。
auto_ptr
简单模拟实现:
template<class T>
class AutoPtr
{
private:
T* _ptr;
public:
AutoPtr(T* ptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& sp)
{
if(this != sp)
{
if(_ptr)
delete _ptr;
//转移资源到当前对象
_ptr = sp._ptr;
sp._ptr = nullptr;
}
}
~AutoPtr()
{
if(_ptr)
delete _ptr;
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
T& Get()
{
return _ptr;
}
};
由以上代码可以看到,auto_ptr的实现是利用了管理权转移的思想——一旦发生拷贝,就将ap/sp中资源转移到当前对象中,然后令ap/sp与其所管理资源断开联系,这样就解决了一块空间被多个对象使用而造成程序奔溃问题。
但是, 通过实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空,通过ap对象访问资源时就会出现问题,于是,引出了一种资源管理权转移的方式
template<class T>
class Autoptr
{
public:
Autoptr(T* ptr=nullptr)
:_ptr(ptr)
,_owner(false)
{
if(_ptr)
_owner = true;
}
Autoptr(const Autoptr<T>& ap)
:_ptr(ap._ptr)
,_owner(ap._owner)
{
ap._owner = false;
}
Autoptr<T>& operator=(const Autoptr<T>& sp)
{
if(this !=&sp)
{
Test();
_ptr = sp._ptr;
_owner = sp._owner;
sp._owner = false;
}
return this;
}
~Autoptr()
{
Test();
}
T& operator*()
{
return *_ptr;
}
T& operator->()
{
return _ptr;
}
T& Get()
{
return _ptr;
}
private:
void Test()
{
if(_ptr && _owner)
{
delete _ptr;
_owner = false;
}
}
private:
T* _ptr;
mutable bool _owner;
};
unique_ptr
因为在拷贝时经常会出现一些问题,所以引出了一种简单粗暴的方式——禁止拷贝,就问你怕不怕!!!
也就是说,将类中的拷贝构造函数和运算符重载函数设为私有的,使得资源独占,只能被一个对象使用
模拟实现:
template<calss T>
class Uniqueptr
{
public:
Uniqueptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~Uniqueptr()
{
if(_ptr)
delete _ptr;
}
T* operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
//C98格式
Uniqueptr(const Uniqueptr<T>& );
Uniqueptr<T> operator=(const Uniqueptr& );
//C11格式
Uniqueptr(const Uniqueptr<T>& ) = delete;
Uniqueptr<T> operator=(const Uniqueptr& ) = delete;
private:
T* _ptr;
};
shared_str:
在C++11中,引入了shared_str这一更靠谱且支持拷贝的智能指针。
shared_str的原理是:
-
通过引用计数的方式来实现多个shared_str对象之间共享资源。也就是说:shared_str在其内部,为每个资源都维护了一份计数,用来记录该资源被几个对象共享。
-
在对象被销毁时,就说明该对象不使用这一资源了,对象的引用计数减一。
-
当引用计数减为0时,说明自己是最后一个使用该资源的对象必须释放该资源。
-
如果不是0,说明该资源还被其他的对象使用着,就不能释放该资源,否则其他对象就会成为野指针
//采用引用计数的方式
//为了线程安全,需要上锁,但是可能会造成死锁(锁内调用的其他代码如果抛异常则会造成死锁),守卫锁,进入时创建对象,函数退出时自动销毁
// 模拟实现一份简答的SharedPtr,了解原理#include <thread> #include <mutex> template <class T> class SharedPtr { public: SharedPtr(T* ptr = nullptr) : _ptr(ptr) , _pRefCount(new int(1)) , _pMutex(new mutex) { // 如果是一个空指针对象,则引用计数给0 if (_ptr == nullptr) *_pRefCount = 0; } ~SharedPtr() { Release(); } SharedPtr(const SharedPtr<T>& sp) : _ptr(sp._ptr) , _pRefCount(sp._pRefCount) , _pMutex(sp._pMutex) { // 如果是一个空指针对象,则不加引用计数,否则才加引用计数 if (_ptr) AddRefCount(); } // sp1 = sp2 SharedPtr<T>& operator=(const SharedPtr<T>& sp) { //if (this != &sp) if (_ptr != sp._ptr) { // 释放管理的旧资源 Release(); // 共享管理新对象的资源,并增加引用计数 _ptr = sp._ptr; _pRefCount = sp._pRefCount; _pMutex = sp._pMutex; if (_ptr) AddRefCount(); } return *this; } T& operator*() { return *_ptr; } T* operator->() { return _ptr; } int UseCount() { return *_pRefCount; } T* Get() { return _ptr; } int AddRefCount() { // 加锁或者使用加1的原子操作 _pMutex->lock(); ++(*_pRefCount); _pMutex->unlock(); return *_pRefCount; } int SubRefCount() { // 加锁或者使用减1的原子操作 _pMutex->lock(); --(*_pRefCount); _pMutex->unlock(); return *_pRefCount; } private: void Release() { // 引用计数减1,如果减到0,则释放资源 if (_ptr && SubRefCount() == 0) { delete _ptr; delete _pRefCount; } } private: int* _pRefCount; // 引用计数 T* _ptr; // 指向管理资源的指针 mutex* _pMutex; // 互斥锁 }; int main() { 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; return 0; }
std::shared_str的线程安全问题:
通过以上的代码细心的同学就会发现了:在对引用计数进行加1,减1操作时我们对其进行了上锁操作。
那么为什么要对其进行上锁操作呢?细想一下其实也很简单:
1.智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或–,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2。这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、–是需要加锁的,也就是说引用计数的操作必须是线程安全的。
2.智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
std::shared_str的循环引用问题:
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
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;
return 0;
}
在上边代码上可以看出一个问题——循环引用,分析如下:
- node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
- node1的next指向node2,node2的prev指向node1,引用计数变成2。
- node1和node2析构,引用计数减到1,但是next还指向下一个节点。但是prev还指向上一个节点。
- 也就是说_next析构了,node2就释放了。
- 也就是说_prev析构了,node1就释放了。
- 但是next属于node的成员,node1释放了,next才会析构,而node1由prev管理,prev属于node2
成员,所以这就叫循环引用,谁也不会释放。
// 解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
// 原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加
node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
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;
return 0;
}