底软驱动 | 大厂面试爱考的C++内存相关

news2024/9/19 10:46:45

文章目录

  • C++内存相关
    • C++内存分区
      • C++对象的成员函数存放在内存哪里
    • 堆和栈的区别
    • 堆和栈的访问效率
    • “野指针”
    • 有了malloc/free为什么还要new/delete
    • alloca
    • 内存崩溃
    • C++内存泄漏的几种情况
    • 内存对齐
    • 柔性数组
    • 参考
    • 推荐阅读

C++内存相关

本篇介绍了 C++ 内存相关的知识。

C++内存分区

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

  • :在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • :就是那些由 new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
  • 全局/静态存储区:全局变量和静态变量被分配到同一块内存中。在以前的C语言中,全局变量又分为初始化的和未初始化的。在C++里面没有这个区分了,他们共同占用同一块内存区。
  • 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
  • 代码段:代码段(code segment / text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

根据c/c++对象生命周期不同,c/c++的内存模型有三种不同的内存区域,即

  • 自由存储区,动态区、静态区。

  • 自由存储区:局部非静态变量的存储区域,即平常所说的栈。

  • 动态区: 用operator new ,malloc分配的内存,即平常所说的堆。

  • 静态区:全局变量 静态变量 字符串常量存在位置。

下图为 C++ 内存模型,来自C++ Essentials。

  • .text 部分是编译后程序的主体,也就是程序的机器指令。
  • .data 和 .bss 保存了程序的全局变量,.data保存有初始化的全局变量,.bss保存只有声明没有初始化的全局变量。
  • heap(堆)中保存程序中动态分配的内存,比如 C 的malloc申请的内存,或者C++中new申请的内存。堆向高地址方向增长。
  • stack(栈)用来进行函数调用,保存函数参数,临时变量,返回地址等。
  • 共享内存的位置在堆和栈之间。

更详细的内存段解释见C与C++内存管理详解。

下面的文章介绍了Linux虚拟地址空间布局。

  • x86 程序内存堆栈模型
  • Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

C++对象的成员函数存放在内存哪里

类成员函数和非成员函数代码存放在代码段。如果类有虚函数,则该类就会存在虚函数表。虚函数表在Linux/Unix 中存放在可执行文件的只读数据段中(rodata),即前面起到的代码段,而微软的编译器将虚函数表存放在常量段

堆和栈的区别

管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak

空间大小:一般来讲在 32 位系统下,堆内存可以达到 4G 的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,栈顶和栈底是之前预设好的,大小固定,可以通过ulimit -a查看,使用ulimit -s修改。

碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,它们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。

生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

堆和栈的访问效率

  • 堆和栈访问效率哪个更高
  • 栈为什么效率比堆高

“野指针”

“野指针”不是NULL指针,是指向“垃圾”内存的指针。“野指针”的成因主要有三种:

  1. 指针变量没有被初始化,缺省值是随机的;
  2. 指针被free/delete之后,没有置为NULL,让人误以为该指针是个合法的指针;
  3. 指针操作超越了变量的作用域范围(内存越界)。

有了malloc/free为什么还要new/delete

mallocfree是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可用于申请动态内存和释放内存。

对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。**对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。**由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。因此 C++ 语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete

既然new/delete的功能完全覆盖了malloc/free,为什么C++不把malloc/free淘汰出局呢?这是因为C++程序经常要调用C函数,而C程序只能用malloc/free管理动态内存

如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,结果也会导致程序出错,该程序的可读性也很差。所以new/delete必须配对使用,malloc/free也一样。

alloca

man中的介绍:

The alloca() function allocates size bytes of space in the stack frame of the caller. This temporary space is automatically freed when the function that called alloca() returns to its caller.

alloca是从栈中分配空间。正因其从栈中分配的内存,因此无需手动释放内存。

讨论见stackoverflow。

内存崩溃

错误类型原因备注
声明错误变量未声明编译时错误
初始化错误未初始化或初始化错误运行不正确
访问错误1. 数组索引访问越界
2. 指针对象访问越界
3. 访问空指针对象
4. 访问无效指针对象
5. 迭代器访问越界
内存泄漏1. 内存未释放
2. 内存局部释放
参数错误本地代理、空指针、强制转换
堆栈溢出1. 递归调用
2. 循环调用
3. 消息循环
4.大对象参数
5. 大对象变量
参数、局部变量都在栈(Stack)上分配
转换错误有符号类型和无符号类型转换
内存碎片小内存块重复分配释放导致的内存碎片,最后出现内存不足数据对齐,机器字整数倍分配

其它如内存分配失败创建对象失败等都是容易理解和相对少见的错误,因为目前的系统大部分情况下内存够用;此外除 0 错误也是容易理解和防范。

C++内存泄漏的几种情况

1. 在类的构造函数和析构函数中没有匹配的调用new和delete函数

两种情况下会出现这种内存泄露:一是在堆里创建了对象占用了内存,但是没有显示地释放对象占用的内存;二是在类的构造函数中动态的分配了内存,但是在析构函数中没有释放内存或者没有正确的释放内存

2. 没有正确地清除嵌套的对象指针

3. 在释放对象数组时在delete中没有使用方括号

方括号是告诉编译器这个指针指向的是一个对象数组,同时也告诉编译器正确的对象地址值病调用对象的析构函数,如果没有方括号,那么这个指针就被默认为只指向一个对象,对象数组中的其他对象的析构函数就不会被调用,结果造成了内存泄露。如果在方括号中间放了一个比对象数组大小还大的数字,那么编译器就会调用无效对象(内存溢出)的析构函数,会造成堆的奔溃。如果方括号中间的数字值比对象数组的大小小的话,编译器就不能调用足够多个析构函数,结果会造成内存泄露。

释放单个对象、单个基本数据类型的变量或者是基本数据类型的数组不需要大小参数,释放定义了析构函数的对象数组才需要大小参数。

4. 指向对象的指针数组不等同于对象数组

对象数组是指:数组中存放的是对象,只需要delete []p,即可调用对象数组中的每个对象的析构函数释放空间

指向对象的指针数组是指:数组中存放的是指向对象的指针,不仅要释放每个对象的空间,还要释放每个指针的空间,delete []p只是释放了每个指针,但是并没有释放对象的空间,正确的做法,是通过一个循环,将每个对象释放了,然后再把指针释放了

5. 缺少拷贝构造函数

两次释放相同的内存是一种错误的做法,同时可能会造成堆的崩溃。

按值传递会调用(拷贝)构造函数,引用传递不会调用。

在C++中,如果没有定义拷贝构造函数,那么编译器就会调用默认的拷贝构造函数,会逐个成员拷贝的方式来复制数据成员,如果是以逐个成员拷贝的方式来复制指针被定义为将一个变量的地址赋给另一个变量。这种隐式的指针复制结果就是两个对象拥有指向同一个动态分配的内存空间的指针。当释放第一个对象的时候,它的析构函数就会释放与该对象有关的动态分配的内存空间。而释放第二个对象的时候,它的析构函数会释放相同的内存,这样是错误的。

所以,如果一个类里面有指针成员变量,要么必须显示的写拷贝构造函数和重载赋值运算符,要么禁用拷贝构造函数和重载赋值运算符。

6. 缺少重载赋值运算符

这种问题跟上述问题类似,也是逐个成员拷贝的方式复制对象,如果这个类的大小是可变的,那么结果就是造成内存泄露,如下图:

7. 关于nonmodifying运算符重载的常见迷思

a. 返回栈上对象的引用或者指针(也即返回局部对象的引用或者指针)。导致最后返回的是一个空引用或者空指针,因此变成野指针。

b. 返回内部静态对象的引用。

c. 返回一个泄露内存的动态分配的对象。导致内存泄露,并且无法回收。

解决这一类问题的办法是重载运算符函数的返回值不是类型的引用,二应该是类型的返回值,即不是 int&而是int

8. 没有将基类的析构函数定义为虚函数

当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

9. 野指针:指向被释放的或者访问受限内存的指针

造成野指针的原因:

  1. 指针变量没有被初始化(如果值不定,可以初始化为NULL)。
  2. 指针被free或者delete后,没有置为NULLfreedelete只是把指针所指向的内存给释放掉,并没有把指针本身干掉,此时指针指向的是“垃圾”内存。释放后的指针应该被置为NULL
  3. 指针操作超越了变量的作用范围,比如返回指向栈内存的指针就是野指针。

内存对齐

CPU是按字读取内存。所以内存对齐的话,不会出现某个类型的数据读一半的情况,需要再二次读取内存。可以提升访问效率。

内存对齐的作用:

  • 可移植性:因为不同平台对数据的在内存中的访问规则不同,不是所有的硬件都可以访问任意地址上的数据,某些硬件平台只能在特定的地址开始访问数据。所以需要内存对齐。
  • 性能原因:一般使用内存对齐可以提高CPU访问内存的效率。如32位的intel处理器通过总线访问内存数据,每个总线周期从偶地址开始访问32位的内存数据,内存数据以字节为单位存放。如果32为的数据没有存放在4字节整除的内存地址处,那么处理器需要两个总线周期对数据进行访问,显然效率下降很多;另外合理的利用字节对齐可以有效的节省存储空间。
  • C/C++语言内存对齐
  • 内存对齐的规则以及作用
  • C语言内存对齐
  • 内存对齐
  • C/C++ 各数据类型占用字节数
  • C/C++ 结构体字节对齐
  • C/C++内存对齐

柔性数组

柔性数组结构成员:C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

  • C语言0长度数组(可变数组/柔性数组)详解
  • C99柔性数组成员介绍(其一)
  • C99柔性数组成员介绍(其二)

参考

  • C++ Essentials
  • C和C++内存模型
  • C与C++内存管理详解
  • C++ 常见崩溃问题分析
  • C++虚函数表
  • 虚函数表在对象内存中的布局
  • C++内存泄漏的几种情况
  • 实习面经 --C/C++ 基础

推荐阅读

  • C/C++程序内存的各种变量存储区域和各个区域详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1929300.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【python学习】numpy第三方库的定义、功能、使用场景和使用以及遇到的一些问题

引言 python学习学习到第三方库知识,首先学习的就是机器学习以及对应的numpy第三方库 文章目录 引言一、numpy第三方库的定义二、numpy第三方库的功能2.1数组操作2.2 线性代数计算2.3 随机数生成2.4 文件读写 三、numpy第三方库的使用场景3.1需要进行数值计算3.2 需…

PyCharm软件初始化配置

安装完pycharm后,需要对其进行个性化设置,分别设置方法如下 目录 一、修改主题二、修改默认字体和大小三、设置拖动滚轮改变字体大小四、常见快捷键 一、修改主题 1、界面右上角点击红框的内容 2、选择Theme选项 3、选择对应的主题 第一二个是白色主题…

通俗易懂多图透彻讲解二叉树的遍历--前序, 中序和后序

二叉树的遍历是一个数据结构中经常会遇到的知识点, 具体又分为前序, 中序和后序三种. 什么是树? 先来理解一下什么是树, 从一个我们相对熟悉的家谱树(Family Tree)说起吧. 家族的根是爷爷, 然后生了两个娃, 大伯和你爸爸. 继续往下, 有堂哥堂姐, 还有你以及你妹, 等等. 一个…

工业智能网关的边缘计算能力赋能工业4.0

边缘计算是将数据处理和分析能力推向网络边缘的技术,使得终端设备能够实时、快速地响应环境变化,并做出相应决策。在智能制造中,通过5G工业网关的边缘计算能力,企业可以实现对生产线上大量传感器数据的实时采集、处理和分析&#…

Linux0715

一切皆文件,文件IO已经学习完毕,这两天完成一个minishell的小项目 文件操作 1. 标准IO 具有缓冲区 是对普通文件的读写 1. fopen ----------------------------->文件流指针 FILE * …

联发科又放大招,天玑9400支持10.7Gbps LPDDR5X内存性能拉满!

三星官方消息称,联发科天玑9400将支持全球最快的手机内存10.7Gbps LPDDR5X!而且数码达人科技九州君也在微博上透露,天玑9400将首发支持全球最快的移动DRAM。顶级的内存加上天玑9400采用的黑鹰架构和配置的大CPU缓存,性能直接拉满了…

基于springboot和mybatis的RealWorld后端项目实战三之添加swagger

pom.xml添加依赖 <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><arti…

基于python的层次聚类

目录 一、层次聚类概述 二、凝聚法&#xff08;Agglomerative Clustering&#xff09; 1. 初始化 2. 计算距离 3. 合并簇 4. 重复迭代 三、分裂法&#xff08;Divisive Clustering&#xff09; 1. 初始化 2. 分裂簇 3. 分配样本点 4. 重复迭代 四、其他考虑因素 五、总结 …

51、数据库的概念及sql语句

1、数据库 1.1、数据库管理&#xff1a; sql语句 数据库用来增删改查的语句。重要* 备份 数据库的数据进行备份。 主从复制&#xff0c;读写分离 高可用。重要*&#xff0c;原理–面试。 数据库的概念、语法和规范 1.2、数据库的定义 数据库&#xff1a;组织&#xff0c…

ACL实验

目录 一、实验拓扑​编辑 二. 实验要求&#xff08;在图中&#xff09; 三、实验思路 配IP 全网可达 创建模拟机pc1 创建telent r1 r2 由题目可得 截图 pcr1​编辑 pcr2​编辑 四、实验总结&#xff08;写实验完成后的总结心得&#xff09; 一、实验拓扑 二. 实验…

硅纪元AI应用推荐 | 精准识别用户意图,夸克真AI搜索引擎

“硅纪元AI应用推荐”栏目&#xff0c;为您精选最新、最实用的人工智能应用&#xff0c;无论您是AI发烧友还是新手&#xff0c;都能在这里找到提升生活和工作的利器。与我们一起探索AI的无限可能&#xff0c;开启智慧新时代&#xff01; 在数字化时代&#xff0c;搜索引擎成为我…

本地多模态看图说话-llava

其中图片为bast64转码&#xff0c;方便json序列化。 其中模型llava为本地ollama运行的模型&#xff0c;如&#xff1a;ollama run llava 还有其它的模型如&#xff1a;llava-phi3&#xff0c;通过phi3微调过的版本。 实际测试下来&#xff0c;发现本地多模型的性能不佳&…

EasyPoi一对多excel表格导出

效果如下图&#xff1a; 1、引入pom文件 <!--easypoi 一对多导入导出 --> <dependency><groupId>cn.afterturn</groupId><artifactId>easypoi-base</artifactId><version>4.2.0</version> </dependency> <dependenc…

AIGC降痕实战:论文降AI率的深度解析与应用

随着AI技术的飞速发展&#xff0c;AI论文工具正逐渐成为学术界的新宠。它们以高效、便捷的优势&#xff0c;吸引了众多学者的目光。然而&#xff0c;随之而来的学术诚信与原创性问题&#xff0c;也成为人们关注的焦点。 如何在享受AI带来的便利的同时&#xff0c;确保论文的原…

pear-admin-fast项目修改为集成PostgreSQL启动

全局搜索代码中的sysdate()&#xff0c;修改为now() 【前者是mysql特有的&#xff0c;后者是postgre特有的】修改application-dev.yml中的数据库url使用DBeaver把mysql中的数据库表导出csv&#xff0c;再从postgre中导入csv脚本转换后出现了bpchar(xx)类型&#xff0c;那么一定…

《数字通信世界》是什么级别的期刊?是正规期刊吗?能评职称吗?

​问题解答 问&#xff1a;《数字通信世界》是不是核心期刊&#xff1f; 答&#xff1a;不是&#xff0c;是知网收录的第二批认定学术期刊。 问&#xff1a;《数字通信世界》级别&#xff1f; 答&#xff1a;国家级。主管单位&#xff1a;工业和信息化部 主办单位&#x…

ubuntu22.04安装SecureCRT8.7.3,完成顺利使用

材料准备 scrt-sfx安装包 &#xff0c; securecrt_linux_crack.pl 补丁脚本&#xff0c;和两个依赖库 其中securecrt_linux_crack.pl是找的专门适合 8.7.3版本的&#xff0c;网上很多版本的crack.pl只能打补丁以前的老版本。 而更老版本的SecureCRT对ubuntu22支持更不好&#…

DBeaver导入脚本和导出数据

DBeaver导入脚本和导出数据 前言&#xff1a; 通常产品会要求&#xff0c;把xx表导出Excel&#xff0c;navicat一般公司不让用。讲解使用DBeaver 导入脚本 我们将sql脚本导入DBeaver 1&#xff0c;选择数据库&#xff0c;找到执行脚本 2&#xff0c;选用sql脚本&#xff0…

mavsdk客户端(java)通过mavsdk_server与PX4进行通信

1.启动PX4容器: 2.启动14550与14540监听 3.启动QGC 4.启动mavsdk_server通信中间件 5.启动mavsdk客户端(java) 注:官方示例默认无法连接,修改如下: import io.mavsdk.telemetry.Telemetry;增加设备健康检查才能执行解锁指令Telemetry telemetry = new Telemetry();teleme…

【读书笔】口才三绝

【读书笔记】口才三绝 一、引言二、主要内容概述赞美之绝幽默之绝拒绝之绝 三、个人感悟四、结语我是杰叔叔&#xff0c;一名沪漂的码农&#xff0c;下期再会&#xff01; 一、引言 《口才三绝》是一本专注于提升个人口才与沟通技巧的书籍&#xff0c;它通过丰富的案例和实用的…