• 【C++】内存管理


    目录

    一、C/C++内存分布

    二、C语言中动态内存管理方式

    三、C++中动态内存管理

    1、开辟空间

    2、释放空间

    四、operator new与operator delete函数

    五、内存泄漏

    1、什么是内存泄漏

    2、如何避免内存泄漏

    总结


    一、C/C++内存分布

    对于我们C/C++程序员来说,我们认为内存区域划分是这样的

     其中栈和堆是在运行时开辟空间,堆栈相生,栈向下生长,堆向上生长。

    代码段,为程序代码在内存中的映射,一个程序可以在内存中有多个副本,这个代码是二进制指令,并不是我们已经写好的代码,我们写好的代码储存在磁盘中。

    堆存储局部临时变量,在程序块开始时自动分配内存,结束时自动释放内存,存储函数的返回指针。


    我们看以下问题:

    1. int globalVar = 1;
    2. static int staticGlobalVar = 1;
    3. void Test()
    4. {
    5. static int staticVar = 1;
    6. int localVar = 1;
    7. int num1[10] = { 1, 2, 3, 4 };
    8. char char2[] = "abcd";
    9. const char* pChar3 = "abcd";
    10. int* ptr1 = (int*)malloc(sizeof(int) * 4);
    11. int* ptr2 = (int*)calloc(4, sizeof(int));
    12. int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
    13. free(ptr1);
    14. free(ptr3);
    15. }
    16.   选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)
    17.   globalVar在哪里?____   staticGlobalVar在哪里?____
    18.   staticVar在哪里?____   localVar在哪里?____
    19.   num1 在哪里?____
    20.  
    21.   char2在哪里?____   *char2在哪里?___
    22.   pChar3在哪里?____      *pChar3在哪里?____
    23.   ptr1在哪里?____        *ptr1在哪里?____

    答案:C    C    C    A    A    

               A    A    A    D    A    B

    char2是一个数组,将代码段的abcd拷贝到栈

    pchar3是一个指针,它是指向代码段的abcd


    以及,我们经常说的内存泄漏,通常指的是堆区的空间,因为只有堆区我们才能操控,其它区域的空间都是我们操纵不了的。

    二、C语言中动态内存管理方式

    C语言动态内存管理通常使用malloc/calloc/realloc/free 等函数,操作比较麻烦,需要我们手动1计算开辟空间大小,以及强制类型转换

    例:

    1. void Test ()
    2. {
    3. int* p1 = (int*) malloc(sizeof(int));
    4. free(p1);
    5. // 1.malloc/calloc/realloc的区别是什么?
    6. int* p2 = (int*)calloc(4, sizeof (int));
    7. int* p3 = (int*)realloc(p2, sizeof(int)*10);
    8. // 这里需要free(p2)吗?
    9. free(p3 );
    10. }

    我们1并不需要free(p2),因为我们的p3创建是通过realloc,在p2指向的空间的基础上再次扩容。

    如果空间不够了,会自动free掉p2指向的空间,在另一个地方重新开辟新的空间。

    三、C++中动态内存管理

    C++中使用的是new和delete。

    它们两个是操作符,并不是函数

    1、开辟空间

    例:

    1. int* p0 = new int;//申请一个int类型的对象
    2. int* p1 = new int(0);//申请一个int类型的对象并初始化为0
    3. int* p2 = new int[5]{ 0 };//申请一个int类型的数组,数组中有5个元素,并且将数组初始化为0

    C++98是不支持对new出来的数组进行初始化的

    C++11支持,数组初始化有以下几种

    1. int* p2 = new int[5]{ 0 };
    2. int* p3 = new int[5]{ 0,1,2,3,4 };
    3. int* p4 = new int[5]{ 0,1,2 };

    与普通的数组初始化方式是类似的

    2、释放空间

    释放空间是利用delete操作符来进行的

    释放普通的变量是直接delete + 变量名

    释放聚合类型,例如数组要加上[]  delete[ ] + 变量名

    例:

    1. void test1()
    2. {
    3. int* p0 = new int;
    4. int* p1 = new int(0);
    5. int* p2 = new int[5]{ 0 };
    6. int* p3 = new int[5]{ 0,1,2,3,4 };
    7. int* p4 = new int[5]{ 0,1,2 };
    8. delete p0;
    9. delete p1;
    10. delete[] p2;
    11. delete[] p3;
    12. delete[] p4;
    13. }

    new和delete花括号要对应,如果不对应,可能会出现一定的问题。

    四、operator newoperator delete函数

    对于内置类型来说,new/delete 和 malloc/free 没有本质区别,只有用法区别。

    对于自定义类型:new会调用构造函数,如果不传参数会调用默认构造函数

    new可以传参,调用全缺省构造函数或者是非默认构造函数

    1. class A
    2. {
    3. public:
    4. A(int a,int b, int c)
    5. :_a(a)
    6. ,_b(b)
    7. ,_c(c)
    8. {
    9. cout << "A()" << this << endl;
    10. }
    11. ~A()
    12. {
    13. cout << "~A" << this << endl;
    14. }
    15. void show()
    16. {
    17. cout << _a << " " << _b << " " << _c << endl;
    18. }
    19. private:
    20. int _a;
    21. int _b;
    22. int _c;
    23. };
    24. void test2()
    25. {
    26. A* pa = new A(1, 2, 3);
    27. pa->show();
    28. delete pa;
    29. }

    delete调用析构函数,清理对象中的资源,释放空间,如果不delete对象,该对象就不能调用析构函数

    这是delete之后的情况,我们在构造函数和析构函数中打印了一句话,证明调用了它们两个

    然后我们去掉delete

     

    发现并没有调用析构函数。


    我们一定想要知道如何对new出来的多个对象初始化。

    1. void test3()
    2. {
    3. A* pa1 = new A[2]{ A(2,3,4) ,A(4,3,5) };
    4. A* pa2 = new A[2]{ {1,2,3},{2,3,4} };
    5. delete[] pa1;
    6. delete[] pa2;
    7. }

     以上就是两种,初始化的方式。


    接下来回归正题,operator new 和operator delete是new和delete的底层。

    我们知道malloc失败会返回NULL,new失败会出现什么情况呢?

    new失败会抛异常,并不需要检查返回值

    1. void test4()
    2. {
    3. try
    4. {
    5. char* p2 = new char[1024u * 1024u * 1024u * 2 - 1];
    6. printf("%p\n", p2);
    7. }
    8. catch (const exception& e)
    9. {
    10. cout << e.what() << endl;
    11. }
    12. }


    我们还以上面的代码为例,调出反汇编,我们重点观察标红的位置,我们发现new一个对象时,会调用operator new和A的构造函数。

    下面是operator new和operator delete的底层实现

    1. /*
    2. operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试
    3. 执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常。
    4. */
    5. void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
    6. {
    7. // try to allocate size bytes
    8. void *p;
    9. while ((p = malloc(size)) == 0)
    10. if (_callnewh(size) == 0)
    11. {
    12. // report no memory
    13. // 如果申请内存失败了,这里会抛出bad_alloc 类型异常
    14. static const std::bad_alloc nomem;
    15. _RAISE(nomem);
    16. }
    17. return (p);
    18. }
    19. /*
    20. operator delete: 该函数最终是通过free来释放空间的
    21. */
    22. void operator delete(void *pUserData)
    23. {
    24. _CrtMemBlockHeader * pHead;
    25. RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
    26. if (pUserData == NULL)
    27. return;
    28. _mlock(_HEAP_LOCK); /* block other threads */
    29. __TRY
    30. /* get a pointer to memory block header */
    31. pHead = pHdr(pUserData);
    32. /* verify block type */
    33. _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
    34. _free_dbg( pUserData, pHead->nBlockUse );
    35. __FINALLY
    36. _munlock(_HEAP_LOCK); /* release other threads */
    37. __END_TRY_FINALLY
    38. return;
    39. }
    40. /*
    41. free的实现
    42. */
    43. #define free(p) _free_dbg(p, _NORMAL_BLOCK)

     我们观察operator new和operator delete的底层是调用了malloc和free,并且加入了一些对于异常的判断。将malloc和free进行了封装。

    operator new和operator delete 对于我们与malloc和free没有什么区别,我们也没有什么必须使用它的理由。

    operator new和operator delete的用法与malloc和free没有区别


    对于operator new和operator delete的重载,我们可以重载,在实现原本功能的基础上,加入判断内存泄漏的机制

    我们也可以写一个类的专属operator new和operator delete,例如创建一个内存池。

    严格意义上来说这并不是函数重载,因为专属的operator new是在类域中,而全局operator new

    在全局作用域中,二者并不构成重载。

    五、内存泄漏

    1、什么是内存泄漏

    内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

    内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

    随着计算机应用需求的日益增加,应用程序的设计与开发也相应的日趋复杂,开发人员在程序实现的过程中处理的变量也大量增加,如何有效进行内存分配和释放,防止内存泄漏的问题变得越来越突出。例如服务器应用软件,需要长时间的运行,不断的处理由客户端发来的请求,如果没有有效的内存管理,每处理一次请求信息就有一定的内存泄漏。这样不仅影响到服务器的性能,还可能造成整个系统的崩溃。因此,内存管理成为软件设计开发人员在设计中考虑的主要方面 。

    2、如何避免内存泄漏

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

    总结


    例如:以上就是今天要讲的内容,本文仅仅简单介绍了C++的动态内存管理。

  • 相关阅读:
    机器学习 —— 线性回归 简单使用
    KubeVela 插件指南:轻松扩展你的平台专属能力
    一个使用typescript实现的excel转json的工具
    springboot在idea中可以访问jsp页面打包之后访问不了
    linux redis自启动
    微信小程序注册指引
    2022年7月31日--使用C#迈出第一步--使用C#中的数组和foreach语句来存储和循环访问数据序列
    面试大厂Java工程师后整理份300+页Java面试宝典
    VsCode 配置java环境(详细教程)
    高频微观结构:日内及隔夜动量因子
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/126212657