C++的内存管理
一个C/C++编译的程序占用内存分为以下几个部分:
- 栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
- 堆区(heap):一般由程序员分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
- 全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
- 常量区(文字常量区):存放常量字符串,程序结束后由系统释放。
- 代码区(*存储区):存放函数体(类成员函数和全局区)的二进制代码。
堆和栈的区别
1.堆和栈的生长方向不同
对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
2.堆会随使用产生碎片,栈不会
对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列
3.管理方式不同
对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。
4.堆未及时释放容易造成内存泄漏
5.堆都是动态分配的,栈可以动态分配可以静态分配
静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
6.分配效率
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,因此栈的效率比较高。
堆是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。
7.应用场景
堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,程序开发时应尽可能使用栈。
虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。
*存储区和堆的区别
malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在*存储区上,使用delete来释放。
插播new与delete的区别
new-delete是运算符,而malloc-free是库函数,malloc-free不能进行构造和析构。
new-delete的功能可以完全覆盖malloc-free,由于C++会调用C函数,而C只能使用malloc-free,因此保留了malloc-free。
malloc-free |
new-delete |
|
内存分配位置 |
堆 |
*存储区 |
分配成功返回值 |
void* |
对应类型指针 |
分配失败返回值 |
NULL |
抛出异常 |
分配内存大小 |
用户计算 |
自动 |
处理数组 |
× |
√ |
已分配内存的扩充 |
√ |
× |
互相调用 |
× |
√ |
分配内存时内存不足 |
用户不可处理 |
用户可重新申请 |
重载 |
× |
√ |
调用构造/析构 |
× |
√ |
Q:很多编译器的new/delete都是以malloc/free为基础来实现的,借以malloc实现的new,所申请的内存是在堆上还是在*存储区上?*存储区与堆是两块不同的内存区域吗?它们有可能相同吗?
A:从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。
而*存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为*存储区。
基本上,所有的C++编译器默认使用堆来实现*存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时借由new运算符分配的对象,说它在堆上也对,说它在*存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现*存储,例如全局变量做的对象池,这时*存储区就区别于堆了。
因此:堆是操作系统维护的一块内存,而*存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与*存储区并不等价。
常见的内存错误和解决方法
1. 内存分配未成功,却使用了它
使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
2. 内存分配虽然成功,但是尚未初始化就引用它
犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
3. 内存使用越界
例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组操作越界。
4. 内存泄漏
多由程序员忘记释放堆内存引起。动态内存的申请与释放必须配对,程序中malloc与free的使用次数一定要相同,否则肯定有错误(new/delete同理)。
5. 使用已释放内存
a) 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
b) 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
c) 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
6. 浅复制造成的释放已释放的内存
复制构造函数,在用户未定义复制函数时,C++提供的默认复制函数,作用是把两个类复制。
应用场景:
i. 形参是本类类型对象的引用:String test1(test2);
ii. 形参为按值传递,未使用指针or引用:void show_String(const String)
iii. string B; string A=B;
问题的产生:
i. 复制构造函数采用的是浅复制,即复制了相关类成员的指针,而没有开辟新的空间,内存中的数据还是那一份数据。
ii. 当释放某一个指针所指对象后,操作由复制构造函数形成的另外指针将会引发内存使用错误。
解决方法:
定义开辟空间的复制函数进行深拷贝
内存泄漏&预防
内存泄漏源于new出的内存没有全部被delete掉导致的内存消耗,使用智能指针能将内存泄漏风险降至最低。
智能指针利用C++ RAII(Resource Acquisition Is Initialization,资源获取就是初始化)原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。(std::mutex互斥锁就是用的这个原则,退出局部变量使用域的时候自动释放锁)
几种智能指针↓
1. auto_ptr(旧)--> unique_ptr(C++11)
在构造的时候获取资源,在析构的时候释放资源,并进行相关指针操作的重载,使用起来就像普通的指针。独享资源所有权。由于其构造函数声明为explicit的,因此不能通过隐式转换来构造,只能显示调用构造函数。
2. share_ptr(C++11)
允许限定的资源被多个指针共享。
3. weak_ptr
weak_ptr是一种用于解决shared_ptr相互引用时产生死锁问题的智能指针。如果有两个shared_ptr相互引用,那么这两个shared_ptr指针的引用计数永远不会下降为0,资源永远不会释放。weak_ptr是对对象的一种弱引用,它不会增加对象的use_count,weak_ptr和shared_ptr可以相互转化,shared_ptr可以直接赋值给weak_ptr,weak_ptr也可以通过调用lock函数来获得shared_ptr。