【C++】内存管理,深入解析new、delete

[复制链接]
发表于 2025-7-27 22:13:11 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
【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. }
复制代码
选择题:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?C
staticGlobalVar在哪里?C
staticVar在哪里?C
localVar在哪里?A
num1 在哪里?A
char2在哪里?A
*char2在哪里?A
pChar3在哪里?A
*pChar3在哪里?D
ptr1在哪里?A
*ptr1在哪里?B


  • 栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。
  • 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可利用体系接口创建共享共享内存,做进程间通讯。(Linux课程假如没学到这块,现在只须要相识一下)
  • 堆用于程序运行时动态内存分配,堆是可以上增长的。
  • 数据段–存储全局数据和静态数据

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

在C语言中,我们会用到malloc/calloc/realloc/free这些函数来动态管理内存
简单做道题,回顾一下malloc、calloc、realloc的特性:
  1. void Test ()
  2. {
  3.         int* p1 = (int*)malloc(sizeof(int));
  4.         int* p2 = (int*)calloc(4, sizeof (int));
  5.         int* p3 = (int*)realloc(p2, sizeof(int)*10);
  6.         // 这里需要free(p2)吗?
  7.    
  8.     free(p1 );
  9.         free(p3 );
  10. }
复制代码
ans:
假如是原地扩容,那p2和p3指向的就是同一块空间,p2、p3开释其中一个即可。若是异地扩容,返回p3指向的新空间之前就会自动把原先p2的空间开释掉,以是这里两种环境,我们只开释p3就可以了
口试题:

  • malloc/calloc/realloc的区别?
    在 C 语言中,malloc、calloc 和 realloc 均用于动态内存分配,但功能不同:malloc 分配指定字节的未初始化内存,实用于通用场景;calloc 按元素数量和巨细分配内存并初始化为全零,适合须要默认清零的布局或数组;realloc 用于调整已分配内存块的巨细(扩展或缩小),可能复制数据到新地点。区别在于 malloc 不初始化且参数为总字节数,calloc 自动初始化但效率略低,而 realloc 可动态伸缩内存,需注意其可能返回新指针,且未扩展地区内容不确定。三者均需共同 free 开释内存,避免泄漏。
关键区别总结

特性malloccallocrealloc初始化未初始化初始化为零保存原数据(新地区未初始化)参数情势总字节数元素个数 × 元素巨细原指针 + 新字节数实用场景通用动态分配须要零初始化的布局/数组动态调整内存巨细性能开销低(无初始化)较高(需清零)可能涉及内存复制内存连续性保证连续保证连续可能改变地点(需重新赋值指针)
  1. //函数原型:
  2. void* malloc (size_t size);//参数:size:内存块的大小(以字节为单位)
  3. //返回值
  4. //成功后,指向函数分配的内存块的指针。
  5. //此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
  6. //如果函数无法分配请求的内存块,则返回 null 指针。void*
  7. void* calloc (size_t num, size_t size);//参数:num:需要分配元素的个数,size:每个元素的大小
  8. //返回值
  9. //成功后,指向函数分配的内存块的指针。
  10. //此指针的类型始终为 ,可以将其转换为所需的数据指针类型,以便可取消引用。
  11. //如果函数无法分配请求的内存块,则返回 null 指针。void*
  12. void* realloc (void* ptr, size_t size);//参数ptr:指向先前使用或分配的内存块的指针。或者这可以是 null 指针,在这种情况下,将分配一块新的空间(就像被调用一样)。
  13. //返回值:
  14. //指向重新分配的内存块的指针,该块可能与 相同或新位置。
  15. //此指针的类型为 ,可以将其转换为所需的数据指针类型,以便可取消引用。ptrvoid*
复制代码

三、C++内存管理方式

C语言内存管理方式在C++中可以继续利用,但是C++是面向对象的语言,有些地方就无能为力,而且利用起来比较麻烦,特别是自定义范例的初始化和烧毁。因此C++又提出了自己的内存管理方式:通过new和delete操作符举行动态内存管理。
3.1new和delete操作内置范例:

  1. void Test()
  2. {
  3.         // 动态申请一个int类型的空间
  4.         int* ptr4 = new int;
  5.         // 动态申请一个int类型的空间并初始化为10
  6.         int* ptr5 = new int(10);
  7.         // 动态申请10个int类型的空间
  8.         int* ptr6 = new int[3];
  9.    
  10.     //释放空间
  11.         delete ptr4;
  12.         delete ptr5;
  13.         delete[] ptr6;
  14. }
复制代码

注意:申请和开释单个元素的空间,利用new和delete操作符,申请和开释连续的空间,利用
new[]和delete[],注意:匹配起来利用。

实现原理:
假如申请的是内置范例的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和开释的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

3.2 new和delete操作自定义范例

  1. #include <iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6.         A(int a1 = 0, int a2 = 0)
  7.                 : _a1(a1)
  8.                 , _a2(a2)
  9.         {
  10.                 cout << "A(int a1 = 0, int a2 = 0)" << this << endl;
  11.         }
  12.         A(const A& ra)
  13.                 : _a1(ra._a1)
  14.                 , _a2(ra._a2)
  15.         {
  16.                 cout << "A(const A& ra)" << endl;
  17.         }
  18.         ~A()
  19.         {
  20.                 cout << "~A():" << this << endl;
  21.         }
  22. private:
  23.         int _a1;
  24.         int _a2;
  25. };
  26. int main()
  27. {
  28. // new/delete 和 malloc/free最大区别是
  29. // new/delete 对于【自定义类型】除了开空间还会调用构造函数和析构函数
  30.     A* p1 = (A*)malloc(sizeof(A));
  31.     A* p2 = new A(1, 2);
  32.     free(p1);
  33.     delete p2;
  34.    
  35. // 内置类型是几乎是一样的
  36.     int* p3 = (int*)malloc(sizeof(int));
  37.     int* p4 = new int;
  38.     free(p3);
  39.     delete p4;
  40. //数组:
  41.     A* p5 = (A*)malloc(sizeof(A) * 10);
  42.     A* p6 = new A[10];
  43.     free(p5);
  44.     delete[] p6;
  45.    
  46.     return 0;
  47. }
复制代码

注意:在申请自定义范例的空间时,new会调用构造函数,delete会调用析构函数,malloc/free不会。 对于内置范例来说两者没有什么区别
换句话可以说,new/delete是malloc/free的升级版,new和delete其实是为了处置惩罚自定义范例而生的!!

3.2 new申请空间失败

失败的原因大概率为假造内存空间不足:
在申请失败时,会抛异常

64位


四、new 和 delete 的实现原理

4.1 operator new 与 operator delete函数

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

4.2 new 和 delete 的实现原理

4.2.1 内置范例

假如申请的是内置范例的空间,new和malloc,delete和free基本类似,不同的地方是:new / delete申请和开释的是单个元素的空间,new[]/delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
4.2.2 自定义范例



  • new 原理

    • 调用operator new 的函数申请空间
    • 在申请的空间上执行构造函数,完成对象的构造


  • delete的原理

    • 在空间上执行析构函数,完成对象中资源的清理工作
    • 调用operator delete函数开释对象的空间


  • new T[N]的原理

    • 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
    • 在申请的空间上执行N次构造函数


  • delete[]的原理

    • 在开释的对象空间上执行N次析构函数,完成N个对象中资源的清理
    • 调用operator delete[]开释空间,实际在operator delete[]中调用operator delete来开释空间



4.3 不匹配利用的后果

4.3.1 内置范例

  1. int main()
  2. {
  3.     int* p1 = new int[10];
  4.     free(p1); //正常释放
  5.     return 0;
  6. }
复制代码
假如是内置范例,构造函数和析构函数都对内置范例不处置惩罚,以是用delete和free没什么区别
4.3.2 自定义范例



  • 不须要资源清理的自定义范例
    1. class A
    2. {
    3. private:
    4.     int _a1 = 0;
    5. };
    6. int main()
    7. {
    8.         A* p2 = new A;
    9.         free(p2);//只释放了空间,没有调析构函数
    10.         return 0;
    11. }
    复制代码
    自定义范例,构造函数和析构函数都会处置惩罚,但假如我们用free,就少调了一次析构函数,但是该类的析构函数并不须要清理资源,所不调用也是无所谓的。
  • 须要资源清理的自定义范例
    1. class Stack
    2. {
    3. public:
    4.         Stack()
    5.     {
    6.         cout << "Stack()" << endl;
    7.         _a = new int[4];
    8.                 _top = 0;
    9.         _capacity = 4;
    10.     }
    11.    
    12.     ~Stack()
    13.     {
    14.         cout << "~Stack()" << endl;
    15.         delete[] _a;
    16.         _top = _capacity = 0;
    17.     }
    18.        
    19. private:
    20.     int _top;
    21.     int _capacity;
    22.     int* _a;
    23. }
    24. int main()
    25. {
    26.     Stack* st = new Stack;
    27.    
    28.     //delete st;
    29.     free(st);//没有调析构函数
    30.     return 0;
    31. }
    复制代码

    这里的Stack范例是有向堆另外申请空间的,当我们利用free来开释空间时,free只会将st所指向的空间开释,而成员变量_a所指向的空间就会泄漏
    但是这个地方编译器并没有报错,这阐明编译器对于内存泄露不是很敏感,我们作为程序员要尽量自己去避免这种环境!!


4.3.3 数组

我们运行一下下面的代码:
  1. class A
  2. {
  3. public:
  4.         A(char a1 = 0)//默认构造函数
  5.                 : _a1(a1)
  6.         {
  7.                 cout << "A(char a1 = 0)" << this << endl;
  8.         }
  9.         ~A()//析构函数
  10.         {
  11.                 cout << "~A():" << this << endl;
  12.         }
  13. private:
  14.         char _a1;
  15. };
  16. class B
  17. {
  18. public:
  19.         B(char b1 = 2)
  20.                 : _b1(b1)
  21.         {
  22.                 cout << "B(char b1 = 2)" << this << endl;
  23.         }
  24. private:
  25.         char _b1;
  26. };
  27. int main()
  28. {
  29.         //int* p1 = new int[10];
  30.         A* p1 = new A[5];
  31.         B* p2 = new B[5];
  32.     //没有匹配使用
  33.         //free(p1);//直接崩溃
  34.         //delete p1;//调用了一次析构,然后崩溃
  35.     //匹配使用
  36.         delete[] p1;//正常运行
  37.        
  38.     //没有匹配使用
  39.         //free(p2);//正常运行
  40.         //delete p2;//正常运行
  41.     //匹配使用
  42.         delete[] p2;//正常运行
  43.         return 0;
  44. }
复制代码

通过上面的代码,我们发现当我们用new开了一个元素个数为10的A范例数组,开的时间没题目,但当程序走到free的时间却瓦解了?我们知道这是因为没有匹配利用new/delete,但本质上是为什么呢?
题目重要在于new的机制:
在new数组时,编译器会在开数组前,会在数组前面多开四个字节的空间来记载开辟元素的个数,这个记载重要是当我们在用delete[]时[]中并没有指明有多少个元素,也就不知道要析构多少次。以是就是给反面delete[]用来调用析构函数的
我们分别看一下A、B范例的空间创建和烧毁的过程


  • 有析构函数的A范例的数组:


    对比上面的Stack范例,固然Stack范例内存泄漏,但他却没有直接程序瓦解。而这里就直接瓦解了。检查机制是由编译器决定的,以是在不同的编译器下,对于空间泄漏的检查都会有所差异,以是我们为了淘汰这种偶然性的题目,我们就必须匹配利用new/delete,有空间资源申请的自定义范例,就必须要写析构函数,从而更加好的管理内存。
  • 没有析构函数的B范例的数组:

    因为没有析构函数,在烧毁空间时也不会调用,以是就不许要多开4个字节

4.3.4 总结


  • 对于内存泄漏题目,不同编译器的检查机制是不一样的。
  • 不匹配利用new/delete的风险是不可控制的,我们必须要做到匹配利用
  • 对于有空间申请,须要用到资源清理的自定义范例,必须要写好析构函数,否则会导致内存泄漏。

五、内存泄漏

C++ 的内存泄漏题目与其他语言(如 Java、Python、C#、Rust 等)有明显差异,重要源于其 手动内存管理 的特性。

5.1 内存泄漏的定义

内存泄漏(Memory Leak):程序在动态分配内存后,未能正确开释已不再利用的内存,导致体系可用内存逐渐淘汰,终极可能引发性能降落或瓦解。

5.2 C++ 的内存泄漏题目

原因


  • 手动管理内存:C++ 中需显式调用 new/malloc 分配内存,delete/free 开释内存。
  • 未开释的场景:

    • 忘记调用 delete。
    • 异常导致未执行开释代码。
    • 指针所有权不清晰(如多个指针指向同一内存,重复开释或未开释)。
    • 容器或对象内部未正确开释资源。

示例
  1. void leak() {
  2.     int* ptr = new int[100];  // 分配内存
  3.     // ...使用 ptr...
  4.    
  5.     // 不匹配使用delete,或者忘记使用
  6. }
复制代码
后果


  • 长期运行的程序(如服务器、嵌入式体系)会逐渐耗尽内存。
  • 调试困难:泄漏点可能隐蔽,需借助工具(如 Valgrind、AddressSanitizer)检测。

5.3 对比其他语言

(1) 手动管理内存的语言(如 C)


  • 相似性:与 C++ 类似,需手动管理 malloc/free。
  • 差异:C++ 的构造函数/析构函数、异常机制可能增长泄漏风险(如构造函数抛出异常导致未执行析构函数)。
(2) 半自动管理语言(如 Objective-C、Rust)


  • Objective-C(ARC):

    • 自动引用计数(ARC):编译器自动插入 retain/release 代码,淘汰泄漏。
    • 循环引用:仍需开辟者处置惩罚(通过 weak 引用)。

  • Rust:

    • 所有权体系:编译器静态检查内存生命周期,确保无泄漏(除非显式利用 unsafe)。
    • 无垃圾回收:通过所有权、借用规则实现内存安全。

(3) 自动垃圾回收语言(如 Java、Python、C#、Go)


  • 垃圾回收(GC):内存由运行时自动回收,开辟者无需手动开释。

    • 优点:险些无需担心内存泄漏。
    • 缺点:

      • 不可控性:GC 触发时机不确定,可能引发短暂停顿。
      • 循环引用:某些语言(如 Python)的 GC 能处置惩罚循环引用,但需依赖算法(如分代回收)。
      • 非内存资源泄漏:如文件句柄、数据库毗连仍需手动开释(需 try-with-resources 或 using 语句)。


(4) 智能指针语言(当代 C++)


  • C++11 引入的智能指针:

    • std::unique_ptr:独占所有权,自动开释内存。
    • std::shared_ptr:共享所有权,引用计数为 0 时开释。
    • std::weak_ptr:解决 shared_ptr 的循环引用题目。

  • 示例:
    1. void no_leak() {
    2.     auto ptr = std::make_unique<int[]>(100); // 自动释放
    3. }
    复制代码
  • 优点:大幅淘汰泄漏风险,但需正确利用(如避免循环引用)。

5.4 内存泄漏的常见场景对比

语言泄漏风险重要原因典型场景C++高手动管理、异常、复杂对象生命周期未配对的 new/deleteC高类似 C++,但无异常和析构函数malloc 后忘记 freeJava低循环引用或未关闭资源静态聚集持有对象、未关闭流Python低循环引用(需 GC 支持)复杂对象引用关系Rust极低所有权体系强制安全需显式利用 unsafe 代码C#低非托管资源未开释未调用 Dispose() 的文件操作
5.5 怎样避免内存泄漏

C++ 的解决方案


  • 优先利用智能指针:替换原始指针。
  • RAII 原则:资源获取即初始化(如通过构造函数分配,析构函数开释)。
  • 工具检测:

    • Valgrind:检测内存泄漏和越界访问。
    • AddressSanitizer(ASan):实时检测内存错误。

  • 避免裸指针:仅在须要时利用(如与 C 库交互)。
其他语言的解决方案



  • Java/Python:依赖 GC,但需注意非内存资源(如文件、网络毗连)的手动开释。
  • Rust:依赖编译器检查,险些无需手动管理。
  • C#:利用 using 语句或 IDisposable 接口开释资源。

5.6 总结



  • C++ 的内存泄漏风险最高:源于手动管理和复杂生命周期。
  • 其他语言通过自动管理低落风险:

    • GC 语言:以运行时开销为代价,简化内存管理。
    • Rust:通过编译时检查实现零资本安全性。

  • 当代 C++ 的最佳实践:智能指针 + RAII + 工具检测,可靠近自动管理语言的安全性。
通过理解不同语言的内存管理机制,可以更好地规避泄漏题目,选择适合场景的工具和战略。

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

定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象
利用格式:
  1. //不带参
  2. new(place_address)type;
  3. //带参
  4. new(place_address) type(initializer-list);
  5. //       |         |           |
  6. //      地址       类型      类型的初始化列表
复制代码
  1. #include <iostream>
  2. using namespace std;
  3. class A
  4. {
  5. public:
  6.         A(int a = 0)
  7.                 : _a(a)
  8.         {
  9.                 cout << "A():" << this << endl;
  10.         }
  11.         ~A()
  12.         {
  13.                 cout << "~A():" << this << endl;
  14.         }
  15.         void Printt()
  16.         {
  17.                 cout << _a << endl;
  18.         }
  19. private:
  20.         int _a;
  21. };
  22. // 定位new/replacement new
  23. int main()
  24. {
  25.         // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
  26.         A* p1 = (A*)malloc(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象
  27.         cout << "p1:" << p1 << endl;
  28.         new(p1)A; // 注意:如果A类的构造函数不是默认构造函数时,就必须要传参。但这里是默认构造,所以可以不用传参。
  29.         p1->Printt();
  30.         //资源清理
  31.         p1->~A();
  32.         free(p1);
  33.         //delete p1;//因为定位出了对象,所以也可以通过delete释放空间,当然最好还是要匹配使用
  34.         cout << endl;
  35.         A* p2 = (A*)operator new(sizeof(A));//只申请了空间,并没有调用构造函数,没有构造出对象
  36.         cout << "p2:" << p2 << endl;
  37.         new(p2)A(10);//带参数构造出A对象
  38.         p2->Printt();
  39.         //资源清理
  40.         p2->~A();
  41.         operator delete(p2);
  42.        
  43.         //delete p2;
  44.         return 0;
  45. }
复制代码

new就可以完成初始化这个工作,那为什么我们还要用malloc、再用定位new初始化,这不是多此一举吗??
确实是这样的!!!定位new表达式在实际中一般是共同内存池利用。因为内存池分配出的内存没有初始化,以是假如是自定义范例的对象,须要利用new的定义表达式举行显示调构造函数举行初始化。
在有些场景下,我们须要不停地new一块新地区,这个过程是和操作体系上的堆完成交互的,假如我们不停地交互,其实是很麻烦的,为了优化性能,有了内存池这个概念,也就是我们先在堆上一次性malloc出一大块的地区,之后假如是处置惩罚小数据,我就不须要不停跟操作体系交互,只须要在我们的内存池内里拿空间就可以了,再用定位new去初始化!!这样淘汰了对象之间的交互,提高了效率。


七、总结:malloc/free和new/delete的区别

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

  • malloc和free是函数,new和delete是操作符
  • malloc申请的空间不会初始化,new可以初始化
  • malloc申请空间时,须要手动计算空间巨细并传递,new只需在其后跟上空间的范例即可,假如是多个对象,[]中指定对象个数即可
  • . malloc的返回值为void*, 在利用时必须强转,new不须要,因为new后跟的是空间的范例
  • malloc申请空间失败时,返回的是NULL,因此利用时必须判空,new不须要,但是new须要捕获异常
    式在实际中一般是共同内存池利用。因为内存池分配出的内存没有初始化,以是假如是自定义范例的对象,须要利用new的定义表达式举行显示调构造函数举行初始化。
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表