【C++八股 - 第三期】内存管理 - 24年春招特供

提纲:

👉 八股:

  1. 说说静态变量在代码执行的什么阶段进行初始化
  2. 给我介绍一下 静态全局变量、静态局部变量、全局变量、局部变量的特点,以及他们的应用场景。
  3. 你了解虚拟空间么 or 你了解C++的内存分布模型吗?
  4. 简单概述一下堆和栈的区别
  5. 为什么使用虚拟内存;其好处与坏处是什么?
  6. 申请堆内存时需要注意什么?
  7. 你了解内存泄漏么?
  8. malloc内存管理原理
  9. 了解内存碎片么?
  10. 了解野指针么?
  11. 用new和malloc申请内存时有什么区别?你了解他们的底层实现么?
  12. 什么是内存池
  13. 在使用指针的时候你都从哪些方面考虑?
  14. 初始化为0的全局变量在bss还是data
  15. 在1G的内存中,能成功分配4G的数组么

👉 代码:

C++八股内容:

1、说说静态变量在代码执行的什么阶段进行初始化

static int value  //静态变量初始化语句
  • 对于C语言: 静态变量和全局变量均在编译期进行初始化,及初始化发生在任何代码执行之前。
  • 对于C++: 静态变量和全局变量仅当首次被使用的时候才进行初始化。

助记: 如果你使用过C/C++你会发现,C语言要求在程序的最开头声明全部的变量,而C++则可以随时使用随时声明;这个规律是不是和答案类似呢?

2. 给我介绍一下 静态全局变量、静态局部变量、全局变量、局部变量的特点,以及他们的应用场景。

  • 先分析一下他们各自是什么(此处以静态变量进行讨论)

    • 全局变量就是定义在函数外的变量。

    • 局部变量就是函数内定义的变量。

    • 静态变量就是加了static的变量。 例如:static int value = 1

  • 各自存储的位置:

    • 全局变量,存储在常量区(静态存储区)。

    • 局部变量, 存储在栈区。

    • 静态变量,存储在常量区(静态存储区)。

    • 注意: 因为静态变量都在静态存储区(常量区),所以下次调用函数的时候还是能取到原来的值。

  • 各自初始化的值:

    • 局部变量一般是不初始化的,

    • 全局变量和静态变量,都是初始化为0的,有一个初始值。

    • 如果是类变量,会调用默认构造函数初始化。

  • 从作用域来考虑:

    首先你需要清楚:C++里作用域可分为6种:全局局部语句命名空间文件作用域

    • 全局变量(函数体外定义): 全局作用域,可以通过extern(引入C的那个)作用于其他非定义的源文件;都会一直存在,直到程序结束。

    • 静态全局变量 : 全局作用域+文件作用域,所以无法在其他文件中使用。

    • 局部变量: 局部作用域,比如函数的参数,函数内的局部变量等等;它从进入作用域遇到该变量的时候开始出现,在离开的时候销毁。

    • 静态局部变量 : 局部作用域,只被初始化一次,直到程序结束。

  • 各自的应用场景:

    • 局部变量就是我们经常用的,进入函数,逐个构造,最后统一销毁。

    • 全局变量主要是用来给不同的文件之间进行通信。

    • 静态变量:只在本文件中使用,局部静态变量在函数内起作用,可以作为一个计数器。

    • 例子:

       void func(){
         static int count;
         count ++;
       }
       int main(int argc, char** argv){
         for(int i = 0; i < 10; i++)
           func();
       }
    

3. 你了解虚拟空间么 or 你了解C++的内存分布模型吗?

引子:

  • 首先你需要了解 物理内存

    • 物理内存实际上是 CPU中能直接寻址的地址线条数。由于物理内存是有限的,例如32位平台下,寻址的大小是4G,并且是固定的。内存很快就会被分配完,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作是很没效率的。
  • 这就需要用到 虚拟内存 了。

    • 在那个进程创建的时候,系统都会给每个进程分配4G的内存空间,这其实是虚拟内存空间。进程得到的这4G虚拟内存,进程自身以为是一段连续的空间,而实际上,通常被分隔成多个物理内存碎片,还有一部分存储在外部磁盘存储器上,需要的时候进行数据交换。
  • 关于虚拟内存与物理内存的联系,下面这张图可以帮助我们巩固。

    • alt
    • alt

虚拟内存机理及优点:

  • 虚拟内存是如何工作的?

    • 当每个进程创建的时候,内核会为进程分配4G的虚拟内存,当进程还没有开始运行时,这只是一个内存布局。实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射)。这个时候数据和代码还是在磁盘上的。当运行到对应的程序时,进程去寻找页表,发现页表中地址没有存放在物理内存上,而是在磁盘上,于是发生缺页异常,于是将磁盘上的数据拷贝到物理内存中。

    • 另外在进程运行过程中,要通过malloc来动态分配内存时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

    • 可以认为虚拟空间都被映射到了磁盘空间中(事实上也是按需要映射到磁盘空间上,通过mmap,mmap是用来建立虚拟空间和磁盘空间的映射关系的)

  • 利用虚拟内存机制的优点 ?

    • 既然每个进程的内存空间都是一致而且固定的(32位平台下都是4G),所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际内存地址,这交给内核来完成映射关系

    • 当不同的进程使用同一段代码时,比如库文件的代码,在物理内存中可以只存储一份这样的代码,不同进程只要将自己的虚拟内存映射过去就好了,这样可以节省物理内存

    • 在程序需要分配连续空间的时候,只需要在虚拟内存分配连续空间,而不需要物理内存时连续的,实际上,往往物理内存都是断断续续的内存碎片。这样就可以有效地利用我们的物理内存

C++的内存分布模型:

alt

  • 从高地址到低地址,一个程序由内核空间栈区堆区BSS段数据段(data)代码区组成。

  • 可执行程序在运行时会多出两个区域:

    • 堆区: 动态申请内存用。堆从低地址向高地址增长。

    • 栈区: 存储 局部变量、函数参数值 。栈从高地址向低地址增长。是一块连续的空间。

  • 在堆栈之间有一个 共享区(文件映射区)

  • BSS 段:一块存放程序中未初始化全局变量静态变量 的内存区域。

  • 数据段data:一块存放程序中已初始化全局变量静态变量的内存区域。

  • 代码段: 存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)

  • 在linux下size命令可以查看一个可执行二进制文件基本情况:

    • alt

4.简单概述一下堆和栈的区别

  • 申请方式及申请效率不同:

    • 总结:栈申请的快,效率比堆高。
    • 栈由系统自动分配,存放函数的参数值,局部变量的值;申请速度较快;而堆是人为申请开辟,也需要程序员手动释放,申请速度慢。
  • 申请所得空间大小不同:

    • 栈的空间大小并不大,一般最多为2M,超过之后会报Overflow错误。堆的空间非常大,理论上可以接近3G。(针对32位程序来说,可以看到内存分布,1G用于内核空间,用户空间中栈、BSS、data又要占一部分,所以堆理论上可以接近3G,实际上在2G-3G之间)。

    就是上面那个图我画的 3G 和 1G 的分布。

  • 缓存方式不同:

    • 栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
  • 是否会产生内存碎片(内存碎片是操作系统知识,不知道百度即可)

    • 栈不会产生内存碎片,堆会;因为栈采用后进先出,不可能进入空弹出空,而堆是人为频繁调用new或者malloc,所以总会产生碎片。

此题总结:

1、申请方式的不同。 栈由系统自动分配,而堆是人为申请开辟;

2、申请大小的不同。 栈获得的空间较小,而堆获得的空间较大;

3、申请效率的不同。 栈由系统自动分配,速度较快,而堆一般速度比较慢;

4、存储内容的不同。 栈在函数调用时,函数调用语句的下一条可执行语句的地址第一个进栈,然后函数的各个参数进栈,其中静态变量是不入栈的。而堆一般是在头部用一个字节存放堆的大小,堆中的具体内容是人为安排;

5、底层不同。 栈是连续的空间,而堆是不连续的空间。

5. 为什么使用虚拟内存;其好处与坏处是什么?

面试官可能会问:你知道虚拟内存么?需要结合第3个问题回答

  • 为什么要用虚拟内存:

    因为早期的内存分配方法存在以下问题:

    (1) 进程地址空间不隔离。会导致数据被随意修改。

    (2) 内存使用效率低。

    (3) 程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。

  • 使用虚拟内存的好处、作用:
    (1)扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。

    (2)内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。

    (3)可以实现内存共享,方便进程通信。

    (4)可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。

  • 使用虚拟内存的缺点:
    (1)虚拟内存需要额外构建数据结构,占用空间。

    (2)虚拟地址到物理地址的转换,增加了执行时间。

    (3)页面换入换出耗时。

    (4)一页如果只有一部分数据,浪费内存。

6. 申请堆内存时需要注意什么?

需要注意:

  • (1)不要错误地返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡。
  • (2)不要返回了常量区的内存空间。因为常量字符串,存放在代码段的常量区,生命期内恒定不变,只读不可修改
  • (3)通过传入一级指针不能解决,因为函数内部的指针将指向新的内存地址。
  • (4)注意防止内存泄露:内存无法再使用,又无法释放,而再次使用时只能重新申请,然后重复以上过程,日积月累后系统中可用内存就会越来越少.

解决办法:

  • (1)使用二级指针
  • (2)通过指针函数解决,返回新申请的内存空间的地址。

二级指针: 指向指针的指针;用于存放二级指针的变量称为二级指针变量。根据B的不同情况,二级指针又分为指向指针变量的指针和指向数组的指针。

7. 你了解内存泄漏么?

  • 简单地说就是申请了一块内存空间,使用完毕后没有释放掉。

  • 造成的问题: 内存无法再使用,又无法释放,而再次使用时只能重新申请,然后重复以上过程,日积月累后系统中可用内存就会越来越少。

  • 导致发生内存泄漏的操作:

    (1)new和malloc申请资源使用后,没有用delete和free释放;

    (2)子类继承父类时,父类析构函数不是虚函数;

    (3)比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数;

    (4)shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。

  • 解决对策:

    简单来说就是: 谁申请,谁释放;谁知道该释放谁释放

    (1)良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。

    (2)将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

    (3)使用智能指针 🔥🔥🔥(面试大热点!!!)

    (4)一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。

  • 如何定位内存泄漏:

    (1)查看内存的使用情况 win 任务管理器 linux ps -aux

    (2)分析代码、分析代码的工具检查malloc的调用情况

    (3)封装malloc、free,记录申请、释放的信息到日志中

8. malloc内存管理原理

  • 当开辟的空间小于 128K 时,调用 brk()函数;

  • 当开辟的空间大于 128K 时,调用mmap()。

  • malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块,每一个空闲块记录了一个未分配的、连续的内存地址。

  • 当首次向malloc申请内存时,malloc会向操作系统申请内存,操作系统会直接分配33页(1页=4096字节)内存交给malloc管理,但是不意味着可以越界访问,因为malloc会把使用权分配给别人,此时就会产生脏数据

  • 每个内存块之间一定会有一些间隙(12~4字节)这些空隙是为了内存对齐,其中一定有4个字节记录malloc的维护信息,这些维护信息决定了下次分配内存的位置,还可以借助这些位置计算出每个内存块的大小,如果这些维护信息被破坏就会影响下一次malloc

9. 了解内存碎片么?

概念:

  • 内存碎片分为:内部碎片外部碎片

  • 内部碎片: 是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免;

  • 外部碎片: 是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。再比如堆内存的频繁申请释放,也容易产生外部碎片。

解决方法:

  • 段页式管理
  • 内存池

10. 你了解野指针么?

  • 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
    • 指针变量在定义时如果未初始化,其值是随机的,指针变量的值是别的变量的地址,意味着指针指向了一个地址是不确定的变量,此时去解引用就是去访问了一个不确定的地址,所以结果是不可知的。

注意: 野指针不同于空指针,空指针是指一个指针的值为null ,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针

11. 用new和malloc申请内存时有什么区别?你了解他们的底层实现么?

区别:

new是操作符 malloc是函数
new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数 malloc没有构造函数和析构函数
new会调用构造函数,不用指定内存的大小,返回指针不用强转 malloc需要给定申请内存的大小,返回的指针需要强转
new可以被重载 malloc不行
new分配内存更直接和安全 malloc没有new安全
new发生错误抛出异常 malloc返回null

malloc底层实现:

      当开辟的空间小于128k时,调用brak()函数;当开辟的空间大于128k时,调用mmap(),malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲块。采用隐式链表将所有空闲块,每个空闲块记录了一个未分配的、连续的内存地址。

new底层实现:

关键字new在调用构造函数的时候实际上进行了如下的几个步骤:

  • 创建一个新的对象

  • 将构造函数的作用域赋值给这个新的对象(因此this指向了这个新的对象)

  • 执行构造函数中的代码(为这个新对象添加属性)

  • 返回新对象

12. 什么是内存池

  • 内存池 也是一种对象池,是提前向系统申请一块较大的内存放在’池子‘中,当程序需要内存的时候,自动去池子之中获取内存,当使用完毕之后,再将内存释放回去,这样可以达到内存重复利用的目的。这样合理的分配回收内存使得内存分配效率得到提升。

池化技术:

池是计算机中常用的一种设计模式,其特点是将资源提前申请好,放在"资源池"之中由程序自己控制资源的使用,这样减少了与内核的交互

因为资源的申请是需要通过内核来完成的,与内核交互的频率越高, 程序的效率就越低,提前将资源申请出来,使用资源的时候不需要再向内核申请,直接就可以使用,在一定程度上提高了程序的效率

常见的池化技术有线程池,内存池等等

13. 在使用指针的时候你都从哪些方面考虑?

  • 定义指针时,先初始化为NULL。

  • 用malloc申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。

  • 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  • 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作

  • 动态内存的申请与释放必须配对,防止内存泄漏

  • 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”

14. 初始化为0的全局变量在bss还是data

  • BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。

15. 在1G的内存中,能成功分配4G的数组么

  • 用malloc(4G)就行;

  • malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。程序运行时,堆空间只是程序向操作系统申请划出来的一大块虚拟地址空间。应用程序通过malloc申请空间,得到的是在虚拟地址空间中的地址,之后程序运行所提供的物理内存是由操作系统完成的。

注意:

  • 32位操作系统只允许进程最大申请3GB的虚拟内存,因此4G数组会申请失败

  • 而64位操作系统则能申请128TB的虚拟内存,即使物理内存只有1G,申请4G也没问题。

  • 如果这块虚拟内存被访问了,要看系统有没有Swap分区

    • 如果没有Swap分区,因为物理空间不够,进程会被操作系统杀掉,原因是OOM(内存溢出)

    • 如果有Swap分区,即使物理内存只有4GB,程序也能正常使用8GB的内存,进程可以正常运行;

  • 【知识拓展】malloc能够申请的空间到底能达到多大
#include <stdio.h>
#include <stdlib.h>
unsigned maximum = 1024 * 1024 * 1024;
int main(int argc, char *argv[])
{
	unsigned blocksize[] = { 1024 * 1024, 1024, 1 };
	int i, count;
	void* block;
	for (i = 0; i < sizeof(blocksize) / sizeof(unsigned); i++){
		for (count = 1; ; count++){
			block = malloc(maximum + blocksize[i] * count);
			if (block != NULL) {
				maximum = maximum + blocksize[i] * count;
				free(block);
			}else {
				break;
			}
		}
	}
	printf("maximum malloc size = %u bytes\n", maximum);
	return 0;
}

代码推荐:剑指Offer(说在**中的题号)

第一天:

  • 剑指 Offer 03

  • 剑指 Offer 53-I

  • 剑指 Offer 53-II

第二天:

  • 剑指 Offer 04

  • 剑指 Offer 11

  • 剑指 Offer 50

第三天:

  • 剑指 Offer 32-I

  • 剑指 Offer 32-II

  • 剑指 Offer 32-III

相关阅读:

【Java八股】:点击跳转

【简历制作】:点击跳转

【C++八股】:点击跳转

全部评论

相关推荐

4 28 评论
分享
牛客网
牛客企业服务