C++】内存管理(万字详解)

85 阅读8分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第N天,点击查看活动详情 目录 ✨1. C/C++内存分布 ✨2. C语言中动态内存管理方式 ✨3. C++内存管理方式 🌟3.1 new / delete 操作内置类型 🌟3.2 new和delete操作自定义类型 ✨4. operator new与operator delete函数(重点) 🌟4.1 operator new与operator delete函数(重点) 🌟4.2 重载operator new与operator delete(了解) ✨5. new和delete的实现原理 🌟5.1 内置类型 🌟5.2 自定义类型 ✨ 6. 定位new表达式(placement-new) (了解) ✨ 7. 常见面试题 🌟7.1 malloc/free和new/delete的区别 🌟7.2 内存泄露(初步讲解,后面细讲) 🌟7.2.1 什么是内存泄漏,内存泄漏的危害 🌟7.2.2 内存泄漏分类(了解) 🌟7.2.3 如何检测内存泄漏(了解) 🌟 7.2.4如何避免内存泄漏 ✨1. C/C++内存分布 我们先来看下面的一段代码和相关问题

int globalVar = 1; static int staticGlobalVar = 1; void Test() { static int staticVar = 1; int localVar = 1; const int localVal1 = 1;

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 选择题:选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区) globalVar在哪里?静态区 staticGlobalVar在哪里?静态区 staticVar在哪里?静态区 localVar在哪里?栈 num1 在哪里?栈 char2在哪里?栈 *char2在哪里?栈 pChar3在哪里?栈 *pChar3在哪里?常量区 ptr1在哪里?栈 *ptr1在哪里?堆 填空题: sizeof(num1) = 40 sizeof(char2) = 5 sizeof(pChar3) = 4/8 sizeof(ptr1) = 4/8 strlen(char2) = 4 strlen(pChar3) = 4 sizeof 和 strlen 区别? sizeof是关键字,strlen是函数 sizeof的参数可以是数组,指针,类型,对象,函数等.strlen的参数只能是字符指针,且必须以’\0’结尾 数组名做sizeof的参数不退化,做strlen的参数会退化为指针 编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来.并且sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度.

【说明】

栈又叫堆栈–非静态局部变量/函数参数/返回值等等,栈是向下增长的。 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(Linux课程如果没学到这块,现在只需要了解一下) 堆用于程序运行时动态内存分配,堆是可以上增长的。 数据段–存储全局数据和静态数据。 代码段–可执行的代码/只读常量。 ✨2. C语言中动态内存管理方式 malloc/calloc/realloc/free void Test () { int* p1 = (int*) malloc(sizeof(int)); free(p1); int* p2 = (int*)calloc(4, sizeof (int)); int* p3 = (int*)realloc(p2, sizeof(int)*10);

2 3 4 5 6 7 8 9 10 11 12 【面试题】

  1. malloc/calloc/realloc的区别

malloc -> 开空间 calloc 等价于 malloc + memset(0) -> 开空间 + 初始化 realloc 单独使用时能实现 malloc 的效果 (不会初始化) -> 开空间 | 对 malloc/calloc 的空间扩容 2. malloc的实现原理?

glibc中malloc实现原理

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

🌟3.1 new / delete 操作内置类型 void Test() { // 动态申请一个int类型的空间 int* ptr4 = new int; // 动态申请一个int类型的空间并初始化为10 int* ptr5 = new int(10); // 动态申请10个int类型的空间 int* ptr6 = new int[3]; delete ptr4; delete ptr5; delete[] ptr6; } 1 2 3 4 5 6 7 8 9 10 11 12

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

问题:malloc/free 和 new/delete 有什么区别 ?

如果动态申请的对象是内置类型,那么 malloc/free 和 new/delete 没有区别 如果动态申请的对象是自定义类型,那么 malloc/free 和 new/delete 有区别 如果是自定义类型,new和delete会分别调用构造函数和析构函数

🌟3.2 new和delete操作自定义类型 class A { public: A(int a = 0/int b = 0/) :_a(a) { cout << "A()" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { A* p3 = (A*)malloc(sizeof(A)); free(p3);

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 注意我们在 new A 类时不需要默认构造函数;但是在 new A[10] 时则需要默认构造函数 在 C++ 中建议尽量使用 new/delete,因为 malloc/free 能做到的,new/delete 也能做到;new/delete 能做到的,malloc/free 不一定能做到。

注意申请和释放单个元素的空间,使用 new 和 delete 操作符,申请和释放连续的空间,使用 new[] 和 delete[] struct ListNode { int _val; ListNode* _next; ListNode(int val) : _val(val) , _next(nullptr) {} }; int main() { //C ListNode* n1 = (ListNode*)malloc(sizeof(ListNode)); n1->_val = 1; n1->_next = nullptr;

//C++
ListNode* n2 = new ListNode(1);

return 0;

}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 一定要匹配使用:malloc ↔ free 、new ↔ delete、new 类型[] ↔ delete[]类型,否则可能会崩溃。

int* p1 = (int*)malloc(sizeof(int) * 10); free(p1); delete p1;

int* p2 = new int; delete p2; free(p2);

int* p2 = new int[10]; delete[]p2; delete[10]p2; delete p2;//err free p2;/err

1 2 3 4 5 6 7 8 9 10 11 12 13 14 ✨4. operator new与operator delete函数(重点) 🌟4.1 operator new与operator delete函数(重点) new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。

new A:

申请内存 —— 调用 operator new 构造函数 delete A:

析构函数 释放内存 —— 调用 operator delete

new 的底层的实现,new 的底层申请内存时是不能让 malloc 去完成的,因为 malloc 失败就直接返回空了,就无法达到让它失败后抛异常的机制,所以其中就产生了 operator new对于 delete 就不存在失败了抛异常,我们说 malloc 会失败,但没有说 free 会失败 (free 的失败是对越界的空间 free 等),free 失败就不是说抛异常或返回空这样的概念了,这种是属于比较严重的错误,它是直接中止掉程序

operator new 和 operator delete 源码 /* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否 则抛异常。 */

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的。

operator new 就是对 malloc 的封装,目的就是如果申请内存失败了抛异常:

new = 封装 malloc + 失败抛异常 + 调用构造函数 operator delete 就是对 free 的封装,目的主要还是和 operator new 对应起来

delete = 调用析构函数 + operator delete 🌟4.2 重载operator new与operator delete(了解) 注意:一般情况下不需要对 operator new 和 operator delete进行重载,除非在申请和释放空间时候有某些特殊的需求。比如:在使用new和delete申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏。

样例一:移除链表元素

//重载一个类ListNode,专属的operator new: struct ListNode { int val; struct ListNode* next; static int _count;//统计 ListNode(int x) : val(x) , next(nullptr) {} //new _count就++,delete _count就-- void* operator new(size_t n) { ++_count; return ::operator new(n);//::代表是全局的 } void operator delete(void* p) { --_count; return ::operator delete(p);//::代表是全局的 } };

int ListNode::_count = 0; struct ListNode* removeElements(struct ListNode* head, int val) { struct ListNode* prev = NULL, cur = head; while(cur) { if(cur->val == val) { prev->next = cur->next; delete cur; cur = prev->next; } else { prev = cur; cur = prev->next; } } return head; } int main() { ListNode n1 = new ListNode(1); ListNode* n2 = new ListNode(2); ListNode* n3 = new ListNode(2); ListNode* n4 = new ListNode(3); ListNode* n5 = new ListNode(4); ListNode* n6 = new ListNode(2); n1->next = n2; n2->next = n3; n3->next = n4; n4->next = n5; n5->next = n6; n6->next = nullptr;

ListNode* list = removeElements(n1, 2);
cout << "没有释放的节点数量:" << ListNode::_count << endl;

return 0;

}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 注意:这里的重载和函数重载不一样!当 new ListNode 时,那么申请空间就会调用专属的 operator new 和 operator delete ,了解一下即可,用处不是很大

样例二:

// 重载operator delete,在申请空间时:打印在哪个文件、哪个函数、第多少行,申请了多少个 字节 void* operator new(size_t size, const char* fileName, const char* funcName, size_t lineNo) { void* p = ::operator new(size); cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << "-" << size << endl; return p; } // 重载operator delete,在释放空间时:打印再那个文件、哪个函数、第多少行释放 void operator delete(void* p, const char* fileName, const char* funcName, size_t lineNo) { cout << fileName << "-" << funcName << "-" << lineNo << "-" << p << endl; ::operator delete(p); } int main() { // 对重载的operator new 和 operator delete进行调用 int* p = new(FILE, FUNCTION, LINE) int; operator delete(p, FILE, FUNCTION, LINE); return 0; } // 上述调用显然太麻烦了,可以使用宏对调用进行简化 // 只有在Debug方式下,才调用用户重载的 operator new 和 operator delete #ifdef _DEBUG #define new new(FILE, FUNCTION, LINE) #define delete(p) operator delete(p, FILE, FUNCTION, LINE) #endif int main() { int* p = new int; delete(p); return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ✨5. new和delete的实现原理 🌟5.1 内置类型 如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是: new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。

🌟5.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 来释放空间 使用场景:

class Stack { public: Stack(int capacity = 4) : _a(new int[capacity]) , _size(0) , _capacity(capacity) { cout << "Stack(int capacity = 4)" << endl; } ~Stack() { delete[] _a; _size = _capacity = 0; cout << "~Stack()" << endl; } private: int* _a; int _size; int _capacity; }; int main() { //1 Stack st;

//2
Stack* ps = new Stack;
delete ps;

return 0;

}

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 ✨ 6. 定位new表达式(placement-new) (了解) 定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

使用格式: new (place_address) type或者new (place_address) type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表

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

class A { public: A(int a = 0) : _a(a) { cout << "A():" << this << endl; } ~A() { cout << "~A():" << this << endl; } private: int _a; }; // 定位new/replacement new int main() { // p1现在指向的只不过是与A对象相同大小的一段空间,还不能算是一个对象,因为构造函数没有执行 A* p1 = (A*)malloc(sizeof(A)); new(p1)A; // 注意:如果A类的构造函数有参数时,此处需要传参 p1->~A(); free(p1); A* p2 = (A*)operator new(sizeof(A)); new(p2)A(10); p2->~A(); operator delete(p2); return 0; }

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 ✨ 7. 常见面试题 🌟7.1 malloc/free和new/delete的区别 malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:

malloc和free是函数,new和delete是操作符 malloc申请的空间不会初始化,new可以初始化 malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可 malloc的返回值为void*, 在使用时必须强转,new不需 ————————————————