您的位置:首页 > 编程语言 > C语言/C++

C++智能指针(1)

2016-03-30 21:25 399 查看
一、问题引入
关于C++中的new和delete操作符,
我们知道这两个操作符必须成对存在,才能避免内存泄漏。

这一点在学习的时候被认为是常识,然而,在实际编写代码的过程中,却常常很难做到。
下面有3种情况:

1、代码很长。
当需要用到delete的地方离使用与之对应的new操作符距离非常远时,我们很容易忘记delete。当然,这种情况是完全可以避免的。

2、如下面代码:
void Test()
{
int *pi = new int(1);
if(1)
{
return;
}
delete pi;     //程序并没有执行到这一步
}
int main()
{
void Test();
return 0;
}
这里我们在Test函数中,开辟了一段长度为 1个int类型大小 的动态内存,
但接下来,进入if语句,直接return掉了,因此之前开辟的那段内存没有得到回收,导致内存泄漏。
对于这种情况,我们可以这样修改:
void Test()
{
int *pi = new int(1);
if(1)
{
delete pi;
return;
}
delete pi;    //程序并没有执行到这一步
}
int main()
{
void Test();
return 0;
}


3、让情况再复杂一些,看看这段代码:
void DoSomeThing()
{
if(1)
{
throw 1;
}
}
void Test()
{
int *pi = new int(1);
DoSomeThing();
delete pi;
}
int main()
{
try
{
Test1();
}
catch(...)
{
;
}
return 0;
}
在Test()函数中,看似new和delete是成对存在的,中间也只有一行代码,
然而当中间这个DoSomeThing()抛出异常导致这个程序结构不是按部就班地进行时,
仍然没有执行delete pi 这一步。
这种情况我们仍然可以做如下修正:
void DoSomeThing()
{
if(1)
{
throw 1;
}
}
void Test()
{
int *pi = new int(1);
try
{
DoSomeThing();
}
catch(...)
{
delete pi;
throw;
}
delete pi;
}
int main()
{
try
{
Test1();
}
catch(...)
{
;
}
return 0;
}
在Test中加上一个try catch语句,作为DoSomeThing()函数抛出异常的“中介”,处理动态内存。

以上3种情况都是完全可以解决的。
但特别是当程序是类似情况3的结构,甚至更复杂的时候,我们不得不加上一大堆的代码,却仅仅是为了处理动态内存的回收。
这是十分影响开发效率的,同时程序也变得难以阅读。

二、简单的智能指针
我们知道,类的成员函数中,析构函数的存在似乎能解决动态内存回收的问题:当程序出了类的作用域,会自动调用该类的析构函数。
带着这个思想,我们定义一个名为AutoPtr的类模板
template<typename T>
class AutoPtr
{
public:
AutoPtr(T *ptr = NULL)
:_ptr(ptr)
{}
AutoPtr(AutoPtr<T> &ap)
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
~AutoPtr()
{
if (_ptr != NULL)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = NULL;
}
}
AutoPtr<T> operator=(AutoPtr<T> &ap)
{
if(this != &ap)
{
if (_ptr != ap._ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T &operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
protected:
T* _ptr;
值得注意的是,当这个模板类的模板类型为结构体或类时,我们需要访问该类的成员,因此需要一个
"->"操作符的重载,举个例子:
已知结构体stru的定义如下:
struct stru
{
void PrintTest()
{
std::cout << "hi" << std::endl;
}
};
对于这样的类,当我们执行如下代码
stru st1;
stru *ps1 = &st1;
AutoPtr<stru> aps1(ps1);
aps1->PrintTest();//操作符的重载
最后一行发生 "->"操作符的重载,但根据之前关于 "->"操作符的定义严格意义上,访问到的是
_ps1; 这行代码实际上经过重载后,应该为:_ps1PrintTest()
其实这里是编译器为了保证代码的可读性,把_ps1PrintTest优化为 _ps1->PrintTest()。
(这个优化在g++和VS2015中都是存在的)

当然,这个智能指针并不完美,仔细观察我的拷贝构造函数和赋值操作符的重载,你会发现,每当我们进行拷贝或者赋值的时候,永远都是让源智能指针置空,也就是说,同一时间一段动态内存只能由一个智能指针来维护。
这样做是有原因的:如果有若干个个智能指针指向同一块动态空间,那么在析构的时候,将会对这块空间析构若干次,程序必然崩溃。所以我只能让这个类具有“同一时间一段动态内存只能由一个智能指针来维护”的特性。

总之,尽管不完美,但我们还是实现类一个简单的智能指针AutoPtr。有了它,我们可以不用手动delete,将释放内存的工作全部交给析构函数来处理。

上面说的“不完美”主要体现为以下几点,也是我接下来要解决的问题:

1、“同一时间一段动态内存只能由一个智能指针来维护”的特性:这个特性让它和普通的指针“不太像”,不符合普遍的编程习惯,
此外如果手动构造多个指向同一内存的智能指针,仍会导致析构函数的时候对同一内存析构多次,仍会令程序崩溃,因此这样的做法并没有根本上解决问题;
2、智能指针只能指向动态内存,如果指向静态内存,在析构的过程中必然崩溃。

最初AutoPtr还有另外一种实现方式,
代码如下:
template <class T>
class OldAutoPtr
{
public:
OldAutoPtr(T *ptr)
:_ptr(ptr)
,_IsHost(true)
{
std::cout << "构造" << std::endl;
}

OldAutoPtr(OldAutoPtr<T> &oap)
:_ptr(oap._ptr)
,_IsHost(true)
{
oap._IsHost = false;
}

~OldAutoPtr()
{
if (_IsHost)
{
std::cout << "释放" << std::endl;
delete _ptr;
}
}

public:
T &operator*()
{
return *_ptr;
}

T* operator->()
{
return _ptr;
}

OldAutoPtr<T>& operator=(OldAutoPtr<T> &oap)
{
if (_IsHost)
{
cout << "释放" << endl;
delete _ptr;
}
_ptr = oap._ptr;
_IsHost = true;
oap._IsHost = false;
return *this;
}
void Test()
{
cout << "test" << endl;
}

protected:
T* _ptr;
bool _IsHost;
};
成员变量_IdHost需要保证对于多个指向同一个动态内存的指针,只能有一个指针为ture。
这样,在析构时,只对_IsHost值为ture的指针进行内存释放。
在这种实现方式下,允许多个指针指向同一块动态内存。
但这种方式存在隐患,如下面这种情况:
int a = 0;
int b = 1;
auto_ptr<int> ap1(new int(1));
if (a < b)
{
auto_ptr<int> ap2(ap1);
}
这时候,ap1指向的空间已经释放,但仍然可以访问ap1指向的空间,这样就成为了野指针。
因此这种实现方式存在严重漏洞。

三、防拷贝措施(ScopedPtr)
我之前写的AutoPtr有种种不完美之处,需要对其进行一些改进,其中由于拷贝和赋值的不合理导致同一时间在特定的一段动态内存,只能存在一个AutoPtr维护,针对这个问题,ScopedPtr 和 SharedPtr都是对AutoPtr的改进
ScopedPtr类模板的定义如下:
template<typename T>
class ScopedPtr
{
public:
ScopedPtr(T* ptr = NULL)
:_ptr(ptr)
{}

~ScopedPtr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
_ptr = NULL;
}
}
T &operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
protected:
ScopedPtr(ScopedPtr<T> &sp);
ScopedPtr<T> &operator=(ScopedPtr<T> &sp);
protected:
T* _ptr;
};
这里跟AutoPtr的区别在于
1:拷贝构造函数和赋值操作符重载函数只声明,没有定义
2:并且以上两个函数的声明放在了protected限定符内

采用这样的做法,当我们想对ScopedPtr类型的变量进行拷贝构造或者赋值时,由于并没有定义相应的函数,程序是无法编译通过的,因此根本上就防止了赋值行为的发生
此外,将这两个函数放在protected限定符内也是有意义的:假如我们声明这两个函数为public,那么的确可以起到同样防止调用的效果。
但是一旦这样做,造成的后果就是,其他人在读这样的代码时,有可能会误解为我们没来得及定义这两个函数,然后“画蛇添足”地加上相应的定义,如此一来,这个ScopedPrt类就与之前我们定义的AutoPtr类没两样了。
此外,心怀恶意的“捣乱者”一旦看到这样的漏洞,破坏掉你的程序也将会轻而易举——只需要给这两个函数写上定义就可以了。
总之,将这两个函数声明为protected是有意义的,它可以防止其他人对程序的破坏行为。

四、进一步改进(SharedPtr)
到这里,简单的ScopedPtr类已经实现了,它可以防止让多个指针对同一片动态内存的行为。
但前提是你必须按照规范,一旦像下面这段代码的方式使用ScopedPtr,那么我们刚才做的一系列保护就失去作用了:
int *pi = new int(1);
ScopedPtr<int> sp1(pi);
ScopedPtr<int> sp2(pi);
执行上面这段代码,仍然可以让两个ScopedPtr类型的智能指针指针指向同一片动态内存,因此这两个
ScopedPtr指针在退出作用域自动调用析构函数时,必然会对同一块内存释放2次,程序崩溃。

看来,ScopedPtr仍不是最佳的解决方式。我们希望实现一种新的智能指针,它能够允许多个该智能指针指向同一块内存。

于是,我们试着实现一个名为 SharedPtr 的类,要想实现允许多个智能指针指向同一块内存,需要修改一下我们之前定义的析构函数中的那个delete语句的条件。
举个例子,有2个智能指针sp1 sp2指向同一块动态内存,那么在这两个指针的析构函数调用时,delete的行为应该只发生一次。
也就是说 假设当析构sp1时,我们应该在delete之前检测sp1所指向的内存是否还有被其他指针维护,
若有,则不delete,
若无,才发生delete。

因此,我们还需要一个计数器变量_pCount,能够告诉我们指定的内存有几个智能指针在维护。
我们可以将_pCount定义在SharedPtr类内,SharedPtr类的定义如下:
template<typename T>
class SharedPtr
{
public:
SharedPtr(T *ptr = NULL)
:_ptr(ptr)
,_pCount(new long long(1))
{}
SharedPtr(const SharedPtr<T> &sp)
:_ptr(sp._ptr)
,_pCount(sp._pCount)
{
++*(_pCount);
}
~SharedPtr()
{
if (_ptr)
{
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
}
}
SharedPtr<T> &operator=(SharedPtr<T> &sp)
{
if (this != &sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
}
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
long long UseCount()
{
return *(_pCount);
}
protected:
T *_ptr;
long long* _pCount;
};
对于类内的这个_ptr指针成员,,每个我们在拷贝构造和赋值的时候,需要不断维护它,以保证每个指向相同内存的智能指针也同时指向相同的计数器。
有了这个计数器,在析构的时候,就可以通过计数器的值判断是否应该delete掉相应的内存了:
如果只有析构的这一个智能指针在维护这段内存,则将这段内存delete掉,同时别忘了将计数器_pCount指向的内存也delete掉。
如果判断出除了当前析构的智能指针,还有其他智能指针也在维护相应的内存,则不delete,但计数器的值要减1。

当然,在该类的赋值操作符重载函数中,为了减少程序执行的步骤,我们也可以用如下的现代式写法的方式进行定义,具体实现如下:

SharedPtr<T> &operator=(SharedPtr<T> &sp)
{
--(*_pCount);
SharedPtr<T> tmp(sp);
swap(_ptr, tmp._ptr);
_pCount = sp._pCount;
++(*_pCount);
return *this;
}


五、智能数组
我们还可以参照这些智能指针类的设计思路,设计出智能数组。
智能数组用来存放动态开辟的数组,因此析构函数释放内存采用delete[]操作符。
此外,智能数组不需要* 和 -> 操作符,但需要重载[]操作符,具体的实现如下:
template<typename T>
class SharedArr
{
public:
SharedArr(T *ptr = NULL)
:_ptr(ptr)
, _pCount(new long long(1))
{}
SharedArr(const SharedPtr<T> &sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++*(_pCount);
}
~SharedArr()
{
if (_ptr)
{
if (--(*_pCount) == 0)
{
delete[] _ptr;
delete _pCount;
}
}
}
SharedArr<T> &operator=(SharedArr<T> &sp)
{
if (this != &sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
}
return *this;
}
T &operator[](size_t index)
{
return _ptr[index];
}
long long UseCount()
{
return *(_pCount);
}
protected:
T *_ptr;
long long* _pCount;
};


可以增加一个指针成员变量保存数组的长度,修改相应的构造、拷贝构造、赋值操作符等,从而实现数组的打印等操作。
但缺点是在调用这个类的构造函数时,必须传入准确的数组长度,否则程序会出错
实现代码如下:
template<typename T>
class SharedArr
{
public:
SharedArr(T *ptr = NULL, long long size = 0)
:_ptr(ptr)
, _pSize(new long long(size))
, _pCount(new long long(1))
{}
SharedArr(const SharedPtr<T> &sp)
:_ptr(sp._ptr)
,_pSize(sp._pSize)
, _pCount(sp._pCount)
{
++*(_pCount);
}
~SharedArr()
{
if (_ptr)
{
if (--(*_pCount) == 0)
{
delete[] _ptr;
delete _pCount;
delete _pSize;
}
}
}
SharedArr<T> &operator=(SharedArr<T> &sp)
{
if (this != &sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pCount) == 0)
{
delete _ptr;
delete _pCount;
delete _pSize;
}
_ptr = sp._ptr;
_pCount = sp._pCount;
_size = sp._pSize;
++(*_pCount);
}
}
return *this;
}
T &operator[](size_t index)
{
return _ptr[index];
}
long long UseCount()
{
return *(_pCount);
}
void Print()
{
for (int i = 0; i < *(_pSize); i++)
{
cout << _ptr[i] << " ";
}
cout << endl;
}
protected:
T *_ptr;
long long* _pSize;
long long* _pCount;
};


六、定置删除器
在我之前实现的SharedPtr中,在一些需要释放内存的地方(如析构函数、赋值操作符重载),采用了delete操作符,然而,对于非new操作符开辟的内存,这会导致程序崩溃。
比如:
int a = 10;
int *p = &a;
SharedPtr<int> sp1(p);
此外,当智能指针用于文件操作时,我们希望成员方法中用到delete的地方能产生类似fopen的效果,因此需要用到定置删除器。实现方式如下:
struct DefaultDel
{
void operator() (void *ptr)
{
std::cout << "DefaultDel::operator()" << std::endl;
delete ptr;
}
};
struct Free
{
void operator() (void *ptr)
{
std::cout << "Free::operator()" << std::endl;
free(ptr);
}
};

struct Fclose
{
void operator() (void *ptr)
{
std::cout << "Fclose::operator()" << std::endl;
fclose((FILE*)ptr);
}
};

struct Stack
{
void operator() (void *ptr)
{
std::cout << "Stack::operator()" << std::endl;
}
};

//基于定置删除器的类模板

template<class T, class D = DefaultDel>
class SharedPtr
{
public:
SharedPtr(T *ptr)
:_ptr(ptr)
, _pCount(new long long(1))
{}

SharedPtr(T *ptr, D del)
:_ptr(ptr)
,_pCount(new long long(1))
,_del(del)
{}

SharedPtr(const SharedPtr<T, D> &sp)
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++*(_pCount);
}

void _Release()
{
_del(_ptr);
delete _pCount;
}
~SharedPtr()
{
if (_ptr)
{
if (--(*_pCount) == 0)
{
_Release();
}
}
}
SharedPtr<T, D> &operator=(SharedPtr<T, D> &sp)
{
if (this != &sp)
{
if (_ptr != sp._ptr)
{
if (--(*_pCount) == 0)
{
_Release();
}
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);
}
}
return *this;
}
T &operator[](size_t index)
{
return _ptr[index];
}
long long UseCount()
{
return *(_pCount);
}
protected:
T *_ptr;
long long* _pCount;
D _del;
};
在这种实现方式中,我定义了四种类:DefaultDel、Free、Fclose、Stack,它们分别可以对应new、malloc开辟的动态内存,以及用于文件操作(指向fopen类型数据)的指针、指向栈空间(静态内存)的指针。
然后在每个类中实现()操作符的重载,重载函数实现了它们对于指定内存类型的处理方式。这样就可以用类似函数的方式调用这些重载函数。
为了方便起见,在SharedPtr类中,我还声明了D类型的成员变量_del,这样调用_del()就可以实现相应类型的结构体中的()重载函数,也就是定置删除的功能。
使用方式如下:
int a = 10;
int *p = &a;
SharedPtr<int, Stack> sp1(p);
由于p指向的内存不是new操作符开辟的,因此模板参数不能用缺省值,
Stack是针对栈内存的定置删除器,因此采用Stack

七、梳理

智能指针的实现,是利用了类的析构函数“已构造的对象最终会销毁,即它的析构函数最终会被调用”的特性,即RAII。智能指针的意义在于让我们不用关心指针指向对象的回收、处理。因此对于特定的内存,我们需要写特定的定置删除器进行处理。以上是我实现的AutoPtr ScopedPtr SharedPtr,实际上在boost库中,已经实现同名的智能指针,只不过命名方式为下划线法(如shared_ptr)此外,在C++新标准中,这些智能指针也是标准库中的成员,我们只需要引头文件 memory即可使用这些智能指针(其中,scoped_ptr被“改名”为 unique_ptr)。
//关于boost库的编译,这是我的步骤http://zhweizhi.blog.51cto.com/10800691/1760312
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  C++