C++面试——内存管理、堆栈、指针50问
一、内存管理
1、简述C++的内存管理
内存分配方式:
在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。
栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
堆,就是那些由new分配的内存块,一般一个new就要对应一个delete。
自由存储区,就是那些由malloc等分配的内存块,和堆是十分相似的,不过是用free来结束自己的生命。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中
常量存储区,这是一块比较特殊的存储区,里面存放的是常量,不允许修改。
常见的内存错误及其对策:
(1)内存分配未成功,却使用了它。
(2)内存分配虽然成功,但是尚未初始化就引用它。
(3)内存分配成功并且已经初始化,但操作越过了内存的边界。
(4)忘记了释放内存,造成内存泄露。
(5)释放了内存却继续使用它。
对策:
(1)定义指针时,先初始化为NULL。
(2)用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
(3)不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
(4)避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
(5)动态内存的申请与释放必须配对,防止内存泄漏
(6)用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
(7)使用智能指针。
内存泄露及解决办法:
什么是内存泄露?
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。(1)new和malloc申请资源使用后,没有用delete和free释放;(2)子类继承父类时,父类析构函数不是虚函数。(3)Windows句柄资源使用后没有释放。
怎么检测?
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查该链表。
第三:使用智能指针。
第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
2、malloc和局部变量分配在堆还是栈?
malloc是在堆上分配内存,需要程序员自己回收内存;局部变量是在栈中分配内存,超过作用域就自动回收。
3、简要说明C++的内存分区?
C++内存分区,分别是栈、堆、自由存储区、全局/静态存储区、常量存储区、代码存储区
① 当执行函数时,函数内部的局部变量的存储单元都可以在栈上创建,程序结束时,这些存储单元会被自动释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存有限。
② 就是那些由new分配的内存块,程序结束后编译器不会自动释放内存,需要程序员手动释放,一个new对应一个delete,如果程序员没有将内存释放掉,程序结束后会由操作系统自动回收。
③ 自由存储区:如果说堆是操作系统维护的一块内存,那么自由存储区就是C++通过new和delete分配和释放对象的抽象概念,它们相象但不等价。
④ 全局/静态存储区:将静态变量和全局变量分配到同一块内存中,以前C语言还分已初始化的变量和未初始化的变量,C++中没有这个区分了,它们共同占用一块内存区,在该区定义的变量如果没有初始化,会被自动初始化。
⑤ 常量存储区:特殊的存储区,存放常量,不允许修改。
⑥ 代码存储区:存储函数体的二进制代码
4、程序有哪些部分,分别的作用?程序启动的过程?怎么判断数据分配在栈上还是堆上?
一个程序有哪些部分:
如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆、共享区、栈等组成。
数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
最后还有一个共享区,位于堆和栈之间。
程序启动的过程:
操作系统首先创建相应的进程并分配私有的进程空间,然后操作系统的加载器负责把可执行文件的数据段和代码段映射到进程的虚拟内存空间中。
加载器读入可执行程序的导入符号表,根据这些符号表可以查找出该可执行程序的所有依赖的动态链接库。
加载器针对该程序的每一个动态链接库调用LoadLibrary (1)查找对应的动态库文件,加载器为该动态链接库确定一个合适的基地址。(2)加载器读取该动态链接库的导入符号表和导出符号表,比较应用程序要求的导入符号是否匹配该库的导出符号。(3)针对该库的导入符号表,查找对应的依赖的动态链接库,如有跳转,则跳到3 (4)调用该动态链接库的初始化函数
初始化应用程序的全局变量,对于全局对象自动调用构造函数。
进入应用程序入口点函数开始执行。
怎么判断数据分配在栈上还是堆上:首先局部变量分配在栈上;而通过malloc和new申请的空间是在堆上。
5、初始化为0的全局变量在bss还是data
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
6、内存泄漏的后果?如何检测?解决办法?
1)内存泄漏
内存泄漏是指由于疏忽或错误造成了程序未能释放掉不能使用内存的情况。内存泄漏并非指内存在物理上消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制。
2)后果
小的内存泄漏可能不被注意,大量的内存泄漏的程序会出现各种征兆,性能下降到内存逐步用完,导致另一个程序失败。
3)如何排除
BoundsChecker是一个运行时错误检测工具,它主要定位程序运行期间发生的各种错误。
调式运行DEBUG版程序,运行以下技术:CRT、运行时函数调用堆、栈、内存泄漏时提示的内存分配序号,综合分析,排除内存泄漏。
4)解决办法
智能指针
5)检查、定位内存泄漏
检查方法:在main函数最后面一行,加上_CrtDumpMemoryLeaks()。调式程序
7、什么是内存泄露,如何检测与避免
内存泄露
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了
避免内存泄露的几种方式:
计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄一定要将基类的析构函数声明为虚函数
对象数组的释放一定要用delete []
有new就有delete,有malloc就有free,保证它们一定成对出现
检测工具
Linux下可以使用Valgrind工具
Windows下可以使用CRT库
视频讲解:linux c/c++后端开发中的重点技术:内存管理(内存管理架构、numa、slab、vmalloc、内存池、内存泄漏、物理内存、虚拟内存、MMU机制)
8、请简述一下atomoic内存顺序。
有六个内存顺序选项可应用于对原子类型的操作:
memory_order_relaxed:在原子类型上的操作以自由序列执行,没有任何同步关系,仅对此操作要求原子性。
memory_order_consume:memory_order_consume只会对其标识的对象保证该对象存储先行于那些需要加载该对象的操作。
memory_order_acquire:使用memory_order_acquire的原子操作,当前线程的读写操作都不能重排到此操作之前。
memory_order_release:使用memory_order_release的原子操作,当前线程的读写操作都不能重排到此操作之后。
memory_order_acq_rel:memory_order_acq_rel在此内存顺序的读-改-写操作既是获得加载又是释放操作。没有操作能够从此操作之后被重排到此操作之前,也没有操作能够从此操作之前被重排到此操作之后。
memory_order_seq_cst:memory_order_seq_cst比std::memory_order_acq_rel更为严格。memory_order_seq_cst不仅是一个"获取释放"内存顺序,它还会对所有拥有此标签的内存操作建立一个单独全序。
除非你为特定的操作指定一个顺序选项,否则内存顺序选项对于所有原子类型默认都是memory_order_seq_cst。
9、内存模型,堆栈,常量区。
内存模型(内存布局):
如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆、共享区、栈等组成。
数据段:存放程序中已初始化的全局变量和静态变量的一块内存区域。
代码段:存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
可执行程序在运行时又会多出两个区域:堆区和栈区。
堆区:动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
最后还有一个共享区,位于堆和栈之间。
堆 heap :由new分配的内存块,其释放由程序员控制(一个new对应一个delete)
栈 stack :是那些编译器在需要时分配,在不需要时自动清除的存储区。存放局部变量、函数参数。
常量存储区 :存放常量,不允许修改。
10、类的对象存储空间?
①非静态成员数据类型大小之和
②编译器加入额外的变量
③为了边缘对齐优化加入的padding
④ 空类(无静态成员)的对象size=0,作为基类时size=1。
11、类对象的大小受哪些因素影响?
1)类的非静态成员变量大小,静态成员不占据类的空间,成员函数也不占据类的空间大小。
2)内存对齐另外分配的空间大小,类内的数据也是需要进行内存对齐操作的。
3)虚函数的话,会在对象插入vptr指针,加上指针大小
4)当该该类是某类的派生类,那么派生类继承的基类部分的数据成员也会存在派生类中的空间中,也会对派生类进行扩展。
12、简述C++中内存对齐的使用场景
内存对齐应用于三种数据类型中:struct/class/union
struct/class/union内存对齐原则有四个:
数据成员对齐规则:结构(struct)或联合(union)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小的整数倍开始。
结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部"最宽基本类型成员"的整数倍地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储)。
收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的"最宽基本类型成员"的整数倍。不足的要补齐。(基本类型不包括struct/class/uinon)。
sizeof(union),以结构里面size最大元素为union的size,因为在某一时刻,union只有一个成员真正存储于该地址。
13、什么是内存对齐?
那么什么是字节对齐?在C语言中,结构体是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构体、联合体等)的数据单元。在结构体中,编译器为结构体的每个成员按其自然边界(alignment)分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构体的地址相同。
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
比如在32位cpu下,假设一个整型变量的地址为0x00000004(为4的倍数),那它就是自然对齐的,而如果其地址为0x00000002(非4的倍数)则是非对齐的。现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问,这就需要各种类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
14、为什么要字节对齐?
需要字节对齐的根本原因在于CPU访问数据的效率问题。假设上面整型变量的地址不是自然对齐,比如为0x00000002,则CPU如果取它的值的话需要访问两次内存,第一次取从0x00000002-0x00000003的一个short,第二次取从0x00000004-0x00000005的一个short然后组合得到所要的数据,如果变量在0x00000003地址上的话则要访问三次内存,第一次为char,第二次为short,第三次为char,然后组合得到整型数据。
而如果变量在自然对齐位置上,则只要一次就可以取出数据。一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该32bit数据。显然在读取效率上下降很多。
15、结构体内存对齐问题?
① 结构体成员顺序按成员声明顺序存储,第一个成员的地址和整个结构体地址相同。
② 无特殊说明下:结构体按size大的成员对齐(有double的情况下,按8字节对齐)。
C++11后引入关键字alignas和alignof,前者可指定结构体对齐方式,后者能够计算出类型的对齐方式。
16、说说你理解的内存对齐以及原因
分配内存的顺序是按照声明的顺序。
每个变量相对于起始位置的偏移量必须是该变量类型大小的整数倍,不是整数倍空出内存,直到偏移量是整数倍为止。
最后整个结构体的大小必须是里面变量类型最大值的整数倍。
添加了#pragma pack(n)后规则就变成了下面这样:
偏移量要是n和当前变量大小中较小值的整数倍
整体大小要是n和最大变量大小中较小值的整数倍
n值必须为1,2,4,8…,为其他值时就按照默认的分配规则
17、C++中类的数据成员和成员函数内存分布情况?
C++类是由结构体发展得来的,所以它们的成员变量(C语言的结构体只有成员变量)的内存分配机制是一样的。
一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。
所有的函数包括成员函数都是放在代码区的。
静态成员函数与一般成员函数的唯一区别就是没有this指针,因此不能访问非静态数据成员。所有函数都存放在代码区,静态函数也不例外。
18、你知道空类的大小是多少?
① C++空类的大小不为0,不同编译器设置不一样,vs设置为1.
② C++标准指出,不允许一个对象(包括类对象)的大小为0,不同的对象不能具有相同的地址。
③ 带虚函数的C++类大小不为1,因为每个对象会有一个vptr指向虚函数表,具体大小根据指针大小确定
④ C++中要求对类的每个实例都必须有独一无二的地址,那么编译器自动为空类分配一个字节大小,这样便保证了每个实例均有独一无二的内存地址。
19、关于this指针你知道什么?
this指针是类的指针,指向对象的首地址。
this指针只能在成员函数中使用。
this指针只有在成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。
用处:
一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员时,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行访问。
使用:
一般情况:在类的非静态成员函数中返回类对象本身的时候,直接使用return *this;
另外一种就是当形参与成员变量名相同的时候用于区分如this->n=n
类的this指针特点:
this只能在成员函数中使用,实际上成员函数默认第一个参数为T* const this
this在成员函数的开始前构造,在成员函数的结束时清除。这个生命周期同任何一个函数的参数是一样的,无任何区别。
20、成员函数中调用delete this会出现什么问题?对象还可以使用?
在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。在调用成员函数时,隐含传递一个this指针,让成员函数知道当前是哪个对象在调用它,调用delete this时,类对象的内存空间被释放。在delete this之后进行其他任何函数调用,只要不涉及this的内容都可正常运行,一旦涉及到this,如操作数据成员,调用虚函数等,就会出现不可预期的错误。
21、为什么是不可预期的错误?
delete this之后不是释放类对象的内存空间吗,那么这段内存应当被回收,不再属于这个进程,照这个逻辑,应该发生指针错误,无访问权限等错误,但这个问题涉及到操作系统的内存管理策略,delete this释放类对象的内存空间,但是内存空间却并不是马上被回收到系统中,可能是缓冲或者其他,导致这段内存空间暂时没有被系统回收,此时这段内存是可以访问的,但并不知道其具体值,当你获取数据成员,可能得到的是一串很长的未初始化的随机数,访问虚函数表,指针无效的可能性非能高,造成系统崩溃。
22、如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。原因很简单,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存。显然,delete this会去调用本对象的析构函数,而析构函数中又调用析构函数delete this,形成无限递归,造成堆栈溢出,系统崩溃。
视频讲解:【精准突击】c/c++后端服务器开发面试八股文,上百道大厂面试题,刷完必斩offer!!!
二、堆、栈
23、堆和栈的区别?
① 申请方式不同:
栈由系统自动申请,堆是程序员申请和释放的。
② 申请大小限制不同
栈默认是4M,堆区一般是1G-4G。
栈顶和栈底是之前预设好的,大小固定可以用ulinit -a查看,ulimit -s修改
堆是高地址扩展,是不连续的内存空间,大小可灵活调整。
③ 申请效率不同
栈是系统自动分配,效率高,不会产生碎片。
堆是程序员手动分配,效率低,会产生碎片。
24、你觉得是堆快还是栈快?
栈快
因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存,并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
25、new / delete 与 malloc / free的异同
相同点:
都可以用于内存的动态申请和释放。
不同点:
前者是C++运算符,后者是C/C++语言标准库函数
new自动计算要分配的空间大小,malloc需要手工计算
new是类型安全的,malloc不是
new调用名为operator new的标准库函数分配足够空间并调用相关对象的构造函数,delete对指针所指对象运行适当的析构函数,然后通过调用名为operator delete的标准库函数是释放该对象所用内存,后者均无相关调用
后者需要库函数支持,前者不需要
new是封装了malloc,直接free不会报错,但这只是释放内存,而不会析构对象。
26、new和delete是如何实现的?
① new 的实现过程是调用名为operator new的标准库函数,分配足够大的一块内存,以保存指定类型的一个对象;接下来运行该类型的一个构造函数,用指定初始化构造对象;最后返回指向新分配并构造后的对象的指针。
② delete实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存。
27、malloc和new的区别?
① malloc和free是标准库函数,支持覆盖;new和delete是运算符,不重载
② malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数的功能,用malloc分配内存存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
③ malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
28、既然有了malloc/free,C++中为什么还需要new/delete呢?直接用malloc/free不好吗?
① malloc/free和new/delete都是用来申请和释放内存的。
② 在执行非基本数据类型的时候,对象创建的时候需要执行构造函数,销毁的时候需要执行析构函数。malloc/free是库函数,是已经编译的代码,所以不能把构造函数和析构函数强加给malloc和free,new/delete是必不可少的。
29、被free回收的内存是立即返还给操作系统吗?
不是的,被free回收的内存首先会被ptmalloc使用双链表保存起来,当用户下一次申请内存时,会尝试在这些内存中找到合适的返回,这样就避免频繁的系统调用,占用过多系统资源,同时ptmalloc会将较小的内块存碎片进行合并,避免过多的内存碎片。
30、C++中有几种类型的new
在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new
(1)plain new
言下之意就是普通的new,就是我们常用的new,plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL
(2)nothrow new
nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL
(3)placement new
这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。
使用placement new需要注意两点:
palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组。
placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
31、说说new和malloc的区别,各自底层实现原理。
new是操作符,而malloc是函数。
new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
new可以被重载;malloc不行
new分配内存更直接和安全。
new发生错误抛出异常,malloc返回null
malloc底层实现:当开辟的空间小于 128K 时,调用 brk()函数;当开辟的空间大于 128K 时,调用mmap()。malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。
new底层实现:关键字new在调用构造函数的时候实际上进行了如下的几个步骤:
创建一个新的对象
将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)
执行构造函数中的代码(为这个新对象添加属性)
返回新对象
32、delete p、delete [] p、allocator都有什么作用?
动态数组管理new一个数组时,[]中必须是一个整数,但是不一定是常量整数,普通数组必须是一个常量整数;
new动态数组返回的并不是数组类型,而是一个元素类型的指针;
delete[]时,数组中的元素按逆序的顺序进行销毁;
new在内存分配上面有一些局限性,new的机制是将内存分配和对象构造组合在一起,同样的,delete也是将对象析构和内存释放组合在一起的。allocator将这两部分分开进行,allocator申请一部分内存,不进行初始化对象,只有当需要的时候才进行初始化操作。
33、new和delete的实现原理, delete是如何知道释放内存的大小的?
new简单类型直接调用operator new分配内存;而对于复杂结构,先调用operator new分配内存,然后在分配的内存上调用构造函数;对于简单类型,new[]计算好大小后调用operator new[];对于复杂数据结构,new[]先调用operator new[]分配内存,然后在p的前四个字节写入数组大小n,然后调用n次构造函数,针对复杂类型,new[]会额外存储数组大小;
new表达式调用一个名为operator new(operator new[])函数,分配一块足够大的、原始的、未命名的内存空间;
编译器运行相应的构造函数以构造这些对象,并为其传入初始值;
对象被分配了空间并构造完成,返回一个指向该对象的指针。
delete简单数据类型默认只是调用free函数;复杂数据类型先调用析构函数再调用operator delete;针对简单类型,delete和delete[]等同。假设指针p指向new[]分配的内存。因为要4字节存储数组大小,实际分配的内存地址为[p-4],系统记录的也是这个地址。delete[]实际释放的就是p-4指向的内存。而delete会直接释放p指向的内存,这个内存根本没有被系统记录,所以会崩溃。
需要在new [] 一个对象数组时,需要保存数组的维度,C++ 的做法是在分配数组空间时多分配了 4 个字节的大小,专门保存数组的大小,在 delete [] 时就可以取出这个保存的数,就知道了需要调用析构函数多少次了。
34、malloc申请的存储空间能用delete释放吗?
不能,malloc /free主要为了兼容C,new和delete 完全可以取代malloc /free的。
malloc /free的操作对象都是必须明确大小的,而且不能用在动态类上。
new 和delete会自动进行类型检查和大小,malloc/free不能执行构造函数与析构函数,所以动态对象它是不行的。
当然从理论上说使用malloc申请的内存是可以通过delete释放的。不过一般不这样写的。而且也不能保证每个C++的运行时都能正常。
35、malloc与free的实现原理?
在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk、mmap、,munmap这些系统调用实现的;
brk是将数据段(.data)的最高地址指针edata往高地址推,mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存。这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系;_
malloc小于128k的内存,使用brk分配内存,将edata往高地址推;malloc大于128k的内存,使用mmap分配内存,在堆和栈之间找一块空闲内存分配;brk分配的内存需要等到高地址内存释放以后才能释放,而mmap分配的内存可以单独释放。当最高地址空间的空闲内存超过128K(可由M_TRIM_THRESHOLD选项调节)时,执行内存紧缩操作(trim)。在上一个步骤free的时候,发现最高地址空闲内存超过128K,于是内存紧缩。
操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
36、malloc、realloc、calloc的区别
malloc函数:
void* malloc(unsigned int num_size);
int *p = malloc(20*sizeof(int));、//申请20个int类型的空间;
calloc函数:
void* calloc(size_t n,size_t size);
int *p = calloc(20, sizeof(int));
省去了人为空间计算;malloc申请的空间的值是随机初始化的,calloc申请的空间的值是初始化为0的;
realloc函数:
void realloc(void *p, size_t new_size);
给动态分配的空间分配额外的空间,用于扩充容量。
37、delete和delete[]区别?
delete只会调用一次析构函数。
delete[]会调用数组中每个元素的析构函数。
三、指针与引用
38、指针与引用的区别?
① 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名。
② 指针可以有多级,引用只有一级
③ 指针可以为空,引用不能为NULL且在定义时必须初始化
④ 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
⑤ sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
⑥ 当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,两者指向的地址相同,但不是同一个变量,在函数中改变这个变量的指向不影响实参,而引用却可以。
⑦ 引用本质是一个指针,同样会占4字节内存;指针是具体变量,需要占用存储空间(,具体情况还要具体分析)。
⑧ 引用在声明时必须初始化为另一变量,一旦出现必须为typename refname &varname形式;指针声明和定义可以分开,可以先只声明指针变量而不初始化,等用到时再指向具体变量。
⑨ 引用一旦初始化之后就不可以再改变(变量可以被引用为多次,但引用只能作为一个变量引用);指针变量可以重新指向别的变量。
⑩ 不存在指向空值的引用,必须有具体实体;但是存在指向空值的指针。
39、在传递函数参数时,什么时候该使用指针,什么时候该使用引用呢?
① 当返回函数内部局部变量的内存时使用指针传递参数,使用指针需要开辟内存,所以需要始放内存,不然会内存泄漏,返回局部变量的引用无意义。
② 对栈空间大小比较敏感(如递归)时使用引用。使用引用传递不需要创建临时变量,开销更小。
③ 类对象作为参数进行传递时使用引用。
40、一个指针占多少字节?
在64位的编译环境下的,指针的占用大小为8字节;而在32位环境下,指针占用大小为4字节。一个指针占内存的大小跟编译环境有关,而与机器的位数无关。
41、常量指针和指针常量区别?
指针常量是一个指针,读成常量的指针,指向一个只读变量,也就是后面所指明的int const 和 const int,都是一个常量,可以写作int const *p或const int *p
常量指针是一个不能给改变指向的指针。指针是个常量,必须初始化,一旦初始化完成,它的值(也就是存放在指针中的地址)就不能在改变了,即不能中途改变指向,如int *const p。
42、说说数组和指针的区别
概念:
(1)数组:数组是用于储存多个相同类型数据的集合。数组名是首元素的地址。
(2)指针:指针相当于一个变量,但是它和不同变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。
区别:
(1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)存储方式:
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。
指针:指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。由于指针本身就是一个变量,再加上它所存放的也是变量,所以指针的存储空间不能确定。
(3)求sizeof:
数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
43、说什么是野指针,怎么产生的,如何避免?
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
产生原因:释放内存后指针不及时置空(野指针),依然指向了该内存,那么可能出现非法访问的错误。这些我们都要注意避免。
避免办法:
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
(4)使用智能指针
44、野指针和悬空指针
都是是指向无效内存区域(这里的无效指的是"不安全不可控")的指针,访问行为将会导致未定义行为。
野指针
野指针,指的是没有被初始化过的指针,因此,为了防止出错,对于指针初始化时都是赋值为 nullptr,这样在使用时编译器就会直接报错,产生非法内存访问。
悬空指针
悬空指针,指针最初指向的内存已经被释放了的一种指针。
产生原因及解决办法:
野指针:指针变量未及时初始化,定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空,释放操作后立即置空。
45、浅拷贝和深拷贝的区别
浅拷贝
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
深拷贝
深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。
46、指针和const的用法
① 当const修饰指针时,由于const的位置不同,它的修饰对象会有所不同。
② int *const p2中const修饰p2的值,所以理解为p2的值不可以改变,即p2只能指向固定的一个变量地址,但可以通过*p2读写这个变量的值。顶层指针表示指针本身是一个常量
③ int const *p1或者const int *p1两种情况中const修饰*p1,所以理解为*p1的值不可以改变,即不可以给*p1赋值改变p1指向变量的值,但可以通过给p赋值不同的地址改变这个指针指向。底层指针表示指针所指向的变量是一个常量。
47、const* 和 *const的区别(46问简版)
//const* 是常量指针,*const 是指针常量
int const *a; //a指针所指向的内存里的值不变,即(*a)不变
int *const a; //a指针所指向的内存地址不变,即a不变
48、说说const int *a, int const *a, const int a, int *const a, const int *const a分别是什么,有什么特点。
1. const int a; //指的是a是一个常量,不允许修改。
2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变
3. int const *a; //同const int *a;
4. int *const a; //a指针所指向的内存地址不变,即a不变
5. const int *const a; //都不变,即(*a)不变,a也不变
48、说说使用指针需要注意什么?
定义指针时,先初始化为NULL。
用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
动态内存的申请与释放必须配对,防止内存泄漏
用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
总结
(1)初始化置NULL
(2)申请内存后判空
(3)指针释放后置NULL
49、说说什么是函数指针,如何定义函数指针,有什么使用场景
概念:函数指针就是指向函数的指针变量。每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。
定义形式如下:
int func(int a);
int (*f)(int a);
f = &func;
函数指针的应用场景:回调(callback)。我们调用别人提供的 API函数(Application Programming Interface,应用程序编程接口),称为Call;如果别人的库里面调用我们的函数,就叫Callback。
//以库函数qsort排序函数为例,它的原型如下:
void qsort(void *base,//void*类型,代表原始数组
size_t nmemb, //第二个是size_t类型,代表数据数量
size_t size, //第三个是size_t类型,代表单个数据占用空间大小
int(*compar)(const void *,const void *)//第四个参数是函数指针
);
//第四个参数告诉qsort,应该使用哪个函数来比较元素,即只要我们告诉qsort比较大小的规则,它就可以帮我们对任意数据类型的数组进行排序。在库函数qsort调用我们自定义的比较函数,这就是回调的应用。
//示例
int num[100];
int cmp_int(const void* _a , const void* _b){//参数格式固定
int* a = (int*)_a; //强制类型转换
int* b = (int*)_b;
return *a - *b;
}
qsort(num,100,sizeof(num[0]),cmp_int); //回调
50、说说C++中函数指针和指针函数的区别。
定义不同 指针函数本质是一个函数,其返回值为指针。函数指针本质是一个指针,其指向一个函数。
写法不同
指针函数:int *fun(int x,int y);
函数指针:int (*fun)(int x,int y);
用法不同
//指针函数示例
typedef struct _Data{
int a;
int b;
}Data;
//指针函数
Data* f(int a,int b){
Data * data = new Data;
//...
return data;
}
int main(){
//调用指针函数
Data * myData = f(4,5);
//Data * myData = static_cast<Data*>(f(4,5));
//...
}
//函数指针示例
int add(int x,int y){
return x+y;
}
//函数指针
int (*fun)(int x,int y);
//赋值
fun = add;
//调用
cout << "(*fun)(1,2) = " << (*fun)(1,2) ;
//输出结果
//(*fun)(1,2) = 3