内存管理面经总结

5 篇文章 0 订阅

说起内存管理,参加过面试的同学应该都知道这块的重要性了,但是还没接触到这块的同学,也别不把它放在眼里,并且还要把它搞清楚,面试C++岗位一定会被问到这个的。

1、内存分区(C++中分成5个区)
按照内存地址从低到高的顺序排列,代码区->常量区->全局静态区->堆区->栈区。这是我自己画的分布图,大家可自行参考。
在这里插入图片描述

栈区(stack):在执行函数的时候,函数中的局部变量的存储单元都可以从栈中分配,函数执行结束后这些存储单元都会被自动释放,实现从栈中分配存储单元运算操作内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区是从高地址低地址扩展,是一块连续的内存区域,遵循数据结构中的栈的先进后出的原则。
堆区(Heap):也称动态内存分配,在程序运行期间,可以使用malloc和new申请任意数量的内存单元,由程序员决定在什么时候使用free和delete释放内存。堆区是从低地址向高地址扩展,这个空间是可以不连续的,以链表的结构存在,与栈区相反,遵循先进先出的原则。开辟出的空间的首地址是在栈区。
全局静态区:这是内存在程序编译阶段就已经分配好,该内存在程序运行的整个周期都有效,如:全局变量,static静态变量。static修饰的变量仅执行一次,生命周期为整个程序运行期
常量区:这是比较特殊的存储区,他们里面存放的是常量,不允许修改。已初始化的全局变量和已初始化的静态变量。空间由系统管理,生命周期为整个程序运行期
代码区:存放程序执行的CPU指令,一种二进制文件。

2、malloc/new、free/delete
在使用的时候 new和delete 搭配使用,malloc 和 free 搭配使用。
属性:malloc/free 是库函数,需要头文件的支持。
new/delete 是关键字,需要编译器的支持参数。
new 申请空间时,无需指定分配空间的大小,编译器会根据类型自行计算。
malloc 在申请空间时,需要确定所申请空间的大小返回值。
new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,符合类型安全的操作符。
malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。分配失败:new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针
重载:new/delete 支持重载,malloc/free 不能进行重载
自定义类型实现:new 首先调用 operator new 函数申请空间(底层通过 malloc 实现),然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete 释放空间(底层通过 free 实现)。malloc/free 无法进行自定义类型的对象的构造和析构。
内存区域:new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)。
new类型是安全的的,而malloc不是。
malloc和free,称作C的库函数;new和delete,称作运算符。
new的种类

int *p1 = new int(20);
int *p2 = new (nothrow) int; // 不抛出异常操作
const int *p3 = new const int(40); //在堆上开辟了一个常量

delete和delete[]的区别
对于简单类型来说,使用new分配后,不管是数组形式还是非数组形式,两种方式都可以释放内存:

int *a = new int(1);
delete a;
int *b = new int (2);
delete[] b;
int *c = new int[3];
delete c;
int *d = new int[4];
delete[] d;

对于自定义类型来说,就需要对于单个对象使用delete,对于对象数组使用delete[],逐个调用数组中对象的析构函数,从而释放所有内存。如果反过来使用,即对于单个对象使用delete[],对于对象数组使用delete,其行为是未定义的。
最恰当的方式就是如果用了new,就用delete;如果用了new[],就用delete[]。

new和delete的实现原理
new和delete在C++中其实被定义为两个运算符,我们在使用这两个运算符的时候它就会在底层调用全局函数operator new 和 operator delete。

operator new
operator new在底层实现的源代码

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc){
  // try to allocate size bytes
  void *p;
  while ((p = malloc(size)) == 0)
  if (_callnewh(size) == 0){
  // report no memory
  // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
  static const std::bad_alloc nomem;
     _RAISE(nomem);
     }
  return (p);
}

operator delete
operator delete在底层实现的源代码

void operator delete(void *pUserData){
    _CrtMemBlockHeader * pHead;
    RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    if (pUserData == NULL)
      return;
    _mlock(_HEAP_LOCK); /* block other threads */
    __TRY
    /* get a pointer to memory block header */
    pHead = pHdr(pUserData);
    /* verify block type */
     _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
     _free_dbg( pUserData, pHead->nBlockUse );
     __FINALLY
     _munlock(_HEAP_LOCK); /* release other threads */
     __END_TRY_FINALLY
  return;
}

从源码中能看出的是operator new和operator delete 在底层也是利用malloc和free分配内存的,因此可以说new和delete不过是malloc和free的一层封装。

3、内存泄漏
内存泄漏是因为疏忽或错误造成程序已经不再使用的内存没有被释放的情况。
图片

这是baidu给你解释:
图片

那它有什么危害呢?
如果说长期运行的程序出现内存泄漏,就会浪费空间,比较好理解的就是操作系统和后台服务的这个,然后就会导致响应越来越慢,最终卡死。

for example:

void MemoryLeaks(){
  // 1.内存申请了忘记释放
  int* p1 = (int*)malloc(sizeof(int));
  int* p2 = new int;
  
  // 2.异常安全问题
  int* p3 = new int[10];
  Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.
  delete[] p3;
}

内存泄漏的分类
堆内存泄漏
程序执行中依据需要分配通过malloc/alloc/realloc/new等从堆中分配一块内存,用完后必须通过调用响应的free或delete释放掉。假如程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存的泄漏。
系统资源泄漏
程序使用系统分配的资源,比如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重导致系统效能减少,系统执行不稳定,产生了系统资源泄露。
如何检测内存泄漏
在Linux下内存泄漏检测
valgrind、mtrace、dmalloc、memwatch、mpatrol、dbgmem、Electric Fence
在Windows下内存检测
VLD
如何避免内存泄漏
工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
采用RAII思想或者智能指针来管理资源。
有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
出问题了使用内存泄漏工具检测。
如何一次在堆上申请4G的内存
在堆上申请4G的内存:

#include <iostream>
using namespace std;
int main()
{
  //0xffffffff转换为十进制就是4G
  void* p = malloc(0xfffffffful);
  cout << p << endl;
  
  return 0;
}

在32位的平台下,内存大小为4G,但是堆只占了其中的2G左右,所以我们不可能在32位平台下,一次性在堆上申请4G的内存。这时我们可以将编译器上的Win32改为Win64,这样就可以申请4G的内存。
4、堆和栈有什么区别
分配和管理方式不同:
堆是动态分配的,其空间的分配和释放都由程序员控制。
栈是由编译器自动管理的,其分配方式有两种:静态分配由编译器完成,比如局部变量的分配;动态分配由alloc()函数进行分配,但是会由编译器释放。
产生碎片不同:
对堆来说,频繁使用malloc/delete或者 new/delete会造成内存空间的不连续,产生大量的碎片,使程序效率降低。
对栈来说,不存在碎片问题,因为栈具有先进后出的特性。
生长方向不同:
堆是向着内存地址增加的方向增长的,从内存的低地址向高地址方向增长。
栈是向着内存地址减小的方向增长的,从内存的高地址向低地址方向增长。
申请大小限制不同:
栈顶和栈底是预设好的,大小固定。
堆是不连续的内存区域,其大小可以灵活调整。
5、深浅拷贝、写时拷贝
深拷贝:不仅拷贝指针,对指针指向的内容进行拷贝,深拷贝后两个指针指向两个不同的地址。

浅拷贝:只是对指针的拷贝,拷贝后两个指针指向同一个内存空间。

写时拷贝:内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟地址结构,但是不为这段分配物理内存,它们共享父进程的物理空间,当父进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

6、内存对齐
大端对齐和小端对齐
大端对齐:数据的低位存储在内存的高地址上,数据高位存储存储在内存的低地址上;(字符串存储)。

小端对齐:数据的低位存储在内存的低地址上,数据高位存储存储在内存的高地址上。

7、Hash
哈希冲突的产生原因:哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值。这时候就产生了哈希冲突。

Hash哈希冲突发生的场景:当关键字值域远大于哈希表的长度,而且事先并不知道关键字的具体取值时。hash冲突就会发生。

Hash溢出发生的场景:当关键字的实际取值大于哈希表的长度时,而且表中已装满了记录,如果插入一个新纪录,不仅发生冲突,而且还会发生溢出。

解决哈希冲突的方法主要有:开放地址法和拉链法

开放地址法:

线性探测:按顺序决定值时,如果数据的值已经存在,则在原来值的基础上往后加一个单位,直至不发生哈希冲突。

再平方探测:按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平方个单位,若仍然存在则减1的平方个单位。随之是2的平方,3的平方等。直至不发生哈希冲突。

伪随机探测:按顺序决定值时,如果某数据已经存在,通过随机函数随机生成一个数,在原来值得基础上加上随机数,直至不发生哈希冲突。

链式地址法:

对于相同的值,使用链表进行连接。使用数组存储每一个链表

优点:

拉链法处理冲突简单,且无堆积现象,即非同义词绝不会发生冲突,因此平均查找长度较短。

由于拉链中个链表的节点空间时动态申请的,故它更适合于造表前无法确定表长的情况。

开放定址法为减少冲突,要求装填因子较小,故当节点规模较大时会浪费很多空间。而拉链法中可取a>=1,且节点较大时,拉链法中增加的指针域可忽略不计,因此节省空间。

再用拉链法构造的散列表中,删除节点的操作易于实现,只要简单地山区链表上相应的节点即可。

缺点:

指针占用较大空间时,会造成空间浪费,若空间用于增大散列表规模进而提高开放地址法的效率。

建立公共溢出区存储所有哈希冲突的数据。

对于冲突的哈希值再次进行哈希处理,直至没有哈希冲突。

8、静态联编和动态联编
静态联编是指在编译阶段就将函数实现和函数调用关联起来,也叫早绑定。对函数的选择是基于指向对象的指针(或者引用)的类型,C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。

动态联编是指在程序执行的时候才将函数实现和函数调用关联,也叫运行时绑定或者晚绑定。对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。涉及到多态和虚拟函数就必须要用动态联编。

9、C++中,使用malloc申请的内存是否能通过delete释放?new和free呢?
不能

malloc/free主要是为了兼容C,new和delete完全可以取代malloc/free的,malloc/free的操作对象都是必须明确大小的。
也不能用在动态类上,new和delete会自动进行类型检查,也不需要明确自己的内存大小,malloc/free不能执行构造函数和析构函数。

记得三连嗷~(亲测对面试非常有用!!!)

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值