c++ - 第21节 - 智能指针
创始人
2024-05-09 21:51:02
0

1.为什么需要智能指针

分析一下下面这段程序有没有什么内存方面的问题? 前面在异常的博客中,我们分析了下图一的代码Func函数中如果div()函数抛异常则程序会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,那么Func函数中new的空间没有执行后面的delete释放空间代码会造成内存泄漏。 因此在异常的博客中我们的处理办法是在Func函数中增加try catch捕获拦截异常,将delete释放空间操作处理完之后再把异常抛出去,如下图二所示。

在异常的博客中new开辟一次空间上图二所示的方式是可以的,但上图二所示的代码两次new开辟空间,如果第二次new开辟空间开辟失败抛了异常,例如上面代码int* p2 = new int开辟失败报异常会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,那么Func函数中代码int* p2 = new int往后的代码都没有运行,因此第一次new开辟空间后也没有delete释放空间,会造成内存泄漏。

那么如何解决上面的问题呢?c++提出了智能指针的解决方案。


2.智能指针的使用及原理

2.1.RAII

RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。这种做法有两大好处: \bullet 不需要显式地释放资源。 \bullet 采用这种方式,对象所需的资源在其生命期内始终保持有效。 注:RAII技术是智能指针实现的指导思想。 如下图所示,我们使用智能指针类来解决本博客第一节的问题,这样即使Func函数中第二次new开辟空间失败即SmartPtr sp2(new int)中new int开辟空间失败抛异常,程序会直接跳到主函数的catch捕获程序部分,然后接着主函数catch捕获程序部分往后执行代码,此时Func函数栈帧已经销毁,sp1智能指针通过系统自动调用智能指针类的析构函数将sp1智能指针指向的空间进行了释放,解决了内存泄漏的问题。
// 使用RAII思想设计的SmartPtr类
template
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){}~SmartPtr(){if (_ptr)delete _ptr;}private:T* _ptr;
};
int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}
void Func()
{//使用方法一int* p1 = new int;SmartPtr sp1(p1);//使用方法二SmartPtr sp2(new int);cout << div() << endl;
}
int main()
{try {Func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}

2.2.智能指针的原理

上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此AutoPtr模板类中还得需要将operator*函数和operator->函数重载一下,才可让其像指针一样去使用。
template
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;
};
int main()
{SmartPtr sp1(new int);*sp1 = 10;cout << *sp1 << endl;SmartPtr sparray(new Date);// 需要注意的是这里应该是sparray.operator->()->_year = 2018;// 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->sparray->_year = 2023;sparray->_month = 1;sparray->_day = 1;
}
总结一下智能指针的原理: 1. RAII特性。 2. 重载operator*和opertaor->,具有像指针一样的行为。

智能指针的拷贝问题:

智能指针类模拟的是原生指针的行为,因此智能指针类的拷贝应该是浅拷贝。

智能指针类正常的浅拷贝会出现问题,原智能指针和拷贝对象智能指针都指向同一块内存空间,析构时调用智能指针类的析构函数,两个智能指针会调用两次析构函数分别释放,指向的内存空间会释放两次,系统报错。

问题:容器的迭代器也是模拟原生指针的行为,拷贝的时候也是浅拷贝,为什么智能指针浅拷贝会出错而迭代器不会出错?

答:容器的迭代器类中,迭代器类的析构函数不负责节点的释放,节点属于容器由容器类的析构函数释放,迭代器生命周期结束调用析构函数时不会释放空间,不会报错。对于智能指针,智能指针类的析构函数要负责释放资源,所以智能指针生命周期结束调用析构函数时,如果两个智能指针指向同一空间会连续释放就会报错。

为了解决智能指针拷贝的问题,STL官方库中给出了auto_ptr、unique_ptr、shared_ptr和weak_ptr四种智能指针来解决,下面我们详细介绍这四种智能指针。

2.3.std::auto_ptr

auto_ptr文档介绍:auto_ptr - C++ Reference (cplusplus.com)

auto_ptr的实现原理:管理权转移的思想,被拷贝的对象悬空,如下图所示,被拷贝对象无法再使用。 注: 1.很多公司明确要求不能使用auto_ptr。 2.auto_ptr在标准库的中,因此要使用auto_ptr需要#include

C++98版本的库中就提供了auto_ptr的智能指针。下面简化模拟实现了一份bit::auto_ptr来了解它的原理。

SmartPtr.h文件:

// C++98 管理权转移 auto_ptr
namespace bit
{templateclass auto_ptr{public:auto_ptr(T* ptr):_ptr(ptr){}auto_ptr(auto_ptr& sp):_ptr(sp._ptr){// 管理权转移sp._ptr = nullptr;}auto_ptr& operator=(auto_ptr& ap){// 检测是否为自己给自己赋值if (this != &ap){// 释放当前对象中资源if (_ptr)delete _ptr;// 转移ap中资源到当前对象中_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;};
}

2.4.std::unique_ptr

boost库:Boost是为c++标准库提供扩展的一些C++程序库的总称。boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称。boost库由C++标准委员会库工作组成员发起,其中有些内容有望成为下一代C++标准库内容。在C++社区中影响甚大,是不折不扣的“准”标准库。

C++11标准库库才更新智能指针实现,C++11出来之前,boost库出了比标准库中auto_ptr更好用的scoped_ptr、shared_ptr、weak_ptr,C++11标准库将boost库中智能指针精华部分吸收了过来,产生了unique_ptr、shared_ptr、weak_ptr智能指针。

unique_ptr文档介绍:unique_ptr - C++ Reference (cplusplus.com)

unique_ptr的实现原理:简单粗暴的防拷贝,即不让拷贝,如下图所示,如果拷贝编译就报错。

注:

1.标准库中unique_ptr的原型是boost库中的scoped_ptr。 2.unique_ptr在标准库的中,因此要使用unique_ptr需要#include 下面简化模拟实现了一份UniquePtr来了解它的原理。 SmartPtr.h文件:
// C++11 防拷贝 unique_ptr
namespace bit
{templateclass 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& sp) = delete;unique_ptr& operator=(const unique_ptr& sp) = delete;private:T* _ptr;};
}

注:

1.防拷贝的方法是拷贝函数只声明不实现,但是仅仅只声明不实现是不行的,不排除一些老六在外面自己实现,如下图的①所示。为了防止这种情况,c++98的方法是将拷贝函数声明设置为私有,这样类外面就无法调用(即使老六自己实现了在类外面也无法调用拷贝函数),如下图②所示,c++11的方法是直接在拷贝函数声明的后面加一个=delete即可,这样拷贝函数不能再被调用(即使老六自己实现了在任何地方也都无法调用拷贝函数),如下图的③所示。

2.赋值运算符重载函数与拷贝构造函数相同,要做到防赋值,即如果赋值编译就报错。

2.5.std::shared_ptr

shared_ptr文档介绍:shared_ptr - C++ Reference (cplusplus.com)

shared_ptr的实现原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。例如实验课老师在上完课之后都会通知,让最后走的学生记得把门锁下。

原理详细解释:shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。

注:

1.标准库中shared_ptr的原型是boost库中的shared_ptr。 2.shared_ptr在标准库的中,因此要使用shared_ptr需要#include

下面简化模拟实现了一份UniquePtr来了解它的原理。

SmartPtr.h文件:

注:

1.

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...