C/C++内存管理详解

x33g5p2x  于2021-10-25 转载在 C/C++  
字(8.9k)|赞(0)|评价(0)|浏览(761)

内存的静态分配和动态分配的区别主要是 两个

一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。

二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由函数malloc进行分配。不过栈的动态分配和堆不同,他的动态分配是由编译器进行释放,无需我们手工实现

C/C++内存分布

栈区:就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。函数调用建立栈帧,参数、函数中局部变量都存在栈帧中,栈是向下增长的,向下增长的意思是:从栈申请的内存地址会越来越小,在栈区当中是先使用高地址再使用低地址:

  1. void f()
  2. {
  3. int b = 0;
  4. cout << &b << endl;
  5. }
  6. int main( )
  7. {
  8. int a = 0;
  9. cout << &a << endl;
  10. f();
  11. return 0;
  12. }

可以看到先使用高地址再使用低地址。

堆区

一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。理论上而言,后malloc的内存地址比先malloc的要大,但是也不一定,但是不一定,因为有可能下一次申请的是之前其他空间释放回来的。

数据段(全局数据,静态数据)

全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的(DATA段)和未初始化的(BSS段),在C++里面没有这个区分了,它们共同占用同一块内存区。

代码段

可执行代码其实就是二进制指令,这里是存放二进制代码的。通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。 在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

内核空间

是给操作系统用的

这几个区域划分的特点:

1、这几个区域中堆是很大的,比如有32位-4G空间,内核空间占用1G,而堆差不多就占用了快3G

2、实际上,栈是很小的,Linux下一般只有8M,所以递归调用深度太深会导致栈溢出

3、数据段和代码段也不是很大,因为没有多少数据

下面我们来看一下下面代码的数据是分别存储在哪里的?

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

数据段 数据段 数据段 栈区 栈区 栈区 栈区 栈区 代码段 栈区 堆区

  1. 1、选择题
  2. 选项: A.栈 B.堆 C.数据段 D.代码段
  3. globalVar在哪里?____ staticGlobalVar在哪里?____
  4. staticVar在哪里?____ localVar在哪里?____
  5. num1 在哪里?____
  6. char2在哪里?____ *char2在哪里?___
  7. pChar3在哪里?____ *pChar3在哪里?____
  8. ptr1在哪里?____ *ptr1在哪里?____

globalVar在哪里?

globalVar是全局变量所以它在数据段

staticGlobalVar在哪里?

staticGlobalVar是静态变量所以它在数据段

staticVar在哪里?

staticVar是静态变量所以它在数据段

localVar在哪里?

localVar创建在test函数栈帧中,它是局部变量,在栈区

num1 在哪里?

num1是数组,在函数栈帧中创建,存储在栈区

char2在哪里?

char2是数组,在函数栈帧中创建,存储在栈区

/*char2在哪里?

**/char2拿到的是字符a的地址,因为char2是数组,里面的元素也是存储在栈帧里面的,所以/char2也在栈区

pchar3在哪里?

pchar3是指针变量,存储在栈区,它指向字符串的首字符地址

/*pchar3在哪里?

/*pchar3是常量字符a,常量字符串是存储在常量区的,就是在代码段

ptr1在哪里?

ptr1是指针变量,他存储在栈区

/*ptr1在哪里?

*ptr1指向的内存空间是malloc出来的,所以/ptr1是在堆区

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

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

calloc等价于malloc+memest(0),开空间+初始化为0

realloc是对malloc或calloc的空间进行扩容

C兼容C,c语言这一套动态内存申请与释放在C中都可以用

C++中动态内存管理

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

注意:

malloc/free是库函数,而new/delete是操作符

  1. void Test()
  2. {
  3. //C语言 malloc等是库函数
  4. int* p1 = (int*)malloc(sizeof(int));
  5. free(p1);
  6. //C++ new/delete是操作符
  7. // 动态申请一个int类型的空间
  8. int* ptr4 = new int;
  9. // 动态申请一个int类型的空间并初始化为10
  10. int* ptr5 = new int(10);
  11. delete ptr4;
  12. delete ptr5;
  13. }

这里new/delete和malloc/free有没有什么区别呢?

如果动态申请的对象是内置类型,用malloc和new没有区别,如果动态申请的对象是自定义类型,有区别,例如:

  1. class A
  2. {
  3. public:
  4. A(int a=0)
  5. :_a(a)
  6. {
  7. cout<<"A()"<<endl;
  8. }
  9. ~A()
  10. {
  11. cout<<"~A()"<<endl;
  12. }
  13. private:
  14. int _a;
  15. };
  16. int main()
  17. {
  18. //C语言自定义类型开辟空间
  19. A* p3 = (A*)malloc(sizeof(A));//没有初始化
  20. free(p3);
  21. //C++自定义类型开辟空间
  22. A* p4 = new A;//调用构造函数初始化
  23. delete p4;//调用析构函数
  24. }

new和delete不仅仅会开空间/释放空间,还会调用构造函数和析构函数

使用malloc没有初始化:

使用new进行了初始化:

这里其实是调用默认的构造函数进行了初始化,并且delete时调用了析构函数:

那我们想给new的对象传参怎么写呢?这样写:

  1. A* p4 = new A(10);//调用构造函数初始化

我们还可以new数组:

  1. int *ptr = new int[10];
  2. delete[] ptr;
  1. A *ptr1 = new A[10];//new了10个对象,调用了10次构造函数
  2. delete[] ptr1;

注意在new数组时,delete时需要加[]

对于内置类型用malloc和new没什么区别,但是自定义类型,用malloc和new有区别,new和delete还会调用构造函数和析构函数,一定要匹配使用,否则可能会崩溃,在C++中,建议使用new和delete,malloc和free能做到的,new和delete都能做到,new和delete能做到的,malloc和free不一定能做到,并且new和delete更方便一些,为什么使用new和delete更方便呢?例如我们在写链表时:

  1. struct ListNode
  2. {
  3. int _val;
  4. ListNode* _next;
  5. ListNode(int val):_val(val),_next(nullptr)
  6. {}
  7. };

用C创建节点并初始化它:

  1. //C
  2. ListNode* n1 = (ListNode*)malloc(sizeof(ListNode));
  3. n1->val = 1;
  4. n1->next = nullptr;

用C++创建节点并初始化它:

  1. //C++
  2. ListNode* n2 = new ListNode(1);//一次性完成了开辟空间和初始化

C++一次性就完成了开辟空间和初始化,所以new和delete更方便。

接下来我们来看一下new和delete到底时怎么样的原理:

operator new与operator delete函数

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

注意:

它们两个只是全局的库函数,它们并不是new和delete的重载

我们在new一个对象T时,其实编译器会这样做:

1、申请内存,调用operator new(底层其实是将malloc的封装实现)

2、调用构造函数

在delete时,编译器会这么做:

1、调用T的析构函数

2、调用operator delete(底层其实是将free的封装实现)

我们之前在使用malloc时,malloc失败会返回NULL:

  1. int main()
  2. {
  3. //malloc失败,返回NULL
  4. char* p1 = (char*)malloc((size_t)2*1024*1024*1024);
  5. if(p1==NULL)
  6. {
  7. printf("malloc fail\n");
  8. }
  9. else
  10. {
  11. printf("malloc success\n");
  12. }
  13. return 0;
  14. }

那么new操作符在new失败时会做什么处理呢?

  1. int main()
  2. {
  3. char* p2 = new char[0x7fffffff];
  4. //并没有执行
  5. if(p2==NULL)
  6. {
  7. printf("new fail\n");
  8. }
  9. else
  10. {
  11. printf("new success\n");
  12. }
  13. return 0;
  14. }

我们可以看到new失败时,并没有执行if语句就出错了,为什么呢?是因为new和malloc不一样,申请空间失败了,它会抛异常,下面我们用一段捕获异常的代码来说明:

  1. int main()
  2. {
  3. //new和malloc不一样,申请空间失败了,会抛异常
  4. try
  5. {
  6. char* p2 = new char[0x7fffffff];//如果出错抛异常,跳到捕获异常的位置,没有捕获会报错
  7. }
  8. catch(const exception& e)
  9. {
  10. cout<<e.what()<<endl;
  11. }
  12. }

operator new其实就是对malloc的封装,如果申请内存失败了,抛异常,封装malloc+抛异常,我们进行调试反汇编可以看到执行new时,他要调用operator new函数和A的构造函数:

有operator new,那么也有operator delete,operator deltete也是对free进行封装。
通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

我们也可以直接用operator new和operator delete函数

  1. int* p3 = (int*)operator new(sizeof(int)*10);
  2. operator delete(p3);

operator new与operator delete的类专属重载

假设我们写了一个链表的程序,怎么检测一下有没有ListNode的节点没有释放?

我们可以重载一个类ListNode专属的operator new和operator delete成员函数

我们在new一个ListNode时,那么申请空间就会调用专属的operator new,delete时,释放空间调用专属的operator delete,我们可以定义一个静态变量_count来记录,申请时_count++,释放_count–:

  1. struct ListNode
  2. {
  3. int val;
  4. struct ListNode* next;
  5. static int _count;
  6. ListNode(int x)
  7. :val(x)
  8. , next(nullptr)
  9. {
  10. cout << "ListNode()" << endl;
  11. }
  12. ~ListNode()
  13. {
  14. cout << "~ListNode()" << endl;
  15. }
  16. void* operator new(size_t n)
  17. {
  18. ++_count;
  19. return ::operator new(n);
  20. // 去内存池
  21. }
  22. void operator delete(void* p)
  23. {
  24. --_count;
  25. return ::operator delete(p);
  26. // 释放去内存池
  27. }
  28. };
  29. int ListNode::_count = 0;
  30. struct ListNode* removeElements(struct ListNode* head, int val)
  31. {
  32. struct ListNode* prev = NULL, *cur = head;
  33. while (cur)
  34. {
  35. if (cur->val == val)
  36. {
  37. // 1、头删
  38. // 2、中间删除
  39. prev->next = cur->next;
  40. delete cur;
  41. cur = prev->next;
  42. }
  43. else
  44. {
  45. // 迭代往后走
  46. prev = cur;
  47. cur = cur->next;
  48. }
  49. }
  50. return head;
  51. }
  52. // 检测一下有没有ListNode的节点没有释放
  53. int main()
  54. {
  55. ListNode* n1 = new ListNode(1);
  56. ListNode* n2 = new ListNode(2);
  57. ListNode* n3 = new ListNode(2);
  58. ListNode* n4 = new ListNode(3);
  59. ListNode* n5 = new ListNode(4);
  60. ListNode* n6 = new ListNode(2);
  61. n1->next = n2;
  62. n2->next = n3;
  63. n3->next = n4;
  64. n4->next = n5;
  65. n5->next = n6;
  66. n6->next = nullptr;
  67. ListNode* list = removeElements(n1, 2);
  68. cout <<"没有释放节点数量:" <<ListNode::_count << endl;
  69. return 0;
  70. }

这个在实际中并不常用,了解一下即可。

new和delete的实现原理

内置类型

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

自定义类型

  • 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来释放空间
  1. int main()
  2. {
  3. Stack st;
  4. Stack* ps = new Stack;
  5. delete ps;
  6. retunr 0;
  7. }

直接创建对象和new对象有什么区别呢?下面我们画图讲解:

定位new表达式(placement-new)

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

使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

构造函数调用什么时候自动调用呢?

1、创建对象时

2、new一个对象时

  1. //显式对一块空间调用构造函数初始化
  2. class Test
  3. {
  4. public:
  5. Test()
  6. : _data(0)
  7. {
  8. cout<<"Test():"<<this<<endl;
  9. }
  10. ~Test()
  11. {
  12. cout<<"~Test():"<<this<<endl;
  13. }
  14. private:
  15. int _data;
  16. };
  17. void Test()
  18. {
  19. // pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行
  20. Test* pt = (Test*)malloc(sizeof(Test));
  21. new(pt) Test; // 注意:如果Test类的构造函数有参数时,此处需要传参
  22. }

pt现在指向的只不过是与Test对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行,显式对一块空间调用构造函数初始化,new(pt) Test;

如果我们复制一份a数组到另外一块空间b数组,怎么复制呢?

也许你想的是这样,通过一个循环来拷贝:

  1. int main()
  2. {
  3. A a[5];
  4. //复制一份a数组到另外一块空间b
  5. A* pb1 = new A[5];
  6. for(int i =0;i<5;++i)
  7. {
  8. b[i]=a[i];
  9. }
  10. cout<<endl;
  11. //代价大 构造+赋值
  12. return 0;
  13. }

但是这样代价大,经过了构造+赋值重载,那么能不能直接构造呢?通过定位new表达式就可以直接构造:

  1. int main()
  2. {
  3. A a[5];
  4. //能不能直接构造?
  5. A *pb = (A*)malloc(sizeof(A)*5);
  6. for(int i =0;i<5;++i)
  7. {
  8. new(pb+i)A(a[i]);
  9. }//只有构造
  10. return 0;
  11. }

常见面试题

malloc/free和new/delete的区别

1、特点和用法
malloc和free是函数,new和delete是操作符;malloc申请空间时,需要手动计算空间大小并传递,new只需在后面跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可;malloc的返回值是void/*,在使用时需要强转,new不需要,因为new后面跟的是空间的类型

2、底层原理区别

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

3、处理错误的方式

malloc申请空间失败时,返回NULL,因为使用时需要判空,new不需要,但是new需要捕获异常

内存泄漏

什么是内存泄漏呢?

在堆上申请了的空间,在我们不用了以后也没有释放,就存在内存泄漏,因为你不用了,也没有还给系统,别人也用不了。俗话说:占着茅坑不拉屎

  1. int main()
  2. {
  3. char* p = new char[1024*1024*1024];
  4. return 0;
  5. }

运行结束后:

上面程序存在内存泄漏,一次泄漏1G,但是程序运行结束后,对我们系统也好像没有影响,好像会将我们申请的内存自动还回去,实际上一个进程正常结束后,会把映射的内存都会释放掉,所以上面的程序,我们没有主动释放,但是进程结束也释放,那么内存泄漏好像也没啥事,因为进程正常结束都会释放。

其实并不是,你在看看下面的场景:

1、要是进程没有正常结束呢!僵尸进程,就可能存在一些资源没有释放

2、长期运行的服务器程序。比如王者荣耀后台服务,长期运行,只有维护升级的时候才会停,内存泄漏会导致可用内存越来越少,程序越来越慢,甚至挂掉。事故

3、物联网设备:扫地机器人、冰箱等等,内存很小,也会经不起内存泄漏折腾

在C++中我们需要主动释放内存,而Java不需要主动释放内存,Java后台有垃圾回收器,接管了内存释放

那么我们如何预防内存泄漏呢?

1、智能指针

2、内存泄漏的检测工具

如何申请4G的内存呢?将操作系统换为64位就可以了:

  1. int main()
  2. {
  3. try
  4. {
  5. char* p = new char[0x7fffffff];
  6. //char* p = new char[0xffffffff];
  7. printf("%p\n", p);
  8. }
  9. catch (const exception& e)
  10. {
  11. cout << "内存申请失败" << endl;
  12. }
  13. return 0;
  14. }

我们在32位的操作系统上申请,会申请失败:

将他改为64位就能申请成功:

可以看到申请成功了

欢迎大家学习交流!

相关文章