文章目录

  • 内存泄漏
    • 什么是内存泄漏
    • 内存泄漏的危害:
    • 如何避免内存泄漏
  • RAII
  • 智能指针
  • auto_ptr
  • unique_ptr
  • shared_ptr
    • 循环引用问题
  • weak_ptr
  • 定制删除器

内存泄漏

什么是内存泄漏

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

内存泄漏的危害:

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

一般来说,内存泄漏大多数存在于c/c++程序中,因为现在的主流语言如java,python,c#等都具有完善的垃圾回收机制,所以一般不会存在内存泄漏的情况,但也因为这种上述语言存在这种垃圾回收机制,所以在回收内存的时候也会花费宝贵的CPU资源,导致速度有所下降,所以对于c和c++,这是一把双刃剑,全靠程序员如何掌控。

C/C++程序中一般我们关心两种方面的内存泄漏:

  • 堆内存泄漏(Heap leak) 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete
    删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
  • 系统资源泄漏指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

如何避免内存泄漏

  1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。(即使注意了释放,也可能会因为异常的抛出导致内存泄漏,需要智能指针来管理才有保证)
  2. 采用RAII思想或者智能指针来管理资源。
  3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
  4. 内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具

RAII

RAII ,也称为“资源获取就是初始化”,是C++语言的一种管理资源、避免泄漏的惯用法,是利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等) 的简单技术。C++标准保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。简单的说,RAII 的做法是使用一个对象,在其构造时获取资源,在对象生命期控制对资源的访问使之始终保持有效,最后在对象析构的时候释放资源

这种利用对象的声明周期来进行资源管理的方法有以下好处

  • 不需要显式地释放资源。
  • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

智能指针

智能指针就是借助RAII思想来完成的,利用智能指针的生命周期来管理指针指向的资源。

实现的思路很简单

  1. 运用RAII的原理,利用构造函数来获取数据,析构函数来销毁数据
  2. 重载操作符->和*使其操作更像指针。
template<class T>
class smart_ptr
{public:smart_ptr(T* ptr): _ptr(ptr){}~smart_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;
};

这样一个简单的智能指针雏形就完成了,他已经可以完成基本的智能指针的功能,但是还有一个问题没有解决,也就是当多个智能指针管理同一个资源的问题,如果不进行处理,就会对同一个资源进行多次释放,导致错误。

在C++中,对于这个问题,通过管理权转移、防拷贝、引用计数的方法分别实现了auto_ptr、unique_ptr、shared_ptr


auto_ptr

auto_ptr文档

在C++98中给出了第一个智能指针auto_ptr,他的实现思路就是管理权的转移,当通过赋值或者拷贝使两个智能指针指向同一对象时,就会将管理权从老的智能指针转到新的智能指针,此时老的智能指针就会悬空,不再指向资源。

这也是其最大的缺陷,因为被转移的指针悬空,此时访问再访问它就会导致访问空指针报错,对于不熟悉它特性的人很容易就会导致错误,所以在C++11中已经不再推荐使用auto_ptr

下面是auto_ptr的模拟实现,具体思路都在注释中

namespace lee
{/*c++98 auto_ptr实现思路:管理权转移缺陷:当管理权转移后会导致被转移的指针悬空,访问就会报错,如果不熟悉它的特性就会出问题。*/template<class T>class auto_ptr{public:auto_ptr(T* ptr): _ptr(ptr){}~auto_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}//管理权转移,被转移的指针悬空auto_ptr(auto_ptr<T>& ap): _ptr(ap._ptr){ap._ptr = nullptr;}auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){//释放当前资源if (_ptr){delete _ptr;}//管理权转移_ptr = ap._ptr;ap._ptr = nullptr;}return *this;}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
};

unique_ptr

unique_ptr文档

C++11中又引入了unique_ptr,他的实现思路非常简单粗暴,就是防拷贝,既然多个智能指针指向同一资源会导致问题,那就干脆不让你这样做,这样问题自然也就解决了。C++11中也是非常推荐使用这种智能指针,因为其比起shared_ptr和auto_ptr来说较为稳定,不会导致严重的错误

但是其也存在缺陷,就是在需要拷贝的场景下他没有办法使用。

下面是unique_ptr的模拟实现,具体思路都在注释中

namespace lee
{/*c++11 unique_ptr实现思路:防拷贝缺陷:对于需要拷贝的场景,他无法使用*/template<class T>class unique_ptr{public:unique_ptr(T* ptr): _ptr(ptr){}//防拷贝,简单粗暴unique_ptr(unique_ptr<T>&) = delete;unique_ptr<T>& operator=(unique_ptr<T>&) = delete;~unique_ptr(){if (_ptr){delete _ptr;_ptr = nullptr;}}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}private:T* _ptr;};
};

shared_ptr

shared_ptr文档

为了弥补unique_ptr不能拷贝的缺陷,C++11中还引入了shared_ptr,他的实现思路是引用计数通过计数的方式来实现多个智能指针共同管理一个资源。

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

相较于unique_ptr,它弥补了不能拷贝的缺陷,但是因为需要保证多线程并发时的线程安全问题,所以对于计数操作要进行加锁,所以导致其效率相对来说会低一些,并且还存在循环引用的问题,所以大部分情况下如果不需要进行拷贝,都会使用unique_ptr,需要拷贝时才使用shared_ptr.

下面是shared_ptr的模拟实现,具体思路都在注释中

namespace lee
{/*c++11 shared_ptr实现思路:引用计数缺陷:会出现循环引用的问题*/template<class T>class shared_ptr{public:shared_ptr(T* ptr): _ptr(ptr), _pcount(new int(1)), _pmtx(new std::mutex){}shared_ptr(shared_ptr<T>& sp): _ptr(sp._ptr), _pcount(sp._pcount), _pmtx(sp._pmtx){//新增指向同一资源的指针,计数器+1add_ref_count();}shared_ptr<T>& operator=(shared_ptr<T>& sp){//防止自己拷贝自己if (this != &sp){//释放之前指向的资源release();//此时指向同一资源,计数器加一_ptr = sp._ptr;_pcount = sp._pcount;_pmtx = sp._pmtx;add_ref_count();}return *this;}~shared_ptr(){release();}T& operator *(){return *_ptr;}T* operator->(){return _ptr;}T* get() const{return _ptr;}size_t use_count() const {return *_pcount;}private://加锁保证线程安全void add_ref_count(){_pmtx->lock();++(*_pcount);_pmtx->unlock();}void release(){//需要用到一个标志位,当需要释放资源时,就在解锁后把锁给释放了bool flag = false;_pmtx->lock();//如果为0,则释放资源if(--(*_pcount) == 0){if (_ptr){delete _ptr;_ptr = nullptr;}delete _pcount;_pcount = nullptr;flag = true;}_pmtx->unlock();if (flag == true){delete _pmtx;_pmtx = nullptr;}}int* _pcount;std::mutex* _pmtx;T* _ptr;};
};

循环引用问题

例如我们用shared_ptr来管理一个双向链表节点

template<class T>
struct ListNode
{T data;struct ListNode<T>* next;struct ListNode<T>* prev;
};```cpp
int main()
{shared_ptr<ListNode> Node1(new ListNode);shared_ptr<ListNode> Node2(new ListNode);Node1->_next = Node2;Node2->_prev = Node1;return 0;
}

  1. 在这种情况下,当我们用智能指针分别指向Node1和Node2时,引用计数都为1。
  2. 紧接着,我们将Node1的next指向Node2,Node2的prev指向Node1,此时引用计数都为2。
  3. 当对象生命周期结束时,调用Node1和Node2的析构函数,引用计数都为1
  4. 此时问题就来了,因为next和prev还互相指向对方,所以此时资源还没有得到释放,而如果想要资源得到释放,就必须得析构掉next和prev,但是next和prev又是Node的成员,只能等待Node释放后他才能释放此时就引发了循环引用的问题,导致资源无法释放。

在c++11中,引入了weak_ptr来解决这个问题。


weak_ptr

weak_ptr文档

weak_ptr并不是智能指针,其没有RAII资源管理机制,他是专门用来解决shared_ptr的循环引用问题
它的实现思路就是对于会导致循环引用的地方,如上面的**Node1->_next = Node2, Node2->_prev = Node1;**这两条语句,直接进行赋值,不再进行计数

所以只需要将ListNode结构体中的指针用weak_ptr管理即可。

template<class T>
struct ListNode
{T data;struct weak_ptr<ListNode<T>> next;struct weak_ptr<ListNode<T>> prev;
};

下面是weak_ptr的模拟实现,具体思路都在注释中

namespace lee
{/*用于解决shared_ptr的循环引用问题在循环引用时直接赋值,不再计数*/template<class T>class weak_ptr{public:weak_ptr() = default;weak_ptr(const shared_ptr<T>& sp): _ptr(sp){}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;};
};

定制删除器

从上面的实现可以看到,智能指针的删除方式都是默认的调用一个delete(库中也是),但是如果我们管理的是一个文件描述符、一个数组、又或者是一个malloc出来的资源,此时就会因为无法释放资源或者资源释放不完全导致程序崩溃。
因为根据类型的不同,销毁资源的方式有很多种,所以要向正确的释放资源,就需要通过仿函数的方式,来为智能指针传递一个对应资源的释放方法,这种方法也被称为删除器,在shared_ptr和unique_ptr中也为我们提供了对应的接口。

例如以下几种常见的删除器

//默认delete
template<class T>
struct Del
{void operator()(T* p){delete p;}
};template<class T>
struct DelArray
{void operator()(T* p){delete[] p;}
};struct Free
{void operator()(void* p){free(p);}
};struct Fclose
{void operator()(FILE* p){fclose(p);}
};

使用时根据需求传递对应删除器即可

int main()
{std::shared_ptr<A> sp1(new A);std::shared_ptr<A> sp2(new A[10], DelArray<A>());std::shared_ptr<A> sp3((A*)malloc(sizeof(A)), Free());std::shared_ptr<FILE> sp4(fopen("test.txt", "w"), Fclose());return 0;
}

C++ 智能指针 :内存泄漏、 RAII、智能指针、auto_ptr、unique_ptr、shared_ptr、weak_ptr、定制删除器deleter相关推荐

  1. linux c 指针 内存 泄漏几种情况

    引言 对于任何使用C语言的人,如果问他们C语言的最大烦恼是什么,其中许多人可能会回答说是指针和内存泄漏.这些的确是消耗了开发人员大多数调试时间的事项.指针和内存泄漏对某些开发人员来说似乎令人畏惧,但是 ...

  2. 使用智能指针错误导致内存泄漏_C++智能指针使用的那些事

    指针指针的由来 在C/C++里面,内存管理由开发者自己管理.指针变量总是指向一片内存空间,这片内存空间可以是局部变量.也可以是通过malloc.new申请的.如果申请的内存没有释放,就会导致内存泄漏. ...

  3. C++ -- 智能指针 auto_ptr,unique_ptr,shared_ptr的简单实现和原理

    一,为什么需要智能指针 智能指针是一种预防型的内存泄漏的解决方案.由于C++没有垃圾回收器机制,所以每次new出来的资源都需要手动的delete,如果没有手动释放,就会造成资源泄漏等问题.因此,为了避 ...

  4. 虚析构函数? vptr? 指针偏移?多态数组? delete 基类指针 内存泄漏?崩溃?...

    五条基本规则: 1.如果基类已经插入了vptr, 则派生类将继承和重用该vptr.vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的 ...

  5. C/C++内存泄漏和野指针的区别

    内存泄漏 概念解释 使用动态存储分配函数或关键字申请的内存空间,在使用完毕后未释放,结果导致一直占据该内存单元,不能被任何程序再次使用,直到程序结束.即所谓内存泄漏. 表现行为 程序运行时间越长,占用 ...

  6. C++中的三种智能指针分析(RAII思想)

    2019独角兽企业重金招聘Python工程师标准>>> 智能指针 首先我们在理解智能指针之前我们先了解一下什么是RAII思想.RAII(Resource Acquisition Is ...

  7. C++智能指针剖析(上)std::auto_ptr与boost::scoped_ptr

    1. 引入 C++语言中的动态内存分配没有自动回收机制,动态开辟的空间需要用户自己来维护,在出函数作用域或者程序正常退出前必须释放掉. 即程序员每次 new 出来的内存都要手动 delete,否则会造 ...

  8. C++智能指针(一)智能指针的简单介绍

    https://blog.csdn.net/nou_camp/article/details/70176949 C++智能指针  在正式了解智能指针前先看一下下面的一段代码 #include<i ...

  9. 智能指针shared_ptr、unique_ptr、weak_ptr

    智能指针 智能指针解决的问题 智能指针分类 shared_ptr 内存模型图 shared_ptr示例 shared_ptr含义 shared_ptr基本用法及常用函数 常用函数 智能指针的构造,初始 ...

最新文章

  1. 聊聊flink JobManager的heap大小设置
  2. php中的interface和implements及其他
  3. Rhel7 Ldap为本地用户认证方式,设置域、服务器位置和下载key
  4. mysql系列之5--完全备份和增量备份
  5. IJCAI 2020 | 淡妆浓抹总相宜之人脸上妆
  6. PHP如何处理emoji表情存入utf8的数据库
  7. win7计算机个性化设置,笔记本电脑windows7系统如何用好电脑个性化设置
  8. mysql 获取操作系统信息_php获取服务器操作系统相关信息的方法
  9. popen() 函数 讲解
  10. mysql 删除创建表分区_创建,增加,删除mysql表分区
  11. 计算机职业学校杭州,杭州2021年计算机学校是干什么的
  12. 关于微信精选留言点赞刷赞之公众号评论点赞及文章评论点赞软件使用方法
  13. magicbook大学计算机系,大学开学选择哪款笔记本? Redmibook 14全面对比荣耀Magicbook...
  14. 摸鱼刷题||听说打工和摸鱼更配
  15. 北京集训队2016 Day4 超级跳
  16. 关于写专利的一点感想
  17. THUOCL:清华大学开放中文词库
  18. 计算一阶导数的四阶中心差分格式
  19. 陀螺财经研究院郭润华:2019年将迎来区块链真正成熟的井喷时代
  20. 安装haproxy-1.6.9.tar.gz

热门文章

  1. URL编码 - Java加密与安全
  2. 使用Docker运行java项目需要注意的glibc依赖库问题
  3. 200912阶段一C++友元、运算符重载
  4. 零基础前端入门,真正难在哪里?简说编程思想和逻辑思维
  5. Android加载大图、多图解决方案
  6. webservice linux 杀进程
  7. 【转】学习笔记:GoogLeNet
  8. 查看mysql进程--show processlist
  9. 2019ICPC(上海) - Spanning Tree Removal(构造)
  10. CodeForces - 1208F Bits And Pieces(SOSdp+贪心)