智能指针主要用来释放资源,防止某些情况下导致内存泄漏
当我们使用new关键字在堆上申请了一块空间,而没有进行delete,这就造成了内存泄漏;还有如果fopen打开文件后,没有关闭。这也造成了隐患。
当然上面这一些明显的错误,还有一些难以察觉的错误。
int*p= new int (0);
fun();
delete p;
//在执行fun函数的时候,发生异常情况,直接退出,而此时申请的资源还没有释放,而且不会再执行到下面的delete p这个语句,也就造成了内存泄漏
而通过Effecitive C++,我们可以了解到以对象管理系统资源,其实也就是用到了我们今天的智能指针,利用 对象生命资源周期 来控制资源的释放 即RAII
智能指针就是一个基于RAII思想的一个管理资源的类,把需要释放的资源放到类对象中,在类对象生命周期结束时,自动释放资源
基于上面的思想,我们可以自己设计一个简单的类来初步实现上面的想法
template
class smart_pointer{
public:
smart_pointer( T* ptr)
:_ptr(ptr)
{}
~smart_pointer()
{
std::cout << "资源释放完毕" << std::endl;
delete _ptr;
_ptr = nullptr;
}
T& operator* ()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
我们可以将资源放到我们写的smartpointer类对象中,不需要我们手动释放资源;当smartpointer对象出作用域后,自动释放资源,并且能够通过类对象访问资源;但是还存在一个致命问题:倘若一份资源保存到两个smartpointer类中,就会将同一个资源析构两次即出现错误double free
为了避免出现上面的情况,C++实现了智能指针auto_ptr、unique_ptr、shared_ptr;我们下面也将就这三个只能指针就行了解
特点:转移对资源的管理权
若通过拷贝构造函数或赋值拷贝操作符复制auto_ptr,他们就会变成nullptr,而复制所得的指针将取得资源的唯一拥有权!
int* p = new int(7); //资源p auto_ptr
ap1(p); //ap1管理资源p auto_ptr ap2(ap1); //ap2管理资源p,ap1为nullptr auto_ptr ap3 = ap2; //ap3管理资源p,ap2为nullptr 如果有疑惑,可参考一下实现这种功能的下列代码
template
class auto_ptr{ public: auto_ptr(T* ptr) :_ptr(ptr) {} ~auto_ptr() { std::cout << "资源释放完毕" << std::endl; if (_ptr!=nullptr) delete _ptr; _ptr = nullptr; } auto_ptr(auto_ptr & autoptr) { _ptr = autoptr._ptr; autoptr._ptr = nullptr; } auto_ptr & operator=(auto_ptr & autoptr) { if (_ptr != autoptr._ptr) { if (_ptr!=nullptr) delete _ptr; _ptr = autoptr._ptr; autoptr._ptr = nullptr; } return *this; } T& operator* () { return *_ptr; } T* operator->() { return _ptr; } private: T* _ptr; };
int* p = new int(7);
auto_ptr ap1(p);
auto_ptr ap2(p);
将资源p同时用两个auto_ptr管理,这也会导致刚才我们所说的同一资源析构两次,而对于这种情况而言,库函数没有相应的措施应对。
所以,我们在使用智能指针的时候,一定要注意这个问题:
别让多个智能指针同时指向同一对象。如果多个指向同一对象,会被删除一次以上,导致不确定的行为,程序会崩溃。
对于问题一的解决方法,还有其余智能指针
特点:禁止使用赋值拷贝和赋值运算符函数
即在代码中使用C++11的delete
unique_ptr(unique_ptr& up)=delete;
unique_ptr
& operator=(unique_ptr & up) = delete;
特点:引用计数型指针(RCSP) ,
对管理资源的智能指针进行计数,在无智能指针管理这个资源时会自动删除该资源。
实现原理
template
class shared_ptr{
void add_count()
{
_pmutex->lock();
++(*_pcount);
_pmutex->unlock();
}
void release()
{
bool flag = false;
_pmutex->lock();
--(*_pcount);
if ((*_pcount) == 0)
{
if (_ptr!=nullptr)
delete _ptr;
if (_pcount!=nullptr)
delete _pcount;
_ptr = nullptr;
_pcount = nullptr;
flag = true;
}
_pmutex->unlock();
if (flag == true)
{
delete _pmutex;
_pmutex = nullptr;
}
}
public:
shared_ptr(T* ptr=nullptr)
:_ptr(ptr)
, _pcount(new int(1))
, _pmutex(new mutex)
{}
~shared_ptr()
{
release();
}
shared_ptr(shared_ptr& sharedptr)
{
_ptr = sharedptr._ptr;
_pcount = sharedptr._pcount;
_pmutex = sharedptr._pmutex;
add_count();
}
shared_ptr& operator=(shared_ptr& sharedptr)
{
if (_ptr != sharedptr._ptr)
{
release(); //将原来管理的资源的引用计数减1;
_ptr = sharedptr._ptr;
_pcount = sharedptr._pcount;
_pmutex = sharedptr._pmutex;
add_count();//将现在管理的资源的引用计数加1;
}
return *this;
}
T* get_ptr() const
{
return _ptr;
}
T& operator* ()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;
std::mutex * _pmutex;
};
由shared_ptr的实现原理,我们发现它是线程安全的,但是shared_ptr还存在一个魔咒,那就是环形引用;
而想解决环形引用这个问题,就需要提到weak_ptr
weak_ptr实现原理:
template
class weak_ptr{
weak_ptr() = default;
weak_ptr(const shared_ptr& p)
{
_ptr = p.get_ptr();
}
weak_ptr& oprator = (const shared_ptr& p)
{
_ptr = p.get_ptr();
return *this;
}
T& operator* ()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
就上面的环形引用,我们现在看一下这个场景
struct ListNode{
int data;
smartpointer::shared_ptr _prev;//smartpointer::weak_ptr _prev;
smartpointer::shared_ptr _next;//smartpointer::weak_ptr _next;
~ListNode()
{
cout << "析构" << endl;
}
};
int main()
{
smartpointer::shared_ptr p1(new ListNode);
smartpointer::shared_ptr p2(new ListNode);
p1->_prev = p2;
p2->_next = p1;
return 0;
}
相当于一个链表不过是一个环形的,而在这种情况下,shared_ptr就不能很好地完成释放资源的任务,原因就在于shared_ptr的特性,当引用计数为0时,自动释放资源,而在环形引用的情况下,计数引用不可能为0;
而weak_ptr可以解决这种环形引用的情况,是因为weak_ptr实际是封装了shared_ptr,不会使shared_ptr的计数器加一.
通过观察上面的实现代码,我们发现一点构造函数调用new,析构函数也调用delete;如果我们使用new[] 申请资源,并用上面的智能指针管理,程序会崩溃! 那我们对new[]这种方法申请的资源怎么办呢?还是想以前一样的老方法嘛!
其实不急!智能指针也可以解决,智能你给一个定制的删除器,就是自己写一个对应释放资源的仿函数,传入智能指针即可
template
class Delete{
public:
void operator()(T* p)
{
delete [] p;
}
};
int* p = new int[7];
std::shared_ptr p1(p, Delete());
最后结合Effective C++探讨一下管理资源的问题:
- 我们要使用对象管理资源,如智能指针
- 为了防止内存泄漏,我们申请到资源,就立即放到智能指针中进行管理;防止因异常等情况的造成的内存泄漏发生
- new和delete、new [] 和delete [] 要成对使用,在使用智能指针时,我们要判断是否要传入定制删除器
- 以独立语句将new对象放到智能指针对象中
int priority();
void processWidget(std::shared_ptr (new Widget) , int priority )//Widget是一个类
在调用processWidget之前,编译器必须创建代码,做以下三种事情:
- 调用priority 函数
- 执行new Widget
- 调用 shared_ptr构造函数
C++编译器以什么样的次序完成这件事情呢?不确定
可以确定的是 new Widget 一定执行于tr1::shared_ptr构造函数被调用之前,因为这个表达式的结果还要被传递作为shared_ptr构造函数的一个实参,但对priority的调用可以排在第一、第二或第三。
如果第二位执行:
执行new Widget
调用priority
调用 shared_ptr构造函数
调用priority异常,极有可能造成资源泄漏。在此情况下 new Widget 的返回指针遗失,尚未放入shared_ptr(我们用来防止资源泄漏的武器)
所以这就有了我们的忠告 :以独立语句将new对象放到智能指针对象
即:
shared_ptr
pw(new Widget) void processWidget(shared_ptr
pw , int priority )