C/C++的内存分布
- 每个进程都有这样的虚拟内存分布方式;
- C++中的
new
与new[]
都是在堆上开辟(同C语言的malloc、calloc、realloc ...
); - OS之所以将完整的内存划分为这样不同的区域,是为了方便对内存空间的管理;
C语言中动态内存管理
- C标准函数库中,常见的堆上内存管理函数有
malloc()
,calloc()
,recalloc()
,free()
; - 注意这些函数使用是大多需要类型强转;
- C语言动态内存管理所有接口的头文件
<stdlib.h>
; - 堆上的内存空间不会自动释放,直到调用free()函数,才会释放堆上的存储空间;
申请堆空间
malloc:在堆上直接分配 n 字节大小的空间
- 原型:
void * malloc(int n);
- 含义:在堆上,分配n个字节,并返回void指针类型;
- 返回值:
分配成功:返回堆空间首地址;
分配失败:返回NULL;
calloc:在堆上,分配n*size个字节,并初始化为 0
- 原型:
void *calloc(int n, int size);
- 含义:在堆上,分配 n*size 个字节,并初始化为0,返回void* 类型;
- 返回值:
分配成功:返回堆空间首地址;
分配失败:返回NULL;
recalloc:重新分配堆上的 void指针p 所指的空间为 n 个字节
- 原型:
void * realloc(void * p,int n);
- 含义:重新分配堆上的void指针p所指的空间为n个字节,同时会复制原有内容到新分配的堆上存储空间。
注意,若原来的void指针p在堆上的空间不大于n个字节,则保持不变; - 返回值:
分配成功:返回堆空间首地址;
分配失败:返回NULL;
释放堆空间——free
- 原型:
void free (void * p);
- 含义:释放void指针p所指的堆上的空间。
- 返回值:无
- 任何通过malloc、calloc、realloc开辟的堆空间,在使用结束前必须使用
free()
接口释放内存,否则会发生内存泄漏;
malloc实现原理
// 有时间补充代码展示
free函数如何知道释放多少个空间?
- 因为在堆上开辟的空间会多余一部分用来存储这块空间的信息,即堆空间的管理结构体;
- 释放时会偏移的结构体的首地址释放空间;
C++中动态内存管理
- C++的动态内存管理方式完全兼容C语言的所有方式;
- C++特有方式:
new
申请堆空间、delete
释放堆空间,两个关键字(也可以看作是操作符,但不是函数);
new/delete
和malloc/free
的区别&联系:
malloc/free
是C/C++标准库的函数,new/delete
是C++操作符(操作符的使用不需要头文件);new
直接跟类型,malloc
跟字节数个数;new
申请出堆空间后不需要判空,malloc
必需判空(具体原因下面会说);malloc/free
需要手动计算类型大小且返回值为void*
,new/delete
可自己计算类型的大小并返回对应类型的指针;new
申请空间时可以顺便将对象初始化为任意值,calloc
只能将每个字节初始化为0;new
和delete
就像malloc
和free
一样,都要成对使用;- C语言与C++特有的动态管理内存的方式,开辟与释放不能混用(若不匹配使用,可能会造成内存泄漏或程序崩溃)(具体原因下面会说);
- 一般情况下,在C++中使用其特有的方式管理动态内存(更简单、更强大);
new/delete 动态开辟普通变量
new[ ]/delete[ ] 动态申请一段连续的空间
- 注意在堆上开辟二维数组,不能直接这样定义
int **array = new int[m][n];
new[]
与delete[]
也要成对使用;
new/delete 动态开辟自定义类型对象
这里有一个类,我们想在堆上开辟该类对象
// new & delete 自定义对象
class Test
{
public:
// 无参构造函数
Test()
{
cout << "无参构造函数,this:" << this << endl;
}
// 带参构造函数
Test(int a) :_a(a){}
// 析构函数
~Test()
{
cout << "析构函数,this:" << this << endl;
}
private:
int _a;
};
【测试 1】采用malloc
之类的C语言方式在堆上开辟类对象,由于是C语言标准库的函数,没有类与对象这类自定义类型的概念,因此:
malloc
只是从堆上申请了指定大小的空间,并没有调用构造函数初始化该对象;free
只是将该内存归还给OS,并没有调用析构函数清理对象资源;
【测试 2】采用new
和delete
在堆上开辟类对象:
- 这是C++中管理动态内存的关键字,会调用
operator new
&&operator delete
函数开辟堆空间; new
会开辟对象大小的空间,并会调用构造函数初始化对象;delete
是释放对象大小空间,释放前先会调用析构函数清理对象空间;- 注意new与delete的特性:先构造的后释放;
结论:C++中定义一个对象:
- 如果在栈上开辟这个对象,那么编译器在代码编译阶段直接在栈帧上会为这个局部变量开辟空间,再通过构造函数来初始化这个对象;
- 栈上释放这个对象,先通过析构函数清理对象资源,再通过编译器回收栈帧空间;
- 如果在堆上开辟这个对象,
new
操作符先通过调用operator new()
函数在堆上开辟空间,再调用构造函数
初始化该空间; - 堆上释放这个对象,
delete
操作符先通过析构函数
清理对象资源,在调用operator delete
函数回收堆上空间;
new和delete的实现原理
new
和delete
既是一个关键字,也是一个操作符,那么就可以重载;operator new()
与operator delete()
函数是系统提供的全局重载函数;new
在底层通过调用operator new
全局函数来申请空间;delete
在底层通过调用operator delete
全局函数来释放空间;- 如果堆上申请的是自定义类对象,
new
在调用operator new
之后还会调用构造函数初始化对象;delete
会先调用析构函数销毁对象后,再调用operator delete
;
注:
- 本博客代码测试环境均在VS2013,若版本更高,operator new函数文件可能进一步封装,而无法轻易观测;
- 下列函数的源码是通过VS安装目录的源文件找到,实际代码调试并不会直接进入;
operator new()
与operator delete()
函数
operator new[]()
与operator delete[]()
函数
new / delete 底层流程
定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
// 定位new表达式
class Test
{
public:
// 无参构造函数
Test()
{
cout << "无参构造函数,this:" << this << endl;
}
// 带参构造函数
Test(int a) :_a(a)
{
cout << "带参构造函数,this:" << this << endl;
}
// 析构函数
~Test()
{
cout << "析构函数,this:" << this << endl;
}
private:
int _a;
};
int main()
{
// 通过malloc开辟一段对象大小的空间,但只是一个空间
Test* pt_1 = (Test*)malloc(sizeof(Test));
Test* pt_2 = (Test*)malloc(sizeof(Test));
// 通过定位new表达式为指定空间初始化对象
new(pt_1)Test; //调用无参构造函数
new(pt_2)Test(10); // 调用带参构造函数
// 主动调用析构
pt_1->~Test();
free(pt_1);
pt_2->~Test();
free(pt_2);
return 0;
}
对于堆空间的管理,经常容易引发内存泄漏问题,这对于编程是很难避免的。但仍有一系列方法可以检测和避免内存泄漏,具体内容详见后续博客~