底软驱动 | C++内存相关

文章目录

  • 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.mzph.cn/web/45547.shtml

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

相关文章

Ctrl+C、Ctrl+V、Ctrl+X 和 Ctrl+Z 的起源

注:机翻,未校对。 The Origins of CtrlC, CtrlV, CtrlX, and CtrlZ Explained We use them dozens of times a day: The CtrlZ, CtrlX, CtrlC, and CtrlV shortcuts that trigger Undo, Cut, Copy, and Paste. But where did they come from, and why do…

文件上传接口

文章目录 开发前端接口 开发前端接口 首先这个前端的文件上传组件使用了,前端组件 首先这个接口不是一般的接口,这个接口可以提取出来,之后那里使用了,就直接放到哪里 所以这是一个万能文件上传接口 写完之后选择 头像组件 在图库中添加组件 写前端组件之后,写了前端的组件…

[深度学习]基于yolov10+streamlit目标检测演示系统设计

YOLOv10结合Streamlit构建的目标检测系统,不仅极大地增强了实时目标识别的能力,还通过其直观的用户界面实现了对图片、视频乃至摄像头输入的无缝支持。该系统利用YOLOv10的高效检测算法,能够快速准确地识别图像中的多个对象,并标注…

Billu_b0x靶机

信息收集 使用arp-scan 生成网络接口地址来查看ip 输入命令: arp-scan -l 可以查看到我们的目标ip为192.168.187.153 nmap扫描端口开放 输入命令: nmap -min-rate 10000 -p- 192.168.187.153 可以看到开放2个端口 nmap扫描端口信息 输入命令&…

配置PYTHONPATH环境变量

配置PYTHONPATH环境变量 前言Win系统临时配置永久配置 Linux系统临时配置永久配置 前言 在运行py脚本时不仅需要import官方库,经常会import自己编写的脚本,但此时会出现模块找不到的如下报错。解决方法是配置PYTHONPATH,下文介绍Win系统和Li…

禹神:一小时快速上手Electron,前端Electron开发教程,笔记。一篇文章入门Electron

一、Electron是什么 简单的一句话,就是用htmlcssjsnodejs(Native Api)做兼容多个系统(Windows、Linux、Mac)的软件。 官网解释如下(有点像绕口令): Electron是一个使用 JavaScript、HTML 和 CSS 构建桌面…

Resources.Load返回null

Resources.Load返回null 在unity中Resources.Load从Assets下的任意Resources目录下读取资源&#xff0c;比如从Assets\Resources下读取Cube&#xff08;预制体&#xff09;&#xff0c;当然也可以读取其他资源 代码为 GameObject prefab Resources.Load<GameObject>(…

微软Edge浏览器深度解析:性能、安全性与特色功能全面评测

一、引言 自Windows 10操作系统推出以来&#xff0c;微软Edge浏览器作为默认的网页浏览器&#xff0c;凭借其现代化的设计和出色的性能表现&#xff0c;逐渐获得了用户的认可。本文旨在对Edge浏览器进行深入分析&#xff0c;探讨其在多个方面的表现。 二、界面与操作体验 界面…

在 PostgreSQL 里如何处理数据的存储优化和数据库备份的效率平衡?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 在 PostgreSQL 里如何处理数据的存储优化和数据库备份的效率平衡&#xff1f;一、数据存储优化&#x…

HTML表格表单及框架标签

一.表格标签 1.<table></table> 创建表格 2.<caption></caption> 表格的标题 3.<tr></tr>Table Row&#xff08;表格行&#xff09; 4.<td></td>Table Data&#xff08;表格数据&#xff09;其中有属性rowspan"2&quo…

Linux操作系统——数据库

数据库 sun solaris gnu 1、分类&#xff1a; 大型 中型 小型 ORACLE MYSQL/MSSQL SQLITE DBII powdb 关系型数据库 2、名词&#xff1a; DB 数据库 select update database DBMS 数据…

距离变换 Distance Transformation

以下为该学习地址的学习笔记&#xff1a;Distance transformation in image - Python OpenCV - GeeksforGeeks 其他学习资料&#xff1a;Morphology - Distance Transform 简介 距离变换是一种用于计算图像中每个像素与最近的非零像素之间距离的技术。它通常用于图像分割和物体…

51单片机5(GPIO简介)

一、序言&#xff1a;不论学习什么单片机&#xff0c;最简单的外设莫过于I口的高低电平的操作&#xff0c;接下来&#xff0c;我们将给大家介绍一下如何在创建好的工程模板上面&#xff0c;通过控制51单片机的GPIO来使我们的开发板上的LED来点亮。 二、51单片机GPIO介绍&#…

PySide在Qt Designer中使用QTableView 显示表格数据

在 PySide6 中&#xff0c;可以使用 Qt Model View 架构中的 QTableView 部件来显示和编辑表格数据。 1、创建ui文件 在Qt Designer中新建QMainWindow&#xff0c;命名为csvShow.ui。QMainWindow上有两个部件&#xff1a;tableview和btn_exit。 2、使用pyuic工具将ui文件转换为…

Kafka(四) Consumer消费者

一&#xff0c;基础知识 1&#xff0c;消费者与消费组 每个消费者都有对应的消费组&#xff0c;不同消费组之间互不影响。 Partition的消息只能被一个消费组中的一个消费者所消费&#xff0c; 但Partition也可能被再平衡分配给新的消费者。 一个Topic的不同Partition会根据分配…

MySQL集群、Redis集群、RabbitMQ集群

一、MySQL集群 1、集群原理 MySQL-MMM 是 Master-Master Replication Manager for MySQL&#xff08;mysql 主主复制管理器&#xff09;的简称。脚本&#xff09;。MMM 基于 MySQL Replication 做的扩展架构&#xff0c;主要用来监控 mysql 主主复制并做失败转移。其原理是将真…

基于Faster R-CNN的安全帽目标检测

基于Faster R-CNN的安全帽目标检测项目通常旨在解决工作场所&#xff0c;特别是建筑工地的安全监管问题。这类项目使用计算机视觉技术&#xff0c;特别是深度学习中的Faster R-CNN算法&#xff0c;来自动检测工人是否正确佩戴了安全帽&#xff0c;从而确保遵守安全规定并减少事…

实验一:图像信号的数字化

目录 一、实验目的 二、实验原理 三、实验内容 四、源程序及结果 源程序&#xff08;python&#xff09;&#xff1a; 结果&#xff1a; 五、结果分析 一、实验目的 通过本实验了解图像的数字化过程&#xff0c;了解数字图像的数据矩阵表示法。掌握取样&#xff08;象素个…

Linux 网络配置与连接

一、网络配置 1.1 ifconfig 网卡配置查询 ifconfig #查看所有启动的网络接口信息 ifconfig 指定的网卡 #查看指定网络接口信息 1.2 修改网络配置文件 vim /etc/sysconfig/network-scripts/ifcfg-ens33 #ens33网络配置文…

【电源拓扑】反激拓扑

目录 工作模式 固定频率 CCM连续电流模式 DCM不连续电流模式 可变频率 CRM电流临界模式 反激电源CRM工作模式为什么要跳频 反激电源应用场景 为什么反激电源功率做不大 电感电流爬升 反激变压器的限制条件 精通反激电源设计的关键-反激电源变压器设计 反激电源变压…