目录
1、内存泄漏
1.1 什么是内存泄漏
1.2 内存泄漏的危害
1.3 如何避免内存泄漏
2、智能指针的应用场景
3、智能指针的原理
3.1 RAII
3.2 智能指针的使用
4、智能指针的拷贝问题
5、auto_ptr
6、unique_ptr
7.1 循环引用
7.2 weak_ptr
结语
前言:
C++的智能指针是普通指针“升级版”,采用了面向对象的思想对普通指针进行了又一层封装,将原本是内置类型的普通指针封装成用户自己定义的一个类对象。智能指针存在的原因:普通指针在进行申请空间时,需要手动释放该指针,有些场景下用户可能来不及释放导致内存泄漏,而一个类对象在销毁时是一定会调用该对象的析构函数的。所以把释放指针的动作交给类的析构函数处理则指针所指向的空间一定会得到释放,这就是智能指针的核心思想。
1、内存泄漏
1.1 什么是内存泄漏
内存泄漏指的是程序员向系统动态申请一块空间,用完之后却没能将其释放掉,最后导致失去了对该空间控制,并且此时系统也无权利用该空间的资源,把这类情况叫做内存泄漏。
1.2 内存泄漏的危害
若一个程序长期运行并出现了内存泄漏,同样的程序却会让内存不断的消耗殆尽,并且使程序的运行越来越慢,最后当内存都泄漏完了程序就会卡死。
1.3 如何避免内存泄漏
1、养成良好的代码习惯,并且设计程序时严格遵守规范,但凡是申请的空间都记得释放。(但是若遇到了抛异常的情况,哪怕记得释放空间也会造成内存泄漏)
2、针对抛异常的情况,采用RAII思想或者智能指针来管理内存(下文细讲)。
3、使用内存泄漏工具来检测程序中是否有内存泄漏。
因此可以把上述的方法分为两种思路: 1、事先预防型、如上述的1、2点。2、事后查错型,上述的第3点。
2、智能指针的应用场景
有些场景下,哪怕程序员已经手动释放了空间,但是依然会造成内存泄漏,比如在进行抛异常时,会出现抛出的异常会连续结束好几个栈帧空间,直到被最外层的栈帧空间所捕获,若之前栈帧空间中还有一些指针没来得及释放该栈帧就结束了,就会造成内存泄漏。
示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
void _func()
{
int a = -2;
if (a < 0)
throw string("a为负数发生错误");
}
void Func()
{
int* p1 = new int;
_func();//若_func函数抛异常,则p1会来不及释放,造成内存泄漏
delete p1;//虽然释放了p1,但仍然可能会造成内存泄漏
}
int main()
{
try
{
Func();
}
catch (string& e)
{
cout << e << endl;
}
return 0;
}
上述代码的具体示意图如下:
虽然上述代码的情况可以用重新抛出的问题解决,但是用智能指针的方式显然会更好,智能指针的大概思路是:将p1封装成一个类,再将delete p1写进该类的析构函数,这样一来当结束Func栈帧时p1也会被释放。
3、智能指针的原理
智能指针采用的是RAII的思想,只不过智能指针在此基础上增添了指针的行为,因此需要先了解RAII的具体思想。
3.1 RAII
RAII全称(Resource Acquisition Is Initialization),是利用对象的生命周期来控制程序资源,比如当一个对象的生命周期结束时,因为其析构函数是自动调用的,这时候就可以在该析构函数内实现对资源的管控,达到资源的“自动化”。
用上述代码来举例RAII思想,先构造一个对象,把p1指向的空间管理权也给到该对象,并且在该对象的析构函数内释放该空间,当该函数栈帧销毁时,该对象会自动的调用析构函数从而释放p1指向的空间,所以无需手动释放p1也不会造成内存泄漏。
将RAII思想转变为类的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<class T>
class RAII
{
public:
RAII(T* ptr = nullptr)
: _ptr(ptr)
{
cout << "RAII()" << endl;
}
~RAII()
{
cout << "~RAII()" << endl;
if (_ptr)
delete _ptr;
}
private:
T* _ptr;
};
void _func()
{
int a = -2;
if (a < 0)
throw string("a为负数发生错误");
}
void Func()
{
int* p1 = new int(4);
RAII<int> ra = p1;//ra内的指针_ptr指向和p1同一块空间
_func();
delete p1;
}
int main()
{
try
{
Func();
}
catch (string& e)
{
cout << e << endl;
}
return 0;
}
运行结果:
3.2 智能指针的使用
智能指针是在RAII类的原有基础上实现了指针的行为,因为上述代码的RAII并不是一个智能指针,他不具备指针的基本功能,因此需要在上述RAII的基础上加上解引用操作:‘*’,‘->'。
智能指针的基本功能实现:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<class T>
class SmartPtr//智能指针基本功能
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
if (_ptr)
delete _ptr;
}
//解引用功能
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct Date//日期类
{
int _year;
int _month;
int _day;
friend ostream& operator<<(ostream& out,const Date& d);
};
ostream& operator<<(ostream& out,const Date& d)//流提取
{
cout << d._year<<"."<< d._month << "." << d._day;
return out;
}
int main()
{
SmartPtr<int> sp1(new int);
*sp1 = 12;
cout << *sp1 << endl;
SmartPtr<Date> sd(new Date);
//此处应该是两个->,但是经过编译器优化后变成一个->
sd->_year = 2024;
sd->_month = 3;
sd->_day = 13;
cout << *sd << endl;//打印日期类
return 0;
}
运行结果:
从代码中可以发现,智能指针实际上就是一个普通指针进行了各种的封装,最后封装成一个类,并且智能指针的大小和普通指针大小是一样的,但是功能却比普通指针更加智能。
4、智能指针的拷贝问题
以上虽然已经实现了智能指针,但是有一部分问题还未得到解决,即:指针之间的拷贝。虽然以上的SmartPtr可以实现浅拷贝,但是会导致同一块空间被析构两次,而普通指针的相互拷贝是不会出现这种问题的。
测试智能指针的拷贝场景:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
template<class T>
class SmartPtr//智能指针基本功能
{
public:
SmartPtr(T* ptr = nullptr)
: _ptr(ptr)
{}
~SmartPtr()
{
cout << _ptr << endl;//观察释放的地址
if (_ptr)
delete _ptr;
}
//解引用功能
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
int main()
{
SmartPtr<int> sp1(new int);
SmartPtr<int> sp2(sp1);//浅拷贝
//sp1和sp2调用各种的析构函数会导致同一块空间被析构两次
return 0;
}
运行结果:
可以从结果看到,程序运行过程中发生了崩溃,并且析构函数释放的是同一块空间。
5、auto_ptr
C++标准库中提供了一个名为auto_ptr的智能指针来解决以上问题,该指针的思想是:管理权的转移,即拷贝的对象拥有对该空间的唯一管理权,而被拷贝的对象就失去了对该空间的管理权。
auto_ptr实现代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
namespace ZH
{
template<class T>
class auto_ptr
{
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造
auto_ptr(auto_ptr<T>& sp)
:_ptr(sp._ptr)
{
// 被拷贝对象的指针置为空,失去了管理权
sp._ptr = nullptr;
}
//拷贝赋值
auto_ptr<T>& operator=(auto_ptr<T>& ap)
{
if (this != &ap)
{
// 释放拷贝对象中的资源
if (_ptr)
delete _ptr;
// 转移管理权
_ptr = ap._ptr;
ap._ptr = NULL;
}
return *this;
}
~auto_ptr()
{
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
}
int main()
{
ZH::auto_ptr<int> ap1(new int(12));
ZH::auto_ptr<int> ap2(ap1);
//cout << *ap1 << endl;//此时*ap1不再是12,因为ap1的值为空
cout << *ap2 << endl;
return 0;
}
运行结果:
auto_ptr只是解决了一部分问题,并没有完全解决,因为普通指针即使拷贝给另一个指针,则他们两个都可以访问该空间的数据,并不存在管理权转移的概念,于是C++标准库提出了更加靠谱的unique_ptr指针。
6、unique_ptr
unique_ptr的思路很简单,就是该类单纯的不支持拷贝构造和拷贝赋值,所以不能直接用unique_ptr类型的对象进行相互的赋值和拷贝,因此不会出现同一块空间被析构两次的情况。
unique_ptr的实现代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
namespace ZH
{
template<class T>
class unique_ptr
{
public:
unique_ptr(T* ptr)
:_ptr(ptr)
{}
~unique_ptr() {
if (_ptr)
{
cout << "delete:" << _ptr << endl;
delete _ptr;
}
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
/*unique_ptr(const unique_ptr<T>& sp) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;*/
private:
T* _ptr;
unique_ptr(const unique_ptr<T>& sp);
unique_ptr<T>& operator=(const unique_ptr<T>& sp);
};
}
int main()
{
ZH::unique_ptr<int> up1(new int(4));
ZH::unique_ptr<int> up3(new int(40));
//ZH::unique_ptr<int> up2(up1);//不支持拷贝构造
//up3 = up1;//不支持拷贝赋值
return 0;
}
运行结果:
但是有些场景下是需要智能指针进行相互的拷贝和赋值的,unique_ptr是专门处理不需要拷贝的场景,而shared_ptr是既可以支持拷贝和赋值又不会对同一块空间析构两次。
7、share_ptr
share_ptr的原理:通过计数的方式来决定管理的空间是否需要被释放,若一块空间的管理者多了一个,那么计数+1,当该空间的管理者被销毁了则不会直接释放该空间(因为还有其他的管理者在管理该空间),而是计数-1,直到计数减为0的时候,即表示该空间只剩最后一个管理者,此时将该空间释放。
share_ptr的管理逻辑示意图如下:
share_ptr的代码实现如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
namespace ZH
{
template<class T>
class shared_ptr
{
public:
shared_ptr(T* ptr = nullptr)//构造
:_ptr(ptr)
, _pCount(new int(1))
{}
shared_ptr(const shared_ptr<T>& sp)//拷贝构造
:_ptr(sp._ptr)
, _pCount(sp._pCount)
{
++(*_pCount);//新加入管理者则计数+1
}
void Release()//资源处理
{
if (--(*_pCount) == 0 && _ptr)//计数为0或者_ptr不为空则释放空间
{
cout << "delete:" << _ptr << endl;
delete _ptr;
delete _pCount;
}
}
shared_ptr<T>& operator=(const shared_ptr<T>& sp)//拷贝故障
{
if (_ptr != sp._ptr)
{
Release(); //处理被赋值对象原先的资源
//赋予该对象新的内容
_ptr = sp._ptr;
_pCount = sp._pCount;
++(*_pCount);//新加入管理者则计数+1
}
return *this;
}
~shared_ptr()//析构函数
{
Release();
}
// 像指针一样使用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
int* _pCount;
};
}
int main()
{
//测试拷贝构造
ZH::shared_ptr<int> sp1(new int(12));
ZH::shared_ptr<int> sp2(sp1);
cout << *sp1 << endl;
cout << *sp2 << endl;
//测试拷贝赋值
ZH::shared_ptr<int> sp3(new int(20));
sp1 = sp3;
cout << *sp1 << endl;
cout << *sp3 << endl;
return 0;
}
运行结果:
从结果可以看到,share_ptr可以正常的拷贝和赋值,并且最后只调用了两次析构函数释放不同的空间。
7.1 循环引用
循环引用的问题发生在链表上,链表中的节点有两个智能指针,分别链接前一个节点和后一个节点,具体如下:
struct ListNode//节点
{
int _data;
shared_ptr<ListNode> _prev;//注意节点内的成员类型还是share_ptr
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
//创建两个节点指针,并且指向两个节点
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2;//单链表链接
return 0;
}
以上这种情况的示意图如下:
若是单链表结构则还是会正常的调用析构函数释空间,不会出问题,具体过程为:
n2销毁的时候,n2的引用计数从2变为1,此时不会释放掉n2指向的节点,因为n1的_next还指向n2,只有当n1销毁时,才会把n1指向的节点释放(节点的析构函数会再次调用share_ptr的析构函数释放掉_next和_prev指向的空间),n1的计数是1,n1销毁时该计数从1变为0会把n1指向的节点释放,这时候n1节点成员变量也会调用他们的析构函数,即会调用_next的析构函数,因为_next指向n2,因此会把n2引用计数从1变为0,将n2指向的节点给释放掉,至此两个节点全部都被正常释放了。
以上单链表的结构不会引发循环引用,若以上n2的_prev指针又指向n1,则此时就会引发循环引用,导致内存泄漏的问题。
循环引用示意图如下:
这种情况称为闭环,当n1和n2都销毁时,n1和n2指向的节点却不会被释放,因为n2指向的节点被n1节点的成员_next管着,要等n1节点释放了然后调用_next成员的析构函数将n2的计数变为0,n2节点才能释放。而n1节点又被n2节点的_prev管着,n1节点要等n2节点释放了,然后释放_prev指向的节点,即将n1的计数减为0,从而释放n1节点。
总结就是:1节点等n2节点释放才会释放,而n2节点要等n1节点释放才会释放。若想解决循环引用,则需要用到weak_ptr。
7.2 weak_ptr
weak_ptr之所以可以解决循环引用,原理就是:当两个节点相互连接时,n1->_next = n2;和n2->_prev = n1;时weak_ptr的_next和 _prev不会增加n1和n2的引用计数。
测试代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
namespace ZH
{
template<class T>
class weak_ptr
{
public:
weak_ptr()
:_ptr(nullptr)
{}
weak_ptr(const shared_ptr<T>& sp)
:_ptr(sp.get())
{}
weak_ptr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
struct ListNode//节点
{
int _data;
weak_ptr<ListNode> _prev;//注意节点内的成员类型还是weak_ptr
weak_ptr<ListNode> _next;
~ListNode() { cout << "~ListNode()" << endl; }//观察节点释放了几次
};
};
int main()
{
//创建两个节点指针,并且指向两个节点
shared_ptr<ZH::ListNode> n1(new ZH::ListNode);
shared_ptr<ZH::ListNode> n2(new ZH::ListNode);
n1->_next = n2;
n2->_prev = n1;
return 0;
}
运行结果:
从结果可以发现,调用了两次节点的析构函数,说明两个节点都被正确的释放了。
结语
以上就是关于智能指针的讲解,智能指针结合了类自动调用析构函数的特性,因此可以实现自动释放空间资源的功能,相比于普通指针显得更加”智能“。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!