一丶内存分配方式
C++中内存分为五个区,分别是栈(stack),堆(heap),自由存储区,全局/静态存储区(bss),常量存储区
栈:在执行函数时,函数内局部变量的存储单元,函数参数都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器指令集中,效率高,但分配内存容量有限。
堆:那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete,如果程序员没有释放掉,程序结束后,操作系统会自动回收(内存泄漏不是系统无法回收那片内存,而是你自己的应用程序无法使用那片内存。操作系统本身就有管理内存的职责,在进程结束后,操作系统会自动回收内存的)
自由存储区:就是那些由malloc等分配的内存块,他和堆是十分相似的,不过他是用free来结束自己的生命的
引申出来的问题:
很多编译器中new/delete都是以melloc和/free为基础实现的,那么就会引申出一个问题,自由存储区和堆相同么?
答案:堆是操作系统维护的一块内存,而自由存储时c++中通过new和delete动态分配和释放对象的抽象概念,堆和自由存储区并不等价
全局/静态存储区
全局变量和静态变量被分配到同一块内存中,在以前的c语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区
常量存储区
这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改
二丶堆和栈的区别
void func() {
int* p = new int[5];
}
在这条语句里,看到了new就想到分配了一块堆内存,而指针p分配了一块栈内存。这句话的意思是,在栈内存中存放了一个指向对内存的指针p
堆和栈的区别主要分为以下六点:
1.管理方式不同:
对于栈来说,是编译器自动管理的,不需要我们手动控制
对于堆来说,释放工作由程序员控制,容易产生内存泄漏
2.空间大小:
一般在32位系统下,堆内存可以达到4g空间
对于栈来说,一般是有一定的空间大小,vc下面栈的空间默认1M(可以通过编译器设置修改)
3.碎片问题:
对于堆来说,频繁的new/delete势必会造成内存空间不连续,从而造成大量碎片,使程序效率降低
对于栈来说,不会存在这个问题,因为栈使先进后出的队列,他们一一对应,永远不可能由一个内存块从栈中间弹出,在他弹出之前,它上面的后进的栈内容已被弹出
4.生长方向:
对于堆来说,生长方向是向上的,也就是向着内存地址增加的方向
对于栈来说,它的生长方向是向下的,是向着内存地址减小的方向增长
5.分配方式不同:
堆都是动态分配的
栈有两种分配方式动态分配和静态分配,静态分配是编译器完成的,比如局部变量的分配,动态分配是由alloca完成的,栈的动态分配是由编译器进行释放,无需我们手动实现
6.分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,他的机制很是复杂,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间,就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分配到足够大小的内存,然后进行返回,显然,堆的效率比栈要低的多
虽然栈有众多好处,但是和堆比不是那么灵活,有时候分配大量的内存空间,还是堆好一些
三丶常见的内存错误及其对策
1.内存分配未成功,却使用了它
常见的解决办法是在使用内存之前检查指针是否为NULL。如果指针是函数的参数,那么在函数入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理
2.内存分配虽然成功,但是尚未初始化就引用它
犯这种错误主要是有两个起因:一是没有初始化的概念,二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么没有统一的标准,尽管有时候为零值,我们宁可信其无不可信其有。所以无论如何用哪种方式创建数组,都别忘了赋初值,即使是赋零值也不可省略,不要嫌麻烦。
3.内存分配成功已经初始化,但操作越过了内存的边界
例如在使用数组时经常发生下标”多1“或者”少1“的操作。特别是在for循环语句中,循环次数很容易搞错,导致数组越界
4.忘记了释放内存,造成内存泄露
含有这种错误的函数每被调用一次就会丢失一块内存,刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态的申请和释放必须配对,程序中malloc与free的次数一定要相同,否则肯定有错误
5.释放了内存却继续使用它
有三种情况:
1)程序中的对象调用过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理混乱的局面
2)函数的return语句写错了,注意不要返回指向”栈内存“的指针”或者引用“,因为该内存在函数体结束时被自动销毁
3)使用free或delete释放了内存后,没有将指针设置为NULL,导致产生”野指针“
【规则1】在使用malloc或new申请内存之后,应该立即检查指针值是否为NULL,防止使用指针值作为NULL的内存
【规则2】不要忘记为数组或动态内存赋初值。防止将违背初始化的内存作为右值使用
【规则3】避免数组或指针下标越界,特别当心发生”多1“或者”少1“操作
【规则4】动态内存的申请和释放必须配对,防止内存泄露
【规则5】用free或delete释放内存之后,立即将指针设置为NULL,防止产生野指针
四丶野指针
”野指针“不是NULL指针,是指向”垃圾“内存的指针。人们一般不会用错NULL指针,因为用if语句很容易判断,但是野指针是很危险的,if语句对它不起作用,”野指针“的成因主要有两种
1.指针变量没有初始化,任何指针变量被创建时不会自动成为NULL,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存
char *p = NULL;
char *str = (char *)malloc(100);
2.指针p被free或者delete之后,没有置为NULL,让人误以为p是一个合法的指针
3.指针操作超越了变量的作用域范围,这种情况让人防不胜防
class A {
public:
void Func(void) {
cout << "Func of class A" << endl;
}
};
void Test(void) {
A *p;
{
A a;
p = &a;//注意a的生命周期
}
p->Func();//p是"野指针"
}
函数Test在执行语句p->Func()时,对象a已经消失,而p是指向a的,所以p就成了野指针,但奇怪的是这个程序没有报错,可能和编译器有关
五丶内存耗尽怎么办
如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败。通常有三种方式处理”内存耗尽“问题
1.判断指针是否为NULL,如果是则马上用return语句终止本函数,例如:
void Func(void) {
A* a = new A;
if (a == NULL) {
return;
}
}
2.判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行,例如:
void Func(void) {
A* a = new A;
if (a == NULL) {
exit(1);
}
}
六丶内存泄漏
内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存
进一步解释:并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费
内存泄露的分类:
1.堆内存泄露:堆内存指的是程序运行中根据需要分配malloc,realloc,new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉,如果程序的设计的错误到这这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生堆内存泄露
2.系统资源泄露:主要指程序使用系统分配的资源比如bitmap,handle,socket等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定
3.没有将基类的析构函数定义为虚函数。当基类指针指向子类对象的时候,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄漏
内存泄漏和内存溢出的区别和联系
1.内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出
2.内存溢出:所谓的内存溢出指的是程序申请内存时,没有足够的内存供申请者使用,或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错,比如说栈,栈满时再做进栈必定产生空间溢出,叫上溢,栈空时再做退栈也会产生空间溢出,成为下溢
防止内存泄漏的方法
1.内部封装:
将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存
说明:这样做并不是最佳做法,在类的对象赋值的时候,程序会出现同一块内存空间释放两次的情况
可以增加引用计数来避免这种情况
2.智能指针
智能指针是C++中已经对内存泄漏封装好了的一个工具
检测内存泄露
请你回答一下如何判断内存泄漏
1.一方面可以使用Linux环境下的内存泄漏检查工具valgrind,另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,来判断内存是否泄露
2.windows使用运行时库CRT,可以定位到内存泄露的地方
如何定位内存泄漏
C++程序缺乏相应的手段来检测内存信息,只能通过使用top指令观察进程的动态内存总额,而且程序退出时,我们无法获知任何内存泄露的信息
内存泄漏检测工具的实现原理
这里重点介绍一下valgring
valgrind是一套Linux下开发源码的仿真调试工具集合,包括以下工具
1.Memcheck:内存检查器,能够发现开发中绝大多数内存错误的使用情况,比如:使用未初始化的内存,使用已经释放的内存,内存访问越界等
2.Callgrind:检查程序中函数调用过程中出现的问题
3.Cachegrind:检查程序中缓存使用出现的问题
4.Helgrind:检查多线程中出现的竞争问题
5.Massif:检查程序中堆栈使用中出现的问题
6.Extension:可以利用core提供的功能,自己编写特定的内存调试工具
Memcheck能够检测出内存问题,关键在于其建立了两个全局表:
Valid-Value表:对于进程的整个地址空间中的每一个字节,都有与之对应的8bits,对于CPU的每个寄存器,也有一个与之对应的bit向量,这些bits负责记录该字节或者寄存器值是否具有有效的,已初始化的值
Valid-Address表:对于进程整个地址空间中的每一个字节(byte),还有与之对应的1个bit,负责记录地址是否能够被读写
检测原理:
1.当要读写内存中的某个字节时,首先检查这个字节对应的Valid-Address表中对应的bit。如果该bit显式该位置是无效位置,Memcheck则报告读写错误
2.内核类似一个虚拟的CPU环境,这样当内存中的某个字节被加载到真实的CPU中时,该字节在Valid-Value表中对应的bits也被加载到虚拟的CPU环境中,一旦寄存器中的值,被用来产生内存地址,或者该值能够影响程序输出,则Memcheck会检查Valid-Value表对应的bits,如果该值尚未初始化,则会报告使用未初始化内存错误
malloc原理,brk系统调用和mmap系统调用的作用分别是什么
malloc函数用于动态分配内存,为了减少内存碎片和系统调用的开销,malloc采用了内存池的方式,先申请大块的内存作为堆区,然后将堆区区分为多个内存块,以块作为内存管理的基本单位,当用户申请内存时,直接从堆区分配一块合适的空闲块,malloc采用隐式链表结构将堆区分成连续的,大小不一的块,包含已分配块和未分配块,同时malloc采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块链接起来,每一个空闲块记录了一个连续的,未分配的地址
当进行内存分配时,malloc会通过隐式链表遍历所有空闲块,选择满足要求的的块进行分配,当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并
malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。其中当申请的内存小于128k时,会使用系统函数调用brk(),通过移动_enddata来实现堆区中的分配;而当申请内存大于128k时会使用系统函数mmap在虚拟地址空间映射区分配
malloc的底层实现
brk()函数实现原理:像高地址的方向移动指向数据段的高地址的指针_enddata
mmap内存映射原理:
1.进程启动映射过程,并在虚拟低地址空间中为映射创建虚拟映射内存;
2.调用内核空间的系统调用函数mmap(),实现文件物理地址和进程虚拟地址的一一映射关系
3.进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝
malloc,calloc,realloc,alloca
1.malloc:申请指定字节数的内存。申请到的内存中的初始值不确定
2.calloc:为指定对象长大夫,分配能容纳其指定个数的内存,申请到的内存中的每一位(bit)都初始化为0
3.realloc:更改以前分配的内存长度(增或减)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值不确定
4.alloca:在栈上申请内存。程序在出栈的时候,会自动释放内存,但需要注意的是,alloca不具有可移植性,而且没有在传统堆栈的机器上很难实现。alloca不宜使用在必须广泛移植的程序中。
new和malloc的区别
特征 | new/delete | malloc/free |
---|---|---|
分配内存的位置 | 自由存储区 | 堆 |
内存分配失败返回值 | 完美类型指针 | void* |
内存分配失败安全性 | 默认抛出异常 | 返回NULL |
分配内存大小 | 编译器根据类型计算得出 | 必须显式的指定字节数 |
处理数组 | 有处理数组的new[]版本 | 需要用户计算数组的大小后进行内存分配 |
已分配内存的扩充 | 无法直观的处理 | 使用realloc简单完成 |
是否相互调用 | 可以,看具体的operator new/delete实现 | 不可以调用new |
分配内存时内存不足 | 客户能够指定处理函数或者重新制定分配器 | 无法通过用户代码进行处理 |
函数重载 | 允许 | 不允许 |
构造函数与析构函数 | 调用 | 不调用 |