c++11 :智能指针
目录
一 为什么需要智能指针?
二 智能指针的使用及原理
1. RAII
2. auto_ptr
3. unique_ptr
5. weak_ptr
三 内存泄漏
1.什么是内存泄漏,内存泄漏的危害
2. 如何避免内存泄漏?
一 为什么需要智能指针?
🚀为什么需要智能指针? 下面我们先分析一下下面这段程序有没有什么内存方面的问题?
#include <iostream>
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)//抛异常throw invalid_argument("除0错误");return a / b;
}
void f1()
{int* p = new int;cout << div() << endl;delete p;
}
int main()
{//捕异常try{f1();}catch (exception& e){cout << e.what() << endl;}return 0;
}
运行结果
通过上面的程序中我们可以看到,new了以后,而且也delete了,但是因为抛异常有点早,程序执行不到delete的位置,所以就导致内存泄露了,此外如果我们在写代码的过程中忘了释放资源的话也会导致内存泄漏。为了解决上述问题,接下来引入智能指针。
🍉:我们首先写一个类
#pragma oncetemplate<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){if (_ptr){std::cout << "delete" << _ptr<<std::endl;delete _ptr;}}
private:T* _ptr;
};
#include <iostream>
#include "SmartPtr.h"
using namespace std;int div()
{int a, b;cin >> a >> b;if (b == 0)//抛异常throw invalid_argument("除0错误");return a / b;
}
void f1()
{/// 修改部分,将指针存储在SmartPtr这个类中int* p = new int;SmartPtr<int> sp(p);cout << div() << endl;//delete p;
}
int main()
{//捕异常try{f1();}catch (exception& e){cout << e.what() << endl;}return 0;
}
测试结果:
上面我们通过创建一个类SmartPtr ,让 SmartPtr sp(p)对p进行管理资源的释放。无论函数正常结束,还是抛异常,都会导致sp对象的生命周期到了以后,调用析构函数~SmartPtr()释放内存。
上述我们通过类对资源p进行管理,帮我们管理资源的释放,这个类我们就叫智能指针。
二 智能指针的使用及原理
1. RAII
RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、互斥量等)的简单技术。
在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的 时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处:
- 不需要显式地释放资源。
- 采用这种方式,对象所需的资源在其生命期内始终保持有效。
RAII和智能指针的关系:RAII是一种托管资源的思想,智能指针就是依靠这种RAII实现的。
观察上述代码
void f1()
{/// 修改部分,将指针存储在SmartPtr这个类中int* p = new int;SmartPtr<int> sp(p);cout << div() << endl;//delete p;
}
我们还可以直接这样
SmartPtr<int> sp(new int);/修改后的cout << div() << endl;
但是这样的话我们又会面临一个问题那就是 如果我们想要访问这个指针变量,我们需要再加以下成员函数。
T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
这样的话我们就可以访问和修改指针变量了
SmartPtr<int> sp1(new int);*sp1 = 10;SmartPtr<pair<int, int>> sp2(new pair<int, int>);sp2->first = 20;sp2->second = 30;
并且会自动释放内存
以上是智能指针的简单demo,但是上述代码还存在很多问题,我们通过以下代码进行测试
int main()
{SmartPtr<int> sp1(new int);SmartPtr<int> sp2 = sp1;//拷贝构造sp2(sp1);return 0;
}
测试结果:
🍎问题分析:我们并没有给SmartPtr构造拷贝构造函数,编译器会自动生成默认拷贝构造即值拷贝(浅拷贝),sp1---------->资源, sp2----------->资源 ,当sp1和sp2出了作用域会对资源进行析构,即sp2先对资源进行析构,sp1又对同一份资源进行了析构,同一份资源不能析构二次,因为第一次析构以及释放了所以出现了报错。
上述原因就在于浅拷贝造成了报错,但是我们不能说即然浅拷贝有问题,我们进行深拷贝不就行了吗?答案是不可以的,智能指针是用来模拟原生指针的 ,原生指针 p1=p2;就是值拷贝代表着指向同一块空间。
接下来我们引出解决上述问题的三种解决方法:
- 管理器转移:c++98 auto_ptr
- 防拷贝: c++11 unique_ptr
- 引言计数 c++11 shared_ptr (循环引言的问题,又需要weak_ptr来解决)
2. auto_ptr
我们对auto_ptr的拷贝函数进行构造
//拷贝构造auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}
测试如下:
int main()
{lt::auto_ptr<int> sp1(new int);lt::auto_ptr<int> sp2 = sp1;//拷贝构造sp2(sp1);return 0;
}
为什么只析构一次,并且为什么auto_ptr叫做管理权转移呢? 我们通过下图进行描述
管理权转移:早期c++98设计缺陷,因为它会把其他指针置为空,不建议使用。
3. unique_ptr
unique_ptr的实现原理:简单粗暴的防拷贝
unique_ptr(unique_ptr<T>& up) = delete;unique_ptr<T>& operator==(unique_ptr<T>& up) = delete;
但是unique也有缺陷,如果有需要拷贝的场景,就无法使用。 所以c++11又搞出一个智能shared_ptr.
4. shared_ptr
shared_ptr :是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
- shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
- 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。 3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源; 4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指 针了。
#pragma oncenamespace lt
{template<class T>class shared_ptr{public:shared_ptr(T* ptr):_ptr(ptr),_pcount(new int(1)){}shared_ptr(shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (this != &sp){if (--*(_pcount) == 0){delete _ptr;delete _pcount;}_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount); }}T& operator*()//const 这里加是为了const this 指针。不能说(const){return *_ptr;}T* operator->(){return _ptr;}~shared_ptr(){if (--(*_pcount)==0 && _ptr){std::cout << "delete" << _ptr << std::endl;delete _ptr;_ptr = nullptr;delete _pcount;_pcount = nullptr;}}private:T* _ptr;int* _pcount;};
}
接下来我们侧重讲一下拷贝构造
这里有个注意的点就是当计数为1进行拷贝是需要注意 delete _ptr delete _pcount
5. weak_ptr
shared_ptr⼤多数情况下管理资源⾮常合适,⽀持RAII,也⽀持拷⻉。但是在循环引⽤的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引⽤的场景和资源没释放的原因,并且学会使⽤weak_ptr解决这种问题。
struct listNode
{int _data;shared_ptr<listNode> _next;shared_ptr<listNode> _prev;};
如图所示:
- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
- 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
- _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
此逻辑上成功形成回旋镖似的循环引⽤,谁都不会释放就形成了循环引⽤,导致内存泄漏。
解决方法:把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr 不增加它的引⽤计数,就成功打破了循环引⽤,这就解决了这⾥的问题。
🍏weak_ptr 的简单实现
template<class T>class weak_ptr{public:weak_ptr() = default;weak_ptr(shared_ptr<T>& sp):_ptr(sp.get_ptr()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get_ptr();return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;} private:T* _ptr;};
}
weak_ptr严格来说不是智能指针,因为它没有RAII资源管理,weak_ptr是用来专门解决shared_ptr循环引用造成的问题。我们知道ListNode这个结点本身放在智能指针shared_ptr是没有什么问题的,可以很好的对资源ListNode*进行管理,但是就是因为ListNode 结点中还包含 _next ,_prev这就造成了循环引用。我们希望的是ListNode内的指针不参与计数,
所以我们创建了weak_ptr(shared_ptr<T>& sp) ,目的就是希望把_next,_prev指针存储再weak_ptr中不参与计数。
struct ListNode
{int val;lt::weak_ptr<ListNode> _spnext;lt::weak_ptr<ListNode> _spprev;~ListNode(){cout << "~ListNode()" << endl;}
};
int main()
{lt::shared_ptr<ListNode> spn1(new ListNode);lt::shared_ptr<ListNode> spn2(new ListNode);spn1->_spnext = spn2;//不想参与计数所以我们希望把spn2(shared_ptr<ListNode>)赋值给spn1->_spnext(weak_ptr<ListNode>)//这种操作不计数//所以我们构造了 _spnext为lt::weak_ptr<ListNode>类型,并且构造了weak_ptr<T>& operator=(const shared_ptr<T>& sp)//仅仅把指针拷贝不计数。spn2->_spprev = spn1;return 0;
}
三 内存泄漏
1.什么是内存泄漏,内存泄漏的危害
- 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使⽤的内存,⼀般是忘记释放或者发⽣异常释放程序未能执⾏导致的。内存泄漏并不是指内存在物理上的消失,⽽是应⽤程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因⽽造成了内存的浪费
- .内存泄漏的危害:普通程序运⾏⼀会就结束了出现内存泄漏问题也不⼤,进程正常结束,⻚表的映射关系解除,物理内存也可以释放。⻓期运⾏的程序出现内存泄漏,影响很⼤,如操作系统、后台服务、⻓时间运⾏的客⼾端等等,不断出现内存泄漏会导致可⽤内存不断变少,各种功能响应越来越慢,最终卡死
2. 如何避免内存泄漏?
- 尽量使⽤智能指针来管理资源
- 定期使⽤内存泄漏⼯具检测