C/C++内存管理
前言
环境:vs2019
一、C/C++中程序的内存分布
1 栈:
又叫堆栈,向下增长(高地址向低地址增长),非静态局部变量、函数参数、返回值等。
2 内存映射段:
是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存做进程间通信。
3 堆:
用于程序运行时动态内存分配,堆是可以向上增长(低地址向高地址增长)的。
4 数据段:
存储全局数据、静态数据。
5 代码段:
可执行的代码、只读常量。
二、C/C++的内存管理方式
1 C语言中动态内存管理方式:malloc/calloc/realloc/free
(1)malloc
void* malloc(unsigned int num_bytes);
num_byte:申请的空间大小,需手动进行计算。
//int大小为4的情况下,申请连续的40byte的空间
int* ptr1=(int*)malloc(10*sizeof(int));
★ malloc向内存申请一块连续可用的空间,并返回指向这块空间的指针。
★ 如果开辟成功,则返回一个指向开辟好空间的指针。
★ 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
★ 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
(2)calloc
void* calloc(size_t n,size_t size);
n:申请的空间大小,不需要计算。
//申请20byte的空间
int* ptr2=(int*)calloc(20,sizeof(int));
malloc在申请空间后,其空间内的内容是随机的;但calloc在申请空间后会将空间内容全部进行初始化为0。
(3)realloc
void realloc(void* ptr,size_t new_size);
realloc用于对动态内存空间进行扩容,即当已申请的动态空间不够用的时候进行扩容,ptr表示指向原来空间首地址的指针,new_size为需扩容容量的大小。返回值为调整之后的内存起始位置。
(4)free
void free(void* ptr);
直接将申请的空间进行释放,ptr为指向要释放空间首地址的指针。
★ 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
★ 如果参数 ptr 是NULL指针,则函数什么事都不做。
2 C++内存管理方式:new/delete
C++兼容了C语言的特性,此外,C++还提供了自己的内存管理方式:new和delete操作符进行动态内存管理。
2.1 new/delete操作对象为内置类型。
void Test1()
{
//1.动态申请一个int类型的空间
int* ptr1 = new int;
//2.动态申请一个int类型的空间并初始化为2
int* ptr2 = new int(2);
//3.动态申请5个int类型的空间
int* ptr3 = new int[5];
//4.动态申请5个int类型的空间并初始化
int* ptr4 = new int[5]{ 1,2,3,4,5 };
delete ptr1;
delete ptr2;
delete[] ptr3;
delete[] ptr4;
}
申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[]。
2.2 new/delete操作对象为自定义类型。
class A
{
public:
A(int a=0)
:_a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A()" << this << endl;
}
private:
int _a;
};
void Test2()
{
//1. malloc & free
A* ptr1 = (A*)malloc(sizeof(A));
free(ptr1);
//2. new & delete
A* ptr2 = new A();
delete ptr2;
//3. malloc/free & new/delete 操作内置类型
int* ptr3 = (int*)malloc(sizeof(int));
free(ptr3);
int* ptr4 = new int;
delete ptr4;
//4.
A* ptr5 = (A*)malloc(sizeof(A) * 10);
free(ptr5);
A* ptr6 = new A[10];
delete[] ptr6;
}
(1)malloc操作自定义对象
malloc申请空间后,其空间内容是一个随机值。
(2)new操作自定义对象
申请内置类型的数据空间时,new和malloc并无不同;在申请自定义空间时,new会调用构造函数,delete会调用析构函数,而malloc和free不会。
这也是为何C++中未继续使用malloc/free,而是重新提供了new/delete。
C++是基于面向对象的程序设计语言,malloc只负责从堆上申请空间,并不会调用构造函数给对象空间进行初始化,free也只会将堆空间释放掉,在释放时也不会调用析构函数清理对象中的资源。
new操作符,在申请空间之后,会调用构造函数将空间中内容进行初始化;delete操作符,在释放空间前,会调用析构函数将对象中的资源进行清理掉。
三、new和delete的实现原理
1 内置类型
若申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
2 自定义类型
2.1 new的实现原理
- 调用operator new函数申请空间。
- 调用T类中对应的构造函数对申请空间中的内容进行初始化。
new的使用:
T* p=new T;
step1:调用operator new函数申请空间。
class B
{
public:
B(int b = 0)
:_b(b)
{
cout << "B():" << this << endl;
}
~B()
{
cout << "~B()" << this << endl;
}
private:
int _b;
};
void Test3()
{
B* ptr1 = new B(10);
//...
delete ptr1;
}
在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);
}
其中,size表示要申请空间总的字节数,而方法需要的size参数是编译器编译阶段根据new之后的类型确定下来并传递给函数作为函数参数进行相关处理。
sizeof(B)=4,所以申请空间总字节数为4。
实际上operator new函数内部也是通过malloc来申请空间,即,函数实际上只是对malloc进行了一层封装。
operator new方法内部实现原理: 封装malloc,借助malloc来循环申请空间。
- 若malloc申请空间成功,则直接返回
- 若malloc申请空间失败(内存空间不足)
- 检测函数是否提供内存不足的应对措施,若提供了的话,就执行应对措施然后继续重新malloc空间;若没有提供的话,就抛出bad_alloc异常。
step2:调用T类中对应的构造函数对申请空间中的内容进行初始化。
构造函数不负责给对象开辟空间,只负责将空间中的成员初始化。
★ 对于栈上的对象,栈上的对象存储在系统给函数分配的栈帧中,编译器在编译的时候,函数栈帧总的大小已经计算出来,当程序运行时,系统将栈帧分配分配好,对象的空间已经存在了,程序执行到创建对象的位置时,只需要调用构造方法将对象中的成员初始化完成即可。
★ 对于堆上的对象,调用operator new函数申请空间,调用构造函数,完成空间中成员的初始化。
所以new申请空间要么成功,要么抛出异常,所以new申请的空间不需要判空。
2.2 delete的实现原理
- 空间上执行析构函数,完成对象中资源的清理工作。
- 调用operator delete函数释放对象的空间。
delete的使用:
delete p;
step1:空间上执行析构函数,完成对象中资源的清理工作。
step2:调用operator delete函数释放对象的空间。
编译器生成B::`scalar deleting destructor’函数,在函数中又调用 B::~B (090152Dh) 和 operator delete (09010A5h) 。
operator delete函数:
//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));
//malloc申请空间,free释放空间
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
★ 对于栈上的对象,当出了函数作用域,即函数执行一结束,系统就会将给函数分配的栈帧进行回收,栈的空间也就被自动回收了,之后调用析构函数清理对象资源。
★ 对于堆上的对象,调用析构函数清理对象资源,operator delete函数释放对象空间。
2.3 new T[N]的原理的实现原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请。
- 在申请的空间上调用N次构造函数对空间中每个对象进行初始化。
new T[N]:
T* p=new T[N];
调用operator new[]函数来进行空间的申请,实际上operator new[]函数内部是调用了operator new函数。N个对象的空间申请完后,再调用N次构造函数完成对象的初始化工作。
2.4 delete[]的实现原理
- 调用N次析构函数将各个对象中的资源清理干净。
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间。
delete[]:
delete[] p;
3 重载operator new与operator delete
一般情况下不需要对 operator new 和 operator delete进行重载,除非在申请和释放空间时候有某些特殊的需求。比如:在使用new和delete申请和释放空间时,打印一些日志信息,可以简单帮助用户来检测是否存在内存泄漏。
五、内存泄漏
1 内存泄漏
内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存
漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误
失去了对该段内存的控制,因而造成了内存的浪费。
2 内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
3 内存泄漏分类
堆内存泄漏(Heap leak):
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。
系统资源泄漏:
指程序使用系统分配的资源,如套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
4 内存泄漏检测
在vs下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks()
函数进行简单检测,该函数只会报出了大概泄漏了多少个字节,没有其他更准确的位置信息。
5 内存泄漏预防
采用RAII思想或者智能指针来管理资源。智能指针等事前预防;泄漏检测工具进行事后查错。
总结
C/C++中内存泄漏是非常常见的,这就要求我们有良好的代码习惯,对于申请的内存空间要匹配的去进行释放,采用RAII思想或智能指针来进行资源的管理。