【C++学习】智能指针

这篇具有很好参考价值的文章主要介绍了【C++学习】智能指针。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
【C++学习】智能指针

🥮智能指针

🍢为什么需要智能指针

【C++学习】智能指针
如上图代码所示,在Func中开辟动态空间,在调用完Division函数后释放该空间。

  • 如果Division没有抛异常,那么动态空间会被正常释放。
  • 如果Division抛了异常,就会去匹配对应的catch,而Func中没有catch来捕获异常,所以执行流就直接跳到了main函数中,Func的栈帧被销毁。
  • 此时Func中开辟的动态空间还没有释放,就会导致内存泄漏。

【C++学习】智能指针

  • 可以在Func中捕获Division的异常,在捕获到以后将动态空间释放,然后再将异常重新抛出,让mian中再次捕获并进行具体的处理。
  • 如果Division没有抛异常,就会执行叉掉的那个delete来释放动态空间。

使用上诉办法就可以避免内存泄漏了。

【C++学习】智能指针
此时又在堆区上开辟了一个数组。如果是Division抛出的异常,只需要在捕获该异常的时候将p1和p2都释放。

  • 但是new也有可能抛异常,此时就需要再捕获new的异常,进行相应的处理。

如上图所示,这样虽然能解决办法,但是代码看起来很不美观,可读性非常差劲,而且逻辑比较复杂。

🍢RAII

  • RAII:是英文Resource Acquisition Is Initialization(请求即初始化)的首字母,是一种利用对象生命周期来控制程序资源的简单技术。
  • 这些资源可以是内存,文件句柄,网络连接,互斥量等等。

【C++学习】智能指针
创建一个类模板SmartPtr(智能指针),如上图代码所示。

  • 成员变量只有一个,是一个指针变量,通过构造函数进行初始化。
  • 在析构函数中释放成员指针变量指向的资源。
  • 重载指针常用的操作符,使该类可以像指针一样操作。

【C++学习】智能指针

在Func中,使用两个new以后返回的指针初始化智能指针,分别为sp1和sp2。由于运算符重载,智能指针可以像内置指针一样操作,进行解引用,下标访问等。

  • 当指向Divsion抛出异常后,Func的栈帧也会被销毁,执行流跳转到main中去匹配catch。
  • 在Func栈帧销毁的时候,智能指针对象sp1和sp2的生命周期也会结束,会自动调用析构函数。
    *而我们在析构函数中对new出来的资源进行了释放,所以此时就不存在内存泄漏的问题。

所谓RAII,就是将资源的生命周期和对象的生命周期绑定。从构造函数开始,到析构函数结束。


智能指针就是使用了RAII技术,并且利用对象生命周期结束时,编译器会自动调用对象的析构函数来释放资源。

  • 智能指针的智能就在于资源会被自动释放,不需要显式地释放资源。
  • 采用智能指针,对象所需的资源在其生命周期内始终保持有效。

智能指针包括两大部分:

  • RAII(将资源绑定在智能指针对象上)
  • 具有像指针一样的行为(重载操作符)

🥮auto_ptr

【C++学习】智能指针

将前面的智能指针该名为auto_ptr,仿真wxf命名空间中。

【C++学习】智能指针
使用编译器自动生成的拷贝构造函数。

【C++学习】智能指针

上面代码在运行时会报错。

  • 智能指针ap2拷贝复制了ap1,此时ap1和ap2都指向同一块动态内存空间。
  • 当程序执行结束以后,ap1对象和ap2对象都会销毁,并且会执行各自的析构函数,所以那份动态空间就会被释放两次,所以报错了。

所以需要自己显式定义一个拷贝构造函数,不能让两个智能指针指向同一份动态内存空间。

【C++学习】智能指针

  • 让原本的智能指针置空,不再管理这份动态内存,只让新拷贝出来的智能指针来管理。

【C++学习】智能指针

C++98就提供了这样一个智能指针,同样在拷贝的时候,原本指针会被置空,在使用库里的智能指针时,要包头文件< memory >。

【C++学习】智能指针
上图代码中使用的是std中的auto_ptr,通过调试窗口可以看到,执行完拷贝后,ap2指向了原本ap1管理的动态内存空间,而ap1被置空了。

  • auto_ptr会发生管理权的转移,在拷贝构造后,管理权从ap1转移到了ap2.
  • 而且原本的ap1会被悬空,就不能再使用了。

对于不清除auto_ptr这个特点的人来说,拷贝后再次使用ap1就会出问题。

  • auto_ptr是一个失败的设计,很多公司明确要求不能使用auto_ptr。

🥮unique_ptr

在C++11中提供了更加靠谱的unique_ptr智能指针:

【C++学习】智能指针

  • unique_ptr直接禁止使用拷贝构造函数,即使编译器也不能生成默认的拷贝构造函数,因为使用了delete关键字。

unique_ptr采用的策略就是,既然拷贝有问题,那么就禁止拷贝,这确实解决了悬空等问题,使得unique_ptr是一个独一无二的智能指针。

【C++学习】智能指针

继续在wxf命名空间中写一个unique_ptr智能指针的类模板。同样包括RAII和像指针一样的操作符重载,和之前的auto_ptr一样。

  • 但是要禁止使用拷贝构造函数,使用delete禁止编译器自动生成。

此时unique_ptr就不能被拷贝了,一个智能指针对应一份动态内存空间。

【C++学习】智能指针
可以看到,在拷贝unique_ptr的时候,直接报错"尝试引用已删除的函数",因为拷贝构造使用了delete禁止了拷贝构造。

【C++学习】智能指针

  • 标准库中的unique_ptr在拷贝构造时同样也会报错"尝试引用已删除函数"。

【C++学习】智能指针
可以看到,在标准库中,unique_ptr同样不可以进行赋值,也是使用了delete。

【C++学习】智能指针
我们也要做到和库中一样。

但是如果就想拷贝智能指针呢?

🥮shared_ptr

C++11提供了更加可靠的智能指针,并且支持shared_ptr:

【C++学习】智能指针
【C++学习】智能指针

  • sp2拷贝了sp1,sp1指向一个动态内存空间。
  • sp1将其管理的内容加1,然后使用sp2将这个空间中的数值打印了出来。

拷贝出来的shared_ptr和原本的shared_ptr共同管理着一份动态内存空间,如下图所示:

【C++学习】智能指针

但是在sp1和sp2生命周期结束的时候,这块空间并不会被多次释放而发生错误。

【C++学习】智能指针

在shared_ptr中,除了指向的动态内存空间之外,还维护着一个变量,用来计数,这种方式称为引用计数

  • 只有一个shared_ptr指向这份动态内存空间时,引用计数值就是1。
  • 每拷贝一份引用计数值就会加1。
  • 当某个智能指针被销毁时,说明该指针不使用该资源了,引用计数值就会减一。
  • 如果引用计数值是0,说明当前的智能指针是最后一个使用该资源的对象,必须释放该资源。
  • 如果引用计数不是0,说明还有其他智能指针对象在使用该资源,所以不能释放。

上面描述的就是shared_ptr的原理,下面来看看代码实现:

【C++学习】智能指针

  • 在创建智能指针的时候,同时再创建一个引用计数,而且需要放在堆区上。
  • 每当拷贝一个智能指针的时候,引用计数值就加一,并且新的智能指针也指向那份动态内存空间。
  • 在智能指针析构的时候,将该指针的计数值先减一,然后判断是否为0,如果为0则释放动态内存空间以及引用计数,不为0则直接结束析构。

【C++学习】智能指针
我们自己实现的shared_ptr同样可以实现库里的效果。

🍢智能指针的线程安全

C++11提供了多线程的库,可以直接以C++11的方式实现多线程并发。

【C++学习】智能指针
可以通过创建thread对象来创建线程。

【C++学习】智能指针
可以通过mutex对象来加锁和解锁。

C++11多线程的详细内容之后本喵会详细讲解,这里只是简单介绍一下。

【C++学习】智能指针
在shared_ptr中增加一个获取引用计数的接口。

【C++学习】智能指针
先创建一个shared_ptr智能指针。

  • 创建线程1,在线程1中拷贝n = 50000次智能指针,每次进入for循环作用域拷贝,出作用域销毁。
  • 再创建线程2,做和线程1同样的事情。

在主线程中等待线程成功后,打印引用计数的值。

  • 理论上,线程1和线程2一共拷贝了100000次智能指针,引用计数值也加减了10000次。
  • 最终在获取引用计数值的时候应该是1,因为两个从线程中拷贝的智能指针最终都释放了,只剩下了主线程中的智能指针。

【C++学习】智能指针
但是运行多次,每次的结果都不一样,而且都不是1。

  • 这是因为发生了数据不一致问题,也就是此时的智能指针不是线程安全的。

两个线程及主线程中的所有智能指针都共享引用计数,又因为拷贝构造以及析构都不是原子的,所以导致线程不安全问题。

解决办法和Linux中一样,需要加锁:

【C++学习】智能指针

  • 在shared_ptr中增加互斥锁的指针成员变量。
  • 在创建智能指针的构造函数中,在堆区创建一把互斥锁。

互斥锁同样需要放在堆区,此时不同线程才能共享这把锁。并且每创建一个指向新动态空间的智能指针都需要创建一把锁。

  • 在拷贝构造函数中,引用计数值加1时,需要让其成为原子操作,所以在加1前加锁,在加1后解锁,让多线程串行访问引用计数值。
  • 在析构函数中,引用计数值减1时,同样需要让其成为原子操作,所以在减1前加锁,在减1后解锁。并且当析构最后一个智能指针时,不仅要释放管理的动态内存空间,也要释放互斥锁。

通过加锁和解锁操作,就让多线程互斥访问引用计数值,就不会发生数据不一致的线程不安全问题。

【C++学习】智能指针

此时即使多次运行,最后打印的引用计数值都是1,此时就对于引用计数的访问就成了线程安全的了。

【C++学习】智能指针
再增加一个接口,用来获取管理空间的地址,如上图所示。

【C++学习】智能指针

  • 两个线程中,在拷贝完主线程的智能指针后,都对共同管理的内容加一。
  • 理论上,两个线程各对动态内存空间的值加50000次,最终主线程中输出的值应该是100000。

【C++学习】智能指针

多次运行,只有一次出现理论值100000,其他都不是,而且值不相同。

【C++学习】智能指针
可以看到,库中的智能指针同样会发生这个问题。

  • 这是因为这些智能指针共同管理的动态内存空间是线程不安全的。

【C++学习】智能指针

  • 创建一把锁,在两个线程访问临界资源的位置进行加锁和解锁,让多线程串行访问共同管理的动态内存空间。
  • 这里的锁和引用计数时加的锁不是一把锁,因为管理的动态内存空间和引用计数是两个不同的临界资源,所以需要两把锁。

【C++学习】智能指针

此时无论运行多少次,最终的结果和我们的理论相符,所以此时的智能支持才完全线程安全。

结论:

  • 智能指针本身是线程安全的,因为对引用计数的访问是互斥访问。
  • 智能指针管理的资源是线程不安全的,必须再加锁才能线程安全。

🍢operator=()

unique_ptr是不可以赋值的,shared_ptr作为改进版,必然是可以赋值的:

【C++学习】智能指针
通过调试可以看到sp1成功赋值给了sp2,并且两个智能指针都指向同一块动态内存空间。

那么这是如何实现的呢?

【C++学习】智能指针

  • 先判断两个智能指针是否管理同一块资源,如果是则没有必要赋值,直接返回当前指针。
  • 再该智能指针管理的空间释放,如果是多个指针管理,则引用计数减一,如果只有该指针管理,则直接释放资源,使用前面实现relase()就可以做到。
  • 让两个指针共享同一份资源,包括管理的空间,引用计数,互斥锁。
  • 最后让引用计数值加1。

【C++学习】智能指针

调试可以看到,我们自己实现shared_ptr可以实现和库中一样的效果。

🍢循环引用

shared_ptr就完美了吗?并不是,它有一个死穴——循环引用。

【C++学习】智能指针
创建一个链表节点,如上图所示,在该节点的析构函数中打印提示信息。

【C++学习】智能指针

  • 将node1和node2互相指向,形成循环引用。

执行该程序后,节点析构函数中的打印信息并没有打印,说明析构出了问题。

【C++学习】智能指针
node1和node2刚创建的时候,它两的引用计数值都是1。

  • 当两个节点循环引用后,它们的引用计数值都变成了2。

如果node1释放,还有node2的prev指向node1,所以node1不会被释放,也就不会执行析构函数。

如果node2释放,还有node1的next指向node2,所以node2也不会被释放,也不会执行析构函数。

  • next属于node1的成员,node1释放了,next才会释放,不再指向node2。
  • prev属于node2的成员,node2释放了,prev才会释放,不再指向node1。
  • 但是node1和node2在释放自己后,仅仅是让各自的引用计数值减1,两个节点还是存在,由next和prev管理着。

在循环引用中,节点得不到真正的释放,就会造成内存泄漏。

循环引用的根本原因在于,next和prev也参与了资源的管理

所以解决办法就是让节点中的next和prev仅指向对方,而不参与资源管理,也就是计数值不增加。

🥮weak_ptr

weak_ptr是为解决循环引用问题而产生的,所以它的拷贝构造以及赋值都不会让引用计数值加1,仅仅是指向资源。

【C++学习】智能指针
weak_ptr中只有一个成员变量_ptr,用来指向动态内存空间,在默认构造函数中,仅仅指向动态内存空间。

  • 拷贝构造函数和赋值运算符重载函数中,拷贝和赋值的对象都是shared_ptr指针。

weak_ptr就是用来解决循环引用问题的,所以拷贝和赋值的智能指针必须是shared_ptr。

  • weak_ptr和shared_ptr并不是同一个类,所以获取shared_ptr中的_ptr时,不能直接访问,需要通过shared_ptr的接口get()来获取。

【C++学习】智能指针
将节点中的prev和next使用weak_ptr智能指针。

【C++学习】智能指针
可以看到,此时循环引用就可以正常析构了。

【C++学习】智能指针

  • 使用了weak_ptr以后,循环引用时,各个节点的引用计数值不增加,如上图所示。
  • 所以node1和node2释放时节点也就真的释放了。

库中同样有weak_ptr,也是用来解决循环引用的:

【C++学习】智能指针
标准库中智能指针的使用方法和我们自己实现的是一样的。

🥮定制删除器

【C++学习】智能指针
前面我们自己实现的所有智能指针中,在释放动态内存资源的时候,都只用了delete,也就是所有new出来的资源都是单个的。

  • 如果new int[20],或者malloc(40)呢?
  • 再或者是句柄呢?如FILE* fp。

当需要释放的资源是其他类型的呢?delete肯定就不能满足了,对于不同类型的资源,需要定制删除器。

【C++学习】智能指针
先来看库中是如何实现的,这里仅拿shared_ptr为例,unique_ptr也是一样的。

  • 在构造智能指针的时候,可以传入定制的删除器。
  • 可以采用仿函数的方式,lambda的方式,以及函数指针的方式,只要是可调用对象都可以。

此时的智能指针指向的是动态数组,我们传入的定制删除器也是释放数组的,通过打印信息可以看到成功执行了。

【C++学习】智能指针

  • 写一个默认删除方式的仿函数,执行的是delete ptr。
  • 在shared_ptr类模板的模板参数中增加一个定制删除器的模板参数,缺省值默认删除方式。
  • 在释放资源的时候,在Release()中调用定制的删除器仿函数对象。
  • 成员变量中增加一个删除器的仿函数对象。

【C++学习】智能指针
在创建智能指针对象的时候,实例化时传入定制的删除器类型,这里只能是仿函数,不能是lambda表达式,因为实例化时传入的是类型,不是对象。

【C++学习】智能指针
即使使用decltype来生命lambda表达式类型也不可以,如上图所示。

  • decltype是在执行时推演类型,而这里是实例化是在编译时实例化。

【C++学习】智能指针

还可以定制释放文件指针的删除器,如上图所示。

  • 以写方式打开文件后返回的文件指针初始化智能指针。
  • 在智能指针生命周期结束的时候,在析构函数中调用定制的删除器关闭了文件。

【C++学习】智能指针

  • 标准库中的智能指针,在使用定制删除器的时候,是在构造对象时传入函数对象来实现的。
  • 我们自己实现的智能指针,是在实例化时,传入仿函数类型实现的。

这是因为,C++11标准库实现的方式和我们不一样,它的更加复杂,专门封装了几个类管理引用计数以及定制删除器等内容。

🥮总结

智能指针的发展经过:

  • C++98中的auto_ptr,存在非常大的缺陷,在拷贝构造或者赋值的时候,原本的auto_ptr会被置空,所以这个智能指针存在非常大的缺陷,很多地方都禁止使用。
  • C++11中的unique_ptr,禁止了拷贝和赋值,直接避免了auto_ptr可能存在的缺陷,是一个独一无二的智能指针,但是它不能拷贝和赋值。
  • C++11又提供了shared_ptr,通过引用计数的方式解决了不能拷贝和赋值的缺陷,并且通过互斥锁保证了shared_ptr本身的线程安全,但是它的死穴是循环引用。
  • C++11为了解决shared_ptr的循环引用问题,又提供了weak_ptr智能指针,通过仅指向不管理的方式解决了这个问题。

在使用的时候要根据具体情况选择合适的智能指针,切记最好不要使用auto_ptr。

其实C++委员会还发起了一个库,叫boost库,这个库可以理解为C++标准库的先行版,boost库中好用的东西会被C++标准库收录,标准库中的智能指针就是参照boost库中的智能指针再加以修改定义出来的。文章来源地址https://www.toymoban.com/news/detail-462155.html

到了这里,关于【C++学习】智能指针的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针

    【重学C++】02 脱离指针陷阱:深入浅出 C++ 智能指针 大家好,今天是【重学C++】系列的第二讲,我们来聊聊C++的智能指针。 在上一讲《01 C++如何进行内存资源管理》中,提到了对于堆上的内存资源,需要我们手动分配和释放。管理这些资源是个技术活,一不小心,就会导致内

    2024年02月05日
    浏览(36)
  • 【C++】智能指针详解

    今天我们来讲一下c++中的智能指针。 智能指针不是指针,是一个管理指针的类,用来存储指向动态分配对象的指针,负责自动释放动态分配的对象,防止堆内存泄漏。 动态分配的资源,交给一个类对象去管理,当类对象声明周期结束时,自动调用析构函数释放资源 C++ 98 中产

    2024年02月04日
    浏览(42)
  • 智能指针——C++

    智能指针相较于普通指针的区别,就是智能指针可以不用主动释放内存空间,系统会自动释放,避免了内存泄漏。 需包含的头文件: #include memory unique_ptr 三种定义方式 先定义一个类 使用std::make_unique的方式:推荐使用的方式 程序结束自动调用析构函数 使用new的方式声明智能

    2023年04月22日
    浏览(34)
  • C++——智能指针

    内存泄漏 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内 存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对 该段内存的控制,因而造成了内存的浪费。 内存泄漏的危害:长期运行的

    2024年02月09日
    浏览(58)
  • 面试:C++ 11 智能指针

    内存泄露在维基百科中的解释如下: 在计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了

    2024年02月07日
    浏览(27)
  • C++新特性:智能指针

    智能指针主要解决以下问题: 1)内存泄漏:内存手动释放,使用智能指针可以自动释放 2)共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题,例如同样的数据帧,但是业务A和业务B处理的逻辑不一样(都是只读)。可以用 shared_ptr 共享数据帧对象的所有

    2024年02月09日
    浏览(29)
  • C++进阶 智能指针

    本篇博客简介:介绍C++中的智能指针 我们首先来看下面的这段代码 在上面这段代码中有着一个很明显的内存泄露风险 当我们的程序运行在Func函数内的div函数时 很可能因为除0错误而跳转到另外一个执行流从而导致Func函数内两个new出来的内存没法被回收 为了解决这个问题我

    2024年02月13日
    浏览(35)
  • C++智能指针的发展

    GC–garbage collection垃圾回收,Java里的机制。在头文件 memory 中 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那

    2023年04月19日
    浏览(23)
  • 【C++入门到精通】智能指针 [ C++入门 ]

    在C++编程中,内存管理一直是一个重要的话题。手动分配和释放内存可能会导致各种问题,例如内存泄漏和悬挂指针,这些问题往往会导致程序崩溃或产生不可预测的结果。为了解决这些问题, C++提供了一种称为智能指针的机制,它可以自动管理内存分配和释放,从而避免了

    2024年01月21日
    浏览(31)
  • C++知识点 -- 智能指针

    对于上面的场景,p1和p2在new申请空间后,div函数如果出现了除0错误,那么程序就会抛出异常,跳到接受异常的程序段继续执行,p1和p2申请的空间就没有被正常释放,造成了内存泄漏; 这种场景我们就可以使用智能指针来解决空间的释放问题。 RAII(Resource Acquisition Is Initia

    2024年02月03日
    浏览(51)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包