C++ 内存管理中内存泄漏问题产生原因以及解决方法
C++内存管理中内存泄露(memory leak)一般指的是程序在申请内存后,无法释放已经申请的内存空间,内存泄露的积累往往会导致内存溢出。
一、内存分配方式
通常内存分配方式有以下三种:
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
(2)在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
(3)从堆上分配,亦称动态内存分配。程序在运行的时候用 malloc 或 new 申请任意多少的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
相关视频讲解:
C++服务器开发架构师学习地址:C/C++Linux服务器开发/Linux后台架构师学习视频
二、程序内存空间
一个程序将操作系统分配给其运行的内存分为五个区域:
(1)栈区:由编译器自动分配释放,存放为函数运行的局部变量,函数参数,返回数据,返回地址等。操作方式与数据结构中的类似,栈区有以下特点:
1)由系统自动分配。比如在函数运行中声明一个局部变量 int b = 10;,系统自动在栈中为 b 开辟空间;
2)只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
(2)堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收;分配方式类似于链表,堆区有以下特点:
1)需要程序员自己申请,并指明大小,在 C 中是有 malloc 函数,在 C++中多使用 new 运算符(从 C++角度上说,使用 new 分配堆空间可以调用类的构造函数,而 malloc()函数仅仅是一个函数调用,它不会调用构造函数,它所接受的参数是一个 unsigned long 类型。同样,delete 在释放堆空间之前会调用析构函数,而 free 函数则不会)。
2)在操作系统中有一个记录空闲内存地址的表,这是一种链式结构。它记录了有哪些还未使用的内存空间。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
(3)全局数据区:也叫做静态区,存放全局变量,静态数据。程序结束后由系统释放。
(4)文字常量区:可以理解为常量区,常量字符串存放这里。程序结束后由系统释放。“常量”是指它的值是不可变的,同时,虽然常量也是存储在内存的某个地方,但是无法访问常量的地址的。
(5)程序代码区:存放函数体的二进制代码。但是代码段中也分为代码段和数据段。
一个程序内存分配例子:
三、内存溢出原因
(1)在类的构造函数和析构函数中没有匹配的调用 new 和 delete 函数
两种情况下会出现这种内存泄露:
1)在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;
2)在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存。
(2)没有正确地清除嵌套的对象指针
(3)在释放对象数组时在 delete 中没有使用方括号
方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值并调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。
释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。
(4)指向对象的指针数组不等同于对象数组
对象数组是指:数组中存放的是对象,只需要 delete [ ] p,即可调用对象数组中的每个对象的析构函数释放空间
指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete [ ] p 只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了。
(5)缺少拷贝构造函数
两次释放相同的内存是一种错误的做法,同时可能会造成堆的奔溃。
按值传递会调用(拷贝)构造函数,引用传递不会调用。
在 C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。
所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符。
(6)缺少重载赋值运算符
这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露.
(7)关于 nonmodifying 运算符重载的常见错误
1)返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针(指向被释放的或者访问受限内存的指针);
2)返回内部静态对象的引用;
3)返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收。
解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是 int。
(8)没有将基类的析构函数定义为虚函数
当基类指针指向子类对象时,如果基类的析构函数不是虚函数,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
造成野指针的原因:
1)指针变量没有被初始化(如果值不定,可以初始化为 NULL);
2)指针被 free 或者 delete 后,没有置为 NULL, free 和 delete 只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为 NULL;
3)指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针;
4)shared_ptr 循环引用。
(9)析构的时候使用 void*
delete 掉一个 void*类型的指针,导致没有调用到对象的析构函数,析构的所有清理工作都没有去执行从而导致内存的泄露。
(10)构造的时候浅拷贝,释放的时候调用了两侧 delete
四、常见解决办法
(1)shared_ptr 共享的智能指针:
shared_ptr 使用引用计数,每一个 shared_ptr 的拷贝都指向相同的内存。在最后一个 shared_ptr 析构的时候,内存才会被释放。
注意事项:
1)不要用一个原始指针初始化多个 shared_ptr;
2)不要再函数实参中创建 shared_ptr,在调用函数之前先定义以及初始化它;
3)不要将 this 指针作为 shared_ptr 返回出来;
4)要避免循环引用。
(2)unique_ptr 独占的智能指针:
1)unique_ptr 是一个独占的智能指针,他不允许其他的智能指针共享其内部的指针,不允许通过赋值将一个 unique_ptr 赋值给另外一个 unique_ptr;
2)unique_ptr 不允许复制,但可以通过函数返回给其他的 unique_ptr,还可以通过 std::move 来转移到其他的 unique_ptr,这样它本身就不再 拥有原来指针的所有权了;
3)如果希望只有一个智能指针管理资源或管理数组就用 unique_ptr,如果希望多个智能指针管理同一个资源就用 shared_ptr。
(3)weak_ptr 弱引用的智能指针:
弱引用的智能指针 weak_ptr 是用来监视 shared_ptr 的,不会使引用计数加一,它不管理 shared_ptr 内部的指针,主要是为了监视 shared_ptr 的生命 周期,更像是 shared_ptr 的一个助手。 weak_ptr 没有重载运算符*和->,因为它不共享指针,不能操作资源,主要是为了通过 shared_ptr 获得资源的监测权,它的构造不会增加引用计数,它的析构不会减少引用计数,纯粹只是作为一个旁观者来监视 shared_ptr 中关连的资源是否存在。 weak_ptr 还可以用来返回 this 指针和解决循环引用的问题。
(4)set_new_handler(out_of_memroy); //注意参数传递的是函数的地址
总结:现在 C++程序员面临的竞争压力越来越大。那么,作为一名 C++程序员,怎样努力才能快速成长为一名高级的程序员或者架构师,或者说一名优秀的高级工程师或架构师应该有怎样的技术知识体系,这不仅是一个刚刚踏入职场的初级程序员,也是工作三五年之后开始迷茫的老程序员,都必须要面对和想明白的问题。为了帮助大家少走弯路,技术要做到知其然还要知其所以然。以下视频获取点击:C++架构师学习资料
如果想学习 C++工程化、高性能及分布式、深入浅出。性能调优、TCP,协程,Nginx 源码分析 Nginx,ZeroMQ,MySQL,Redis,MongoDB,ZK,Linux 内核,P2P,K8S,Docker,TCP/IP,协程,DPDK 的朋友可以看一下这个学习地址:
C/C++Linux服务器开发高级架构师/Linux后台架构师
编辑
添加图片注释,不超过 140 字(可选)
评论