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

目录

内存泄漏

介绍

分类 

堆内存泄漏

系统资源泄漏

检测内存泄漏的方式

智能指针

引入

介绍

原理 

引入

RAII原则

指针性质

拷贝 

auto_ptr

介绍

代码

boost库

unique_ptr

介绍

代码 

shared_ptr

介绍

删除器 

代码 

问题(循环引用)

weak_ptr 

介绍

代码 


内存泄漏

介绍

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

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

分类 

堆内存泄漏

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

系统资源泄漏

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

检测内存泄漏的方式

在linux下内存泄漏检测:Linux下几款C++程序中的内存泄露检查工具_c++内存泄露工具分析-CSDN博客
在windows下使用第三方工具:VS编程内存泄漏:VLD(Visual LeakDetector)内存泄露库-CSDN博客
其他工具:https://www.cnblogs.com/liangxiaofeng/p/4318499.html

智能指针

引入

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

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

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

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

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

介绍

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

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

原理 

引入

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

RAII原则

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

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

指针性质

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

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

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

拷贝 

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

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

auto_ptr
介绍
  • 具有独占所有权的特性,也就是在拷贝或赋值时接管内存的所有权,从而避免了多个智能指针同时管理同一块内存的情况
  • 但可能有些人不知道这个特性,使用了被拷贝对象,这样就可能导致不可预测的行为
代码
#include "head.h"

//拷贝时,将被拷贝对象置空
namespace my_auto_ptr
{
    template 
    class auto_ptr
    {
    public:
        auto_ptr(T *p) : _ptr(p)
        {
        }
        auto_ptr(auto_ptr &p) : _ptr(p._ptr)
        {
            p._ptr = nullptr;
        }
        auto_ptr(auto_ptr &&p) : _ptr(p._ptr)
        {
            p._ptr = nullptr;
        }
        auto_ptr &operator=(auto_ptr &p)
        {
            _ptr = p._ptr;
            p._ptr = nullptr;
            return *this;
        }
        T &operator*(){
            return *_ptr;
        }
        T *operator->(){
            return _ptr;
        }

        ~auto_ptr()
        {
            delete _ptr;
        }

    private:
        T *_ptr;
    };
}

然后,在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那样,不禁止却不能多个指向一份资源
代码 
#include "head.h"

//不允许拷贝
namespace my_unique_ptr
{
    template 
    class unique_ptr
    {
    public:
        unique_ptr(T *p) : _ptr(p)
        {
        }
        unique_ptr(unique_ptr &p) = delete; //直接定义为删除的函数
        unique_ptr(unique_ptr &&p) : _ptr(p._ptr)
        {
            p._ptr = nullptr;
        } 
        unique_ptr &operator=(unique_ptr &p) = delete;  //同理
        T &operator*()
        {
            return *_ptr;
        }
        T *operator->()
        {
            return _ptr;
        }

        ~unique_ptr()
        {
            delete _ptr;
        }

    private:
        T *_ptr;
    };
}
shared_ptr
介绍
  • shared_ptr才是我们的重头戏,因为只有他支持了正常的拷贝操作,是我们最为实用的智能指针
  • 允许多个智能指针共享同一个资源,这意味着它可以用于协同管理资源,特别是在涉及共享拥有权的情况下非常有用
  • 他内部维护了一个引用计数,可以记录当前资源有多少个指针引用,当引用计数降至零时,资源会被自动释放
  • 如何保证引用计数可以让每份资源对应一个计数值呢?
  • 如果是int类型成员变量,每个指针就有独立的引用计数了,毫无意义,我们要让指向一片资源的共享引用计数
  • 所以可以考虑动态开辟一个引用计数,在资源被申请时开辟,拷贝时直接拷贝指针即可
删除器 
  • 外部开辟空间的方式有很多种,我们必须得依据开辟方式来确定析构方式
  • 所以我们可以考虑直接向类中传递析构方式(因为在内部无法判断是哪种)
  • 传递也有两种方式,给类传还是给构造函数传
  • 由于库中是给构造函数传的,所以我们也这样做
  • 但是,我们需要一个统一的类型来接收删除器啊
  • 哎~之前学过的适配器就可以用上了,因为需要传的都是一个指针,且没有返回值
  • 所以适配器的类型就是 -- function
代码 
#include "head.h"

#pragma once

// 可以拷贝,但在循环引用的情况下,无法使用
// 外部开辟空间有多种方式:new/new[]/malloc,所以需要传入删除器
namespace my_shared_ptr
{
    template 
    class shared_ptr
    {
    public:
        shared_ptr(T *p = nullptr) : _ptr(p)
        {
            _count = new int(1); // 每份资源对应一个计数
        }
        shared_ptr(const shared_ptr &p, function del) //不传默认是delete
            : _ptr(p._ptr), _count(p._count),_del(del)
        {
            ++(*_count);
        }
        shared_ptr(shared_ptr &&p)
            : _ptr(p._ptr),_del(p._del)
        {
            --(p._count);
            p._ptr = nullptr;

            _count = new int(1);
        }
        shared_ptr &operator=(shared_ptr &p)
        {
            if (p._ptr == this->_ptr) // 防止自赋值(空间会提前释放)/引用同一片资源对其赋值(效率低)
            {
                return *this;
            }
            if (--this->_count == 0) // 如果this指向的空间已经没有人引用了,需要手动释放(因为当前该指针的生命周期还没有结束)
            {
                _del(_ptr);
                delete _count;
            }
            _ptr = p._ptr;
            _count = p._count;
            _del=p._del;
            ++(*_count);
            return *this;
        }
        T &operator*() const
        {
            return *_ptr;
        }
        T *operator->() const
        {
            return _ptr;
        }
        T *get() const //给weak_ptr使用的
        {
            return _ptr;
        }

        ~shared_ptr()
        {
            if (--(*_count) == 0)  //只有当引用计数为0时才释放空间
            {
                _del(_ptr);
            }
        }

    private:
        T *_ptr;
        int *_count; // 引用计数
        function _del = [](T *p)
        { delete p; }; // 删除器
    };
}
问题(循环引用)

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

struct ListNode
{
 int _data;
 shared_ptr _prev;
 shared_ptr _next;
 ~ListNode(){ cout << "~ListNode()" << endl; }
};

int main()
{
 shared_ptr node1(new ListNode);
 shared_ptr 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;
}

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

内存泄漏问题,4种智能指针(介绍+模拟实现)_第1张图片

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

为什么呢?

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

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

代码 
#include "shared_ptr.hpp"

// 由shared_ptr构造,没有其他功能
namespace my_weak_ptr
{
    template 
    class weak_ptr
    {
    public:
        weak_ptr()
            : _ptr(nullptr) {}
        weak_ptr(const my_shared_ptr::shared_ptr &p)
            : _ptr(p.get()) {}

        weak_ptr &operator=(const my_shared_ptr::shared_ptr &p)
        {
            _ptr = p.get();
            return *this;
        }

        T &operator*()
        {
            return *_ptr;
        }
        T *operator->()
        {
            return _ptr;
        }

        ~weak_ptr()
        {
            _ptr=nullptr;
        }

    private:
        T *_ptr;
    };
}

你可能感兴趣的:(c++,1024程序员节,c++)