智能指针
为什么要提出智能指针的概念?能够解决什么问题?优点是什么?
智能指针的提出主要是解决内存泄漏的问题
那么何为内存泄漏呢?
-
内存泄漏
内存泄漏并不是物理内存空间的丢失,而是应用程序分配某段内存空间后,由于疏忽或设计错误导致程序未能释放已经不再使用的内存空间,失去对这部分空间的控制,从而造成内存泄漏
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
-
内存泄漏的分类
-
堆内存泄漏(heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak
- 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
-
内存泄漏解决方法
- 作为一位合格的程序猿或者攻城狮,工程前期就需要养成良好的代码书写规范,以及工程设计规范,凡是涉及到手动内存开辟,一定要记得及时释放,切莫置之不理。但这毕竟是一种理想状态,人为开辟内存或释放肯定会存在人为疏忽,对此,需要借助一种智能指针自动完成。
- 借助RALL思想完成对智能指针的设计
- 借助一下内存检测工具检查程序段是否存在内存泄漏
本文重点介绍通过智能指针如何解决内存泄漏
智能指针的设计主要依靠RALL思想,所谓RALL思想就是通过对象的生命周期来控制资源回收
简言之,把一份资源管理的责任托管给了对象,不再需要显示释放资源而且采用这种方式,对象所需的资源在其生命期内始终保持有效。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。
-
常见的智能指针
- 智能指针smart_ptr
代码如下:
#include<iostream>
using namespace std;
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
void test()
{
SmartPtr<int> p(new int);
//要求必须立即初始化,以下并非立即初始化 写法错误:
/*int* ptr = new int;
SmartPtr<int> p(ptr);*/
*p = 10;
cout << *p << endl;
SmartPtr<Date> sparray(new Date);
sparray->_year = 2021;
sparray->_month = 9;
sparray->_day = 21;
cout << (*sparray)._year << (*sparray)._month << (*sparray)._day << endl;
return;//return后对象生命周期结束,调用析构函数自动释放空间
cout << "test..." << endl;
}
int main()
{
test();
return 0;
}
- Auto_ptr(一般不推荐使用)
实现管理权转移,当发生拷贝或赋值过程,前面的对象就被置空,将原有对象资源转移到当前对象,这虽然解决了一块内存空间多个对象调用导致崩溃的问题,但会使得原有对象失效,后续无法再调用
代码如下:
#include<iostream>
using namespace std;
template<class T>
class AutoPtr
{
public:
AutoPtr(T* ptr = NULL)
: _ptr(ptr)
{}
~AutoPtr()
{
if (_ptr)
delete _ptr;
}
// 一旦发生拷贝,就将ap中资源转移到当前对象中,然后另ap与其所管理资源断开联系,
// 这样就解决了一块空间被多个对象使用而造成程序奔溃问题
AutoPtr(AutoPtr<T>& ap)
: _ptr(ap._ptr)
{
ap._ptr = NULL;
}
AutoPtr<T>& operator=(AutoPtr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
struct Date
{
int _year;
int _month;
int _day;
};
int main()
{
AutoPtr<Date> ap(new Date);
// 现在再从实现原理层来分析会发现,这里拷贝后把ap对象的指针赋空了,导致ap对象悬空
// 通过ap对象访问资源时就会出现问题。
AutoPtr<Date> copy(ap);
ap->_year = 2020;
return 0;
}
- unique_ptr(基于auto_ptr存在问题而提出)
基本思想就是直接屏蔽拷贝和赋值
#include<iostream>
using namespace std;
template<class T>
class Unique_Ptr {
public:
Unique_Ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~Unique_Ptr()
{
if (_ptr)
delete _ptr;
}
//防止拷贝delete
Unique_Ptr(const Unique_Ptr<T>& ptr) = delete;
Unique_Ptr<T>& operator=(const Unique_Ptr<T>& ptr) = delete;
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
- shared_ptr
shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。
1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
代码如下:
#include<iostream>
using namespace std;
template<class T>
class sharedptr
{
public:
sharedptr(T* ptr)
:_ptr(ptr)
,_pRefCount(new int(1))
{}
sharedptr(const sharedptr<T>& s)
:_ptr(s._ptr)
, _pRefCount(s._pRefCount)
{
(*_pRefCount)++;
}
sharedptr<T>& operator=(const sharedptr<T>& sp)
{
if (this != &sp)
{
// 释放管理的旧资源
if (--(*_pRefCount) == 0) //注意:此处计数器指针与下面的计数器指针并非指向同一个 一个指向原有资源计数,另一个指向新资源计数
{ //赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1
delete _ptr;
delete _pRefCount;
}
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
(*_pRefCount)++;
}
return *this;
}
~sharedptr()
{
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
}
}
int UseCount() { return *_pRefCount; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
int* _pRefCount; // 引用计数,
T* _ptr; // 指向管理资源的指针
};
int main()
{
sharedptr<int> sp1(new int(10));
sharedptr<int> sp2(sp1);
*sp2 = 20;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
sharedptr<int> sp3(new int(10));
sp2 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
sp1 = sp3;
cout << sp1.UseCount() << endl;
cout << sp2.UseCount() << endl;
cout << sp3.UseCount() << endl;
return 0;
}
注意:
- 计数器定义为指针,目的是保证不同对象析构后,计数器值要同步更新
- 在赋值运算符重写过程中 此处计数器指针与下面的计数器指针并非指向同一个 一个指向原有资源计数,另一个指向新资源计数。赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1
- shared_ptr循环引用
代码如下:
#include<iostream>
using namespace std;
template<class T>
class sharedptr
{
public:
sharedptr(T* ptr)
:_ptr(ptr)
,_pRefCount(new int(1))
{}
sharedptr(const sharedptr<T>& s)
:_ptr(s._ptr)
, _pRefCount(s._pRefCount)
{
(*_pRefCount)++;
}
sharedptr<T>& operator=(const sharedptr<T>& sp)
{
if (this != &sp)
{
// 释放管理的旧资源
if (--(*_pRefCount) == 0) //注意:此处计数器指针与下面的计数器指针并非指向同一个 一个指向原有资源计数,另一个指向新资源计数
{ //赋值过程中,被赋值的对象切断原有资源指向,且原有资源计数-1,开始指向新资源,新资源计数+1
delete _ptr;
delete _pRefCount;
}
// 共享管理新对象的资源,并增加引用计数
_ptr = sp._ptr;
_pRefCount = sp._pRefCount;
(*_pRefCount)++;
}
return *this;
}
~sharedptr()
{
if (--(*_pRefCount) == 0)
{
delete _ptr;
delete _pRefCount;
}
}
int UseCount() { return *_pRefCount; }
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
int* _pRefCount; // 引用计数,
T* _ptr; // 指向管理资源的指针
};
struct ListNode
{
int _data;
sharedptr<ListNode> _prev;
sharedptr<ListNode> _next;
ListNode()
:_prev(nullptr)
,_next(nullptr)
,_data(10)
{}
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
sharedptr<ListNode> node1(new ListNode);
sharedptr<ListNode> node2(new ListNode);
cout << node1.UseCount() << endl;
cout << node2.UseCount() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.UseCount() << endl;
cout << node2.UseCount() << endl;
return 0;
}
node1->_next = node2;和node2->_prev = node1;时 导致引用计数增加为2
出现问题:
node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。也就是说_next析构了,node2就释放了。 _prev析构了,node1就释放了。
但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁都不会释放资源
- 采用weak_ptr解决上述问题
解决方案:在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了
原理就是,node1->_next = node2;和node2->_prev = node1;时weak_ptr的_next和_prev不会增加node1和node2的引用计数。
struct ListNode
{
int _data;
weak_ptr<ListNode> _prev;
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}