• C++【内存管理】


    目录

    一. C/C++内存分布

    二、C语言中动态内存管理方式:malloc/calloc/realloc/free 

    三、C++内存管理方式 

    内置类型的空间管理

    自定义类型的空间管理

    四、operator new与operator delete函数 

    重载operator new与operator delete

    operator new与operator delete的类专属重载 

    定位new表达式(placement-new)  

    malloc/free和new/delete的区别


    一. C/C++内存分布

    我们平时写的代码是存在磁盘上的文件

    编译连接生成的可执行程序还是一个存在磁盘上的文件(二进制的指令代码+数据)

    当我们的程序真正运行起来之后,我们程序中的二进制代码、只读常量才会被加载到代码段中,全局数据,静态数据这时候被加载到数据段中。

    在我们的函数创建的时候,栈上就会存储有关于我们函数的具体空间

    在我们使用开辟空间,如malloc动态开辟空间的时候就会在我们的堆上开辟空间

    只有堆上的空间是我们自己管理的,其他的空间都是由系统管理的。 

    Linux【操作系统】_桜キャンドル淵的博客-CSDN博客 中的第七章可以看到相关的在Linux下的测试代码。

    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是一个数组,还是在栈上的)

    ⑦*char2在哪里?_A__

    (*char2是数组首元素的地址,还是在栈上)
    ⑧pChar3在哪里?__A__

    (pChar3是一个指针,存在于栈上,其指向的空间“abcd\0”是存在常量区的)

    ⑨*pChar3在哪里?_D___

    (*pChar3是指pchar3所指向的空间,而我们的"abcd\0"是用const修饰的,是存在于常量区的。)
    ⑩ptr1在哪里?__A__

    (ptr1是一个动态开辟的数组空间的指针,还是在栈上)

    ⑪*ptr1在哪里?_B___ 

    (*ptr1是我们所开辟的空间,是在堆上的。)

    sizeof(num1) = _40___;

    (整个数组有十个元素,每一个元素都是4个字节)

    sizeof(char2) = _5___;

    (char2的数组中分别是abcd和\0这五个元素)

    strlen(char2) = ___4_;

    (strlen统计的是\0之前的字符个数,所以是abcd一共四个)
    sizeof(pChar3) = _8___;

    (在64位的平台上一个指针的大小是8个字节。)

    strlen(pChar3) = __4__;

    (strlen统计的是\0之前的字符个数,所以是abcd一共四个)
    sizeof(ptr1) = __8__;

    (ptr1是一个指针,在64位的平台上一个指针的大小是8个字节。)

    sizeof 和 strlen 区别? 

    strlen是求字符串长度的,只能针对字符串求长度-库函数的使用-使用得引用头文件

    sizeof计算变量,数组,类型的大小,单位是字节-操作符

    【说明】
    1. 栈又叫堆栈--非静态局部变量/函数参数/返回值等等,栈是向下增长的。
    2. 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口
    创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下)
    3. 堆用于程序运行时动态内存分配,堆是可以上增长的。
    4. 数据段--存储全局数据和静态数据。
    5. 代码段--可执行的代码/只读常量。 

    二、C语言中动态内存管理方式:malloc/calloc/realloc/free 

    malloc会在堆上开辟一块空间,其不会将其内部空间初始化。

    calloc在开辟空间的时候会将数据初始化为0。

    realloc可以用于扩容。

    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. }

     这里不需要释放p2

    1.如果p指向的空间之后有足够的内存空间可以追加,则直接追加,后返回p

    2.如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域,开辟一块满足需求的空间,并把原来的内存中的数据拷贝过来,释放旧的内存空间,最后返回新开辟的内存空间的地址

    三、C++内存管理方式 

    C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过newdelete操作符进行动态内存管理。

    内置类型的空间管理

    1. #include
    2. #include
    3. using namespace std;
    4. void Test()
    5. {
    6. //c语言中的写法
    7. int *p1=(int *)malloc(sizeof(int));
    8. //c++中的写法
    9. int *p2=new int;
    10. //下面两个有什么区别?
    11. //这是申请有五个元素的int数组
    12. int *p3=new int [5];
    13. //这是申请一个int对象,初始化为5
    14. int * p4=new int(5);
    15. //c++11支持new[] 用{}初始化
    16. int * p5=new int[5]{1,2,3};
    17. free(p1);
    18. //如果是new就直接delete
    19. //如果是new[],就是delete[]
    20. delete p2;
    21. delete[] p3;
    22. delete p4;
    23. delete[] p5;
    24. }
    25. int main()
    26. {
    27. Test();
    28. }

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

    针对内置类型,new/delete跟malloc/free并没有本质区别,只有用法上的区别

    new/delete用法简化了原本c语言中的语法。

    自定义类型的空间管理

    1. #include
    2. #include
    3. using namespace std;
    4. //
    5. class A
    6. {
    7. public:
    8. A(int a = 0)
    9. : _a(a)
    10. {
    11. cout << "A():" << this << endl;
    12. }
    13. ~A()
    14. {
    15. cout << "~A():" << this << endl;
    16. }
    17. private:
    18. int _a;
    19. };
    20. int main()
    21. {
    22. // new/delete 和 malloc/free最大区别是 new/delete对于【自定义类型】除了开空间
    23. //还会调用构造函数和析构函数
    24. A* p1 = (A*)malloc(sizeof(A));
    25. //这里在堆上开辟空间的时候还会调用构造函数对我们的对象进行初始化。
    26. A* p2 = new A(1);
    27. free(p1);
    28. delete p2;
    29. // 内置类型是几乎是一样的
    30. int* p3 = (int*)malloc(sizeof(int)); // C
    31. int* p4 = new int;
    32. //free仅仅是释放空间
    33. free(p3);
    34. //delete在释放空间之前还会调用析构函数清理对象中的资源
    35. //再释放空间
    36. delete p4;
    37. A* p5 = (A*)malloc(sizeof(A)*10);
    38. A* p6 = new A[10];
    39. free(p5);
    40. delete[] p6;
    41. return 0;
    42. }

    注意:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会 

    注意:new/delete new[]/delete[]一定要匹配,不匹配可能会出问题。(自定义类型一般都是会报错的,内置类型一般不会报错。)

     结论:c++产生new/delete是为自定义类型准备的。

    从下面的代码中我们可以看出我们的p6中的十个对象先定义的后析构,后定义的先析构,符合栈的特征。 

     

    operator new/delete不是重载,而是一个完整的指令,从下面的汇编中可以看到底层这是一个完整的函数。 

    这是malloc和new开辟空间失败的时候的返回值情况。

    (这里我们开辟的是虚拟内存,并不是物理内存,所以跟我们的电脑配置没有关系。) 

    1. int main()
    2. {
    3. //u后缀代表无符号
    4. //失败返回null
    5. void *p1=malloc(1024 * 1024u * 1024u*1024);
    6. printf("%p\n",p1);
    7. free(p1);
    8. //new失败不需要检查返回值,他失败是抛异常
    9. char *p2=new char[1024 * 1024u * 1024u*1024];
    10. printf("%p\n",p2);
    11. return 0;
    12. }

    四、operator new与operator delete函数 

    new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是
    系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

     operate帮助new开辟空间,封装malloc,符合C++new的失败机制(失败抛异常)。

    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)

    new Type

    call operator new ->malloc(失败抛异常)

    call Type构造函数

    重载operator new与operator delete

    1. #include
    2. #include
    3. using namespace std;
    4. //
    5. class A
    6. {
    7. public:
    8. A(int a = 0)
    9. : _a(a)
    10. {
    11. // cout << "A():" << this << endl;
    12. }
    13. ~A()
    14. {
    15. // cout << "~A():" << this << endl;
    16. }
    17. private:
    18. int _a;
    19. };
    20. // 重载operator delete,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个字节
    21. void* operator new(size_t size, const char* fileName, const char* funcName,
    22. size_t lineNo)
    23. {
    24. void* p = ::operator new(size);
    25. cout <<"new:"<< fileName << "-" << funcName << "-" << lineNo << "-" << p << "-"
    26. << size << endl;
    27. return p;
    28. }
    29. void* operator new [](size_t size, const char* fileName, const char* funcName,
    30. size_t lineNo)
    31. {
    32. void* p = ::operator new(size);
    33. cout << "new[]:"<"-" << funcName << "-" << lineNo << "-" << p << "-"
    34. << size << endl;
    35. return p;
    36. }
    37. // 重载operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放
    38. void operator delete(void* p, const char* fileName, const char* funcName,
    39. size_t lineNo)
    40. {
    41. cout << "delete:"<"-" << funcName << "-" << lineNo << "-" << p <<
    42. endl;
    43. ::operator delete(p);
    44. }
    45. void operator delete[](void* p, const char* fileName, const char* funcName,
    46. size_t lineNo)
    47. {
    48. cout << "delete[]:"<"-" << funcName << "-" << lineNo << "-" << p <<
    49. endl;
    50. ::operator delete(p);
    51. }
    52. // 上述调用显然太麻烦了,可以使用宏对调用进行简化
    53. // 只有在Debug方式下,才调用用户重载的 operator new 和 operator delete
    54. //#ifdef _DEBUG
    55. #define new new(__FILE__, __FUNCTION__, __LINE__)
    56. #define delete(p) operator delete(p, __FILE__, __FUNCTION__, __LINE__)
    57. //#endif
    58. int main()
    59. {
    60. A* p1=new A;
    61. delete(p1);
    62. A* p2=new A;
    63. delete(p2);
    64. A* p3=new A[4];
    65. delete [] (p3);
    66. A* p4=new A[4];
    67. delete [] (p4);
    68. A* p5=new A[4];
    69. delete [] (p5);
    70. return 0;
    71. }

    operator new与operator delete的类专属重载 

    如果我们反复开辟一块内存就会非常麻烦,并且会造成大量的外部碎片,所以我们可以采用创建内存池的方式来方便我们内存的开辟。

    这里我们就可以使用operator的new和delete的重载来实现。

    这样我们想申请listnode的时候就不用去malloc,而是走定制的内存池。

    只有listnode会调用这种专属的重载,其他的new/delete是不会重载的。

    这个operator new/delete与库函数中的并不会发生运算符重载或者函数重载,因为它们根本就不在一个域汇总。 

    new->operator new +构造函数

    默认情况下operator new 使用全局库函数里面的

    每个类可以去实现自己的专属的operator new

    new这个类对象的时候,它就会调自己实现的这个operator new。 

    1. #include
    2. #include
    3. using namespace std;
    4. struct ListNode
    5. {
    6. ListNode* _next;
    7. ListNode* _prev;
    8. int _data;
    9. //重载一个类的专属的operator new
    10. void* operator new(size_t n)
    11. {
    12. //调用c++库里面的内存池
    13. void* p = nullptr;
    14. //allocator的类是内存池(c++里面的空间配置器),()是我们的模板,这个参数是申请的结点个数
    15. p = allocator().allocate(1);
    16. cout << "memory pool allocate" << endl;
    17. return p;
    18. }
    19. //重载一个类的专属的operator delete
    20. void operator delete(void* p)
    21. {
    22. //deallocate是销毁内存池,参数是要释放几个
    23. allocator().deallocate((ListNode*)p, 1);
    24. cout << "memory pool deallocate" << endl;
    25. }
    26. };
    27. class List
    28. {
    29. public:
    30. List()
    31. {
    32. _head = new ListNode;
    33. _head->_next = _head;
    34. _head->_prev = _head;
    35. }
    36. ~List()
    37. {
    38. ListNode* cur = _head->_next;
    39. while (cur != _head)
    40. {
    41. ListNode* next = cur->_next;
    42. delete cur;
    43. cur = next;
    44. }
    45. delete _head;
    46. _head = nullptr;
    47. }
    48. private:
    49. ListNode* _head;
    50. };
    51. int main()
    52. {
    53. //频繁地申请ListNode
    54. List l;
    55. return 0;
    56. }

    内存池是什么?

    我们每一都要去开辟空间的话就是会非常麻烦。

    但是我们可以预先开辟好几个空间,当我们想要的时候直接拿就可以了。这个预先开辟好了好几个空间的池子就是我们的内存池。 

    new的原理
    1. 调用operator new函数申请空间
    2. 在申请的空间上执行构造函数,完成对象的构造

    delete的原理
    1. 在空间上执行析构函数,完成对象中资源的清理工作
    2. 调用operator delete函数释放对象的空间
    new T[N]的原理
    1. 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对
    象空间的申请
    2. 在申请的空间上执行N次构造函数
    delete[]的原理
    1. 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
    2. 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释
    放空间 

    定位new表达式(placement-new)  

    定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
    使用格式:
    new (place_address) type或者new (place_address) type(initializer-list)
    place_address必须是一个指针,initializer-list是类型的初始化列表


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

    1. #include
    2. #include
    3. using namespace std;
    4. class A
    5. {
    6. public:
    7. A(int a = 0)
    8. : _a(a)
    9. {
    10. cout << "A():" << this << endl;
    11. }
    12. ~A()
    13. {
    14. cout << "~A():" << this << endl;
    15. }
    16. private:
    17. int _a;
    18. };
    19. // 定位new/replacement new
    20. int main()
    21. {
    22. // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
    23. A* p1 = (A*)malloc(sizeof(A));
    24. if(p1== nullptr)
    25. {
    26. perror("malloc fail");
    27. }
    28. new(p1)A(); // 注意:如果A类的构造函数有参数时,此处需要传参
    29. p1->~A();
    30. free(p1);
    31. A* p2 = (A*)operator new(sizeof(A));
    32. new(p2)A(10);
    33. p2->~A();
    34. operator delete(p2);
    35. return 0;
    36. }

     

     可以看到new确实对我们的空间进行初始化,在下面的 代码中我们确实看到了我们的参数变成了10

     

    malloc/free和new/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在释放空间前会调用析构函数完成空间中资源的清理

     

  • 相关阅读:
    Soa: 一个轻量级的微服务库
    【Vue】Vue双向绑定原理
    TSINGSEE青犀智慧机房AI+视频智能监管方案,保障机房设备稳定运转
    【OpenStack云平台】Packmaker 集群
    一句话实现冒泡排序
    apollo源码启动服务,apollo源码分析
    泽众TestOne自动化测试平台,挡板测试(Mock测试)上线了!!
    MFC基础-选项卡控件
    【k8s资源调度-Deployment】
    数据可视化之雷达图:自助数据集处理,完美演绎球员数据可视化
  • 原文地址:https://blog.csdn.net/weixin_62684026/article/details/126195667