• C++内存管理


    目录

    一、C/C++内存分布

    二、C++内存管理方式

    Ⅰ new和delete操作内置类型

    ①new和delete的基本使用

    Ⅱ new和delete操作自定义类型

    Ⅲ new和delete要匹配使用

    Ⅳ new 返回异常

    三、operator new和operator delete函数

    Ⅰ operator new和 malloc

    三、定位new表达式(placement-new)

    Ⅰ使用格式

    Ⅱ使用场景

    Ⅲ 定向new的意义

    四、内存泄漏

     Ⅰ什么是内存泄漏

    Ⅱ内存泄漏的情景

    Ⅲ如何避免内存泄漏

    一、C/C++内存分布

     C语言阶段我们学习过C程序内存区域是如何划分的,C++也是这样划分程序的,只不过在使用方式上和C有所不同,我们先复习C,先看几道题目:

     解析图:

     上面的很多相信大家都可以轻松解决,这里只说几道容易出错的题目。

    1、*char2存储在什么地方?

    很多老铁可能会这样觉得,char2是常量字符串"abcd"的地址,存储在栈上,然后*char2拿到字符串的首字符'a',所以应该存储在代码段(常量区),其实不然。这里我想问老铁一个问题,如果*char2拿到存储在常量区的'a',那么我如果对char2的内容做修改,是否也要修改常量区的内容呢?显然是不可以的,常量区内容不能被修改。所以*char2应该在栈上!!我们只是把常量字符串"abcd"拷贝到了char2上,*char2还在栈上可以被修改。

    2、与之相对应的就是*pchar3pchar3是一个地址,虽然用const修饰,但他就是一个地址,没有实际内容,指向存储在代码段的常量字符串。因为他用const修饰,限制修改,没必要再拷贝。

    【说明】:

    1、栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。

    2、内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。

    3、堆用于程序运行时动态内存分配,堆是向上增长的。

    4、数据段--存储全局数据和静态数据。

    5、代码段--可执行的代码/只读常量。

    二、C++内存管理方式

    C++兼容C语言,C语言使用malloc、calloc、realloc管理内存,C++中可以继续使用这些内存管理方式,但是有些地方用它就不太方便,于是C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理。

    Ⅰ new和delete操作内置类型

    C++提出用new来开辟空间,用delete来释放空间。那么我们怎么使用newdelete呢?

    ①new和delete的基本使用

    new默认不会初始化。

     new给初始化的方式是在内置类型后面加()()里面是要初始化的值

     new想要开辟多个空间,需要加[ ],同样在delete时也需要加[ ]

     new在初始化一个空间时用(),初始化多个空间时,类似于数组,用{ },里面放初始化值,没有给初始化的后续默认是给0如果压根没有给初始化值,那么开辟的多个空间里面默认随机值

    Ⅱ new和delete操作自定义类型

    我们光看newdelete对内置类型,与C语言那一套难分高下,可能有优势的地方在于可以自行给初始值,而calloc最多都设为0newdelete主要是针对自定义类型的。

    我们看到new/delete malloc/free最大区别是 new/delete对于自定义类型除了开空间,还会调用构造析构函数,这一点会给我们带来很大方便。

    举个例子:之前在C语言数据结构阶段构造链表时,我们每开辟一个新节点,就要调用函数对它初始化,这里在C++我们开辟节点,就可以自动调用构造初始化,很方便。

    1. struct ListNode
    2. {
    3. ListNode(int val)
    4. :_next(nullptr)
    5. , _val(val)
    6. {
    7. }
    8. ListNode* _next;
    9. int _val;
    10. };
    11. int main()
    12. {
    13. ListNode* n1 = new ListNode(1);
    14. ListNode* n2 = new ListNode(2);
    15. ListNode* n3 = new ListNode(3);
    16. ListNode* n4 = new ListNode(4);
    17. n1->_next = n2;
    18. //...
    19. //不用写buynode
    20. return 0;
    21. }

    Ⅲ new和delete要匹配使用

    newdelete配套使用,用于开辟一块和销毁一块空间。new[ ]delete[ ]配套使用,用于开辟多块和销毁多块空间。

    new[ ]和delete

     这里就会报错,析构失败。

    但是,这里有个奇怪的现象,我把析构函数屏蔽掉就不会报错!

     这里是vs编译器默认的析构函数对此处有优化,它和编译器的实现有关,不是很重要,重要的是这里一定要匹配使用,不要随意匹配,否则任何错误都有可能发生。

    Ⅳ new 返回异常

    如果开辟失败,在C语言阶段会返回一个nullptr(空指针),而C++则会抛出异常。

     malloc开辟失败会返回nullptr

     new开辟失败抛出异常。

    三、operator new和operator delete函数

    newdelete真的那么神奇吗?其实它们是通过调用两个全局函数来实现开辟空间和释放空间的。

    new和delete是用户进行动态内存申请和释放的操作符,operator newoperator delete是系统提供的全局函数,new在底层调用operator new 全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。(这里要注意operator new不是new的重载!!)

    所以,用C封装的newdelete实际图解就包括这两部分:

    我们通过编译器也可以看出来,new的底层确实是这样实现的:

     

    那么,operator newoperator delete又有什么猫腻呢?

    Ⅰ operator new和 malloc

    operator new:该函数实际通过malloc来申请空间,它的用法和malloc很相似,不一样的地方,在于开辟失败,malloc返回nullptroperator new函数则抛出异常。

    同理,operator delete函数则是通过调用free来实现释放空间的。

    总结一下malloc/freenew/delete的区别

    malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

    1、malloc和free是函数,new/delete是操作符。

    2、malloc申请的空间不会初始化,new可以初始化。

    3、malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[ ]中指定对象个数即可。

    4、malloc的返回值是void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型。

    5、malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但new需要捕获异常。

    6、申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。

    三、定位new表达式(placement-new)

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

    Ⅰ使用格式

    new(place_address) type 或者new(place_address) type(initializer-list)

    place_address必须是一个指针,initializer-list 是类型的初始化列表。

    Ⅱ使用场景

    定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显式构造函数。

     malloc开辟的一块空间,进行显式调用构造函数初始化,这时候p3所指向的空间就可以认为是new开辟的空间,可以delete。如果对malloc开辟的空间没有定向new,也可以delete,但是会有警告。

    Ⅲ 定向new的意义

    有的老铁可能有困惑,为啥要有定向new,直接new初始化不就可以了?这不是脱裤子放屁,多此一举吗?

    既然设计了,那么肯定有它的使用场景的。定向new主要用于池化技术,可以很好的提高效率。什么是池化技术呢?

    我们一般无论是malloc还是new都是去堆上申请空间,类似于以前大家要用水都要去河边打水。而内存池就相当于我们自己在家里做一个蓄水池(malloc 一块空间),用水就可以直接使用(定向new)。这样会提高效率,我们不需要去挤着都去河边打水。类似于各项需要向堆申请空间的工作都被分配给一定空间,使用时各自去使用这么一块空间,每一块空间就是一个内存池供不同的工作使用,提高了工作效率。

    四、内存泄漏

     Ⅰ什么是内存泄漏

    内存泄漏指因为疏忽或错误造成程序未被释放已经不再使用的内存的情况内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,不再使用内存时由于没有返还给操作系统并且失去对该段内存的控制,导致内存消耗越来越大,直到系统崩溃

    C/C++程序中一般我们关心两种方面的内存泄漏:

    🖊堆内存泄漏(Heap leak)

    堆内存指的是程序执行中依据需要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak

    🖊系统资源泄漏

    指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

    Ⅱ内存泄漏的情景

    我们可以看一下程序执行过程中,如果没有写释放空间会发生什么:

     看到这里,我们发现,咦,我没有释放空间,但是程序结束它把申请的空间返还给操作系统了啊,内存泄漏好像没有什么危害吖。

    但是联系现实,程序结束返还操作系统,如果程序不结束呢?比如长期运行的系统,服务器,比如我们玩的游戏服务器就是长期运行不关机的。这时如果有内存泄漏就会导致可用内存越来越少,它的表现就是cpu发热,电脑很卡,直到系统崩溃。

    有时候有这样的场景,我们写了释放空间,但是可能执行不到这一步:

    我们知道C++会抛出异常,如果在释放空间之前抛出异常,导致内存没有泄漏,这是不易发现且易于出错的地方。

    最恐怖的不是一下子服务器挂掉,而是内存一点一点泄漏,每次泄漏一点内存,不易发现,可能过了十几天才发现服务器越来越卡,比较明显的例子就是早期的Android就是长期运行会变得越来越卡,可能就存在内存泄漏。

    C++针对这些情况,官方设计了智能指针事前预防,也有一些第三方推出的内存泄露检测工具,用于事后差错

    第三方内存泄漏检测工具:

            在Linux下内存泄漏检测:Linux下几款内存泄漏检测工具

            在Windows下使用第三方工具:VLD工具说明

            其他工具:内存泄漏工具比较

    Ⅲ如何避免内存泄漏

    1. 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放,不过可能碰到抛出异常导致走不到释放。
    2. 采用RAII思想或者智能指针来管理资源。
    3. 有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    4. 出问题了使用内存泄漏工具检测。ps:不过很多工具都不够靠谱,或者收费昂贵。
    总的来说就两种解决方案:1、事前预防:智能指针
                                               2、事后差错:内存泄漏检测工具

  • 相关阅读:
    AI教程视频 - 零基础玩转illustrator科研绘图-内容介绍-目录
    C++必学!——类与对象 万字总结!建议收藏!
    通过pytorch转换得到ms模型,训练模式下输出和pytorch模型一样,验证模式下通过batchnorm2d算子的输出不同
    找出缺失和重复的数字 - (LeetCode)
    基准测试工具 --- BenchmarkDotNet
    Linux命令从入门到实战----文件目录类
    Redis事务相关源码探究
    请求转发和动态包含/生成响应信息/响应头/重定向/输出流
    OnlyOffice集成Springboot以及web端
    【ASP.NET Core】设置 Web API 响应数据的格式——FormatFilter特性篇
  • 原文地址:https://blog.csdn.net/JJR_YZ/article/details/127322217