智能指针
智能指针的使用以及原理:
智能指针的思想:RAll
RAll是一种利用对象声明周期来控制程序资源的一种技术。它可以在对象构造时获取资源,在对象析构时候释放资源,完全不用用户自己释放,当然也解决了用原生态指针的缺陷。
RAll思想的好处:
1.不用自己显示释放申请的资源,用户不用考虑什么术后该释放资源,将这些交给编译器即可。
2.用这种方法,对象所需的资源在其生命周期内始终保持有效。
下面我们用RAll思想来设计一个智能指针:
template<class T>
class Sptr
{
public:
Sptr(T* ptr=nullptr)
:_ptr(ptr)
{}
~Sptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T* operator->()const//智能指针也是指针,也要有指针的基本功能
{
return _ptr;
}
T& operator*()const
{
return *_ptr;
}
private:
T* _ptr;
};
上述是一个基本的智能指针。我们可以发现,因为涉及资源管理,所以当用一个指针去拷贝另一个指针时,就会产生浅拷贝,并且不能用深拷贝来解决。
因此综上所述我们可以得出在所有不同的智能指针中:
1.因为RAll思想,资源可以自动释放
2.定义的智能指针类中,必须有普通指针类似的行为:operator*()以及operator->()
3.解决浅拷贝
auto_ptr
为了解决浅拷贝的问题,在C++98中,提供了auto_ptr智能指针。
注:所有C++库中智能指针都定义在memory这个头文件中。
通过使用auto_ptr我们可以发现,其解决浅拷贝方式是:资源转移,也就是所拷贝结束后,前面的指针置为空。只有后面的指针有地址。
下面我们来实现这个智能指针:
//模拟智能指针
namespace My
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~auto_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T* operator->()const//智能指针也是指针,也要有指针的基本功能
{
return _ptr;
}
T& operator*()const
{
return *_ptr;
}
auto_ptr(auto_ptr<T>& p)//此处参数前面不能用const,因为要将前面的指针值为空
: _ptr(p._ptr)
{
p._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& p)
{
if (this != &p)
{
if (_ptr)
delete _ptr;
_ptr = p._ptr;//赋值
p._ptr = nullptr;//断开前面指针
}
return *this;
}
private:
T* _ptr;
};
}
上述就是auto_ptr的模拟实现。但是我们发现因为把前面的指针已经与资源断开练联系,所以无法操作自己的资源。这也就是资源转移的缺陷,所以这种类型的auto_ptr在很多情况下不能用。
在解决浅拷贝时,除了上述资源转移的方法,还有一种方法也可以解决浅拷贝——加一个判定条件,让资源只释放一次。
我们来实现一下这个代码:
//模拟第二种解决办法
namespace My
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
:_ptr(ptr)
, adjust(false)
{
if (_ptr)
adjust = true;
}
~auto_ptr()
{
if (_ptr&&adjust)
{
delete _ptr;
_ptr = nullptr;
}
}
T* operator->()const//智能指针也是指针,也要有指针的基本功能
{
return _ptr;
}
T& operator*()const
{
return *_ptr;
}
auto_ptr(const auto_ptr<T>& p)//此处不用改变参数中的资源,但函数中需要访问判定条件,则给adjust前加mutable关键字即可
: _ptr(p._ptr)
, adjust(p.adjust)
{
p.adjust = false;
}
auto_ptr<T>& operator=(const auto_ptr<T>& p)
{
if (this != &p)
{
if (_ptr&&adjust)
delete _ptr;
_ptr = p._ptr;//赋值
adjust = p.adjust;
p.adjust = false;
}
return *this;
}
private:
T* _ptr;
mutable bool adjust;
};
}
上述为第二种解决浅拷贝的办法:加个判定条件,让资源只释放一次。但这种方法也有缺陷:有可能造成野指针,从而使代码崩溃。
unique_ptr
虽然auto_ptr解决了浅拷贝问题,但还是存在很多缺陷。所以在C++11中提供了更靠谱的智能指针unique_ptr。
unique_ptr解决浅拷贝的方式:资源独占(只能一个对象使用,不能共享)也就是禁止调用拷贝构造和赋值运算符重载,使其不能拷贝以及赋值。(防拷贝)
下面来重点模拟实现一下unique_ptr:
namespace My
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
delete _ptr;
_ptr = nullptr;
}
}
T* operator->()const//智能指针也是指针,也要有指针的基本功能
{
return _ptr;
}
T& operator*()const
{
return *_ptr;
}
/*
C++98中
将拷贝构造函数和赋值构造函数只声明,可以让外部无法实现拷贝以及赋值,
但是用户有可能自己在类外定义,所以拷贝构造函数和赋值构造函数必须给定为private权限,这样用户
就不会在类外调用拷贝构造函数
private:
unique_ptr(const unique_ptr<T>& up);
unique_ptr<T>& operator=(const unique_ptr<T>&);*/
// C++11 禁止调用拷贝构造和赋值运算符重载,可以在函数后 =delete
unique_ptr(const unique_ptr<T>&) = delete; //默认成员函数 = delete : 告诉编译器,删除该默认成员函数
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
private:
T* _ptr;
};
}
上述为unique_ptr解决浅拷贝的方法,这种方法虽然有些直接,但解决了浅拷贝以及之前的缺陷。
另外在释放资源期间,由于我们开辟的空间的方式不同,所以如果将释放资源的方式,固定成delete释放,就只能管理new出来的空间,不能任意处理其他类型的资源了。所以需要用仿函数让释放资源的方式丰富起来,使其指针更智能。也就是定制一个删除器。
以下是代码模拟:
template<class T>
class DFDef//用仿函数,定制出只对new出来的资源进行释放
{
public:
void operator()(T*& ptr)
{
if (ptr)
{
delete ptr;
ptr = nullptr;
}
}
};
template<class T>//只对malloc、calloc、realloc出来的资源进行释放
class FREE
{
public:
void operator()(T*& ptr)
{
if (ptr)
{
free(ptr);
ptr = nullptr;
}
}
};
class FCLOSE//只对文件开始的资源进行释放
{
public:
void operator()(FILE*& ptr)
{
if (ptr)
{
fclose(ptr);
ptr = nullptr;
}
}
};
namespace My
{
template<class T, class df = DFDef<T>>//默认为对new申请的资源进行释放
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
df d;
d(_ptr);
}
}
T* operator->()const//智能指针也是指针,也要有指针的基本功能
{
return _ptr;
}
T& operator*()const
{
return *_ptr;
}
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
private:
T* _ptr;
};
}
void test()
{
My::unique_ptr<int> p(new int);
My::unique_ptr<int,FREE<int>> p2((int*)malloc(sizeof(int)));//根据不同类型,来对应调用不同的释放方式。
My::unique_ptr<FILE, FCLOSE>p3(fopen("1.txt", "wb"));
}
shared_ptr
虽然unique_ptr解决了浅拷贝的问题,且基本没有问题。但是应用场景受限,若遇到对于多个资源之间需要共享资源的场景,unique_ptr无法实现。
所以为了解决unique_ptr的问题,在C++11中提出了shared_ptr。此指针不仅解决了浅拷贝的问题,同时可以多个对象之间共享资源。
shared_ptr解决浅拷贝的方式:采用引用计数的方式,就是记录一块资源被多少对象在使用,如果计数为1那么说明只剩最后一个对象在访问资源,则可以释放资源。不为1则不释放。
下面我们来模拟一下shared_ptr:
namespace My
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
:_ptr(ptr)//const类型的变量、引用类型的变量、以及包含类类型的对象 这三个变量一般
//需要在初始化列表的位置进行初始化
, _pcount(nullptr)
{
if (ptr)//若用户给的资源不为空,则记一个数。
_pcount = new int(1);
}
~shared_ptr()
{
if (_ptr && (--*_pcount == 0))
{
delete _ptr;
delete _pcount;
}
}
T* operator->()
{
return _ptr;
}
T& operator*()
{
return *_ptr;
}
shared_ptr(const shared_ptr<T>& sp)
:_ptr(sp._ptr)
, _pcount(sp._pcount)
{
if (_ptr)
++*_pcount;
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
if (this != &sp)
{
if (_ptr&&--*_pcount == 0)//被赋值的对象与旧资源断开,
//如果被赋值的对象是最后一个使用该块资源的对象,则释放旧的资源与计数。
//否则,不用释放旧资源,因为还有对象在用此资源。
{
delete _ptr;
delete _pcount;
}
_ptr = sp._ptr;//与赋值的对象共享资源和计数
_pcount = sp._pcount;
if (_ptr)//如果新资源不为空,计数+1;
++*_pcount;
}
return *this;
}
int show__pcount()
{
return *_pcount;
}
private:
T* _ptr;
int* _pcount;
};
}
以上代码就是shared_ptr的模拟实现。除此外我们还需考虑一点,就是在释放空间时,如果固定用delete来释放资源,则只能管理new出来的资源,对于其他类型资源,无法进行释放操作。这就是代码太局限了。所以我们还需要 定制删除器:使用户可以控制资源具体的释放操作。我们常用仿函数来定制不同的释放方式,然后通过模板参数传入即可。 定制删除器的方法与unique_ptr中相同。
注意:shared_ptr中计数不是线程安全的。因为当有多线程时,很可能会引起计数同时被改写,而造成计数数据不安全。所以就需要进行——加解锁操作,以保证当一个线程对计数进行操作时,其他线程不能对计数进行操作,当此时的操作完时,其他线程才能继续操作。虽然加锁保证计数安全,但不保证用户数据安全。
举例说明:
#include <mutex>//要用加解锁,必须要包含头文件
void Add ()
{
mutex* _pMutex=new mutex;
_pMutex->lock();//加锁
...//进行数据操作
_pMutex->unlock();//解锁
}
shared_ptr最大的缺陷:可能会造成循环引用,造成资源泄露。
下面通过一个双向链表的例子来看看什么是循环引用:
struct SlistNode
{
SlistNode(int data = 0)
:pre(nullptr)
, next(nullptr)
, _data(data)
{
cout << "ListNode(int):" << this << endl;
}
~SlistNode()
{
cout << "~ListNode():" << this << endl;
}
shared_ptr<SlistNode> pre;//前一个指针域
shared_ptr<SlistNode> next;//后一个指针域
int _data;
};
void test()
{
shared_ptr<SlistNode> sp1(new SlistNode(10));
shared_ptr<SlistNode> sp2(new SlistNode(20));
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
sp1->next = sp2;//讲个两个节点连起来
sp2->pre = sp1;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
}

那么怎么解决shared_ptr的循环引用问题呢,就用weak_ptr。
weak_ptr:
1.weak_ptr的实现原理和shared_ptr类似——也是用计数的方式。
2.weak_ptr的对象不能独立的管理资源,必须配合shared_ptr来使用。一起解决循环引用的问题
代码如下:
struct ListNode
{
ListNode(int data = 0)
// : pre(nullptr)因为weak_ptr不能独立的管理资源,所以pre、next不需要初始化。
// , next(nullptr)
: _data(data)
{
cout << "ListNode(int):" << this << endl;
}
~ListNode()
cout << "~ListNode():" << this << endl;
//shared_ptr<ListNode> pre;
//shared_ptr<ListNode> next;
weak_ptr<ListNode> pre;
weak_ptr<ListNode> next;
int _data;
};
//补充:
// weak_ptr<int> sp1;可以编译成功
//weak_ptr<int> sp2(new int);编译失败--原因:weak_ptr不能独立管理资源。

本文介绍了C++中的智能指针,包括RAll思想、智能指针的作用以及如何解决浅拷贝问题。详细讲解了auto_ptr、unique_ptr、shared_ptr的实现原理和应用场景,其中unique_ptr通过禁止拷贝和赋值防止浅拷贝,shared_ptr则通过引用计数实现资源共享。文章还提到了shared_ptr的线程安全问题和循环引用问题,并介绍了weak_ptr如何解决循环引用。
566

被折叠的 条评论
为什么被折叠?



