C++内存列传之RAII宇宙:智能指针
文章目录
- 1.为什么需要智能指针?
- 2.智能指针原理
- 2.1 RAll
- 2.2 像指针一样使用
- 3.C++11的智能指针
- 3.1 auto_ptr
- 3.2 unique_ptr
- 3.3 shared_ptr
- 3.4 weak_ptr
- 4.删除器
- 希望读者们多多三连支持
- 小编会继续更新
- 你们的鼓励就是我前进的动力!
智能指针是 C++
中用于自动管理动态内存的类模板,它通过 RAII
(资源获取即初始化)技术避免手动 new
/ delete
操作,从而显著减少内存泄漏和悬空指针的风险
1.为什么需要智能指针?
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main()
{try{Func();}catch (exception& e){cout << e.what() << endl;}return 0;
}
如果 p1
这里 new
抛异常会如何?
p1
未成功分配,值为nullptr
函数直接跳转到catch
块,p2
未分配,无内存泄漏
如果 p2
这里 new
抛异常会如何?
p1
已分配但未释放,导致内存泄漏
函数跳转到catch
块,p2
未分配,delete p1
和delete p2
均未执行
如果 div
调用这里又会抛异常会如何?
p1
和p2
均已分配但未释放,导致双重内存泄漏
函数跳转到catch
块,打印错误信息(如 “除0
错误”)
C++
不像 java
具有垃圾回收机制,能够自动回收开辟的空间,需要自行手动管理,但是自己管理有时又太麻烦了,况且这里只是两个指针就产生了这么多问题,因此在 C++11
就推出了智能指针用于自动管理内存
2.智能指针原理
2.1 RAll
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}private:T* _ptr;
};int main()
{SmartPtr<int> sp1(new int(1));SmartPtr<string> sp2(new string("xxx"));return 0;
}
RAII
(Resource Acquisition Is Initialization
)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术
简单来说,就是把创建的对象给到 SmartPtr
类来管理,当对象的生命周期结束的时候,刚好类也会自动调用析构函数进行内存释放
这种做法有两大好处:
- 不需要显式地释放资源
- 采用这种方式,对象所需的资源在其生命期内始终保持有效
2.2 像指针一样使用
都叫做智能指针了,那肯定是可以当作指针一样使用了,指针可以解引用,也可
以通过 ->
去访问所指空间中的内容,因此类中还得需要将 *
、->
重载下,才可让其像指针一样去使用
template<class T>
class SmartPtr
{
public:SmartPtr(T* ptr):_ptr(ptr){}~SmartPtr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}
private:T* _ptr;
};
*
重载返回对象,->
重载返回地址,这部分的知识点在迭代器底层分析已经讲过很多遍了,就不过多叙述了,可自行翻阅前文
3.C++11的智能指针
智能指针一般放在 <memery>
文件里,C++11
也参考了第三方库 boost
C++ 98
中产生了第一个智能指针auto_ptr
C++ boost
给出了更实用的scoped_ptr
和shared_ptr
和weak_ptr
C++ TR1
,引入了shared_ptr
等。不过注意的是TR1
并不是标准版C++ 11
,引入了unique_ptr
和shared_ptr
和weak_ptr
。需要注意的是unique_ptr
对应boost
的scoped_ptr
。并且这些智能指针的实现原理是参考boost
中的实现的
3.1 auto_ptr
template<class T>
class auto_ptr
{
public:// RAII// 像指针一样auto_ptr(T* ptr):_ptr(ptr){}~auto_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// ap3(ap1)// 管理权转移auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap) {if (this != &ap) {_ptr = ap._ptr; // 转移所有权ap._ptr = nullptr; // 原指针置空}return *this;}
private:T* _ptr;
};
auto_ptr
在 C++98
就已经被引入,实现了智能指针如上面所讲的最基础的功能,同时他还额外对拷贝构造、=
重载进行了显式调用,但是这种拷贝虽然能解决新对象的初始化,但是对于被拷贝的对象,造成了指针资源所有权被转移走,跟移动构造有些类似
因此,auto_ptr
会导致管理权转移,拷贝对象被悬空,auto_ptr
是一个失败设计,很多公司明确要求不能使用 auto_ptr
3.2 unique_ptr
template<class T>
class unique_ptr
{
public:// RAII// 像指针一样unique_ptr(T* ptr):_ptr(ptr){}~unique_ptr(){cout << "delete:" << _ptr << endl;delete _ptr;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// ap3(ap1)// 管理权转移// 防拷贝unique_ptr(unique_ptr<T>& ap) = delete;unique_ptr<T>& operator=(unique_ptr<T>& ap) = delete;
private:T* _ptr;
};
unique_ptr
很简单粗暴,直接禁止了拷贝机制
因此,建议在不需要拷贝的场景使用该智能指针
3.3 shared_ptr
template<class T>
class shared_ptr
{
public:// RAII// 像指针一样shared_ptr(T* ptr = nullptr):_ptr(ptr),_pcount(new int(1)){}~shared_ptr(){if (--(*_pcount) == 0){cout << "delete:" << _ptr << endl;delete _ptr;delete _pcount;}}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}// sp3(sp1)shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr),_pcount(sp._pcount){++(*_pcount);}shared_ptr<T>& operator=(const 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;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;int* _pcount;
};
C++11
中的智能指针就属 shared_ptr
使用的最多,因为它解决了赋值造成的资源被转移可能会被错误访问的问题
类中增加一个新的指针 _pcount
用于计数,即计数有多少个 _ptr
指向同一片空间,多个 shared_ptr
可以同时指向同一个对象,每次创建新的 shared_ptr
指向该对象,引用计数加 1
;每次 shared_ptr
析构或者被赋值为指向其他对象,引用计数减 1
。当最后一个指向该对象的 shared_ptr
析构时,对象会被自动删除,从而避免内存泄漏
🔥值得注意的是: shared_ptr
同时也支持了无法自己给自己赋值,这里还涉及一些关于线程安全的知识点,待 Linux
学习过后再来补充
3.4 weak_ptr
看似完美的 shared_ptr
其实也会有疏漏,比如:引用循环
struct ListNode
{int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;
};
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;
}
当执行 node1->next = node2
和 node2->prev = node1
时,node1
内部的 _next
指针指向 node2
,node2
内部的 _prev
指针指向 node1
。这就导致两个节点之间形成了循环引用关系。此时,由于互相引用,每个节点的引用计数都变为 2
,因为除了外部的智能指针引用,还多了来自另一个节点内部指针的引用
当 node1
和 node2
智能指针对象离开作用域开始析构时,它们首先会将所指向节点的引用计数减 1
。此时,每个节点的引用计数变为 1
,而不是预期的 0
。这是因为 node1
的 _next
还指向 node2
,node2
的 _prev
还指向 node1
,使得它们的引用计数无法归零
对于 shared_ptr
来说,只有当引用计数变为 0
时才会释放所管理的资源。由于这种循环引用的存在,node1
等待 node2
先释放(因为 node2
的 _prev
引用着 node1
),而 node2
又等待 node1
先释放(因为 node1
的 _next
引用着 node2
),最终导致这两个节点所占用的资源都无法被释放,造成内存泄漏
class ListNode
{
public:weak_ptr<ListNode> _next; weak_ptr<ListNode> _prev;
};
为了解决 shared_ptr
的循环引用问题,通常可以使用 weak_ptr
。weak_ptr
是一种弱引用智能指针,它不会增加所指向对象的引用计数。将循环引用中的某一个引用(比如 ListNode
类中的 _prev
或 _next
其中之一)改为 weak_ptr
类型,就可以打破循环引用
因此,weak_ptr
是一种专门解决循环引用问题的指针
4.删除器
#include <iostream>
#include <memory>
#include <string>using namespace std;class A
{
public:~A() { cout << "A::~A()" << endl; }
};// 仿函数删除器:用于释放malloc分配的内存
template<class T>
struct FreeFunc
{void operator()(T* ptr) const {cout << "FreeFunc: free memory at " << ptr << endl;free(ptr);}
};// 仿函数删除器:用于释放数组
template<class T>
struct DeleteArrayFunc
{void operator()(T* ptr) const {cout << "DeleteArrayFunc: delete[] memory at " << ptr << endl;delete[] ptr;}
};int main()
{// 使用FreeFunc删除器的shared_ptrshared_ptr<int> sp1((int*)malloc(sizeof(int)), FreeFunc<int>());*sp1 = 100;cout << "sp1: " << *sp1 << " at " << sp1.get() << endl;// 离开作用域时调用FreeFunc删除器// 使用DeleteArrayFunc删除器的shared_ptrshared_ptr<int> sp2(new int[5], DeleteArrayFunc<int>());for (int i = 0; i < 5; ++i) {sp2.get()[i] = i;}cout << "sp2 array:";for (int i = 0; i < 5; ++i) {cout << " " << sp2.get()[i];}cout << endl;// 离开作用域时调用DeleteArrayFunc删除器// 使用lambda删除器管理A对象数组shared_ptr<A> sp4(new A[3], [](A* p) {cout << "Lambda: deleting array at " << p << endl;delete[] p;});cout << "sp4 array of A objects created" << endl;// 离开作用域时调用lambda删除器// 使用lambda删除器管理文件句柄shared_ptr<FILE> sp5(fopen("test.txt", "w"), [](FILE* p) {if (p) {cout << "Lambda: closing file" << endl;fclose(p);}});if (sp5) {fprintf(sp5.get(), "Hello, shared_ptr with deleter!\n");cout << "File written" << endl;}// 离开作用域时调用lambda删除器关闭文件return 0;
}
对于所有的指针不一定是 new
出来的对象,因此利用仿函数设置了删除器,这样就可以调用对应的删除