智能指针,可以做到离开作用域自动销毁,不用我们手动去调用析构函数
1.RAII思想
1.1RAII思想介绍
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在 对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。
这种做 法有两大好处: 不需要显式地释放资源。 采用这种方式,对象所需的资源在其生命期内始终保持有效。
1.2一份简单的RAII思想类型的代码:
template<class T>
class myRAII {
public:
myRAII(T* ptr=nullptr)://不能使用const--会导致权限放大
_ptr(ptr)
{}
~myRAII()
{
if(_ptr){
delete _ptr;
}
}
private:
T* _ptr;
};
借助了类实例离开作用域时,会自动调用析构函数的特性。所以就可以有效解决因为忘记释放资源而造成内存泄露。
仅仅只有这个,我们还是无法使用的,因此我们需要它能提供接口,为我们提供访问资源的接口函数,使其能像使用指针一样使用 。
权限放大问题 --只针对指针来说的
2.smartptr
2.1智能指针原理:
这里只提供基础的原理,代码内仍存在许多问题需要我们去解决
代码:
template<class T>
class smartptr {
public:
smartptr(T*ptr):
_ptr(ptr)
{}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~smartptr()
{
delete _ptr;
}
private:
T* _ptr;
};
提供了两个接口:* 与 -> ,能让我们访问到其中的资源。
class Data {
public:
int _year=1;
int _month1;
int _day=1;
};
int main() {
Auto_ptr<Data> ap(new Data);
ap->_day;//这里编译器给我们进行优化了
return 0;
}
2.2在使用时的编译器的优化:
Auto_ptr<Data> ap(new Data);
ap->_day;//这里编译器给我们进行优化了优化:
ap-> 这一步的操作是获取到Data*的地址,需要再次通过Data* ->去访问Data内的元素原本需要ap->->day才能获取到_day,但是这样写不便于阅读,所以编译器会进行优化。
3.auto_ptr
3.1拷贝问题--移交管理权
假如遇到拷贝会导致两个指针指向同一片资源,在释放的时候,如果不加以控制,那么在释放资源时,一个先释放,另一个再去释放时,就会访问到野指针,发生错误。
所以为了防止这种错误,我们可以采取策略:移交管理权
体现在代码上为:
template<class T>
class smartptr {
public:
smartptr(T*ptr):
_ptr(ptr)
{}
smartptr(smartptr<T>&ap):
_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~smartptr()
{
if (_ptr) {
delete _ptr;
}
}
private:
T* _ptr;
};
3.2赋值导致的内存泄漏
赋值存在一种情况,那么就是这两个auto_ptr不能是同一份对象,如果是同一份对象的情况不存在(管理权转移)。
如下图中的情况
因为这两个auto_ptr<int>类型一样,所以允许赋值操作
所以我们还需要处理一下赋值重载问题。
将自己指向其他的资源,并将原来的资源释放掉
Auto_ptr& operator=(Auto_ptr<T>& ap) {
//检查是否自己给自己赋值
if (this!=&ap) {
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
template<class T>
class Auto_ptr {
public:
Auto_ptr(T*ptr=nullptr):
_ptr(ptr)
{}
Auto_ptr(Auto_ptr<T>&ap):
_ptr(ap._ptr)
{
ap._ptr = nullptr;
}
Auto_ptr& operator=(Auto_ptr<T>& ap) {
//检查是否自己给自己赋值
if (this!=&ap) {
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~Auto_ptr()
{
if (_ptr) {
delete _ptr;
}
}
private:
T* _ptr;
};
class Data {
public:
int _year=1;
int _month1;
int _day=1;
};
int main() {
Auto_ptr<Data> ap(new Data);
Auto_ptr<Data> cp;
cp = ap;
return 0;
}
3.2遗留问题
通过移交管理权,我们可以把之前的智能指针悬空,但这又带来了另一个问题,但是用的人并不知道它已经悬空了。因此不推荐使用auto_ptr 。
4.unique_ptr
unique_ptr的解决方法很简单,既然你的拷贝会出现问题,那么你就不要拷贝了,不拷贝就不会出现问题。所以unique_ptr用在不需要拷贝的场景下。
template<class T>
class unique_ptr {
public:
unique_ptr(T* ptr):
_ptr(ptr)
{
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~unique_ptr()
{
delete _ptr;
}
private:
unique_ptr(unique_ptr* ptr) = delete;
unique_ptr& operator=(unique_ptr& p) = delete;
T* _ptr;
};
5.shared_ptr
5.1原理
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减 一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对 象就成野指针了。
template<class T>
class shared_ptr {
public:
shared_ptr(T* ptr):
_ptr(ptr),
_count(new size_t(1))
{
}
shared_ptr(const shared_ptr& sp):
_ptr(sp._ptr),
_count(sp._count)
{
_count++;
}
shared_ptr& operator=(const shared_ptr&sp) {
if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
//不是同一个资源 this = sp。
//如果本资源引用计数为0,就释放资源
if (--(*count) == 0) {
delete _ptr;
delete _count;
}
}
_ptr = sp._count;
_count = sp._count;
*_count++;
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
~shared_ptr()
{
if (--(*count)== 0) {
delete _ptr;
_ptr = nullptr;
delete _count;
_count = nullptr;
}
}
private:
T* _ptr;
size_t* _count;//必须为指针类型,且不能使用static修饰
};
size_t *count 必须是指针类型,而且不能为static修饰
首先如果不是指针而是局部变量,那么这个引用计数不会被修改
其次,如果为 static修饰,就会让整个class类共享同一个计数器,那么现在有两份空间
我们分别用两个shard_ptr指向不同的空间,那么这个计数器就会变为2,不符合我们的要求,我们应该要求一份资源一个计数器。
所以应该使用指针,并且拷贝时,不要忘记指针的拷贝
5.2赋值重载
其中主要注意赋值重载---
针对第二种情况,需要先判断这两个share是不是指向同一个空间,如果不是的话,就更改被赋值的引用计数,并且判断是否需要清理掉该资源。
再让它指向sp所指向的对象,且让sp的count引用计数+1
shared_ptr& operator=(const shared_ptr&sp) {
if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
//不是同一个资源 this = sp。
//如果本资源引用计数为0,就释放资源
if (--(*count) == 0) {
delete _ptr;
delete _count;
}
_ptr = sp._count;
_count = sp._count;
*_count++;
}
}
5.3线程安全问题
shared_ptr需要加锁来保证自身的线程安全问题。
但是它内部管理的对象不一定是线程安全的,所以要想保证内部也是线程安全的,则需要另一把锁,将管理对象加锁。
1. 智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时 ++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错 乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁 的,也就是说引用计数的操作是线程安全的。
2. 智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题。
template<class T>
class shared_ptr_ {
public:
shared_ptr_(T* ptr):
_ptr(ptr),
_count(new size_t(1)),
_cmux(new mutex)
{
cout << "shared->" << _ptr << endl;
}
shared_ptr_(const shared_ptr_<T>& sp):
_ptr(sp._ptr),
_count(sp._count),
_cmux(sp._cmux)
{
countAdd();
}
shared_ptr_& operator=(const shared_ptr_<T>&sp) {
if (sp._ptr!=_ptr) {//必须判断指向的是不是同一片资源,而不是判断判断this==&sp。
//不是同一个资源 this = sp。
//如果本资源引用计数为0,就释放资源
Destory();
_ptr = sp._ptr;
_count = sp._count;
_cmux = sp._cmux;
countAdd();
}
return *this;
}
T& operator*() {
return *_ptr;
}
T* operator->() {
return _ptr;
}
T* get() const{
return _ptr;
}
~shared_ptr_()
{
Destory();
}
void countAdd() {
_cmux->lock();
(*_count)++;
_cmux->unlock();
}
void Destory() {
_cmux->lock();
if (--(*_count) == 0) {
delete _ptr;
delete _count;
//delete _cmux;
cout << "~shared->" << _ptr << endl;
}
_cmux->unlock();
}
private:
T* _ptr;
size_t* _count;//必须为指针类型,且不能使用const修饰
mutex *_cmux;
};
5.4循环引用问题
//循环引用
struct ListNode {
shared_ptr_<ListNode> _next;
shared_ptr_<ListNode> _prev;
};
int main() {
shared_ptr_<ListNode> node1(new ListNode);
shared_ptr_<ListNode> node2(new ListNode);
node1->_next = node2;
node2->_prev=node1;
return 0;
}
1. node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动 delete。
2. node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
3. node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上 一个节点。
4. 也就是说_next析构了,node2就释放了。
5. 也就是说_prev析构了,node1就释放了。
6. 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev 属于node2成员,所以这就叫循环引用,谁也不会释放。
造成这个原因是因为引用计数无法被释放。
所以解决办法也很简单,既然是引用计数问题,那么我们就创建一个辅助的类,来帮助我们解决引用计数问题。
weak_ptr
1.不想常规的智能指针,不支持RAII
2.支持像指针一样
3.专门设计出来,辅助解决shared_ptr循环引用问题
template<class T>
class weak_ptr_ {
public:
weak_ptr_():
_spt(nullptr)
{
}
weak_ptr_(shared_ptr_<T>* spt) :
_spt(spt->get())
{}
T& operator*() {
return *_spt;
}
T* operator->() {
return _spt;
}
private:
T *_spt;
};
5.5 定制删除器
本文的所有智能指针都还不健全,假如我们使用shared_ptr sp(new int(10));又或者shared_ptr fp(fopen(""))...我们会发现在析构时,如果使用delete 释放资源,会出错。因此需要我们传递一个删除器以供它进行删除。在此我们就针对shared_ptr来进行修改。
template<class Del>
shared_ptr_(T* ptr = nullptr,Del del=_del) :
_ptr(ptr),
_count(new size_t(1)),
_cmux(new mutex),
_del(del)
{
cout << "shared->" << _ptr << endl;
}
private:
function<void(T* ptr)> _del = [](T* ptr) {
delete ptr;
};
int main() {
shared_ptr_<int> sp(new int[10], [](int *ptr) {
delete []ptr;
});
}
在类内使用函数模板,(自动推导),我们在初始化时,给出我们自己的想要的删除方式。
auto_ptr在进行赋值重载时,会让原来的智能指针悬空
unque_ptr 不允许拷贝,解决了auto_ptr的缺点
shared_ptr 采用引用计数的方式,允许拷贝,但是会出现循环引用的问题,所以就需要引入新的方式来解决循环引用,即使用weak_ptr去辅助shared_ptr。另外shared_ptr需要注意本身的线程安全问题,需要加锁修改引用计数