C++内存管理中内存泄漏问题产⽣原因以及解决⽅法
C++内存管理中内存泄露(memory leak)⼀般指的是程序在申请内存后,⽆法释放已经申请的内存空间,内存泄露的积累往往会导致内存溢出。
⼀、内存分配⽅式
通常内存分配⽅式有以下三种:
(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运⾏期间都存在。例如全局变量,static变量。
(2)在栈上创建。在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。
(3)从堆上分配,亦称动态内存分配。程序在运⾏的时候⽤malloc或new申请任意多少的内存,程序员⾃⼰负责在何时⽤free或delete释放内存。动态内存的⽣存期由程序员决定,使⽤⾮常灵活,但如果在堆上分配了空间,就有责任回收它,否则运⾏的程序会出现内存泄漏,频繁地分配和释放不同⼤⼩的堆空间将会产⽣堆内碎块。
⼆、程序内存空间
⼀个程序将操作系统分配给其运⾏的内存分为五个区域:指向类成员函数的指针
(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)程序代码区:存放函数体的⼆进制代码。但是代码段中也分为代码段和数据段。
⼀个程序内存分配例⼦:
int a = 0; //全局初始化区
char *p1; //全局未初始化区
int main() {
int b; //栈区
char s[] = /"abc/"; //栈区
char *p2; //栈区
char *p3 = /"123456/"; //123456//0在常量区,p3在栈区。
static int c =0;//全局(静态)初始化区
p1 = new char[10];
p2 = new char[20];
//分配得来得和字节的区域就在堆区。
strcpy(p1, /"123456/"); //123456//0放在常量区,编译器可能会将它与p3所指向的/"123456/"优化成⼀个地⽅。
}
三、内存溢出原因
(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); //注意参数传递的是函数的地址
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论