前言
我们最初熟知的内存开辟方式:
int val = 20: 在栈空间上开辟4个字节 char array[10]: 在栈空间上开辟10个字节的连续空间上述开辟空间的方式有两个特点:
空间开辟大小是固定的。 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。但是对于空间的需求,不仅仅是上述的情况,有时候我们需要的空大小在程序运行时才能知道,那此时静态的开辟空间的方式就不能满足了,我们这时候只能试试动态内存开辟。
这篇博客就来带大家梳理一下C/C++中的内存管理。
一:C/C++内存分布
对内存分段是计算机的管理机制
1.栈又叫堆栈,存放非静态局部变量、函数参数和返回值等等,栈是向下增长的。,处理器的指令集中、效率高,但是分配内存的容量有限。(函数执行结束后这些存储单元自动释放)
2.内存映射段是高效的IO映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。
3.堆用于程序运行时动态内存分配,堆是向上增长的。(一般由人为分配释放,若没有人为释放则程序结束时可能由OS回收。)
4.数据段存储全局数据、静态数据。(程序结束后由系统自动释放)
5.代码段存储可执行的代码、只读常量。
注意:
栈区向下生长,先开辟的空间地址大于后开辟的空间地址。(int a = 10,int b = 20,&a>&b)
堆区向上生长,但是不保证后开辟的空间地址大于先开辟的空间地址,因为堆区存在人为的空间释放。
二:C语言中的内存管理方式
C语言提供了动态内存函数来进行内存的动态开辟工作:malloc、calloc、realloc、free
2.1 malloc
函数功能
void✳ malloc(size_t size以字节为单位的空间大小)
举个栗子:int* ptr = (int*) malloc(sizeof(int)*10);
malloc向内存申请一块大小为size的连续可用空间,并返回指向这块空间的指针。
函数特性
1.开辟成功,返回一个指向该空间的指针。
2.开辟失败,返回一个NULL指针,因此malloc的返回值一定要做检查。
3.返回值的类型是void✳,malloc函数并不知道开辟空间的数据类型,具体在使用的时候由使用者自己决定。
4.如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
2.2 calloc
函数功能
void✳ calloc(size_t num元素个数,size_t size以字节为单位的空间大小)
举个栗子:int* ptr = calloc(10,sizeof(int));
calloc向内存为num个大小为size的元素开辟一块连续空间,并且把空间的每个字节都初始化为0。
函数特性
1.开辟成功,返回一个指向该空间的指针。
2.开辟失败,返回一个NULL指针,因此calloc的返回值一定要做检查。
3.返回值的类型是void✳,calloc函数并不知道开辟空间的数据类型,具体在使用的时候由使用者自己决定。
4.calloc会在返回地址之前把申请的空间每个字节都初始化为0(calloc适用于对申请空间的内容要求初始化的情况)
注意:对申请的空间初始化并不完全是好的事情,当我们要申请一个特别大的空间时,初始化会浪费很多很多的时间。
2.3 realloc
函数功能
void✳ realloc(void✳ ptr要调整的内存地址,size_t size调整之后的空间大小)
举个栗子:int* p = NULL; p = realloc(ptr,1000); if(p!=NULL)-> ptr = p;
realloc可以对动态开辟的内存空间大小进行灵活调整。
函数特性
1.返回值为调整之后内存空间的起始位置。
2.realloc在调整原内存空间大小的基础上,还会将原内存空间中的数据移动到新的空间。
realloc在调整内存空间时存在的两种情况
情况一:原有空间之后有足够大的空间
直接在原有内存空间之后追加空间,原来空间的数据不发生变化
情况二:原有空间之后没有足够大的空间
在堆空间上重新找一块合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
常见的动态内存错误
1、对NULL指针的解引用操作。
2、对动态开辟空间越界访问。
3、对非动态内存使用free释放。
4、释放一块动态开辟内存的一部分。
5、对同一块内存多次释放。
6、动态开辟内存忘记释放。
以上的错误都是十分常见的,因此我们在对内存进行操作的时候一定要万分小心。
典型内存泄漏的例子
int main(){ int* p = (int*)malloc(sizeof(int)); p = (int*)malloc(sizeof(int)); free(p); p = NULL; }
这个例子中我们明明进行了释放却也造成了内存泄漏,这是因为我们申请了两次内存空间,但是用同一个指针来接收,只释放了一次,因此造成了内存的泄漏。
进行动态的内存分配后一定不能忘记在使用完毕后将内存空间释放,并且将指针赋值为NULL,这一点是十分关键的,否则将造成内存泄漏和野指针,对程序造成很大的影响。
三:C++中的内存管理方式
C语言内存管理方式在C++中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此C++又提出了自己的内存管理方式:通过new和delete操作符进行动态内存管理
在C++中我们使用new进行内存的申请,用delete进行内存的释放。
3.1 内置类型的内存分配与释放
new和malloc一样会在堆上开辟空间同时需要我们手动进行内存的释放,但是new的写法更加简单易于理解同时我们还可以对单个申请的变量进行初始化。
举个栗子帮助理解
#include <iostream> using namespace std; int main(){ int* a = new int;//等同于int* a = (int*)malloc(sizeof(int)); int* b = new int[10];//等同于int* b = (int*)malloc(sizeof(int) * 10); int* c = new int(10);//new还可以进行内置类型的初始化 cout << *c << endl; delete a;//等同于free(a); delete[] b;//等同于free(b);(对于多个变量的空间释放要用delete[]) delete c;//等同于free(c); return 0; }
3.2 自定义类型的内存分配和释放
针对自定义类型的内存分配和释放,new不但可以在分配内存的时候手动调用指定的构造函数还会在分配多个对象的空间时自动调用默认构造函数,delete也会自动调用析构函数,而malloc和free却做不到这一点。因此可以理解为malloc和free分配出来的只不过是一个和类一样大小的空间,并不能称作是一个对象,而new和delete分配出来的才能被成为对象。
#include <iostream> #include <stdlib.h> using namespace std; class Stu{ public: Stu(){ cout << "default building" << endl; } Stu(int num, string name):_num(num), _name(name){ cout << "custom building" << endl; } ~Stu(){ cout << "destroying" << endl; } private: int _num; string _name; }; int main(){ cout << "malloc:" << endl; Stu* a = (Stu*)malloc(sizeof(Stu)); cout << "new:" << endl; Stu* b = new Stu(1, "张三"); cout << "malloc:" << endl; Stu* c = (Stu*)malloc(sizeof(Stu) * 5); cout << "new:" << endl; Stu* d = new Stu[5]; cout << "free:" << endl; free(a); cout << "delete:" << endl; delete b; cout << "free:" << endl; free(c); cout << "delete:" << endl; delete[] d; }
运行结果:
malloc:
new:
custom building
malloc:
new:
default building
default building
default building
default building
default building
free:
delete:
destroying
free:
delete:
destroying
destroying
destroying
destroying
destroying
3.3 new和delete的实现原理
new和delete在C++中其实被定义为两个运算符,我们在使用这两个运算符的时候它会在底层调用全局函数operator new和operator delete。
operator new
operator new在底层实现的源代码
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
operator delete在底层实现的源代码
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; }
从源码中能看出的是operator new和operator delete在底层也是利用malloc和free分配内存的,因此可以说new和delete不过是malloc和free的一层封装。
针对内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
针对自定义类型
1.new的原理: 调用operator new申请空间,调用构造函数完成初始化。
2.delete的原理: 调用析构函数完成清理,调用operator delete释放空间。
四:经典面试题
new | delete和malloc | free的相同点和不同点
相同点:
new、delete、malloc、free都是从堆上开辟空间,并且需要用户手动释放。
不同点:
1.new和delete是操作符,malloc和free是函数。
2.malloc申请空间不会进行初始化,new申请空间可以初始化。
3.malloc申请空间失败返回NULL,new申请空间失败会抛出异常。
4.针对自定义类型,new和delete会自动调用构造函数和析构函数处理。
五:内存泄漏
概念:内存泄漏指因为疏忽或错误造成程序已经不再使用的内存没有被释放的情况。
危害:长期运行的程序出现内存泄漏,会浪费空间,如操作系统、后台服务等等,出现内存泄漏会
导致响应越来越慢,最终卡死。
举个栗子帮助理解:
void MemoryLeaks(){ // 1.内存申请了忘记释放 int* p1 = (int*)malloc(sizeof(int)); int* p2 = new int; // 2.异常安全问题 int* p3 = new int[10]; Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放. delete[] p3; }
5.1 内存泄漏的分类
堆内存泄漏
程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,
用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生堆内存泄漏。
系统资源泄漏
程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统
资源的浪费,严重可导致系统效能减少,系统执行不稳定,产生了系统资源泄露。
5.2 如何检测内存泄露
在linux下内存泄漏检测
valgrind、mtrace、dmalloc、memwatch、mpatrol、dbgmem、Electric Fence
在windows下内存泄漏检测
VLD
5.3 如何避免内存泄漏
1.工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。
2.采用RAII思想或者智能指针来管理资源。
5.4 如何在堆上一次申请4G空间
原因:申请失败一般是因为进程地址空间不够大。
解决办法:换用64位的进程地址空间。