首先需要了解RAII思想,什么是RAII思想,RAII是一种利用对象生命周期来控制程序资源(内存、文件句柄、网络连接、互斥量)的简单技术。通俗易懂来讲就是在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。就有如下好处:
- 不需要显式的释放资源
- 对象所需的资源在其生命周期内始终保持有效
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
private:
T* _ptr;
};
上面的SmartPtr不能称之为指针,因为其还不具有指针的行为,指针可以解引用,也可以->去访问所指空间中的内容。
template<class T>
class SmartPtr
(
public:
SmartPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~SmartPtr()
{
if(_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
}
所以综上所述智能指针的原理就是RAII的思想加上解引用和访问对象的方法。
AutoPtr
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = nullptr)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T>& ap)//拷贝构造将参数的资源进行转移
:_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;//检测当前对象有没有管理资源,如果管理需要释放
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~AutoPtr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
虽然AutoPtr解决了赋值运算和拷贝构造的问题,但是仍然存在问题,看下面代码:
int main()
{
int* pa1 = new int;
int* pa2 = pa1;
*pa1 = 10;//不会产生任何问题
AutoPtr<int> sp1(new int);
*sp1 = 10;
AutoPtr<int> sp2(sp1);
//sp1 = 10;//这句话会报错,因为拷贝构造之后sp1已经被释放,所以再进行操作就会报错。
}
AutoPtr所存在的问题:
- 对象作为被拷贝构造的对象或者被赋值运算的对象后,资源进行转移,再进行使用就会访问越界。
当然这个问题也可以解决,是通过转移释放资源的权力来解决
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>& ap)
{
if (this != &ap)
{
if (_ptr && _owner)
{
delete _ptr;
}
_ptr = ap._ptr;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
~AutoPtr()
{
if (_ptr && _owner)
{
delete _ptr;
_ptr = nullptr;
_owner = false;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
T* _ptr;
mutable bool _owner;
};
但是上面的代码仍然存在问题,如执行下面代码:
int main()
{
AutoPtr<int> ap1(new int);
AutoPtr<int> ap2(ap1);
AutoPtr<int> ap3(ap1);
if (1)
{
AutoPtr<int> ap4(ap2);
}
//当ap4出了自己的作用域,就会释放资源,但是下面又通过ap1对资源进行修改,这是就会称造成野指针。
*ap1 = 10;
return 0;
}
所以这样看下来,第二种AutoPtr并没有解决第一种所存在的问题。
那么,为了解决上述的问题,干脆这样,直接让该类不能进行赋值运算和拷贝构造,用起来没有任何问题,简单模拟实现如下:
UniquePtr
template<class T>
class UniquePtr
{
public:
UniquePtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~UniquePtr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
private:
UniquePtr(const UniquePtr<T>& up);
UniquePtr<T>& operator=(const UniquePtr<T>&);
private:
T* _ptr;
};
void test2()
{
UniquePtr<int> up1(new int);
//UniquePtr<int> up2(up1);//已经将拷贝构造重载设为私有
UniquePtr<int> up2;
//up2 = up1;//同样的,已经将赋值运算重载设为私有
}
下面是共享类型指针,较上面的SmartPtr多了一个引用计数,实现方式如下:
SharePtr具有如下功能:
- 可以实现不同类型的资源释放
- 可以解决多线程资源访问安全问题(加锁实现)
template<class T>
struct DFDef
{
void operator()(T*& pf)
{
delete pf;
pf = nullptr;
}
};
template<class T>
struct Free
{
void operator()(T*& p)
{
free(p);
p = nullptr;
}
};
struct FClose
{
void operator()(FILE* pf)
{
fclose(pf);
pf = nullptr;
}
};
template<class T,class DF = DFDef<T>>
class SharePtr
{
public:
SharePtr(T* ptr = nullptr)
:_ptr(ptr)
, _pCount(nullptr)
, _pMutex(new mutex)
{
if (_ptr)
{
_pCount = new int(1);
}
}
SharePtr(const SharePtr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMutex(sp._pMutex)
{
if (_pCount)
AddRef();
}
//1.sp2没有管理资源
//2.sp2管理资源,但是没有共享--释放资源以及计数
//3.sp2管理资源,与其他对象共享--计数-1
//1.sp1没有资源
//2.sp2有资源
SharePtr<T>& operator=(const SharePtr<T>& sp)
{
if (this != &sp)
{
//管理资源,没有共享
Release();
//管理资源也共享
_ptr = sp._ptr;
_pCount = sp._pCount;
if (_pCount)
AddRef();
}
return *this;
}
~SharePtr()
{
Release();
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr
}
int GetCount()
{
assert(_pCount);
return *_pCount;
}
private:
void Release()
{
if (_ptr && 0 == SubRef())
{
DF()(_ptr);
delete _pCount;
}
}
void AddRef()
{
_pMutex->lock();
++(*_pCount);
_pMutex->unlock();
}
void SubRef()
{
_pMutex->lock();
--(*_pCount);
_pMutex->unlock();
}
private:
T* _ptr;
int* _pCount;
mutex* _pMutex;
};
但是前引用会导致一个问题,就是内存泄漏,这里需要引入弱引用与强引用:
- 强引用:一个强引用是指当被引用的对象仍然或者的话,这个引用也存在,也可以说是只要至少有一个强引用,则个对象就不能被释放。shared_ptr就是强引用。
- 弱引用:当引用的对象活着的时候不一定存在,仅仅是当它自身存在的一个引用。
我们看一段代码:
struct Node
{
shared_ptr<Node> _pre;
shared_ptr<Node> _next;
~Node()
{
cout << "~Node" << this << endl;
}
int data;
};
void test4()
{
shared_ptr<Node> Node1(new Node);
shared_ptr<Node> Node2(new Node);
Node1->_next = Node2;;
Node2->_pre = Node1;
cout << "Node1.use_count" << Node1.use_count() << endl;
cout << "Node2.use_count" << Node1.use_count() << endl;
}
运行结果是下图(并没有调用析构函数):
我们同样可以在监视窗口里面看到:
内存模型可以大致理解为这样:
将上面代码进行简单修改:
struct Node
{
weak_ptr<Node> _pre;
weak_ptr<Node> _next;
~Node()
{
cout << "~Node" << this << endl;
}
int data;
};
void test4()
{
shared_ptr<Node> Node1(new Node);
shared_ptr<Node> Node2(new Node);
Node1->_next = Node2;;
Node2->_pre = Node1;
cout << "Node1.use_count" << Node1.use_count() << endl;
cout << "Node2.use_count" << Node1.use_count() << endl;
}
运行结果截然不同:
我们看到监视窗口内容也发生变化:
其内存模型也可以理解为如下:
这里我并没有真正理解weak_ptr的工作原理,所以后面还会更新。