C语言面试高频(内存)
内存
1 请说说内存分布模型⭐⭐⭐⭐⭐
如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆栈段组成。
1.代码段:存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)。
2.数据段data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
3.BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
4.可执行程序在运行时又会多出两个区域:堆区和栈区。
- 堆区:动态申请内存用。堆从低地址向高地址增长。
- 栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
5.最后还有一个文件映射区(共享区),位于堆和栈之间。
2 堆和栈的区别⭐⭐⭐⭐⭐
1.堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等,栈有着很高的效率;堆一般由程序员分配释放,堆的效率比栈要低的多。
2.堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
3.空间大小: 栈的空间大小并不大,一般最多为2M,超过之后会报Overflow错误。堆的空间非常大,理论上可以接近3G。(针对32位程序来说,可以看到内存分布,1G用于内核空间,用户空间中栈、BSS、data又要占一部分,所以堆理论上可以接近3G,实际上在2G-3G之间)
4.能否产生碎片: 栈的操作与数据结构中的栈用法是类似的。‘后进先出’的原则,以至于不可能有一个空的内存块从栈被弹出。因为在它弹出之前,在它上面的后进栈的数据已经被弹出。它是严格按照栈的规则来执行。但是堆是通过new/malloc随机申请的空间,频繁的调用它们,则会产生大量的内存碎片。这是不可避免地。
3 请你说说内存泄露⭐⭐⭐⭐⭐
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
(1)new和malloc申请资源使用后,没有用delete和free释放;
(2)子类继承父类时,父类析构函数不是虚函数。
(3)比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
(4)shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。
有以下几种避免方法:
- 第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
- 第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
- 第三:使用智能指针。
- 第四:一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
4 在函数中申请堆内存需要注意什么?⭐⭐⭐⭐⭐
(在函数中申请堆内存时,需注意以下要点:
内存申请环节
- 检查返回值:使用
malloc
、calloc
、realloc
或new
申请内存后,要检查返回值。C 语言中若为NULL
代表申请失败,C++ 则可能抛出异常,程序应做相应错误处理。 - 精准计算大小:根据实际需求准确计算所需内存字节数,传给相应的内存分配函数。
内存使用环节
- 防止越界访问:操作堆内存时,要确保在申请的内存范围内进行,避免越界引发数据破坏或未定义行为。
- 及时初始化:申请的堆内存值是未定义的,若有特定值需求,要及时初始化。
内存释放环节
- 避免内存泄漏:函数结束前,用
free
(C 语言)或delete
/delete[]
(C++)释放内存。若函数有多个返回路径,要在各路径上都释放内存。 - 避免重复释放:对同一块内存多次释放会导致未定义行为,释放后可将指针置为
NULL
以防误操作。
其他方面
- 线程安全:多线程环境下,多个线程同时操作堆内存可能产生竞争条件,需用同步机制保证安全。
- 减少内存碎片:频繁申请和释放小块内存会导致碎片,可考虑使用内存池技术提升内存利用率。
5 请你说说内存碎片⭐⭐⭐⭐⭐
内存碎片是在计算机内存管理中出现的一种现象,会对系统性能和内存使用效率产生影响。下面将从其定义、产生原因、分类以及影响和解决办法等方面进行详细介绍。
定义
- 内存碎片指的是内存中无法被有效利用的小块空闲内存区域。随着程序不断地申请和释放内存,原本连续的大块内存会被分割成许多不连续的小块,这些小块内存由于太小,可能无法满足后续程序对内存的需求,从而造成内存空间的浪费。
产生原因
- 动态内存分配与释放:程序在运行过程中频繁地进行内存的申请和释放操作。例如,一个程序先申请了一块较大的内存区域,之后释放了其中一部分,这时就会形成一些空闲的内存小块。后续再申请内存时,如果这些小块内存不能满足需求,就会造成碎片。
- 内存分配粒度不一致:不同的程序或进程对内存的需求大小不同,当操作系统按照一定的规则进行内存分配时,可能会导致内存分配的粒度不一致。比如,一个程序需要 10KB 的内存,而操作系统只能以 16KB 为单位进行分配,那么就会产生 6KB 的空闲内存,这部分内存可能就会成为碎片。
- 程序的内存使用模式:某些程序的内存使用模式可能会加剧内存碎片的产生。例如,一些程序会频繁地创建和销毁对象,这些对象的生命周期不同,导致内存的分配和释放时间不一致,从而增加了内存碎片的可能性。
分类
- 内部碎片:指的是已经分配给进程的内存空间中,由于分配策略的原因而未被利用的部分。例如,操作系统为一个进程分配了一个固定大小的内存块,但该进程实际使用的内存小于这个块的大小,剩余的部分就成为了内部碎片。
- 外部碎片:指的是内存中存在许多不连续的空闲小块,这些小块内存的总和可能足够满足一个新的内存请求,但由于它们不连续,无法合并成一个足够大的连续内存块,从而导致无法满足请求。
影响
- 降低内存利用率:内存碎片会使系统中存在大量无法被有效利用的空闲内存,导致内存的实际利用率降低。这意味着即使系统中还有空闲内存,但由于碎片的存在,程序可能无法申请到足够的连续内存空间,从而影响程序的正常运行。
- 增加内存分配时间:当程序需要申请内存时,操作系统需要花费更多的时间来寻找足够大的连续内存块。如果内存碎片严重,可能需要遍历大量的空闲内存块才能找到合适的位置,这会增加内存分配的时间开销,降低系统的性能。
- 限制程序的运行:在极端情况下,内存碎片可能会导致系统无法为程序分配足够的内存,即使系统的物理内存总量还足够。这会使程序无法正常启动或运行,甚至可能导致系统崩溃。
解决办法
- 内存紧凑:通过将分散的内存块移动到一起,合并成一个连续的大块内存,从而消除外部碎片。但这种方法需要暂停程序的运行,并且可能会消耗较多的系统资源。
- 采用更合理的内存分配算法:如伙伴系统算法、位图算法等,这些算法可以更好地管理内存,减少内存碎片的产生。例如,伙伴系统算法将内存按照 2 的幂次方进行划分和分配,能够有效地减少外部碎片。
- 内存池技术:对于一些频繁进行内存分配和释放的程序,可以使用内存池技术。内存池预先分配一大块连续的内存,然后根据程序的需求进行小块内存的分配和回收,这样可以减少内存碎片的产生,提高内存分配的效率。
6 请你说说malloc内存管理原理⭐⭐⭐⭐⭐
malloc
是 C 语言标准库中用于动态内存分配的函数,其原型为 void *malloc(size_t size)
,作用是在堆内存中分配指定大小(size
字节)的连续内存块,并返回指向该内存块起始地址的指针。若分配失败,则返回 NULL
。下面详细介绍 malloc
的内存管理原理。
堆内存
- 在程序运行时,操作系统会为其分配不同类型的内存区域,如栈、堆、数据段和代码段。
malloc
所分配的内存位于堆区。与栈不同,堆内存的分配和释放需由程序员手动控制。
空闲内存块管理
操作系统或 C 运行时库会维护一个空闲内存块列表,用于记录堆中尚未被分配的内存区域。这些空闲内存块会以链表或其他数据结构的形式组织起来,每个空闲内存块包含两部分信息:
- 元数据:包含内存块的大小、是否已分配等信息。
- 数据区域:供程序使用的实际内存空间。
malloc
工作流程
搜索空闲内存块:当调用 malloc(size)
时,系统会在空闲内存块列表中查找大小足够的空闲内存块。搜索算法有多种,常见的有首次适应算法、最佳适应算法和最坏适应算法:
- 首次适应算法:从空闲内存块列表的头部开始搜索,找到第一个大小不小于 size 的空闲内存块就进行分配。
- 最佳适应算法:遍历整个空闲内存块列表,找到大小最接近 size 的空闲内存块进行分配。
- 最坏适应算法:选择最大的空闲内存块进行分配。
分割内存块:若找到的空闲内存块大小比 size
大,系统会将其分割成两部分:一部分大小为 size
,分配给程序;另一部分作为新的空闲内存块,重新加入空闲内存块列表。
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
该专栏面向嵌入式开发工程师、C++开发工程师,包括C语言、C++,操作系统,ARM架构、RTOS、Linux基础、Linux驱动、Linux系统移植、计算机网络、数据结构与算法、数电基础、模电基础、5篇面试题目、HR面试常见问题汇总和嵌入式面试简历模板等文章。超全的嵌入式软件工程师面试题目和高频知识点总结! 另外,专栏分为两个部分,大家可以各取所好,为了有更好的阅读体验,后面会持续更新!!!