引入智能指针:
智能指针的实现原理:
资源分配即初始化RAII(Resource Acquisition Is Initialization):
定义一个类来封装资源的分配和释放,在构造函数完成资源的分配
和初始化,在析构函数完成资源的清理,可以保证资源的正确初始化和释放。
实现机制:利用类的构造和析构函数(释放资源)是由编译器自动调用的。
智能指针:
1.管理指针执行对象的释放问题。
2.可以像指针一样使用。
在c++标准库里主要有四个智能指针:
C++四种智能指针auto_ptr、unique_ptr、shared_ptr和weak_ptr。
其中auto_ptr是C++98标准化引入的;
scope_ptr、shared_ptr和weak_ptr是C++11标准化才引入的(当然,早在C++03的TR1中就已经可以使用了)。我们都知道,auto_ptr虽说简单,但使用起来却到处是坑,以至于大家都不提倡使用。
shared_ptr是引用计数的智能指针,被奉为裸指针的完美替身,因此被广泛使用。也可能正是这个原因,scope_ptr 和 weak_ptr似乎被大家遗忘了(或许还会以为它们纯属多余)。
上述言论引自网上
下面简单总结下这几个智能指针:
下面简单总结下这几个智能指针:
1.auto_ptr
管理权转移
带有缺陷的设计 ----->c++98/03
在任何情况下都不要使用;
在任何情况下都不要使用;
2.scoped_ptr(boost)
unique_ptr(c++11)
防拷贝--->简单粗暴设计--->功能不全。
3、shared_ptr(boost/c++11)
引用计数--->功能强大(支持拷贝、支持定制删除器)
缺陷---->循环引用(weak_ptr配合解决)。
这篇文章主要的内容是讲解auto_ptr所引出的一系列问题:
一、auto_ptr智能指针
(无论在什么情况下都不要使用,下面来详细介绍下为什么不要使用它,可能会有人问,既然不让使用,那么为什么还要把它放在c++的标准库里呢?
是的,一开始我也是有这个疑问的,--->其实是因为c++标准库里的内容一经规定,就不允许修改了)
下面模拟实现其基本功能:
1.AutoPtr的第一种实现方法:
采用资源转移的方法。
一个指针将一块空间的权限完全交给了另一个指针。(权限的转移)
模拟实现代码如下:
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = 0)//构造函数
:_ptr(ptr)
{
ptr = NULL;
}
AutoPtr(AutoPtr<T>& ap)//拷贝构造函数
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)//赋值运算符重载
{
if (this != &ap)
{
if (_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~AutoPtr()//析构函数
{
if (_ptr)
{
delete _ptr;
_ptr = NULL;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
void Test1()
{
AutoPtr<int> ap(new int(1));
AutoPtr<int> ap1(ap);
AutoPtr<int> ap2;
ap2 = ap1;
}
赋值运算符存在问题:
(由此可知对于动态内存空间只能由一个智能指针来管理,从而避免下面的问题。
存在的问题:
问题①:这种将一块空间的权限完全交给别人的方法:
产生问题的是拷贝构造和赋值运算符重载函数。
当调用了拷贝构造或是赋值运算符的重载函数任意一个时:
假设调用了拷贝构造函数,我们的本意是用一个对象去构造一个与之一模一样的对象,可是结果呢,我们把自己给弄丢了,完全没有达到我们预期的目标。
所以当我们在上述代码中的Test1中调用了拷贝构造函数时,如果我们想要修改第一次构造出来的对象的内容时,会发现对象的资源已经被清理了,所以会导致程序崩溃。
问题②:先看如下示例:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = 0)//构造函数
:_ptr(ptr)
{
ptr = NULL;
}
AutoPtr(AutoPtr<T>& ap)//拷贝构造函数
:_ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)//赋值运算符重载
{
if (this != &ap)
{
if (_ptr)
{
delete _ptr;
}
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~AutoPtr()//析构函数
{
if (_ptr)
{
delete _ptr;
_ptr = NULL;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
AutoPtr<int> Funtest()
{
AutoPtr<int> ap(new int(1));
return ap;
}
void Test1()
{
AutoPtr<int> ap(new int(1));
AutoPtr<int> ap1(ap);
AutoPtr<int> ap2;
ap2 = ap1;
AutoPtr<int> ap4(Funtest());//返回值以值的形式返回,则为一个临时对象,
//临时对象具有常性,所以在拷贝构造ap4时,拷贝构造函数的参数应该由const修饰
}
问题③:
AutoPtr<int> ap(AutoPtr<int>(new int(1)))
//括号里是构造一个无名对象,无名对象,具有常性,和第②个问题一样,在拷贝构造ap时,拷贝构造函数的参数应该由const来修饰,还有一个问题就是,在vs2010下,编译器对其做了一些优化,本来这一条语句,应该是先调用构造函数构造匿名对象,然后由匿名对象去拷贝构造对象ap,但是由于在构造了匿名对象之后,又马上去拷贝构造对象,而且是在一条语句执行的,所以编译器对其进行了优化,只会调用构造函数,而不会去调用拷贝构造函数,将构造的无名对象值直接给了ap这个对象,所以在vs2010下程序并没有崩溃,而在Linux下编译时就会报出错误。
下面是我在Linux下测试时的代码:
编译错误:
总结:由上面的编译错误我们可以得知,这种由一个常性对象去拷贝构造一个Auto_ptr的想法是不现实的,所以才有了第三种情况。
实际上,实现方法三就是为了解决用一个临时右值对象去拷贝构造一个对象。
2.AutoPtr的第二种实现方法:
存在野指针的问题:
同样用示例来分析这个问题:
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = 0)//构造函数
:_ptr(ptr)
, _owner(true)//当独占资源时将其设置为true
{
if (!_ptr)
{
_owner = false;
}
}
AutoPtr(const AutoPtr<T>& ap)//拷贝构造函数
:_ptr(ap._ptr)
, _owner(true)
{
ap._owner = false;
}
AutoPtr& operator=(const AutoPtr<T>& ap)//赋值运算符重载
{
if (this != &ap)
{
delete _ptr;
_owner = true;
_ptr = ap._ptr;
ap._owner = false;
}
return *this;
}
~AutoPtr()
{
if (_owner)
{
delete _ptr;
_ptr = NULL;
cout<<"~AutoPtr()"<<endl;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
mutable bool _owner;//可被修改的
};
void TestAutoPtr()
{
/*AutoPtr<int> ap(new int(10));
AutoPtr<int> ap1(new int(20));
AutoPtr<int> ap2(ap1);
AutoPtr<int> ap3;
ap3 = ap;*/
AutoPtr<int> ap4(AutoPtr<int>(new int(20)));
if (true)
{
AutoPtr<int> ap5(ap4);
}//作用域结束之后ap5已被析构,空间已被释放
*ap4 = 10;//这时ap5与ap4共享一块空间,既然ap5已被释放,那么ap4对象维护的指针已成为野指针
}
int main()
{
TestAutoPtr();
return 0;
}
会发生内存泄漏,因为ap4与ap5共同管理一块空间,当ap5出了if语句的作用域后,已被析构,空间被回收,那么在下面在去访问ap4就有了问题,ap4已然成为了一个野指针。
③类型转化
c++标准库里的的auto_ptr的实现:
但还有是有缺陷:访问空指针的问题无法规避,也就是实现方法一存在的第一个问题
我先贴出标准库里的源码:
// TEMPLATE CLASS auto_ptr
template<class _Ty>
class auto_ptr;
template<class _Ty>
struct auto_ptr_ref
{ // proxy reference for auto_ptr copying
explicit auto_ptr_ref(_Ty *_Right)//构造函数
: _Ref(_Right)
{ // construct from generic pointer to auto_ptr ptr
}
_Ty *_Ref; // generic pointer to auto_ptr ptr
};
template<class _Ty>
class auto_ptr
{ // wrap an object pointer to ensure destruction
public:
typedef auto_ptr<_Ty> _Myt;
typedef _Ty element_type;
//一般的构造函数
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
: _Myptr(_Ptr)
{ // construct from object pointer
}
//一般的拷贝构造函数
auto_ptr(_Myt& _Right) _THROW0()
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right auto_ptr
}
//带有隐式类型的构造函数
auto_ptr(auto_ptr_ref<_Ty> _Right) _THROW0()
{ // construct by assuming pointer from _Right auto_ptr_ref
_Ty *_Ptr = _Right._Ref;
_Right._Ref = 0; // release old
_Myptr = _Ptr; // reset this
}
//类型转换
template<class _Other>
operator auto_ptr<_Other>() _THROW0()
{ // convert to compatible auto_ptr
return (auto_ptr<_Other>(*this));
}
//对象的类型转换
template<class _Other>
operator auto_ptr_ref<_Other>() _THROW0()
{ // convert to compatible auto_ptr_ref
_Other *_Cvtptr = _Myptr; // test implicit conversion
auto_ptr_ref<_Other> _Ans(_Cvtptr);
_Myptr = 0; // pass ownership to auto_ptr_ref
return (_Ans);
}
//一般的赋值运算符重载函数
template<class _Other>
_Myt& operator=(auto_ptr<_Other>& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
//拷贝构造函数
template<class _Other>
auto_ptr(auto_ptr<_Other>& _Right) _THROW0()
: _Myptr(_Right.release())
{ // construct by assuming pointer from _Right
}
//一般的构造函数
_Myt& operator=(_Myt& _Right) _THROW0()
{ // assign compatible _Right (assume pointer)
reset(_Right.release());
return (*this);
}
//带有类型转化的赋值运算符重载函数
_Myt& operator=(auto_ptr_ref<_Ty> _Right) _THROW0()
{ // assign compatible _Right._Ref (assume pointer)
_Ty *_Ptr = _Right._Ref;
_Right._Ref = 0; // release old
reset(_Ptr); // set new
return (*this);
}
//析构函数
~auto_ptr()
{ // destroy the object
delete _Myptr;
}
_Ty& operator*() const _THROW0()
{ // return designated value
#if _ITERATOR_DEBUG_LEVEL == 2
if (_Myptr == 0)
_DEBUG_ERROR("auto_ptr not dereferencable");
#endif /* _ITERATOR_DEBUG_LEVEL == 2 */
return (*get());
}
_Ty *operator->() const _THROW0()
{ // return pointer to class object
#if _ITERATOR_DEBUG_LEVEL == 2
if (_Myptr == 0)
_DEBUG_ERROR("auto_ptr not dereferencable");
#endif /* _ITERATOR_DEBUG_LEVEL == 2 */
return (get());
}
_Ty *get() const _THROW0()
{ // return wrapped pointer
return (_Myptr);
}
//为了转交所有权
_Ty *release() _THROW0()
{ // return wrapped pointer and give up ownership
_Ty *_Tmp = _Myptr;
_Myptr = 0;
return (_Tmp);
}
//为了接收所有权
void reset(_Ty *_Ptr = 0)
{ // destroy designated object and store new pointer
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}
private:
_Ty *_Myptr; // the wrapped object pointer
};
示例分析:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<memory>
auto_ptr<int> FunTest()
{
auto_ptr<int> ap1(new int(20));
return ap1;//返回值为一个匿名对象
}
void Test1()
{
auto_ptr<int> ap1(new int(10));//构造函数
auto_ptr<int> ap2(new int(20));//构造函数
auto_ptr<int> ap3;
ap3 = ap2;//赋值运算符重载
auto_ptr<int> ap4(ap1);//拷贝构造
auto_ptr<int> ap5(FunTest());//会发生类型转化 ,会先去调用这个类型转化函数operator auto_ptr_ref<_Other>(),
//然后调用构造函数auto_ptr(auto_ptr_ref<_Ty> _Right)。
}
如果您还想知道标准库里究竟是怎样实现的,可以继续往下看,希望可以对您有所帮助。
源码分析:
为了模拟一个一般指针,所以重载以下两个符号是为了使得智能指针可以像一般指针一样使用。
智能指针内封装的一个原生态指针:
二、ScopedPtr(独占空间--->防拷贝、防赋值)
(
其实在c++标准库里称为unique_ptr,很好理解了,因为AutoPtr就是因为转移资源,以及转交权限从而引发一系列的问题的,追根到底其实就是拷贝构造和赋值运算符重载函数导致的。所以这个智能指针就是防拷贝和赋值的)。
因为AutoPtr智能指针在拷贝构造和赋值运算符重载方面有些许问题,所以就有了ScopedPtr,既然拷贝构造和赋值运算符重载会引发一些问题,那么是不是可以不允许它拷贝和赋值呢?
既然拷贝和赋值容易出现问题,那么这种智能指针就不允许拷贝和赋值。
解决上述问题的方法:
实现这种机制的方法有三种:
1)将拷贝构造函数和赋值运算符重载函数的声明写成公有的,但是对这两个函数不进行定义。
2)将拷贝构造函数和赋值运算符重载函数在类型直接实现,但是将它们的访问限定符设定为私有的。
3)将拷贝构造函数和赋值运算符重载函数的声明写成私有的,但是对这两个函数不进行定义。
分析上述三种方法:
第一种:如果将其声明为公有的,那么如果有其他人在类外对你的拷贝构造和赋值运算符重载函数进行实现,那么还是没有达到防拷贝、防赋值的要求。--->pass掉
第一种:如果将其声明为公有的,那么如果有其他人在类外对你的拷贝构造和赋值运算符重载函数进行实现,那么还是没有达到防拷贝、防赋值的要求。--->pass掉
第二种:个人觉得,既然都已经不允许拷贝和赋值了,为什么还要多此一举的写出其实现方法呢?所以呢----->pass掉
所以:就第三种方法觉得合适。
在这里我只贴出第三种方法的代码:
//防拷贝、防赋值
template<class T>
class ScopedPtr
{
public :
ScopedPtr(T* ptr)//构造函数
:_ptr(ptr)
{}
~ScopedPtr()//析构函数
{
if (NULL != _ptr)
{
delete _ptr;
_ptr = NULL;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private :
ScopedPtr(const ScopedPtr& ap);//拷贝构造函数声明
ScopedPtr& operator=(const ScopedPtr& ap);//赋值运算符重载函数声明
T* _ptr;
};
void Test1()
{
ScopedPtr<int> sp1(new int(10));
*sp1 = 20;
}
scoped_array//管理数组
(boost库里面的,c++标准库由于已经有相似功能的vector,所以并未将其此加入)
总结:c++标准库里的智能指针只能管理单个对象的动态空间,而不能让其管理一段连续空间,例如:动态开辟的数组的空间。
//管理数组(防拷贝、防赋值)
template<class T>
class ScopedArray
{
public:
ScopedArray(T* ptr = 0)//构造函数
:_ptr(ptr)
{}
~ScopedArray()//析构函数
{
if (NULL != _ptr)
{
delete[] _ptr;
_ptr = NULL;
}
}
//基本操作
T& operator*()
{
return *_ptr;
}
//基本操作
T* operator->()
{
return _ptr;
}
//基本操作
T& operator[](size_t index)
{
return _ptr[index];
}
private:
ScopedArray(const ScopedArray<T>& ap);
ScopedArray& operator=(const ScopedArray<T>& ap);
T* _ptr;
};
总结:c++标准库里的智能指针只能管理单个对象的动态空间,而不能让其管理一段连续空间,例如:动态开辟的数组的空间。
auto_ptr无论什么时候都不要使用
scoped_ptr是对auto_ptr所存在的问题的一个解决。
所以一般面试时,没有特别指定让你实现哪个智能指针,最好写出这个,因为这个最简单呀,千万不要写出auto_ptr,这个智能指针存在缺陷!
虽然c++标准库里面加入了auto_ptr智能指针,但是还是不要使用它,c++标准库里仍然保留它,应该是为了向前兼容。
因为既然已经加入了标准库,那么肯定会有程序员使用的,所以还是不能随意废弃掉。
后续我还会继续总结关于shared_ptr的实现细节。
题外话:
因为刚开始接触智能指针,自身能力不足,所以哪里写的有问题的欢迎大家提出来。
共同进步!
每天进步一点点!