【C++篇】:告别手动内存管理!——C++智能指针的快速上手指南
✨感谢您阅读本篇文章,文章内容是个人学习笔记的整理,如果哪里有误的话还请您指正噢✨
✨ 个人主页:余辉zmh–CSDN博客
✨ 文章所属专栏:c++篇–CSDN博客

文章目录
智能指针
一.为什么需要智能指针?
先来分析一下下面这段代码有没有什么内存方面的问题?
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; // 释放p1
delete p2; // 必须释放p2
cout << "delete: " << p1 << endl;
cout << "delete: " << p2 << endl;
throw;
}
delete p1; // 正常情况释放p1
delete p2; // 必须释放p2
cout << "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*().first
cout << (p2->first) << " " << (p2->second) << endl;
// 实际是p2.operator->()->first
return 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++智能指针的讲解,如果哪里有错的话,可以在评论区指正,也欢迎大家一起讨论学习,如果对你的学习有帮助的话,点点赞关注支持一下吧!!!
更多推荐



所有评论(0)