一、C/C++内存分布
先看一下下面这段代码:
int globalVar = 1; static int staticGlobalVar = 1; void Test() { static int staticVar = 1; int localVar = 1; int num1[10] = { 1, 2, 3, 4 }; char char2[] = "abcd"; const char* pChar3 = "abcd"; int* ptr1 = (int*)malloc(sizeof(int) * 4); int* ptr2 = (int*)calloc(4, sizeof(int)); int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4); free(ptr1); free(ptr3); } //1. 选择题: // 选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区) // (1)globalVar在哪里?____ // (2)staticGlobalVar在哪里?____ // (3)staticVar在哪里?____ // (4)localVar在哪里?____ // (5)num1 在哪里?____ // (6)char2在哪里?____ // (7)*char2在哪里?___ // (8)pChar3在哪里?____ // (9)*pChar3在哪里?____ // (10)ptr1在哪里?____ // (11)*ptr1在哪里?____ //3. sizeof 和 strlen 区别?
补充说明:
- 栈又叫堆栈–非静态局部变量/函数参数/返回值等等都存在栈区,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统调用接口创建共享内存,叫做进程间通信。
- 堆用于程序运行时动态内存分配,堆是向上增长的。
- 数据段(静态区)–存储全局数据和静态数据。
- 代码段(字符常量区)–可执行的代码/只读常量。
二、 C语言中的动态内存管理方式
C语言中的动态内存管理方式:malloc/calloc/realloc/free
int main() { //malloc动态开辟10个int的空间(仅开辟空间,不进行初始化) int* p1 = (int*)malloc(sizeof(int) * 10); //calloc动态开辟10个int的空间(开辟空间+初始化(VS2019中默认初始化为0)) int* p2 = (int*)calloc(10, sizeof(int)); //realloc调整已开辟的空间的大小,常用于增容 //如果p1是nullptr,那么realloc就等于malloc int* p3 = (int*)realloc(p1, sizeof(int) * 20); free(p2); //这里不需要再释放p1,因为realloc已经把p1释放了 free(p3); return 0; }
问题:malloc/calloc/realloc的区别?
malloc和calloc的唯一区别就是malloc只开空间,不做初始化;calloc不仅开空间,而且会进行初始化(VS会初始化成0)。
realloc是对已有空间进行扩容的函数。
三、 C++动态内存管理方式
C语言内存管理方式在C++中可以继续使用(C++兼容C语言),但有些地方就无能为力,而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1 new/delete操作内置类型
int main() { //动态申请1个int的空间 int* p1 = new int; //动态申请1个int空间并初始化为10(可以自定义初始化成任何值) int* p2 = new int(10); //动态申请5个int空间 int* p3 = new int[5]; //释放资源 delete p1; delete p2; delete[] p3; return 0; }
值得注意的是:
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:必须要匹配起来使用,否则可能会出问题。
3.2 new和delete操作自定义类型
class A { public: A(int a = 10) :_a(a) {} ~A() {} private: int _a; }; int main() { // new/delete 和 malloc/free最大区别是 new/delete对于 // 【自定义类型】除了开空间还会调用构造函数和析构函数 A* p1 = new A(5); delete p1; return 0; }
值得注意的是:在申请自定义类型的空间时,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
四、operator new与operator delete函数(重点)
4.1 operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,是提供给new和delete使用的,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
operator new:实际也是通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果该应对措施用户设置了,则继续申请,否则就抛异常。
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; while ((p = malloc(size)) == 0) if (_callnewh(size) == 0) { // report no memory // 如果申请内存失败了,会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); }
operator delete: 实际上最终也是通过free来释放空间的。
void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData, 0)); if (pUserData == NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead = pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)); _free_dbg(pUserData, pHead->nBlockUse); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; } /*通过free的实现的*/ #define free(p) _free_dbg(p, _NORMAL_BLOCK)
operator new与operator delete是开空间和释放空间的函数,在我们使用new/new[]申请空间时,首先会调用operator new开辟空间,然后调用构造函数初始化;使用delete/delete[]释放空间时,会先去调用析构函数清理资源,再去调用operator delete释放空间。这些函数的调用顺序是不能够颠倒的。(重要)
4.2 重载operator new与operator delete
其实一般情况下我们都不需要对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); } // 只有在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; }
五、new和delete的实现原理
5.1 内置类型
对于内置类型:new/delete和malloc/free基本上是类似的,不同的是:new/delete是申请和释放单个空间,new[]/delete[]是申请和释放多个连续的空间,申请空间失败会抛异常;而malloc申请失败会返回nullptr。
5.2 自定义类型
new/delete原理:
当我们调用new函数的时候,编译器实际是调用了两个函数:
1、调用operator new申请空间。
2、调用构造函数初始化。
当我们调用delete函数的时候,编译器也调用了两个函数:
1、调用析构函数清理资源。
2、调用operator delete释放空间。
new[N]/delete[]原理:
调用new[T]:
1、调用operator new[]函数申请资源,实际上是在operator new[]函数里面完成N个对象的空间的申请。
2、调用N次构造函数初始化申请的N个对象空间。
调用delete[]:
1、调用N次析构函数清理N个对象空间申请的资源。
2、调用operator delete[]释放空间,实际上是在operator delete[]函数里面完成对N个对象空间的释放。
六、定位new(placeman-new)
定位new表达式是在已经分配的原始空间上调用构造函数初始化出一个对象。
使用格式:
new (place_address) type或者new (place_address) type{initializer-list}
place_address必须是一个指针,initializer-list是类型的初始化列表。
class A { public: A(int a = 10) :_a(a) { cout << "A(int a = 10)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { //先开100个int对象的空间 int* p1 = (int*)malloc(sizeof(int)*100); //对已经分配了的100个int的空间的靠前的一段空间调用构造 //函数初始化出10个A对象,并把前4个初始化成1,2,3,4 new(p1) A[10]{ 1,2,3,4 }; free(p1); return 0; }
使用场景:
定位new一般是和内存池配合使用的,因为内存池分配出来的空间是没有初始化的,所以如果是自定义类型的对象,需要使用定位new调用构造函数初始化。
class A { public: A(int a = 10) :_a(a) { cout << "A(int a = 10)" << endl; } ~A() { cout << "~A()" << endl; } private: int _a; }; int main() { /*A* p1 = (A*)malloc(sizeof(A)); new(p1) A; p1->~A(); free(p1);*/ //先调用operator new开辟一个A对象的空间 A* p2 = (A*)operator new(sizeof(A)); //使用定位new调用构造函数初始化 new(p2)A(10); //显式调用析构函数清理资源 p2->~A(); //调用operator new释放空间 operator delete(p2); return 0; }
七、 malloc/free和new/delete的区别(重要)
malloc和new的相同点,都是在堆上申请空间的,都需要手动释放空间。
用法上:
1、malloc开辟空间时需要手动计算开辟的空间的大小(以字节为单位),返回值时void*,需要强制类型转换。new不需要手动计算开辟的空间的大小,只需要在后面跟上空间的类型即可,如果是多个对象,那么[]中指定对象的个数即可,编译器会自动计算大小。
2、malloc只开辟空间,不初始化;new会先调用operator new开辟空间,然后调用构造函数初始化对象。delete会先调用析构函数清理资源,再调用operator delete释放空间。
底层:
1、malloc/free是库函数,new/delete是操作符(关键字)。
2、malloc申请空间失败会返回nullptr,new申请空间失败会抛异常。
八、内存泄漏
8.1 内存泄露的分类
C/C++程序中内存泄漏一般分为两种:
一种是堆内存泄露,另外一种是系统资源泄漏。
堆内存泄露:
指的是程序执行中通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄露(Heap Leak)。
系统资源泄露:
指的是程序使用系统分配的资源,例如:套接字、文件描述符、管道等使用完之后没有使用对应的函数释放,导致系统资源的浪费,严重的话可导致系统效能减少,系统执行不稳定,就叫做系统资源泄露。
所以我们在写程序的时候,动态申请的内存在使用结束后一定要记得释放,以防内存泄漏。但是毕竟人都是会犯错的,谁都不能百分百保证自己不会忘记释放,那么在一些大项目中出现内存泄露的地方比较多的时候我们就需要借助第三方内存泄露检测工具处理了。
8.2 如何避免内存泄漏
- 养成良好的设计规范,养成良好的编码习惯,申请的内存空间尽量记得去匹配释放。但是如果碰上异常时,就算释放了,还是可能会出问题。所以需要用一个智能指针来管理才有保证。
- 采用RAII(resource application is init),资源申请即刻初始化的思想或者智能指针来管理资源。
- 有些公司内部会规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
- 如果真的遇到问题的话就只能使用内存泄漏工具检测了。
总之就是一句话,尽量做到谁申请,谁释放;申请了尽量记得释放才是避免内存泄漏的最好的方法。