1、智能指针前提知识
1.1 为什么需要智能指针?
- 在c++中进行动态内存申请的过程中,容易忘记delete,
- 即使自己没有忘记,但是因为有异常的抛出,所以也不能保证内存进行完全的释放。
- malloc / new 申请的空间,未得到释放,造成内存泄漏
1.2 简单理解内存泄漏
- 对开辟的空间未得到释放,导致应用程序对该空间失去控制
- 频繁的开辟空间,没有得到释放,会造成内存的碎片化
1.3 内存泄漏的分类
- 堆内存泄漏:程序执行开辟中new / malloc / realloc开辟的空间,没有释放
- 系统资源泄漏:系统分配的资源,例如:套接字、文件描述符,没有释放
1.4 内存泄漏解决方案
- 事前预防型。例如:智能指针
- 事后查错型。例如:泄漏检测工具
2、智能指针的原理
2.1 RAII(资源获取即初始化)
利用对象生命周期来控制程序资源。
其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。
RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
综上所述,RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源必被释放。
2.2 为什么要使用RAII?
RAII是用来管理资源、避免资源泄漏的方法。
在计算机系统中,资源是数量有限且对系统正常运行具有一定作用的元素。比如:网络套接字、互斥锁、文件句柄和内存等等,它们属于系统资源。由于系统的资源是有限的,所以,我们在编程使用系统资源时,都必须遵循一个步骤:
- 1 申请资源;
- 2 使用资源;
- 3 释放资源。
第一步和第三步缺一不可,因为资源必须要申请才能使用的,使用完成以后,必须要释放,如果不释放的话,就会造成资源泄漏。
2.3 具有指针类似的行为。
例如:operator *() 、operator->()
template<class T>
class SmartPtr
{
public:
SmartPtr(T* ptr = nullptr) // 构造函数
: _ptr(ptr)
{}
~SmartPtr()
{
if(_ptr){
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() // operator*
{
return *_ptr;
}
T* operator->() // operator->
{
return _ptr;
}
private:
T* _ptr;
};
3、auto_ptr
3.1 auto_ptr的使用
检测: C++11中的auto_ptr使用的是资源转移实现的
void TestAuto()
{
auto_ptr<int> ap(new int);
auto_ptr<int> ap2(ap);
if (ap.get() == nullptr){
cout << "C++11中的auto_ptr使用的是资源转移实现的" << endl;
}
}
3.2 拷贝问题
浅拷贝问题
- 默认的拷贝构造函数是浅拷贝,一旦涉及到动态分配,就会出现问题。
- 例如:多个对象指向同一块资源,在释放的时候,多个对象都对该快资源进行释放,即造成了double free的错误。
可不可以使用深拷贝?
- 不可以。因为我们使用智能指针的目的是来帮助我们管理资源和释放资源的,没有开辟资源的权限。
3.3 auto_ptr 实现方式一:资源转移
- 资源转移:使用在拷贝构造(或赋值运算符)的时候,将该块资源赋值给新创建的对象,原来的对象不能操作该块空间。即将资源转移给新的对象。
- 例如:原对象ap,新对象ap2,在创建ap2的时候用拷贝构造,那么ap的指针将被置空,ap2可以对该资源进行操作
// 实现代码
namespace test
{
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<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap){
if (_ptr)
delete _ptr;
_ptr = ap._ptr;
ap._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
if (_ptr){
delete _ptr;
_ptr = nullptr;
}
}
T& operator*() // 具有指针类似的行为
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get() // 返回原生态的指针
{
return _ptr;
}
private:
T *_ptr;
};
}
资源转移的缺点
- auto_ptr采用copy语义来转移指针资源,转移指针资源的所有权的同时将原指针置为NULL,这跟通常理解的copy行为是不一致的(不会修改原数据),而这样的行为在有些场合下不是我们希望看到的。
- 例如参考《Effective STL》第8条,sort的快排实现中有将元素复制到某个局部临时对象中,但对于auto_ptr,却将原元素置为null,这就导致最后的排序结果中可能有大量的null。
3.4 auto_ptr 实现方式二:权限管理
- 谁拥有这个资源的权限就能释放它
// 实现代码
namespace test
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _owner(false)
{
if (_ptr)
_owner = true;
}
auto_ptr(auto_ptr<T>& ap)
: _ptr(ap._ptr)
, _owner(ap._owner)
{
ap._owner = false;
}
auto_ptr<int>& operator=(auto_ptr<T>& ap)
{
if (this != &ap){
if (_ptr && ap._ptr){
delete _ptr;
}
_ptr = ap._ptr;
_owner = ap._owner;
ap._owner = false;
}
return *this;
}
~auto_ptr()
{
if (_ptr && _owner){
delete _ptr;
_ptr = nullptr;
_owner = false;
}
}
T& operator*() // 具有指针类似的行为
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
T* get()
{
return _ptr;
}
private:
T* _ptr;
// 在C++中,mutable也是为了突破const的限制而设置的
// 被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中。
mutable bool _owner;
};
}
权限管理的缺点
- 多个对象指向同一块资源,释放的时候只释放一次,那么我们的目的达到了。 但是其他的对象并没有置空,这样就会造成野指针,进而造成内存泄漏。
3.4 auto_ptr的缺点
- 不能用于数组:因为 auto_ptr 在释放的时候,只会释放第一个空间,后序的空间没有释放,就会造成内存泄漏。
- 不能用于非new分配的动态空间
- 两个auto_ptr不能同时指向一个对象
因此委员会强烈建议不要auto_ptr。没有在标准库中修改是因为现在很多的项目还在使用,修改则会导致很多的问题。
4、unique_ptr
4.1 资源独占
采用资源独占的方式。但因为不同的资源释放的方式也不一样,所以得定制删除器。
- 一个资源只能被一个对象管理
// 简单实现
// 定制删除器
template<class T>
class Delete
{
public:
void operator()(T* & p)
{
if (p){
delete p;
p = nullptr;
}
}
};
template<class T>
class Free
{
public:
void operator()(T* &p)
{
if (p){
free(p);
p = nullptr;
}
}
};
class FClose
{
public:
void operator()(FILE* &p)
{
if (p){
fclose(p);
p = nullptr;
}
}
};
namespace test
{
template<class T, class DF = Delete<T>>
class unique_ptr
{
public:
unique_ptr(T* ptr = nullptr)
: _ptr(ptr)
{}
~unique_ptr()
{
if (_ptr){
// delete _ptr; 不能写死,得用资源的类型去释放
// 利用仿函数
DF df;
df(_ptr);
_ptr = nullptr;
}
}
private:
unique_ptr(auto_ptr<T>&) = delete;
unique_ptr<T>& operator=(auto_ptr<T>&) = delete;
private:
T* _ptr;
};
}
void TestUnique()
{
test::unique_ptr<int> up1(new int);
test::unique_ptr<int, Free<int>> up2((int*)malloc(sizeof(int)));
test::unique_ptr<FILE, FClose> up3(fopen("1.txt", "w"));
}
5、shared_ptr
5.1 shared_ptr原理
- 引用计数:通过引用计数的方式来实现多个shared_ptr对象之间共享资源
- 释放规则:最后一个使用资源的对象进行释放
5.2 shared_ptr使用
void TestShared()
{
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
shared_ptr<int> sp3;
sp3 = sp2;
cout << sp1.use_count() << endl;
cout << sp2.use_count() << endl;
cout << sp3.use_count() << endl;
}
打印结果:
2
2
3
3
3
5.3 shared_ptr简单实现
namespace test
{
template<class T>
class Delete
{
public:
void operator()(T* &p)
{
if (_ptr){
delete p;
p = nullptr;
}
}
};
template<class T, class DF = Delete<T>>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)
: _ptr(ptr)
, _pCount(nullptr)
, _pMutex(nullptr)
{
if (ptr){
_pCount = new int(1);
_pMutex = new mutex;
}
}
~shared_ptr()
{
Release();
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
shared_ptr(shared_ptr<T>& sp)
: _ptr(sp._ptr)
, _pCount(sp._pCount)
, _pMutex(sp._pMutex)
{
if (_ptr)
AddRef();
}
shared_ptr<T>operator=(shared_ptr<T> & sp)
{
if (this != &sp){
Release();
_ptr = sp._ptr;
_pCount = sp._pCount;
_pMutex = sp._pMutex;
AddRef();
}
return *this;
}
void AddRef()
{
_pMutex.lock(); // 考虑线程安全问题
++(*_pCount);
_pMutex.unlock();
}
int SubRef()
{
_pMutex.lock();
--(*_pCount);
_pMutex.unlock();
return *_pCount;
}
private:
Release()
{
if (_ptr && 0 == SubRef()){
DF df;
df(_ptr);
delete _pCount;
_pCount = nullptr;
delete _pMutex;
_pMutex = nullptr;
}
}
private:
T* _ptr;
int* _pCount;
mutex* _pMutex;
};
}
计数器为什么使用指针?使用静态变量可以吗?使用普通变量可以吗?
- 不可以使用普通变量。因为普通变量每个对象都会有一个,那么也就是说在构造的时候都给计数器加 1 ,但是在释放的时候只能释放自己的计数器,导致多个对象时,计数器永远不会减到 0 的情况。
- 不可以使用静态变量。静态变量全局只有一份,对于单个的资源的获取没有问题;但是对于多个资源的获取时,也会出现计数器永远不会减到 0 的情况。
- 使用指针指向同一块空间,获取资源时,将该空间的计数器加 1 ;在释放时,将该空间的计数器减 1。
加锁的目的
- 在 share_ptr 的内部实现时,保证引用计数的线程安全。
6、weak_ptr
目的:配合shared_ptr使用,解决循环引用的问题
6.1 循环引用问题
struct ListNode{
ListNode(int x = 0)
: left(nullptr)
, right(nullptr)
, data(x)
{
cout << "ListNode(int):" << this << endl;
}
~ListNode()
{
cout << "~ListNode():" << endl;
}
shared_ptr<ListNode> left;
shared_ptr<ListNode> right;
int data;
};
void TestShared()
{
shared_ptr<ListNode> sp1(new ListNode(10));
shared_ptr<ListNode> sp2(new ListNode(20));
cout << sp1.use_count() << endl;
cout << sp1.use_count() << endl;
sp1->right = sp2;
sp2->left = sp1;
cout << sp1.use_count() << endl;
cout << sp1.use_count() << endl;
}
打印结果:
ListNode(int):00C15058
ListNode(int):00C1A2A8
1
1
2
2
- 我们发现打印结果没有打印析构函数中的内容,所以发生了内存泄漏
为什么会发生内存泄漏呢?
- 上述代码执行过程
- 创建对象sp1和sp2
- sp1->right = sp2;
- sp2->left = sp1;
- 引用计数++
- 销毁sp1,sp2对象
- 引用计数–
- 引用计数不为0
- 所以只销毁sp1和sp2指针,不销毁其指向的地方
- 这样就会造成内存泄漏
6.2 循环引用问题如何解决----weak_ptr
weak_ptr
- weak_ptr是配合shared_ptr使用的,来解决循环引用的问题
- 注意:① weak_ptr不能独立管理资源 ② 底层也是使用引用计数的方式来实现
为什么weak_ptr可以解决循环引用的问题?
- 下来看weak_ptr解决循环应用问题的过程
注意:shared_ptr在销毁对象时,use和weak都减1,而weak_ptr在销毁对象时,weak减1
红色的序号
- ①销毁sp2,因为是shared_ptr,所以use和weak都减1,weak不等于0,所以不销毁引用计数
- ②销毁left中,第③步,right没有,所以不用销毁
- ③sp1的weak减1
- ④销毁data
- ⑤这步需要最后检测引用计数为0的时候才可以销毁
蓝色的序号
- ① 销毁sp1,因为是sp1是shared_ptr,所以ues和weak减1,此时use和weak的计数都为0,所以引用计数可以销毁
- ② 销毁right中,第③步,left没有,所以不用销毁
- ③ 给sp2的引用计数weak减1,可以销毁这个引用计数
- ④ 销毁data
- ⑤ 这步在引用计数为0的时候已经销毁了
6.3 上述代码的改进
struct ListNode{
ListNode(int x = 0)
: data(x)
{
cout << "ListNode(int):" << this << endl;
}
~ListNode()
{
cout << "~ListNode():" << endl;
}
weak_ptr<ListNode> left;
weak_ptr<ListNode> right;
int data;
};
void TestShared()
{
shared_ptr<ListNode> sp1(new ListNode(10));
shared_ptr<ListNode> sp2(new ListNode(20));
cout << sp1.use_count() << endl;
cout << sp1.use_count() << endl;
sp1->right = sp2;
sp2->left = sp1;
cout << sp1.use_count() << endl;
cout << sp1.use_count() << endl;
}
打印结果:
ListNode(int):00BD5058
ListNode(int):00BDADF0
1
1
1
1
~ListNode():
~ListNode():
如果您觉得有帮助就点个赞吧!!!
学而时习之,不亦说乎!!!