【C++篇】:告别手动内存管理!——C++智能指针的快速上手指南
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:c++篇–CSDN博客
文章目录
- 智能指针
- 一.为什么需要智能指针?
- 二.智能指针的原理和使用
- 1.RAII原理
- 2.智能指针的使用
- 3.智能指针的拷贝问题
- 三.智能指针的模拟实现
- 1.`auto_ptr`
- 2.`unique_ptr`
- 3.`shared_ptr`
- 四.循环引用和`weak_ptr`
- 1.循环引用现象
- 2.`weak_ptr`
- 3.模拟实现一个`weak_ptr`
- 补充内容
- 定制删除器
智能指针
一.为什么需要智能指针?
先来分析一下下面这段代码有没有什么内存方面的问题?
int div(){int a, b;cin >> a >> b;if (b == 0){throw invalid_argument("除0错误");}return a / b;
}void f1(){pair<string, string> *p1 = new pair<string, string>;pair<string, string> *p2 = new pair<string, string>;try{div();}catch(...){delete p1;cout << "delete: " << p1 << endl;throw;}delete p1;cout << "delete: " << p1 << endl;
}int main(){try{f1();}catch(const exception& e){cout << e.what() << endl;}return 0;
}
通过分析上面的代码,可以发现,这是一个典型的内存泄漏问题!
上面代码中内存泄露的具体情况:
- 情况一:异常发生时的泄露
div()
抛出异常;- 但是
catch
块中只释放了p1
,p2
并没有被释放,造成了内存泄漏;- 情况二:正常执行时的泄露
- 即使
div()
不抛出异常;- 正常流程中也只释放了
p1
,p2
仍然没有被释放,造成了内存泄漏;
正确的做法就需要我们手动管理内存了:
void f1(){pair<string, string> *p1 = new pair<string, string>;pair<string, string> *p2 = new pair<string, string>;try{div();}catch(...){delete p1; // 释放p1delete p2; // 必须释放p2cout << "delete: " << p1 << endl;cout << "delete: " << p2 << endl;throw;}delete p1; // 正常情况释放p1delete p2; // 必须释放p2cout << "delete: " << p1 << endl;cout << "delete: " << p2 << endl;
}
很显然,手动管理内存容易带来各种各样的问题,不仅仅是内存泄漏,还有代码复杂,需要在多个地方管理内存等;
既然如此,我们就需要一种便捷的方式来替代手动管理内存,这就是为什么需要智能指针的原因:
智能指针可以帮助我们自动管理内存生命周期,代码简洁,防止内存管理错误等;
二.智能指针的原理和使用
1.RAII原理
RAII
是一种利用对象生命周期来控制程序资源(比如内存,文件句柄,网络连接,互斥量等等)的简单技术。
基本思想:
- 在对象构造时获取资源:在构造函数中获取资源,控制对资源的访问使之在对象的生命周期内始终保持有效;
- 在对象析构时释放资源:当作用域结束时,自动调用析构函数,在析构函数中释放资源;
- 实际上是把管理一份资源的责任托管给了一个对象,利用对象的生命周期来自动管理资源!!!
这种做法有两种好处:
- 不需要显示的释放资源;
- 采用这种方式,对象所需的资源在其生命周期内始终保持有效;
智能指针的原理就是RAII思想,是RAII的典型应用;
使用RAII思想设计一个SmartPtr
类:
template <class T>
class SmartPtr{
public:// 构造函数获取资源SmartPtr(T *ptr = nullptr): _ptr(ptr){}// 析构函数释放资源~SmartPtr(){if (_ptr){std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}private:T *_ptr;
};
还是用上面的那份测试代码,这不过这次用我们自己写好的SmartPtr
类来管理资源:
int div(){int a, b;cin >> a >> b;if (b == 0){throw invalid_argument("除0错误");}return a / b;
}void f2(){SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));div();
}int main(){try{f2();}catch(const exception& e){cout << e.what() << endl;}return 0;
}
这次使用SmartPtr
类来管理p1
,p2
,即使我们没有显示的释放资源,最后在进程结束时,也会自动释放掉,这样就不会造成内存泄露的问题了,还能简化代码。
2.智能指针的使用
上面的SmartPtr
类还不能称其为智能指针,因为他还不具备指针的行为。因为指针可以解引用,也可以通过->
去访问所指空间中的内容,因此还需要在SmartPtr
类中增加*
,->
运算符重载,才可以像指针一样使用。
operator*()
——解引用运算符
T& operator*(){return *_ptr;
}
作用:
- 返回指针指向的对象的引用;
- 访问的是指针指向的对象本身;
operator->()
——箭头运算符
T* opeartor->(){return _ptr;
}
作用:
- 返回指针本身;
- 访问的是对象的成员函数或成员变量;
测试代码:
int main(){SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));cout << ((*p1).first) << " " << ((*p1).second) << endl;// 实际是p1.operator*().firstcout << (p2->first) << " " << (p2->second) << endl;// 实际是p2.operator->()->firstreturn 0;
}
注意:
使用*
解引用后,返回的是一个pair
对象,所以需要用.
继续访问成员变量;
而使用->
后,返回的是一个pair
对象的指针,所以需要用->
继续访问成员变量,但是为了可读性一般都是省略第二个->
,编译器会再自动调用,不需要真的写两个->
;
3.智能指针的拷贝问题
先来看一个测试(继续用上面写好的SmartPtr
类):
int main(){SmartPtr<pair<string, string>> p1(new pair<string, string>("aaa", "111"));SmartPtr<pair<string, string>> p2(new pair<string, string>("bbb", "222"));SmartPtr<pair<string, string>> p3(p1);return 0;
}
很明显当我们尝试用一个对象去拷贝构造一个新对象时,发生了错误,这是一个典型的浅拷贝导致的双重释放问题;
问题所在:
- 首先
p1
指向内存空间0x5598ec3dc2b0
;- 因为
SmartPtr
类中没有定义拷贝构造函数,所以编译器会自动生成一个默认的拷贝构造函数,并进行浅拷贝;- 所以用
p1
拷贝构造p3
时,发生浅拷贝,使p1
和p3
指向了同一个内存地址;- 当程序结束时,
p3
析构,释放内存0x5598ec3dc2b0
,p1
析构,再次释放同一个内存地址,导致双重释放;
这个问题其实有好几种解决方案,并且也可以算得上是智能指针的发展历程了。
这里直接展示库里的几个智能指针:
1.C++98中:auto_ptr
(已废弃)
auto_ptr<string> a1(new string("hello"));
auto_ptr<string> a2(a1); // 移交管理权,a1已经置为nullptr
// cout << a1->size() << endl; // 错误使用
解决方式:通过移交管理权导致原指针失效(原指针置为空),弊端也很明显,容易误用原指针,造成悬垂指针(所以现在很少用,基本废弃了);
2.C++11中:unique_ptr
(独占所有权)
unique_ptr<string> u1(new string("hello"));
//unique_ptr<string> u2(u1);unique_ptr<string> u3(move(u1)); // 移动构造 移动后u1置为nullptr
cout << (*u3) << endl; // 输出hello
解决方式:明确禁止拷贝,防止误用;强制使用移动语义(可以移动右值),语义更清晰;并且是在编译时检查,更加安全;
3.C++11中:shared_ptr
(共享所有权)
shared_ptr<string> s1(new string("hello"));
shared_ptr<string> s2(s1);
cout << (*s1) << " " << s1 << endl;
cout << (*s2) << " " << s2 << endl;
解决方式:允许多个对象共享同一分资源,通过引用计算管理,最后一个对象销毁时,自动释放资源;
上面三种智能指针从auto_ptr
的尝试解决拷贝问题,但语义不清晰;到unique_ptr
的明确禁止,强制移动语义;再到shared_ptr
允许共享等,智能指针的设计也从简单到复杂,所以也算得上是智能指针的发展历程了。
三.智能指针的模拟实现
1.auto_ptr
重点是实现拷贝构造和拷贝赋值,像构造,析构,*
和->
重载其实和上面的SmartPtr
类差不多
namespace MySmartPtr{template<typename T>class auto_ptr{public:auto_ptr(T* ptr = nullptr):_ptr(ptr){}~auto_ptr(){if(_ptr){std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}// 拷贝构造auto_ptr(auto_ptr<T>& a):_ptr(a._ptr) // 移交管理权{a._ptr = nullptr; // 原对象置为空}// 拷贝赋值auto_ptr<T>& operator=(auto_ptr<T>& a){if(_ptr != a._ptr){_ptr = a._ptr; // 移交管理权a._ptr = nullptr; // 原对象置为空}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* GetPtr() const {return _ptr;}private:T *_ptr;};
}
测试代码:
int main(){MySmartPtr::auto_ptr<string> a1(new string("hello"));cout << (*a1) << " " << (a1.GetPtr()) << endl; //打印a1的资源以及资源地址MySmartPtr::auto_ptr<string> a2(a1);cout << (*a2) << " " << (a2.GetPtr()) << endl; // 打印移交后的a2的资源以及资源地址return 0;
}
2.unique_ptr
unique_ptr
因为明确禁止拷贝构造和拷贝赋值,所以对于这两个函数可以直接删除;主要是实现移动语义(移动构造和移动赋值),实现思路也很简单,交换资源,然后再将被移动对象置为空即可,但是有一点,在移动赋值这里需要先检查一下自赋值;
namespace MySmartPtr{template<typename T>class unique_ptr{public:unique_ptr(T* ptr = nullptr):_ptr(ptr){}~unique_ptr(){if(_ptr){std::cout << "delete: " << _ptr << std::endl;delete _ptr;}}unique_ptr(unique_ptr<T> &u) = delete; // 删除拷贝构造unique_ptr<T> &operator=(unique_ptr<T> &u) = delete; // 删除拷贝赋值// 移动构造unique_ptr(unique_ptr<T> &&u):_ptr(u._ptr) {u._ptr = nullptr;}// 移动赋值unique_ptr<T> &operator=(unique_ptr<T> &&u){// 自赋值检查if (_ptr != u._ptr){_ptr = u._ptr;u._ptr = nullptr;}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* GetPtr() const {return _ptr;}private:T *_ptr;};
}
测试代码:
int main(){MySmartPtr::unique_ptr<string> u1(new string("hello"));cout << (*u1) << " " << (u1.GetPtr()) << endl;MySmartPtr::unique_ptr<string> u2(move(u1)); // 移动拷贝cout << (*u2) << " " << (u2.GetPtr()) << endl;MySmartPtr::unique_ptr<string> u3(new string);u3 = move(u2); // 移动赋值cout << (*u3) << " " << (u3.GetPtr()) << endl;u3 = move(u3); // 自赋值cout << (*u3) << " " << (u3.GetPtr()) << endl;return 0;
}
3.shared_ptr
shared_ptr
的原理:通过引用计数的方式来实现多个shared_ptr
对象之间的共享资源;
shared_ptr
在其内部,给每个资源都维护了一份计数器,用来记录该资源被几个对象共享;- 在对象被销毁时(也就是调用析构函数时),就说明自己已经不再使用该资源了,对象的引用计数减一;
- 如果减完后引用计数变为0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
- 如果减完后引用计数不为0,就说明除了自己以外还有其他的对象在使用该份资源,不能释放资源,否则其他对象就变成野指针了;
namespace MySmartPtr{template<typename T>class shared_ptr{public:shared_ptr(T* ptr = nullptr):_ptr(ptr),_count(new int(1)){}~shared_ptr(){std::cout << "~shared_ptr()" << std::endl;// 析构时,先将计数器减一,然后再判断是否需要释放if (--(*_count) == 0){std::cout << "delete: " << _ptr << std::endl;delete _ptr;delete _count;}}// 拷贝构造shared_ptr(const shared_ptr<T> &s):_ptr(s._ptr),_count(s._count){++(*_count); //同一个动态计数器,当计数器发生改变,所有共享这份资源的对象都能看到}// 拷贝赋值shared_ptr<T> &operator=(const shared_ptr<T> &s){if (_ptr != s._ptr){ if (--(*_count) == 0){ delete _ptr;delete _count;}_ptr = s._ptr;_count = s._count;++(*_count);}return *this;}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}T* GetPtr() const {return _ptr;}int& use_count() const {return *_count;}public:T *_ptr;int *_count; // 指针形式的计数器};
}
几个细节点:
1.为什么要用指针形式的计数器?
关键点:
int* _count
指向堆上的一个int值所有
shared_ptr
对象都指向同一个计数器当计数器变化时,所有对象都能看到
2.拷贝赋值时的关键点
自赋值检查:
检查当前对象和赋值对象是否指向同一个资源
避免自赋值时的资源释放问题
这是关键的安全检查
引用计数管理:
先将当前对象的引用计数减1
如果减到0,说明没有其他对象共享这个资源,需要释放
释放包括:原始指针
_ptr
和计数器_count
资源转移:
将新资源的指针和计数器赋值给当前对象
增加新资源的引用计数
测试代码:
int main(){MySmartPtr::shared_ptr<string> s1(new string("hello"));cout << (*(s1.GetPtr())) << " " << (s1.GetPtr()) << " " << (s1.use_count()) << endl;MySmartPtr::shared_ptr<string> s2(s1); // 拷贝构造cout << (*(s2.GetPtr())) << " " << (s2.GetPtr()) << " " << (s2.use_count()) << endl;MySmartPtr::shared_ptr<string> s3(new string);s3 = s2; // 拷贝赋值cout << (*(s3.GetPtr())) << " " << (s3.GetPtr()) << " " << (s3.use_count()) << endl;s3 = s3; // 自赋值cout << (*(s3.GetPtr())) << " " << (s3.GetPtr()) << " " << (s3.use_count()) << endl;return 0;
}
四.循环引用和weak_ptr
1.循环引用现象
通过一份测试代码来看一个现象:
struct ListNode{ListNode(){}~ListNode(){cout << "~ListNode()" << endl;}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;
}
先把中间那两句注释掉看一下结果:
最后两个节点都会正常释放掉,但是如果中间两句取消注释后,结果就是:
这一次两个节点的引用计数都变为2,并且最后没有自动释放掉;
这就是shared_ptr
中的一个特殊场景,循环引用问题。
循环引用分析:
node1
和node2
两个智能指针对象指向两个节点,引用计数变成1;node1
的_next
指向node2
,node2
的_prev
指向node1
,引用计数变成2;node1
和node2
析构时,引用计数减一变成1,此时node1
的_next
还指向node2
,node2
的_prev
还指向node1
;- 也就是说
_next
管着node2
,_prev
管着node1
;- 那什么时候
_prev
析构?——node2
析构时,_prev
就会析构;- 那什么时候
node2
析构呢?——_next
析构,node2
就会析构;- 那什么时候
_next
析构呢?——node1
析构,_next
就会析构;- 那什么时候
node1
析构呢?——_prev
析构,node1
就会析构;- 由于在进程结束时引用计数不为 0,所以两个节点都不会被销毁,造成内存泄漏
- 上面这种情况就是循环引用,谁也不会释放;
解决方式就是使用weak_ptr
来设置节点的_next
和_prev
;
2.weak_ptr
weak_ptr的本质
weak_ptr
是shared_ptr
的弱引用,它有以下几个关键特性:
- 不增加引用计数:
weak_ptr
指向一个对象时,不会增加该对象的引用计数;- 不用有对象:
weak_ptr
不拥有对象的所有权,只是观察者;- 可以检测对象是否还存在:通过
expired()
方法检查对象是否已经被销毁;
weak_ptr解决循环引用
将_prev
和_next
改成weak_ptr
:
struct ListNode{ListNode(){}~ListNode(){cout << "~ListNode()" << endl;}int _data;weak_ptr<ListNode> _next;weak_ptr<ListNode> _prev;
};
现在的情况是:
node1
的_next
指向node2
时,node2
的引用计数不变;node2
的_prev
指向node1
时,node1
的引用计数不变;node1
和node2
的引用计数始终保持1不变;- 当函数结束时,
node1
和node2
调用析构,引用计数减一变为0,此时两个节点被释放;
weak_ptr
解决循环引用的核心原理:
-
打破引用计数的循环:
weak_ptr
不增加引用计数,所以不会形成循环 -
提供安全的观察机制:可以检测对象是否还存在,避免访问已销毁的对象
-
保持对象生命周期管理:只有
shared_ptr
才真正拥有对象,weak_ptr
只是观察者
3.模拟实现一个weak_ptr
namespace MySmartPtr{template<typename T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T> &s):_ptr(s.GetPtr()){}// shared_ptr -> weak_ptr 的拷贝构造weak_ptr<T> &operator=(const shared_ptr<T> &s){_ptr = s.GetPtr();return *this;}// weak_ptr -> weak_ptr 的拷贝构造weak_ptr<T> &operator=(const weak_ptr<T> &w){_ptr = w._ptr;return *this;}~weak_ptr(){// weak_ptr 不负责删除对象,只是观察者}T& operator*(){return *_ptr;}T* operator->(){return _ptr;}private:T *_ptr;};
}
weak_ptr 的设计原则
-
不增加引用计数:
weak_ptr
不会影响对象的生命周期 -
不删除对象:只有
shared_ptr
负责删除对象 -
只是观察者:
weak_ptr
只是观察对象是否存在,不拥有对象
测试代码:
struct ListNode{ListNode(){}~ListNode(){cout << "~ListNode()" << endl;}int _data;MySmartPtr::weak_ptr<ListNode> _next;MySmartPtr::weak_ptr<ListNode> _prev;
};int main(){MySmartPtr::shared_ptr<ListNode> node1(new ListNode);MySmartPtr::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;
}
weak_ptr
除了可以解决循环引用外,还可以用来安全观察对象是否存在,避免悬挂指针等各种引用场景,是现代C++中非常重要的一个智能指针!
补充内容
定制删除器
1.什么是定制删除器?
定制删除器就是自定义智能指针销毁对象时调用的函数,而不是默认的delete
;
2.为什么需要定制删除器?
因为不是所有资源都可以像堆内存一样用delete
销毁;
比如下面这些资源
// 文件句柄 需要使用fclose()
FILE* file = fopen("test.txt", "r");// 数组 需要使用delete[]
int* array = new int[100];// C风格内存 需要使用free()
void* men = malloc(1024);// 除此之外,还有网络连接,数据库连接等各种资源
正是因为定制删除器的存在,可以让智能指针管理任何类型的资源,而不单单是堆内存;
3.常用的实现方式
在标准库中,其中一个构造函数可以使用定制删除器,通过第二个模板参数D del
(可调用对象)来传递删除器;
- 函数指针
void file_deleter(FILE* fp) {if (fp) fclose(fp);
}shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), file_deleter);
- Lambda表达式
shared_ptr<FILE> file_ptr(fopen("test.txt", "r"), [](FILE* fp) { if (fp) fclose(fp); });
- 仿函数(函数对象)
struct ArrayDeleter {void operator()(int* p) {delete[] p;}
};shared_ptr<int> array_ptr(new int[100], ArrayDeleter());
- 标准库提供的删除器
// unique_ptr 有专门的数组版本
unique_ptr<int[]> array_ptr(new int[100]); // 自动用 delete[]// shared_ptr 需要手动指定
shared_ptr<int> array_ptr(new int[100], default_delete<int[]>());
4.给自己写的shared_ptr
实现定制删除器功能
设计思路
在shared_ptr
类中添加一个成员变量:由包装器包装的可调用对象_del
std::function<void(T *)> _del = [](T *ptr){if(ptr){delete ptr;}
};
- 将所有的删除器都包装成
std::function
; - 支持函数指针,lambda表达式,函数对象等各种可调用对象;
- 同时利用lambda表达式提供默认的
delete
行为;
增加一个新的构造函数
template<typename D>
shared_ptr(T *ptr, D del)
: _ptr(ptr)
, _count(new int(1))
, _del(del)
{}
- 通过模板参数
D
来自动推导删除器类型; - 没有自定义删除器时,使用默认的
delete
; - 编译时类型检查,避免运行时错误;
同时更改析构函数中的delete
行为
~shared_ptr(){std::cout << "~shared_ptr()" << std::endl;if (--(*_count) == 0){std::cout << "delete: " << _ptr << std::endl;// delete _ptr;_del(_ptr);delete _count;}
}
以及拷贝构造和拷贝赋值中增加删除器的拷贝
// 拷贝构造
shared_ptr(const shared_ptr<T> &s)
:_ptr(s._ptr)
,_count(s._count)
,_del(s._del) // 拷贝删除器
{++(*_count);
}// 拷贝赋值
shared_ptr<T> &operator=(const shared_ptr<T> &s){if (_ptr != s._ptr){ if (--(*_count) == 0){// delete _ptr;_del(_ptr);delete _count;}_ptr = s._ptr;_count = s._count;_del = s._del; // 拷贝删除器++(*_count);}return *this;
}
最后用一段代码来进行测试:
struct ArrayDelete{void operator()(int *p){if(p){cout << "array_delete" << endl;delete[] p;}}
};
int main()
{MySmartPtr::shared_ptr<int> array_ptr(new int[100], ArrayDelete());MySmartPtr::shared_ptr<string> s1(new string("hello"));MySmartPtr::shared_ptr<string> s2(s1);return 0;
}
以上就是关于C++智能指针的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!