• [C++] 超详细分析 C++内存分布、管理(new - delete) ~ C 和 C++ 内存管理关系 ~ 内存泄漏 ~


    image-20221025222143040


    C/C++ 内存分布

    在介绍详细 C++ 内存管理的方法之前,先简单做个铺垫,先介绍一下:C/C++程序 内存区域的划分

    首先先分析以下这段代码,并且思考问题:
    image-20220629144649718

    程序中所有的变量应该存储在什么区域?

    不妨先分析一下,再继续阅读下面的文章


    C/C++ 程序运行之后,程序中的数据的存储区域大致可以划分这样:

    C++程序内存分布

    这张图可以展示出 C/C++ 程序数据在内存中的大致区域

    1. 又叫 堆栈,一般存储 非静态局部变量函数参数返回值等等,向下增长(先使用高地址空间)
    2. 内存映射段高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信
    3. 用于程序运行时 动态内存分配mallocrealloccalloc开辟出的空间即存储在此,向上增长(先使用低地址空间)
    4. 数据段,语言中常称作 静态区 ,存储全局数据和静态数据
    5. 代码段,语言中常称作 常量区 ,存储可执行的代码(二进制代码)、只读常量

    看完数据的存储区域,上面的问题就可以完美的解决了!:
    image-20220629141223975

    globalVarstaticGlobalVarstaticVar 很明显属于 全局或静态变量,所以应该存储在静态区

    localVarnum1char2pChar3ptr1ptr2ptr3 都是在函数定义的局部变量,所以存储在 栈区
    *char2 是 对数组名解引用,即为数组中第一个元素,用常字符串赋值数组,是将字符串数据拷贝至数组中,不是直接将常量放至数组中,所以*char2 也存储在 栈区

    *pChar3,是对 指针pChar3 的解引用,而pChar3指向了 常字符串,所以*pChar3存储在 常量区

    *ptr1*ptr2*ptr3 都是动态开辟处的空间,所以存储在 堆区

    分析完上面的问题 以及 程序数据在内存中的存储区域,应该已经对 C/C++的内存分布有了一定的了解

    下面就正式进入整体 C/C++ 内存管理

    C/C++ 动态内存管理

    C语言为 动态内存管理提供了 四个函数malloccallocreallocfree

    而C++,由于增添了许多特性,即使依旧可以延用C语言的动态管理,但是总有无法处理的地方
    所以 C++ 又提供了两个新的动态管理的关键词 newdelete

    C语言动态内存管理

    malloc

    void* malloc (size_t size);
    
    • 1

    作用:向内存申请一块连续可用的空间,并返回指向这块空间的指针,开辟失败则返回空指针

    使用注意:返回值类型为 空类型指针,所以使用时需要指定 指针类型
    image-20220629153315360

    malloc 开辟出的空间不初始化
    image-20220629153504293

    calloc

    void* calloc (size_t num, size_t size);
    
    • 1

    作用:向内存申请 num 个大小为 size 的连续可用的空间,并将每一字节初始化为0,返回指向这块空间的指针,开辟失败则返回空指针

    使用方式与 malloc 类似
    image-20220629153959427

    realloc

    void* realloc (void* ptr, size_t size);
    
    • 1

    作用:对已经动态开辟出的内存空间进行大小上的调整,返回调整后空间的地址

    使用:ptr 传入已经动态开辟的内存空间的首地址,size 需要调整为的大小

    注意:size 需要传入需要调整到的大小
    比如,原本开辟了 5个int 大小的空间,想要扩充到 10个int 大小,size就传入10
    image-20220629155056038

    realloc 扩充空间,默认从 旧空间向后扩充

    除非旧空间后的空间不够扩充了,则会完全开辟一块新的指定大小的空间,并将旧空间数据拷贝至新空间

    返回新空间的地址

    free

    void free (void* ptr);
    
    • 1

    作用:释放动态开辟的内存空间,防止内存泄露

    使用:free(内存空间地址)

    注意:free只能释放 动态开辟的内存空间,且只能从头释放

    C++ 动态内存管理

    C++ 补充了 newdelete 作为自己的动态内存管理工具

    不过 newdelete 的实现也是嵌套了 mallocfree 的,更再此基础上做了补充 以完善C++的内存管理

    new 和 delete 的用法

    newdelete 的用法 比 C语言动态内存管理的方法 简单许多

    举个栗子 image-20220629161545639

    newdetele 都属于 C++ 中的关键词,而不是函数,所以与C语言中 malloc 等用法不同

    虽然用法不同,但其实对内置类型来说,不论使用 malloc 还是 new ,结果都是一样的,没有一点差别

    new detelemalloc free的差别,在对自定义类型的操作上

    new deletenew [] delete[] 是对应的

    new delete 用于申请和释放单个空间,new [] delete[] 用于申请和释放连续空间,使用时也必须对应


    newdetele 操作自定义类型空间的使用,与操作内置类型的使用相同

    image-20220629163737630

    在对自定义类型空间的操作上,new deletemalloc free 相比,有一个非常适合 C++语法的作用 就是
    newdetele 在操作自定义类型的空间时,会调用 构造函数和析构函数;而 mallocfree 不会

    newdelete 增添了对 类的适配,这是 mallocfree 没有的

    调试
    image-20220629164925433

    所以,new 开辟自定义类型的空间,实际对象的实例化,也是调用 其构造函数实现的

    new 开辟了一块空间,并调用构造函数在这块空间中实例化了对象

    举个栗子

    如果是对下面这个类动态开辟空间:

    image-20220629171128548

    使用 new 动态开辟:image-20220629173132969
    除了开辟空间之外,还会调用构造函数对对象初始化

    使用 malloc 动态开辟:image-20220629173737579
    只负责开辟空间,不调用构造函数,对象不初始化

    对象实例化之后,在对齐进行初始化就不容易了

    因为 对象的成员一般会设为私有的,所以在对象之外无法操作,除非再实现一个初始化函数,得不偿失

    并且,构造函数也不能手动调用,所以 malloc 自定义类型 一般不用

    operator new 和 operator delete

    operator newoperator delete是系统提供的全局函数

    在实际使用newdelete
    new其实是 在底层调用operator new来申请空间,delete则是 在底层通过调用operator delete来释放空间的

    而 这两个全局函数的内容其实也是调用了 mallocfree,并在此基础上进行了改造:

    operator new:

    operator new

    operator delete:

    operator delete

    operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回
    否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常

    operator delete 最终是通过free来释放空间的

    这两个全局函数可以直接调用,也可以通过newdelete 调用
    直接调用的方法与malloc相同

    int* pa = (int*)operator new(sizeof(int));
    
    • 1

    像这样使用 operator new 就可以申请一块空间

    但是对于自定义类型来说,operator new 就只能做到申请空间而已,作用与malloc一样,无法调用对象的构造函数初始化

    new 可以,所以 new 可以被看作 operator new + 构造函数

    同理,delete 也可以被看作 operator delete + 析构函数

    抛异常

    什么是抛异常?

    malloc 申请空间失败返回空指针 不同,new 申请空间失败的表现是 抛异常

    有抛异常,就需要接收异常,否则程序会直接崩溃

    这也是为什么 new 不需要像 malloc 一样,在申请空间结束之后检查是否申请成功

    抛异常演示:
    抛异常未接收

    如果接收了抛出的异常:
    image-20220629181443062

    operator new 和 operator delete 的类专属重载

    介绍过 newdelete 后,其实 使用 newdelete 会调用 两个全局函数

    不过这两个全局函数是 可以在类中重载 的,这就意味着
    使用new申请自定义类型的空间时,其实是可以调用重载的operator new进行申请
    意思就是,可以手动设置 new 申请自定义空间的方式

    这是一个非常重要的特性

    可根据这个特性实现:申请自定义类型空间时,不通过 堆区 而是 从内存池中申请空间

    进而大幅度提升new申请自定义类型空间的效率

    定位new表达式 placement-new

    定位new表达式,是在已经开辟的原始内存空间中调用构造函数初始化一个对象

    即,对使用mallocoperator new 开辟的自定义类型的空间,调用构造函数初始化一个对象

    使用方法是:new(开辟空间的地址)开辟空间的类型(初始化参数)

    初始化参数,即构造函数所需传参

    此操作可以解决 已经开辟的自定义类型空间无法初始化的问题

    举个栗子

    image-20220629213043600
    image-20220629213200638

    C/C++ 关于内存管理的一些问题

    1. malloc、calloc、realloc有什么区别

    void* malloc (size_t size);
    
    • 1
    void* calloc (size_t num, size_t size);
    
    • 1
    void* realloc (void* ptr, size_t size);
    
    • 1
    1. 首先,除了最基础的使用传参不同之外,他们的作用也是不同的
    2. malloc 的作用,是向内存申请一段连续的空间,申请成功返回这块空间的地址,失败则返回空指针,且不对空间数据初始化
    3. calloc 的作用同样是向内存申请一段连续的空间,申请成功返回空间的地址,失败则返回空指针,不过 会将空间数据按每字节初始化为0
    4. realloc 的作用是,调整一段动态开辟的空间的大小,传参除了原空间地址之外,还需要传入的是最终目标大小而不是调整的大小。调整之后,返回结果空间的地址
    5. realloc 扩充空间,原则上在原空间基础上向后扩充,若原空间后的空间充足,则扩充完毕后返回原空间地址;若原空间后的空间不足,则重新申请一段连续的空间,并将原空间数据拷贝至新空间对应位置,然后返回新空间地址

    2. new 与 malloc 有什么区别

    1. 除使用方法不同, new是关键字,而malloc是函数之外,还有具体功能的区别
    2. 对于内置类型,newmalloc 除使用方法不同之外,基本上没有区别,结果也是一模一样的,唯一不同的是就是
      new 申请空间失败抛异常,而 malloc 申请空间失败返回空指针
    3. 对于自定义类型,new 除了申请空间之外,还会调用内置类型的构造函数对对象初始化;而 malloc 不能调用构造函数,毕竟C语言中的函数
    4. 再有就是,自定义类型中,可以对 operator new 进行重载,进而可以将 new 改变为在一定程度上按照指定的思路开辟空间

    3. free 与 delete 有什么区别

    1. freedelete 的作用都是对 动态开辟的内存空间进行释放,在处理内置类型上,没有什么不同
    2. 而 在处理 自定义类型的空间上,delete 还会调用 自定义类型的析构函数进行资源的清理
    3. 另外就是,在自定义类型中,可以对operator delete进行重载,在一定程度上修改delete的某些思路

    4. 什么是内存泄漏?及如避免内存泄漏

    内存泄漏,指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。并且,内存泄露并不是只物理上内存泄露、消失了什么的,而是指 失去了对已经开辟出的 且没有释放的内存空间的控制

    举示例讲,就是
    存在一块被一个指针指向的动态开辟的空间,不过不经意间发生了事故,导致这个指针指向了别的空间,而原空间无法再被找到并释放,就造成了内存泄漏

    内存泄漏对于经常被打开关闭的程序没有什么大的危害,因为程序的关闭会清理资源将使用过的内存还给操作系统,但是长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死

    如何避免内存泄露的出现呢?

    1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状
      态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保
      证。
    2. 采用RAII思想或者智能指针来管理资源。
    3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    4. 出问题了使用内存泄漏工具检测。

    内存泄漏非常常见,解决方案分为两种:1、事前预防型。如智能指针等。2、事后查错型。如泄漏检测工具。

  • 相关阅读:
    高速电路设计笔记----第二章
    项目的打包发布
    《一个程序猿的生命周期》-《发展篇》- 46.2000万预算的项目,为什么跟踪15个月失败了
    小程序发成绩
    JVM系列之java虚拟机与线程
    使用Vite构建Vue3组件库
    Mathorcup数学建模竞赛第四届-【妈妈杯】C题:家庭暑假旅游套餐的设计(附MATLAB代码)
    算法学习(七)判断一个二叉树是否为完全二叉树
    【问题解决】Linux-conda环境下安装PyKDL无法链接.so动态库
    基于映射序列码的自适应查询树防碰撞算法
  • 原文地址:https://blog.csdn.net/dxyt2002/article/details/127523061