C++秋招记录(四)
- 一、内存管理
- 1、C/C++内存有哪几种类型?
- 2、堆和栈的区别?
- 3、堆和自由存储区的区别?
- 4、 请你说一说内存溢出和内存泄漏
- 5、“野指针”产生原因及解决办法如下:
- 6、new、delete、malloc、free关系
- 7、 C++中有了malloc / free , 为什么还需要 new / delete?
- 8、 free、delete为什么不用记录长度?
- 9、memory alignment and padding, 内存对齐的原理与意义
内存管理、new、delete,内存池相关问题
一、内存管理
1、C/C++内存有哪几种类型?
- C中,内存分为5个区:堆(malloc)、栈(如局部变量、函数参数)、程序代码区(存放二进制代码)、全局/静态存储区(全局变量、static变量)和常量存储区(常量)。此外,C++中有自由存储区(new)一说。全局变量、static变量会初始化为零,而堆和栈上的变量是随机的,不确定的。
- 堆(heap)、栈(stack)、代码段(code segment/text segment)、 BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域、数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。
- C、C++中内存分配方式可以分为三种: 栈和静态内存的对象由编译器自动创建和销毁。
1)从静态存储区域分配:
内存在程序编译时就已经分配好,这块内存在程序的整个运行期间都存在。速度快、不容易出错,因为有系统会善后。例如全局变量,static
变量类static数据成员以及定义在任何函数外部的变量等,static对象在使用之前分配,程序结束时销毁。
静态变量什么时候初始化?静态变量存储在虚拟地址空间的数据段和bss段,C语言中其在代码执行之前初始化,属于编译期初始化。而C++中由于引入对象,对象生成必须调用构造函数,因此C++规定全局或局部静态对象当且仅当对象首次用到时进行构造。
2)在栈上分配:
在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
3)从堆上分配:
即动态内存分配。程序在运行的时候用 malloc 或 new 申请任意大小的内存,程序员自己负责在何时用 free 或 delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活。如果在堆上分配了空间,就有责任回收它,否则运行的程序会出现内存泄漏,另外频繁地分配和释放不同大小的堆空间将会产生堆内碎块。
2、堆和栈的区别?
- 堆存放动态分配的对象——即那些在程序运行时分配的对象,比如局部变量,其生存期由程序控制;
- 栈用来保存定义在函数内的非static对象,仅在其定义的程序块运行时才存在;
- 堆是由低地址向高地址扩展;栈是由高地址向低地址扩展;堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存;堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片;堆的分配效率较低,而栈的分配效率较高。
- 栈的效率高的原因:
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一系列分配内存、合并内存和释放内存的算法,因此效率较低。
3、堆和自由存储区的区别?
- 总的来说,堆是C语言和操作系统的术语,是操作系统维护的一块动态分配内存;自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。他们并不是完全一样。
- 从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。
- 而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。
4、 请你说一说内存溢出和内存泄漏
内存溢出
指程序申请内存时,没有足够的内存供申请者使用。内存溢出就是你要的内存空间超过了系统实际分配给你的空间,此时系统相当于没法满足你的需求,就会报内存溢出的错误
原因:
- 内存中加载的数据量过于庞大,如一次从数据库取出过多数据
- 集合类中有对对象的引用,使用完后未清空,使得不能回收
- 代码中存在死循环或循环产生过多重复的对象实体
- 使用的第三方软件中的BUG 启动参数内存值设定的过小
内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
分类:
1、堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak。
2、系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle ,SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。
3、没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。
面对内存泄漏和指针越界,哪些方法来避免和减少这类错误?
- 1). 使用的时候要记得指针的长度.
- 2). malloc的时候得确定在那里free.
- 3).对指针赋值的时候应该注意被赋值指针需要不需要释放.
- 4). 动态分配内存的指针最好不要再次赋值.
- 5).在C++中应该优先考虑使用智能指针
5、“野指针”产生原因及解决办法如下:
(1) 指针变量声明时没有被初始化。解决办法:指针声明时初始化,可以是具体的地址值,也可让它指向 NULL。
(2) 指针 p 被 free 或者 delete 之后,没有置为 NULL。解决办法:指针指向的内存空间被释放后指针应该指向 NULL。
(3) 指针操作超越了变量的作用范围。解决办法:在变量的作用域结束前释放掉变量的地址空间并且让指针指向 NULL。
注意:“野指针”的解决方法也是编程规范的基本原则,平时使用指针时一定要避免产生“野指针”,在使用指针前一定要检验指针的合法性。
- 空指针是指指向地址为NULL(0)的指针变量
- 悬垂指针是指指向一个已经不存在的对象的地址的指针
- 野指针是指因为没有初始化等原因指向一处随机或者无效的地址的指针
- 智能指针首先是在boost库中实现的,后来被C++标准引用
6、new、delete、malloc、free关系
-
new是运算符,malloc是C语言库函数
-
new可以重载,malloc不能重载
-
new的变量是数据类型,malloc的是字节大小
-
new可以调用构造函数,delete可以调用析构函数,malloc/free不能
-
new返回的是指定对象的指针,而malloc返回的是void*,因此malloc的返回值一般都需要进行类型转化
-
malloc分配的内存不够的时候可以使用realloc扩容,new没有这样的操作new内存分配失败抛出bad_malloc,malloc内存分配失败返回NULL值
-
申请的内存所在位置(3. 堆和自由存储区的区别?)
-
返回类型安全性
- new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void* ,需要通过强制类型转换将void*指针转换成我们需要的类型。
-
内存分配失败时的返回值
new内存分配失败时,会抛出bac_alloc异常,它不会返回NULL;malloc分配内存失败时返回NULL。
-
是否需要指定内存大小
- 使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据数据类型自行计算,而malloc则需要显式地指出所需内存的尺寸。是否调用构造函数/析构函数
-
对数组的处理
- new对数组的支持体现在它会分别调用构造函数初始化每个数组元素,释放对象时为每个对象调用析构函数。delete[]要与new[]配套使用,否则数组对象部分释放的现象,造成内存泄漏;malloc,它不知道你在这块内存上要放的数组还是什么,反正它就给你一块原始的内存。如果要动态分配一个数组的内存,还需要我们手动自定数组的大小。
-
是否调用构造函数
总之来说,new/delete会调用对象的构造函数/析构函数以完成对象的构造/析构。使用new操作符来分配对象内存时会经历三个步骤:
- 1.调用operator new 函数(对于数组是operator new[])分配一块足够大的,原始的,未命名的内存空间以便存储特定类型的对象。
- 2.编译器运行相应的构造函数以构造对象,并为其传入初值。
- 3.对象构造完成后,返回一个指向该对象的指针。
-
使用delete操作符来释放对象内存时会经历两个步骤:
- 1.调用对象的析构函数。
- 2.编译器调用operator delete(或operator delete[])函数释放内存空间。
-
是否可以被重载
opeartor new /operator delete可以被重载。标准库是定义了operator new函数和operator delete函数的8个重载版本:
-
new与malloc是否可以相互调用
- operator new /operator delete的实现可以基于malloc,而malloc的实现不可以去调用new。下面是编写operator new /operator delete 的一种简单方式,其他版本也与之类似:
- 能够直观地重新分配内存
- 使用malloc分配的内存后,如果在使用过程中发现内存不足,可以使用realloc函数进行内存重新分配实现内存的扩充。realloc先判断当前的指针所指内存是否有足够的连续空间,如果有,原地扩大可分配的内存地址,并且返回原来的地址指针;如果空间不够,先按照新指定的大小分配空间,将原有数据从头到尾拷贝到新分配的内存区域,而后释放原来的内存区域。new没有这样直观的配套设施来扩充内存。
- 客户处理内存分配不足
- 在operator new抛出异常以反映一个未获得满足的需求之前,它会先调用一个用户指定的错误处理函数,这就是new-handler(设计良好的new_handler两个选择:让更多的memory可用或调用abort()或exit())。new_handler是一个指针类型
7、 C++中有了malloc / free , 为什么还需要 new / delete?
- 1). malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。
- 2).对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。在C++中,如下的代码,用new创建一个对象(new会触发构造函数, delete会触发析构函数),但是malloc仅仅申请了一个空间,所以在C++中引入new和delete来支持面向对象。
3)由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以一个能完成清理与释放内存工作的运算符delete。注意new/delete不是库函数。由于malloc是库函数,需要相应的库支持,因此某些简易的平台可能不支持,但是new就没有这个问题了,因为new是C++语言所自带的运算符。
8、 free、delete为什么不用记录长度?
- 为了管理malloc的空闲空间,每一个独立块的最前面都包含了一个“头部信息”,属于额外开销。malloc实际分配的内存会大于我们需要的size,主要有两方面因素:
① 字节对齐。会对齐到机器最受限的类型。
② “块头部信息”,每个空闲块都有“头部”控制信息,其中包含一个指向链表中的下一块的指针、当前块的大小和一个指向本身的指针。为了简化对齐,所有块的大小都必须是头部大小的整数倍,且头部已正确对齐,(malloc返回的是空闲块的首地址,不是首地址;size字段是必要的,因为malloc控制的块不一定是连续的,这样就不能通过指针算数运算得到其大小) - 实际分配的内存块将多一个单元,用于头部本身。实际分配的块的大小被记录在头部size中。所以当我们new[n]的时候,释放只需要写delete[],不用注明释放的大小的原因了。每次在释放的时候,会先查看释放的块的头部信息,其中就记录了这个块的大小。
- C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete []时就可以取出这个保存的数,就知道了需要调用析构函数多少次了
9、memory alignment and padding, 内存对齐的原理与意义
结构体:1、数据成员对齐规则:结构体(struct)的数据成员,第一个数据成员放在offset为0的地方,之后的每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机子上为4字节,所以要从4的整数倍地址开始存储)。
2、结构体作为成员:如果一个结构体里同时包含结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(如struct a里有struct b,b里有char,int ,double等元素,那么b应该从8(即double类型的大小)的整数倍开始存储)。
3、结构体的总大小:即sizeof的结果。在按之前的对齐原则计算出来的大小的基础上,必须还得是其内部最大成员的整数倍,不足的要补齐(如struct里最大为double,现在计算得到的已经是11,则总大小为16)。