Lesson 19 -- 智能指针
1. 内存泄漏
1.1 什么是内存泄漏,内存泄漏的危害
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
1.2 内存泄漏分类
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
2. 智能指针的使用和原理
2.1 RAII
是利用对象生命周期来控制程序资源。在对象构造时获取资源,最后在对象析构时释放资源,就是把资源托管给另一个对象管理。
2.2 智能指针的原理
- RAII特性
- 重载operator*和operator-> ,具有像指针一样的行为
2.3 std::auto_ptr
不是浅拷贝,是资源管理权限转移,导致被拷贝对象悬空
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 管理权转移
sp._ptr = nullptr;
}
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
// 检测是否为自己给自己赋值
if (this != &ap)
{
// 释放当前对象中资源
if (_ptr)
delete _ptr;
// 转移ap中资源到当前对象中
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
// 结论:auto_ptr是一个失败设计,很多公司明确要求不能使用auto_ptr
//int main()
//{
// std::auto_ptr<int> sp1(new int);
// std::auto_ptr<int> sp2(sp1); // 管理权转移
//
// // sp1悬空
// *sp2 = 10;
// cout << *sp2 << endl;
// cout << *sp1 << endl;
// return 0;
//}
2.4 std::unique_ptr
防拷贝。
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;
private:
T* _ptr;
};
//int main()
//{
// /*bit::unique_ptr<int> sp1(new int);
// bit::unique_ptr<int> sp2(sp1);*/
//
// std::unique_ptr<int> sp1(new int);
// //std::unique_ptr<int> sp2(sp1);
//
// return 0;
//}
2.5 std::shared_ptr
原理:通过引用计数的方式来实现多个shared_ptr对象之间共享资源
- 在内部,给每个资源都维护一份计数,用来记录该份资源被几个对象共享
- 在对象被销毁时,就说明自己不使用该资源了,对象的引用计数减1
- 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
- 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成了野指针
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr == nullptr)
:_ptr(ptr)
,_pcount(new int(1))// 每个资源需要管理时,会给构造函数,就new一个计数
{}
shared_ptr(shared_ptr<T>& sp)
:_ptr(sp._ptr)
,_pcount(sp._pcount)
{
(*_pcount)++;
}
shared_ptr<T>& operator=(shared_ptr<T>& sp)
{
if(_ptr==sp._ptr)
return *this;
// 如果是最后一个对象,需要释放资源
if(--(*_pcount)==0)
{
delete _ptr;
delete _pcount;
}
_ptr=sp._ptr;
_pcount=sp._pcount;
(*_pcount)++;
return *this;
}
~shared_ptr()
{
if(--(*_pcount)==0)
{
delete _ptr;
delete _pcount;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pcount;// 静态计数对象,不可以,要求一个资源配一个计数,多个智能指针对象共同管理,但是静态的,所有资源都只有一个计数,
};
循环引用
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_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;
}
分析:
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、但是node1释放,_next才会析构,而node1由_prev管理,所以叫做循环引用
解决方案:
在引用计数的场景下,将节点中的_prev和_next改为weak_ptr
就可以。原理就是相互指向时,weak_ptr
的_next和_prev不会增加引用计数。weak_ptr
主要由shared_ptr构造的,解决循环引用问题