关于智能指针(上)
引入智能指针的目的是为了避免我们在申请量空间之后忘记delete ,或是当程序出现异常时,程序会直接从一个模块跳出来,导致后面的delete语句不能被执行,因此存在内存泄漏问题。
如何让我们在使用完资源后,使系统自动为我们释放这些资源呢?
我们知道析构函数具有这样的功能。
因此,我们将需要将基本类型指针封装为类对象指针,该类一定是模板类,以适应不同类型指针的需求。在构造函数时,在类外将需要管理的指针传进去,可以先给个空的缺省值,然后重载” -> ” , ” * ” , ” = ” 等符号,然后我们在析构函数中进行释放指针指向的内存空间,对这个指针进行智能的管理。
首先介绍几种智能指针,并模拟实现它们:
AutoPtr
这种智能指针采用的是资源转移的方式,在拷贝和赋值后,原有对象对资源无访问权。
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
:_ptr(ptr)
{
cout << "AutoPtr()" << this << endl;
}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
cout << "AutoPtr(const AutoPtr<T>&)" << this << endl;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*()
{
if(_ptr!=NULL)
return *_ptr;
}
T* operator->()
{
if(_ptr!=NULL)
return _ptr;
}
~AutoPtr()
{
cout << "AutoPtr()" << this << endl;
if (_ptr)
{
delete _ptr;
}
}
private:
T* _ptr;
};
struct A
{
int a;
int b;
int c;
};
void TestAutoPtr()
{
AutoPtr<int> ap1(new int);
AutoPtr<int> ap2(ap1);//ap1资源被转移到ap2,ap1无法进行访问
AutoPtr<int> ap3;
ap3 = ap2;//ap2资源被转移到ap3,ap2无法进行访问
*ap3 = 10;
AutoPtr<A> ap4(new A);
ap4->c = 20;
}
这时,一旦为另一个类对象指针赋值或是拷贝,自己就不能访问这块空间,等于资源完全转移,不建议使用。
我们对其进行改进,为该类加上一个私有成员变量owner,用来标记是否对资源有使用权,当进行拷贝和赋值时,原有类对象指针依旧可以访问这块内存空间,只是对这块空间没有了释放的权利。
这种资源未完全转移,在拷贝和赋值之后,原有对象仍可以进行访问,但是没有资源释放权
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
:_ptr(ptr)
, _owner(true)
{
cout << "AutoPtr(T*):" << this << endl;
if (_ptr == NULL)
_owner = false;
}
AutoPtr(AutoPtr<T>& ap)
:_ptr(ap._ptr)
, _owner(ap._owner)
{
ap._owner = false;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
if (this != &ap)
{
if (_owner)
delete _ptr;
_ptr = ap._ptr;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~AutoPtr()
{
cout << "~AutoPtr():" << this << endl;
if (_owner&&_ptr)
{
delete _ptr;
_owner = false;
}
}
private:
T* _ptr;
mutable bool _owner;//标记是否对资源有使用权
//mutable关键字含义是 可变的 ,
//在const修饰的成员函数中要对类的某个数据成员进行修改时需要声明该变量为mutable
};
void TestAutoPtr()
{
AutoPtr<int> ap1(new int);
AutoPtr<int> ap2(ap1);//ap1资源的释放权被转移到ap2,但ap1还可进行访问
AutoPtr<int> ap3;
ap3 = ap2;//ap2资源释放权被转移到ap3,但ap2还可以进行访问
*ap3 = 10;
}
即便如此,这种做法还是不安全的,存在这样一种情况:
void test()
{
AutoPtr<int> sp1(new int(1));
if(1)
{
AutoPtr<int> sp2(sp1);
}
*sp1=10;//error
}
首先我们定义一个类对象指针sp1,在if函数体中,用sp1拷贝构造sp2,此时,他们共享同一块内存空间,只是将空间的释放权转移给了sp2,出了if函数体后,sp2会将这块空间释放,而此时sp1并不知情,接下来执行*sp1=10一定会出错。
AutoPtr存在这样那样的问题,不建议大家使用。
ScopedPtr
防拷贝的智能指针(可以解决AutoPtr存在的问题)
使拷贝构造函数和赋值运算符重载函数不能被调用
将他们给成私有的,不给出定义,只给出声明
template<class T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr = NULL)
:_ptr(ptr)
{}
~ScopedPtr()
{
if (_ptr)
delete _ptr;
}
private:
//若给成公有,可能被其他人在类外进行定义,也没有起到防拷贝的功能
//不能让其他函数作为该类的友元函数,避免对这两个函数篡改
ScopedPtr(const ScopedPtr<T>& sp);
//{}
ScopedPtr<T>& operator=(const ScopedPtr<T>& sp);
//{}
private:
T* _ptr;
};
void TestAutoPtr()
{
ScopedPtr<int> ap1(new int);
ScopedPtr<int> ap2(ap1);//编译不通过,无法进行访问
ScopedPtr<int> ap3;
ap3 = ap2;//编译不通过,无法进行访问
*ap3 = 10;
}
这种智能指针的确避免了上述问题,但是却不能进行拷贝和赋值,也是未从根本上解决问题,下面将介绍一种我们最常用的智能指针。
SharedPtr
在前面我们学习string类时,运用了一种写时拷贝的方法来解决浅拷贝存在的问题,同样,我们借助它的原理来实现SharedPtr
它在底层多申请了一块空间来保存引用计数,用来检测当前对象所管理的空间是否还被其他智能指针使用,在构造函数中,若该智能指针所指空间非空,将其引用计数初始化为1。析构时,先对其引用计数减一,若减为0,就可以释放该空间,否则不能释放。
template<class T>
class SharedPtr
{
public:
SharedPtr(T* ptr = NULL)
:_ptr(ptr)
, _pCount(NULL)
{
if (_ptr != NULL)
{
_pCount = new int(1);
}
}
SharedPtr(const SharedPtr<T>& sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++GetRef();
}
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
if (this != &sp)
{
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
++GetRef();
}
return *this;
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
int UseCount()
{
return GetRef();
}
~SharedPtr()
{
Release();
}
private:
void Release()
{
if (_ptr&&0==--GetRef())
{
delete _ptr;
delete _pCount;
}
}
int& GetRef()
{
return *_pCount;
}
private:
T* _ptr;
int* _pCount;
};
void TestSharedPtr()
{
SharedPtr<int> sp1(new int(10));
SharedPtr<int> sp2(sp1);
*sp1 = 20;
*sp2 = 30;
cout << sp1.UseCount() << endl;//2
cout << sp2.UseCount() << endl;//2
SharedPtr<int> sp3(new int(50));
SharedPtr<int> sp4(sp3);
sp1 = sp3;
sp2 = sp4;
cout << sp1.UseCount() << endl;//4
cout << sp2.UseCount() << endl;//4
}
这种方式看似很完美,但也有一个问题:
试想有一个双向链表,每个节点的next和prev也都是SharedPtr类型的智能指针,此p
1和p2的引用计数都为2,但函数结束要销p1和p2时,p1的引用计数先减一后为一,说明有其他指针还在使用这块空间,因此不能释放。p2也如此,它的引用计数减一后也为一,无法释放,二者相互制约,最终都无法释放导致内存泄漏。
template<class T>
struct ListNode
{
ListNode(int data = 0)
:_data(data)
, _prev(NULL)
, _next(NULL)
{}
SharedPtr<Listnode<T>> _prev;
SharedPtr<ListNode<T>> _next;
T _data;
ListNode(int data = 0)
:_data(data)
, _prev(NULL)
, _next(NULL)
{}
};
void Test()
{
SharedPtr<ListNode<int>> p1(new ListNode<int>(10));
SharedPtr<ListNode<int>> p2(new ListNode<int>(20));
cout << p1.UseCount() << endl;//1
cout << p2.UseCount() << endl;//1
p1->_next = p2;
p2->_prev = p1;
cout << p1.UseCount() << endl;//2
cout << p2.UseCount() << endl;//2
}
要解决这个问题,就得提出我们的第四种智能指针WeakPtr,今天没时间了,先溜了,后面内容将在智能指针下中介绍~~