• 内存泄漏问题,4种智能指针(介绍+模拟实现)


    目录

    内存泄漏

    介绍

    分类 

    堆内存泄漏

    系统资源泄漏

    检测内存泄漏的方式

    智能指针

    引入

    介绍

    原理 

    引入

    RAII原则

    指针性质

    拷贝 

    auto_ptr

    介绍

    代码

    boost库

    unique_ptr

    介绍

    代码 

    shared_ptr

    介绍

    删除器 

    代码 

    问题(循环引用)

    weak_ptr 

    介绍

    代码 


    内存泄漏

    介绍

    内存泄漏是指在计算机程序中分配的动态内存(通常是堆内存)未被释放或回收的情况

    这意味着程序在分配内存后,却没有及时释放它,使系统中的可用内存逐渐减少,最终可能导致程序运行变慢,系统崩溃,或者需要重新启动

    分类 

    堆内存泄漏

    • 程序执行中必须要通过malloc / calloc / realloc / new等方式,从堆中分配的一块内存
    • 用完后必须通过调用相应的 free或者delete 删掉
    • 假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak

    系统资源泄漏

    • 指程序使用系统分配的资源,比方套接字、文件描述符、管道等
    • 从我们浅薄的linux知识可以知道,系统的各种结构是需要被管理起来的,这也就需要用一些资源去管理
    • 但如果没有使用对应的函数释放掉,就会导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定

    检测内存泄漏的方式

    智能指针

    引入

    前面已经介绍了内存泄漏,但之前我们遇到的内存泄漏问题大多都是因为自己疏忽了,补上delete就行

    但在接触了c++中的异常机制后,内存泄漏的问题就变得不好处理了

    因为捕捉到异常后,会改变当前的执行流,可能会跳出好几层,一旦跳出后,之前申请到的资源就不好释放了

    即使你可以在捕捉到异常后,先释放资源再抛出,一旦套了好几层写代码可累死

    所以,智能指针就被研究了出来

    介绍

    是C++中用于管理动态内存分配的对象的指针,它们可以帮助开发人员避免内存泄漏和资源管理的复杂性

    主要提供了auto_ptr,unique_ptr,shared_ptr和weak_ptr这四种指针

    原理 

    引入

    • 还记得我们遇到的问题吗,是遇到抛异常的情况会不好释放资源,而且有时候我们也会忘记释放
    • 究其根本我们必须要手动释放
    • 如果我们可以让资源自动释放,尤其是出了作用域之后自动释放,也就是借助对象的特性
    • 我们的对象都是出作用域后自动析构
    • 而这个特性其实就是RAII原则

    RAII原则

    RAII 是一种编程范式,代表资源获取即初始化

    • 它是一种用于资源管理的重要原则,尤其在C++中广泛应用
    • RAII 的核心思想是,资源(如内存、文件句柄、数据库连接等)的获取和释放应该与对象的生命周期相关联
    • 资源获取即初始化:在对象被创建时,资源也应该被分配
    • 对象超出作用域时,析构函数会自动调用,从而释放资源
    • 这样能确保资源不会泄漏,即使在出现异常的情况下也能正确处理资源

    指针性质

    除此之外,智能指针也需要具有指针的特性

    我们虽然让他借助对象的特性,但也不能失去指针的性质

    所以,我们需要在类内部重载->和*

    拷贝 

    智能指针之间最显著的区别就是处理拷贝的方法

    最先在c++98就有auto_ptr的出现,但是在当时被骂惨了,现在公司也明确不能使用这玩意,就是因为它处理拷贝的方式很怪

    auto_ptr
    介绍
    • 具有独占所有权的特性,也就是在拷贝或赋值时接管内存的所有权,从而避免了多个智能指针同时管理同一块内存的情况
    • 但可能有些人不知道这个特性,使用了被拷贝对象,这样就可能导致不可预测的行为
    代码
    1. #include "head.h"
    2. //拷贝时,将被拷贝对象置空
    3. namespace my_auto_ptr
    4. {
    5. template <class T>
    6. class auto_ptr
    7. {
    8. public:
    9. auto_ptr(T *p) : _ptr(p)
    10. {
    11. }
    12. auto_ptr(auto_ptr &p) : _ptr(p._ptr)
    13. {
    14. p._ptr = nullptr;
    15. }
    16. auto_ptr(auto_ptr &&p) : _ptr(p._ptr)
    17. {
    18. p._ptr = nullptr;
    19. }
    20. auto_ptr &operator=(auto_ptr &p)
    21. {
    22. _ptr = p._ptr;
    23. p._ptr = nullptr;
    24. return *this;
    25. }
    26. T &operator*(){
    27. return *_ptr;
    28. }
    29. T *operator->(){
    30. return _ptr;
    31. }
    32. ~auto_ptr()
    33. {
    34. delete _ptr;
    35. }
    36. private:
    37. T *_ptr;
    38. };
    39. }

    然后,在boost库中,提供了更加实用的的scoped_ptr和shared_ptr和weak_ptr

    boost库

    Boost C++ 库是一个开源的、高质量的C++库集合,它扩展和增强了C++语言的功能,提供了许多工具和组件,用于各种领域的应用开发

    Boost库的目标是成为C++标准库的候选扩展,因此它的设计非常高质量,且符合现代C++编程标准

    而它也不负众望的被c++标准库采用了

    c++11提供了unique_ptr和shared_ptr和weak_ptr,其中unique_ptr对应boost 的scoped_ptr

    并且这些智能指针的实现原理是参考boost中的实现的

    unique_ptr
    介绍
    • 它是一种独占所有权的智能指针,意味着只有一个实例可以拥有和管理特定资源,它负责在对象不再需要时自动释放资源
    • 虽然和auto_ptr产生的是一样的结果,但处理方式不同,unique_ptr是直接禁止拷贝,而不是像auto_ptr那样,不禁止却不能多个指向一份资源
    代码 
    1. #include "head.h"
    2. //不允许拷贝
    3. namespace my_unique_ptr
    4. {
    5. template <class T>
    6. class unique_ptr
    7. {
    8. public:
    9. unique_ptr(T *p) : _ptr(p)
    10. {
    11. }
    12. unique_ptr(unique_ptr &p) = delete; //直接定义为删除的函数
    13. unique_ptr(unique_ptr &&p) : _ptr(p._ptr)
    14. {
    15. p._ptr = nullptr;
    16. }
    17. unique_ptr &operator=(unique_ptr &p) = delete; //同理
    18. T &operator*()
    19. {
    20. return *_ptr;
    21. }
    22. T *operator->()
    23. {
    24. return _ptr;
    25. }
    26. ~unique_ptr()
    27. {
    28. delete _ptr;
    29. }
    30. private:
    31. T *_ptr;
    32. };
    33. }
    shared_ptr
    介绍
    • shared_ptr才是我们的重头戏,因为只有他支持了正常的拷贝操作,是我们最为实用的智能指针
    • 允许多个智能指针共享同一个资源,这意味着它可以用于协同管理资源,特别是在涉及共享拥有权的情况下非常有用
    • 他内部维护了一个引用计数,可以记录当前资源有多少个指针引用,当引用计数降至零时,资源会被自动释放
    • 如何保证引用计数可以让每份资源对应一个计数值呢?
    • 如果是int类型成员变量,每个指针就有独立的引用计数了,毫无意义,我们要让指向一片资源的共享引用计数
    • 所以可以考虑动态开辟一个引用计数,在资源被申请时开辟,拷贝时直接拷贝指针即可
    删除器 
    • 外部开辟空间的方式有很多种,我们必须得依据开辟方式来确定析构方式
    • 所以我们可以考虑直接向类中传递析构方式(因为在内部无法判断是哪种)
    • 传递也有两种方式,给类传还是给构造函数传
    • 由于库中是给构造函数传的,所以我们也这样做
    • 但是,我们需要一个统一的类型来接收删除器啊
    • 哎~之前学过的适配器就可以用上了,因为需要传的都是一个指针,且没有返回值
    • 所以适配器的类型就是 -- function
    代码 
    1. #include "head.h"
    2. #pragma once
    3. // 可以拷贝,但在循环引用的情况下,无法使用
    4. // 外部开辟空间有多种方式:new/new[]/malloc,所以需要传入删除器
    5. namespace my_shared_ptr
    6. {
    7. template <class T>
    8. class shared_ptr
    9. {
    10. public:
    11. shared_ptr(T *p = nullptr) : _ptr(p)
    12. {
    13. _count = new int(1); // 每份资源对应一个计数
    14. }
    15. shared_ptr(const shared_ptr &p, function<void(T *)> del) //不传默认是delete
    16. : _ptr(p._ptr), _count(p._count),_del(del)
    17. {
    18. ++(*_count);
    19. }
    20. shared_ptr(shared_ptr &&p)
    21. : _ptr(p._ptr),_del(p._del)
    22. {
    23. --(p._count);
    24. p._ptr = nullptr;
    25. _count = new int(1);
    26. }
    27. shared_ptr &operator=(shared_ptr &p)
    28. {
    29. if (p._ptr == this->_ptr) // 防止自赋值(空间会提前释放)/引用同一片资源对其赋值(效率低)
    30. {
    31. return *this;
    32. }
    33. if (--this->_count == 0) // 如果this指向的空间已经没有人引用了,需要手动释放(因为当前该指针的生命周期还没有结束)
    34. {
    35. _del(_ptr);
    36. delete _count;
    37. }
    38. _ptr = p._ptr;
    39. _count = p._count;
    40. _del=p._del;
    41. ++(*_count);
    42. return *this;
    43. }
    44. T &operator*() const
    45. {
    46. return *_ptr;
    47. }
    48. T *operator->() const
    49. {
    50. return _ptr;
    51. }
    52. T *get() const //给weak_ptr使用的
    53. {
    54. return _ptr;
    55. }
    56. ~shared_ptr()
    57. {
    58. if (--(*_count) == 0) //只有当引用计数为0时才释放空间
    59. {
    60. _del(_ptr);
    61. }
    62. }
    63. private:
    64. T *_ptr;
    65. int *_count; // 引用计数
    66. function<void(T *)> _del = [](T *p)
    67. { delete p; }; // 删除器
    68. };
    69. }
    问题(循环引用)

    看着似乎shared_ptr完美无缺了,但是,当遇到下面这种情况时,会发生内存泄漏

    1. struct ListNode
    2. {
    3. int _data;
    4. shared_ptr _prev;
    5. shared_ptr _next;
    6. ~ListNode(){ cout << "~ListNode()" << endl; }
    7. };
    8. int main()
    9. {
    10. shared_ptr node1(new ListNode);
    11. shared_ptr node2(new ListNode);
    12. cout << node1.use_count() << endl;
    13. cout << node2.use_count() << endl;
    14. node1->_next = node2;
    15. node2->_prev = node1;
    16. cout << node1.use_count() << endl;
    17. cout << node2.use_count() << endl;
    18. return 0;
    19. }

    本身,他俩各自引用计数都为1,但是!!!其内部还有俩shared_ptr指针,然后互相一指,让他俩计数都变成2

    最后,当俩对象析构后,引用计数仍为1,这样就会导致内存泄漏

    为什么呢?

    • node1如果要释放,需要_p释放,也就是node2析构,但node2被node1指着,只有当node1释放才行,这就又回到最开始了
    • 也就是node1释放需要node2先释放,而node2也一样,它释放需要让node1先释放,两者成为一种纠缠态
    • 这也被叫做"循环引用"问题
    • 所以,为了解决这个问题,提出了weak_ptr
    weak_ptr 
    介绍

    用于解决潜在的循环引用问题,它允许你共享资源的引用,但不会增加资源的引用计数,从而避免了循环引用导致的内存泄漏

    代码 
    1. #include "shared_ptr.hpp"
    2. // 由shared_ptr构造,没有其他功能
    3. namespace my_weak_ptr
    4. {
    5. template <class T>
    6. class weak_ptr
    7. {
    8. public:
    9. weak_ptr()
    10. : _ptr(nullptr) {}
    11. weak_ptr(const my_shared_ptr::shared_ptr &p)
    12. : _ptr(p.get()) {}
    13. weak_ptr &operator=(const my_shared_ptr::shared_ptr &p)
    14. {
    15. _ptr = p.get();
    16. return *this;
    17. }
    18. T &operator*()
    19. {
    20. return *_ptr;
    21. }
    22. T *operator->()
    23. {
    24. return _ptr;
    25. }
    26. ~weak_ptr()
    27. {
    28. _ptr=nullptr;
    29. }
    30. private:
    31. T *_ptr;
    32. };
    33. }
  • 相关阅读:
    字符贡献度问题
    第3章 决策树
    隆云通吸顶多参数传感器
    kotlin类
    制作一个简单HTML电影网页设计(HTML+CSS)
    android11.0 Launcher3 高端定制之 BubbleTextView 应用名称双行显示
    Flutter:安装依赖报错doesn‘t support null safety
    DQL查询数据(最重点)
    log4j:WARN No appenders could be found for logger
    Financial Statement Analysis with Large Language Models论文精读
  • 原文地址:https://blog.csdn.net/m0_74206992/article/details/134023885