一、为什么要引入智能指针?
我们知道,C++中的动态内存需要用户自己来维护,动态开辟的空间,在出函数作用域或者程序正常退出前必须释放掉,否则会造成内存泄露,虽然有时我们已经非常谨慎了,然而当代吗比较长或比较复杂时,我们仍然可能忘记释放,正所谓防不胜防,一起看一段代码:
void FunTest1()
{
int *p = new int[10];
FILE* pFile = fopen( "1.txt", "r" );//已读的方式打开"1.txt"文件
if (pFile == NULL)//如果打开失败,则返回
{
return;//error:如果打开失败,则造成内存泄漏
}
// DoSomethint();
if (p != NULL)
{
delete[] p;
p = NULL;
}
}
void FunTest2()
{
int *p = new int[10];
try
{
//DoSomething();
}
catch (...)
{
return;//返回之前忘记释放内存,导致内存泄漏
}
delete[] p;
}
从上述两个例子中可以看出,在动态申请空间中,每一次返回之前都必须对动态内存进行释放,否则就内存泄漏,这样做不仅麻烦,还容易忘记,那么,怎样解决上述问题呢?就不得不引入我们的新概念---->智能指针(auto_ptr) 。
二、智能指针的基本情况
1、资源分配即初始化RAII(Resource Acquisition Is Initialization):定义一个类来封装资源的分配和释放,在构造函数完成资源的分配和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
2、Boost库的智能指针
(ps:新的C++11标准中已经引入了unique_ptr/shared_ptr/weak_ptr)
三、模拟实现库中的某些智能指针
1、新库中的智能指针---->auto_ptr
(1)单纯管理空间
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap)
:_p(ap)
{
ap = NULL;//让ap与动态申请的空间脱离关系
cout<<"AutoPtr()"<<endl;
}
~AutoPtr()
{
cout<<"~AutoPtr()"<<endl;
if(NULL != _p)
{
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
(2)拷贝对象
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap)
:_p(ap)
{
ap = NULL;//让ap与动态申请的空间脱离关系
cout<<"AutoPtr()"<<endl;
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
}
int main()
{
Funtest();
system("pause");
return 0;
}
ERROR:同一块被释放了两次,导致程序崩溃。
改进:添加拷贝构造函数
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap)
:_p(ap)
{
ap = NULL;//让ap与动态申请的空间脱离关系
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
{
ap = NULL;//让ap与动态申请的空间脱离关系
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
(3)构造函数的参数要给缺省值
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap)
:_p(ap)
{
ap = NULL;//让ap与动态申请的空间脱离关系
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
{
ap = NULL;//让ap与动态申请的空间脱离关系
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
AutoPtr<int> p2;
}
int main()
{
Funtest();
system("pause");
return 0;
}
error:
改进:
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
{
ap = NULL;//让ap与动态申请的空间脱离关系
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
{
ap = NULL;//让ap与动态申请的空间脱离关系
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
AutoPtr<int> p2;
}
int main()
{
Funtest();
system("pause");
return 0;
}
(4)赋值运算符重载
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
{
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
{
ap._p = NULL;//让ap与动态申请的空间脱离关系
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if(this != &ap)
{
if(NULL != _p)//防止对空对象进行引用导致程序崩溃
{
delete _p;
_p = ap._p;//如果_p为空,则崩溃
ap._p = NULL;//脱离关系
}
}
return *this;
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
AutoPtr<int> p2;
p2 = p1;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行过程截图:
运行结果:
(5)获取原生指针,重载*和->
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
{
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
{
ap._p = NULL;//让ap与动态申请的空间脱离关系
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if(this != &ap)
{
if(NULL != _p)//防止对空对象进行引用导致程序崩溃
{
delete _p;
_p = ap._p;//如果_p为空,则崩溃
ap._p = NULL;//脱离关系
}
}
return *this;
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
T* Get()
{
return _p;
}
T* operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
AutoPtr<int> p2;
p2 = p1;
AutoPtr<int>* p3 = &p1;
cout<<*(p3->Get)()<<endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行过程截图:
运行结果:
auto_ptr致命缺陷:只能由一个对象管理所开辟的空间
2、老库中维护一个布尔变量owner实现---->owner_ptr
实现原理:构造对象时,将该对象的_owner赋值为TRUE,析构对象时,只有当该对象的_owner值为TRUE才释放,否则不释放。
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
,_owner(true)//说明这块空间已有对象占用
{
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
,_owner(true)
{
ap._owner = false;//让ap与动态申请的空间脱离关系
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if(this != &ap)
{
if(NULL != _p)//防止对空对象进行引用导致程序崩溃
{
delete _p;
_p = ap._p;//如果_p为空,则崩溃
_owner = true;
ap._owner = false;//资源转移
}
}
return *this;
}
~AutoPtr()
{
if(_owner &&NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
_owner = false;//释放完后将_owner赋值为false
}
}
T* Get()
{
return _p;
}
T* operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
private:
T* _p;
bool _owner;
};
void Funtest()
{
AutoPtr<int> p = new int;
AutoPtr<int> p1(p);
AutoPtr<int> p2;
p2 = p1;
AutoPtr<int>* p3 = &p1;
cout<<*(p3->Get)()<<endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
不足之处:
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
,_owner(true)//说明这块空间已有对象占用
{
cout<<"AutoPtr()"<<endl;
}
AutoPtr(AutoPtr<T>& ap)//由于内部要改变ap的值,所以去掉const修饰
:_p(ap._p)
,_owner(true)
{
ap._owner = false;//让ap与动态申请的空间脱离关系
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if(this != &ap)
{
if(NULL != _p)//防止对空对象进行引用导致程序崩溃
{
delete _p;
_p = ap._p;//如果_p为空,则崩溃
_owner = true;
ap._owner = false;//资源转移
}
}
return *this;
}
~AutoPtr()
{
if(_owner &&NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
_owner = false;//释放完后将_owner赋值为false
}
}
T* Get()
{
return _p;
}
T* operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
private:
T* _p;
bool _owner;
};
void Funtest()
{
AutoPtr<int> sp1;
if(true)
{
AutoPtr<int> sp2(sp1);
}//出了作用域sp1就成为野指针
AutoPtr<int> sp3;
sp3 = sp1;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
总结:以上两种方式我们都是通过资源转移来防止一块空间被释放多次,auto_ptr的缺点是申请的空间最终只能由一个对象来管理,而owner_ptr虽然可通过多个对象管理空间,但却可能导致野指针等问题,这并不是我们的初衷,那么有什么更好的实现方法呢?
3、单个对象独占空间---->scoped_ptr
(1)首先思考一个问题,一个类如何防止被拷贝
class B
{
public:
B()
{
cout<<"B()"<<endl;
}
private:
B(const B& b);
B& operator=(const B& b);
private:
int _b;
};
void Funtest()
{
B b1;
B b2(b1);
b2 = b1;
}
编译结果:
所以说,一个类要防止被拷贝,要做两件工作:
1>将拷贝构造函数和赋值运算符重载的访问权限设成私有的。
2>将拷贝构造函数和赋值运算符重载只给出声明而不对其进行定义。
(2)仿照上述思想,scopted_ptr的实现思想就是让单个对象独占空间,从而避免对象被多次释放的问题。
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
{
cout<<"AutoPtr()"<<endl;
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
T* Get()
{
return _p;
}
T* operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
private:
AutoPtr(const AutoPtr<T>& ap);
AutoPtr<T>& operator=(const AutoPtr<T>& ap);
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p1 = new int;
AutoPtr<int>* p2 = &p1;
cout<<*(p2->Get)()<<endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
注意:应避免使用出错,不要进行对象拷贝和赋值。
4、unique_ptr(即scopted_array)
template <typename T>
class AutoPtr
{
public:
AutoPtr(T* ap = NULL)
:_p(ap)
{
cout<<"AutoPtr()"<<endl;
}
~AutoPtr()
{
if(NULL != _p)
{
cout<<"~AutoPtr()"<<endl;
delete _p;
_p = NULL;
}
}
T& operator[](size_t index)
{
return _p[index];
}
const T& operator[](size_t index)const
{
return _p[index];
}
T* Get()
{
return _p;
}
private:
AutoPtr(const AutoPtr<T>& ap);
AutoPtr<T>& operator=(const AutoPtr<T>& ap);
private:
T* _p;
};
void Funtest()
{
AutoPtr<int> p(new int[5]);
p[0] = 1;
p[1] = 2;
p[2] = 3;
p[3] = 4;
p[4] = 5;
for(size_t i=0; i<5; i++)
{
cout<<p[i]<<" ";
}
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
注意:unique_ptr在boost库中是没有的,因为它就好比一个动态开辟的数组,可以用vector来实现,接口多,用着还方便,故有点多余。
5、引用计数版本的智能指针---->shared_ptr
前面在string类中我们介绍过引用计数版本的各种情况分析,在这里我们就不啰嗦了,直接给出指针版本的。
有兴趣了解的请看:string类详解:
template <typename T>//引用计数版本
class SharedPtr
{
public:
SharedPtr(T* sp = NULL)
:_p(sp)
,_pCount(NULL)
{
cout<<"SharedPtr()"<<endl;
if(NULL != _p)
{
_pCount = new int(1);//仅当对象不为空时,才为其引用计数赋值
}
}
SharedPtr(SharedPtr<T>& sp)//由于内部要改变ap的值,所以去掉const修饰
:_p(sp._p)
,_pCount(sp._pCount)
{
if(NULL != _p)//防止拷贝的对象本就是空
++(*_pCount);
sp._p = NULL;//让ap与动态申请的空间脱离关系
}
SharedPtr<T>& operator=(SharedPtr<T>& sp)
{
if(this != &sp)//
{
if(NULL == _pCount)//当前对象没有管理空间
{
_p = sp._p;
_pCount = sp._pCount;
if(sp._pCount)//原有空间管理了一段空间
++(*_pCount);
}
else if(_pCount && 1==(*_pCount))//当前对象独占一块空间
{
delete _p;//释放当前对象
delete _pCount;//释放当前对象
_p = sp._p;
_pCount = sp._pCount;
if(sp._pCount)//原有空间管理了一段空间
++(*_pCount);
}
else//当前对象和别人共享一段空间
{
--(*_pCount);
_p = sp._p;
_pCount = sp._pCount;
if(sp._pCount)//原有空间管理了一段空间
++(*_pCount);
}
}
return *this;
}
~SharedPtr()
{
if(_pCount && 0==(--(*_pCount)))//注意释放条件
{
cout<<"~SharedPtr()"<<endl;
delete _p;
delete _pCount;//一定不要忘了释放
_p = NULL;
_pCount = NULL;
}
}
T* operator*()
{
return *_p;
}
T* operator->()
{
return _p;
}
private:
T* _p;
int* _pCount;
};
void FunTest()
{
SharedPtr<int> sp1(NULL);
SharedPtr<int> sp2(new int);
SharedPtr<int> sp3(sp2);//sp2、sp3共享空间
SharedPtr<int> sp4 = new int;//sp4独占空间
SharedPtr<int> sp5(new int);
sp4 = sp2;//当前对象独占空间
sp1 = sp4;//当前对象sp1没有管理空间
sp2 = sp5;
}
int main()
{
FunTest();
system("pause");
return 0;
}
运行过程:
#include<iostream>
#include<memory>
using namespace std;
void Fclose(FILE* pf)
{
if(pf)
fclose(pf);
}
int main()
{
FILE* pf = fopen("1.txt","r");
shared_ptr<FILE> sp(pf,Fclose);
system("pause");
return 0;
}
#include<iostream>
#include<memory>
using namespace std;
class Fclose// 定置的删除器仿函数
{
public:
void operator()(FILE* pf)
{
if(NULL != pf)
fclose(pf);
}
};
int main()
{
FILE* pf = fopen("1.txt","r");
shared_ptr<FILE> sp(pf,Fclose());
system("pause");
return 0;
}
7>智能指针应用举例:实现多重冒泡排序
template <typename T> class Great//按照升序冒泡 { public: bool operator()(T& left,T& right) { return left>right; } }; template <typename T> class Less//按照降序冒泡 { public: bool operator()(T& left,T& right) { return left<right; } }; template <typename T,typename Compare> void BubbleSort(T arr[],size_t size) { size_t i,j; int flag = 0; for(i = 0; i<size-1; i++) { flag = 0; for(j = 0; j<size-1-i; j++) { if(Compare()(arr[j],arr[j+1]))//注意传参 { flag = 1; swap(arr[j],arr[j+1]); } } if(flag == 0) break; } } int main() { int arr[10] = {9,5,3,4,7,2,1,8,6,0}; size_t size = sizeof(arr)/sizeof(arr[0]); BubbleSort<int,Less<int>>(arr,size); size_t idx = 0; cout<<"降序"<<endl; for(idx=0; idx<size; idx++) { cout<<arr[idx]<<" "; } cout<<endl<<endl; BubbleSort<int,Great<int>>(arr,size); cout<<"升序"<<endl; for(idx=0; idx<size; idx++) { cout<<arr[idx]<<" "; } system("pause"); return 0; }
运行结果:
#include <iostream>
#include <memory>
using namespace std;
template <typename T>
class Node
{
public:
Node(const T& value)
:_pPre(NULL)
,_pNext(NULL)
,_value(value)
{
cout<<"Node()"<<endl;
}
~Node()
{
cout<<"~Node()"<<endl;
cout<<"this:"<<this<<endl;
}
shared_ptr<Node<T>> _pPre;
shared_ptr<Node<T>> _pNext;
T _value;
};
void Funtest()
{
shared_ptr<Node<int>> sp1(new Node<int>(1));
shared_ptr<Node<int>> sp2(new Node<int>(2));
cout<<"sp1.use_count:"<<sp1.use_count()<<endl;
cout<<"sp2.use_count:"<<sp2.use_count()<<endl;
sp1->_pNext = sp2;
sp2->_pPre = sp1;
cout<<"sp1.use_count:"<<sp1.use_count()<<endl;
cout<<"sp2.use_count:"<<sp2.use_count()<<endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
从运行结果中我们可以清楚地看到程序运行完成之后并没有释放我们的对象sp1和sp2,那么这是什么原因造成的呢?
从上面shared_ptr的实现中我们知道了只有当引用计数减减之后等于0,析构时才会释放对象,而上述情况造成了一个僵局,那就是析构对象时先析构sp2,可是由于sp2的空间sp1还在使用中,所以sp2.use_count减减之后为1,不释放,sp1也是相同的道理,由于sp1的空间sp2还在使用中,所以sp1.use_count减减之后为1,也不释放。sp1等着sp2先释放,sp2等着sp1先释放,二者互不相让,导致最终都没能释放,内存泄漏。
解决办法:使用弱指针---->weak_ptr
#include <iostream>
#include <memory>
using namespace std;
template <typename T>
struct Node
{
public:
Node(const T& value)
:_value(value)
{
cout<<"Node()"<<endl;
}
~Node()
{
cout<<"~Node()"<<endl;
cout<<"this:"<<this<<endl;
}
weak_ptr<Node<T>> _pPre;
weak_ptr<Node<T>> _pNext;
T _value;
};
void Funtest()
{
shared_ptr<Node<int>> sp1(new Node<int>(1));
shared_ptr<Node<int>> sp2(new Node<int>(2));
cout<<"sp1.use_count:"<<sp1.use_count()<<endl;
cout<<"sp2.use_count:"<<sp2.use_count()<<endl;
sp1->_pNext = sp2;
sp2->_pPre = sp1;
cout<<"sp1.use_count:"<<sp1.use_count()<<endl;
cout<<"sp2.use_count:"<<sp2.use_count()<<endl;
}
int main()
{
Funtest();
system("pause");
return 0;
}
运行结果:
分析过程:
析构对象时,先析构sp2,可是由于sp2.use_count减减之后为0,释放sp2,sp1.use_count减减之后为0,释放sp1.
小细节:由于我们的引用计数也是智能指针,维护了一块空间,当第一次调用完析构函数时,sp2.weak_count--==1未能释放,此时sp1.weak_count也减减等于1,下一次调用析构函数时,sp1.weak_count--==0释放。
boost/shared_ptr的框架uml类图: