目录
一、C/C++内存分布
1.1不同变量的存储位置和大小
1.2C/C++内存分布示意图
1.3栈
1.4堆
1.5内存映射段
1.6虚拟内存 | 物理内存
1.7内存区域特点
二、C语言中动态内存管理方式
2.1 malloc/calloc/realloc和free
2.2 malloc/calloc/realloc的区别
三、C++内存管理方式
3.1new 和 delete 操作符进行动态内存管理
3.2 new和delete操作自定义类型
3.3new delete的种特性有什么用 ?
3.4如果不匹配使用会怎么样?
四、operator new与operator delete函数 —— 重点
4.1 operator new与operator delete函数
4.2 为什么要去调用 operator new 而不是调用其它的呢 ?
4.3 抛异常
4.4operator new 和 operator delete 源码
4.5直接使用 operator new 和 operator delete
五、new和delete的实现原理
5.1 内置类型
5.2 自定义类型
5.2.1new 的原理:
5.2.2delete 的原理:
5.2.3new T[N] 的原理:
5.2.4delete[] 的原理:
5.3malloc/free new/delete 对比
5.4场景使用
六、定位new表达式(placement-new)
6.1概念
6.2使用场景:
6.3malloc/free和new/delete的区别
七, 内存泄漏
7.1什么是内存泄漏,以及内存泄漏的危害
7.2内存泄漏的分类
7.3如何检测内存泄漏
7.4如何避免内存泄漏
7.5 如何一次在堆上申请4G的内存
int globalVar = 1; //全局变量
static int staticGlobalVar = 1; //static 修饰的全局变量
void Test()
{
static int staticVar = 1; //static 修饰的 局部变量
int localVar = 1; // 局部变量
const int localVal1 = 1; //const 修饰的局部变量
int num1[10] = {1, 2, 3, 4}; // 整形数组
char char2[] = "abcd"; //字符型数组
char* pChar3 = "abcd"; //指针
int* ptr1 = (int*)malloc(sizeof (int)*4); //堆上申请4个字节大小空间
int* ptr2 = (int*)calloc(4, sizeof(int)); //堆上申请空间
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4); //堆上申请空间
free (ptr1); //释放空间ptr1
free (ptr3); //释放空间ptr3
}
位置
globalVar 存储于数据段(静态局) 全局变量
staticGlobalVar 存储于数据段(静态区) static修饰的全局变量
staticVar 存储于数据段(静态区) static修饰的局部变量
localVar 存储于栈 函数内局部变量
localVar1 存储于栈 const修饰的局部变量
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
注意这里比较容易出错的是:
栈又叫堆栈,注意区分数据结构中的栈
函数调用建立栈帧,函数中的参数、局部变量都存在栈帧中
栈是向下增长的,比如 main 函数调用 f 函数:
注意区分数据结构中的堆
malloc、calloc、realloc 都会在堆上开辟空间
堆是向上增长的,理论上后 malloc 的内存地址比先 malloc 的要大,但是也不一定,因为有可能下一次申请的是之前别人释放回来的
内存映射段是高效的 I/O 映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
虚拟内存跟物理内存要按页映射,就是对应起来。程序访问虚拟内存,实际要转到对应的物理内存。
这几个区域堆是很大的 —— 你可以认为如果在 32 位下虚拟内存总共占 4G ,内核占 1G,剩下的 3G 空间大部分都是堆的
从图看,栈和堆差不多大,实际上栈很小 —— Linux下一般只有 8M,所以递归深度太深,很容易导致栈溢出 (Stack overflow)
数据段和代码段也不是很大 —— 因为没有多少数据 (全局数据 + 静态数据)
p2 calloc一块空间后,p3又realloc p2,要对p2 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);
free(p3);
}
说明
这里涉及了 realloc 的开辟原理
所以对于上面程序的写法并不好:
int *p = (int *)malloc(20*sizeof(int));
int *pp = (int *)calloc(20, sizeof(int));
int i;
printf("malloc申请的空间值:\n\n");
for ( i=0 ; i < 20; i++)
{
printf("%d ", *p++);
}
printf("\n\n");
printf("calloc申请的空间的值:\n\n");
for ( i=0 ; i < 20; i++)
{
printf("%d ", *pp++);
}
printf("\n");
malloc函数。其原型void *malloc(unsigned int num_bytes);
num_byte为要申请的空间大小,需要我们手动的去计算,如int *p = (int *)malloc(20*sizeof(int)),如果编译器默认int为4字节存储的话,那么计算结果是80Byte,一次申请一个80Byte的连续空间,并将空间基地址强制转换为int类型,赋值给指针p,此时申请的内存值是不确定的。
calloc 等价于 malloc + memset(0) -> 开空间 + 初始化
calloc函数,其原型void *calloc(size_t n, size_t size);
其比malloc函数多一个参数,并不需要人为的计算空间的大小,比如如果他要申请20个int类型空间,会int *p = (int *)calloc(20, sizeof(int)),这样就省去了人为空间计算的麻烦。但这并不是他们之间最重要的区别,malloc申请后空间的值是随机的,并没有进行初始化,而calloc却在申请后,对空间逐一进行初始化,并设置值为0;
int main(void)
{
const int size = 2000;
int *p = (int *)malloc(20*sizeof(int));
int *pp = (int *)realloc(p, size*sizeof(int));
printf("原来的p_Address:%x 扩容后的pp_Address:%x \n\n", p, pp);
return 0;
}
realloc 单独使用时能实现 malloc 的效果 (不会初始化) -> 开空间 | 对 malloc/calloc 的空间扩容
realloc函数和上面两个有本质的区别,其原型void realloc(void *ptr, size_t new_Size)
用于对动态内存进行扩容(及已申请的动态空间不够使用,需要进行空间扩容操作),ptr为指向原来空间基址的指针, new_size为接下来需要扩充容量的大小。
注意:
如果size较小,原来申请的动态内存后面还有空余内存,系统将直接在原内存空间后面扩容,并返回原动态空间基地址;如果size较大,原来申请的空间后面没有足够大的空间扩容,系统将重新申请一块(20+size)*sizeof(int)的内存,并把原来空间的内容拷贝过去,原来空间free;如果size非常大,系统内存申请失败,返回NULL,原来的内存不会释放。注意:如果扩容后的内存空间较原空间小,将会出现数据丢失,如果直接realloc(p, 0);相当于free(p).
C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力而且使用起来比较麻烦,因此 C++ 又提出了自己的内存管理方式:通过 new 和 delete 操作符进行动态内存管理。
int main()
{
//库函数
int* p1 = (int*)malloc(sizeof(int));
free(p1);
//操作符/关键字
int* p2 = new int;
delete p2;
return 0;
}
说明
malloc/free 和 new/delete 有什么区别 ?
- 如果动态申请的对象是内置类型,那么 malloc / free和 new/delete 没有区别
- 如果动态申请的对象是自定义类型,那么 malloc /free和 new/delete 有区别
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A* p3 = (A*)malloc(sizeof(A));
free(p3);
A* p4 = new A;
//A* p4 = new A(10);
//A* p4 = new A(10, 20);
delete p4;
//数组
int* p5 = (int*)malloc(sizeof(int) * 10);
free(p5);
int* p6 = new int[10];
delete[]p6;
A* p7 = new A[10];//调用10次构造
delete[]p7;//调用10次析构
return 0;
}
说明
对于内置类型 malloc/free 仅仅会开空间/释放空间
对于自定义类型 new/delete 不仅仅会开空间/释放空间,还会调用构造函数和析构函数 —— 调用构造函数时还可以传参,且可以传多个参数。
注意:我们在 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;
}
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
说明
注意,一定要匹配使用:malloc ↔ free 、new ↔ delete、new 类型[] ↔ delete[]类型,否则可能会崩溃。
new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 严格来说不是 new 和 delete 的重载 (名字确实容易误导),而是系统提供的全局库函数,new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
new T:
1.申请内存 —— 调用 operator new
2.构造函数
delete T:
1.析构函数
2.释放内存 —— 调用 operator delete
int main()
{
//malloc失败返回空
char* p1 = (char*)malloc(0xffffffff);
if (p1 == NULL)
{
printf("malloc fail\n");
}
else
{
printf("malloc success:%p\n",p1);
}
//new失败抛异常
char* p2 = new char[0x7fffffff];
if (p1 == NULL)
{
printf("malloc fail\n");
}
else
{
printf("malloc success:%p\n", p1);
}
return 0;
}
因为 new 和 malloc 它们失败时,处理的方式不一样
malloc 失败了,这里的检查起作用了。
new 失败了,这里的检查没起作用,还引发了一个崩溃 —— 抛异常后没有解决。
int main()
{
try
{
char* p2 = new char[0x7fffffff];//出错,抛异常,它会跳到捕获异常的位置
}
catch(const exception& e)
{
cout << e.what() << endl;
}
return 0;
}
这里就可以看到 new 和 malloc 处理的方式不一样,毕竟一个是面向过程,一个是面向对象
关于什么是异常后面我们会具体学习
所以再看 new 的底层的实现,new 的底层申请内存时是不能让 malloc 去完成的,因为 malloc 失败就直接返回空了,就无法达到让它失败后抛异常的机制,所以其中就产生了 operator new,如果申请空间失败,就要抛出异常。
对于 delete 就不存在失败了抛异常,我们说 malloc 会失败,但没有说 free 会失败 (free 的失败是对越界的空间 free 等),free 失败就不是说抛异常或返回空这样的概念了,这种是属于比较严重的错误,它是直接中止掉程序
/*
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 实际也是通过 malloc 来申请空间,如果 malloc 申请空间
成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过 free 来释放空间的。
operator new 就是对 malloc 的封装,目的就是如果申请内存失败了抛异常:
new = 封装 malloc + 失败抛异常 + 调用构造函数
operator delete 就是对 free 的封装,目的主要还是和 operator new 对应起来
delete = 调用析构函数 + operator delete
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//调用构造和析构
A* p1 = new A;
delete p1;
//不会调用构造和析构
A*p2 = (A*)operator new(sizeof(A));
operator delete(p2);
return 0;
}
说明
我们直接使用 operator new 和 operator delete 本质上和 malloc 和 free 没有区别
所以平时我们也几乎不会用 operator new 和 operator delete
如果申请的是内置类型的空间,new 和 malloc,delete 和 free 基本类似,不同的地方是:new/delete 申请和释放的是单个元素的空间,new[] 和 delete[] 申请的是连续空间,而且 new 在申请空间失败时会抛异常,malloc 会返回 NULL。
调用 operator new 函数申请空间
在申请的空间上执行构造函数,完成对象的构造
在空间上执行析构函数,完成对象中资源清理的工作
调用 operator delete 函数释放对象的空间
调用 operator new[] 函数,在 operator new[] 中实际调用 operator new 函数完成 N 个对象空间的申请
在申请的空间上执行 N 次默认构造函数
在释放的对象空间上执行 N 次析构函数,完成 N 个对象中资源清理的工作
调用 operator delete[] 释放空间,实际在 operator delete[] 中调用 operator delete 来释放空间
class Test{
public:
Test(int t = 0)
:_t(t)
{
cout << "Test(int):" << this << endl;
}
~Test(){
cout << "~Test()" << endl;
}
private:
int _t;
};
int main(){
//匹配起来使用的
Test* p1 = (Test*)malloc(sizeof(Test)); //malloc在堆上申请一块和Test一样大的空间,但是不是对象。//malloc不会调用构造
free(p1); //free 只负责释放空间 不会调用析构函数清理空间里面的资源
Test* p2 = new Test(100); //new对象,调用构造函数,堆上形成对象。
delete p2; //调用析构函数,对对象进行清理销毁。
Test* p3 = new Test[10]; //十次构造函数
delete[] p3; //十次析构函数
return 0;
}
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;
}
定位 new 表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new(place_address) type 或者 new(place_address) type(initializer-list)
place_address 必须是一个指针,initializer-list 是类型的初始化列表
如果想对 malloc 开辟的已有的一块空间去调用构造函数
struct ListNode
{
int val;
struct ListNode* next;
ListNode(int x)
: val(x)
, next(nullptr)
{
cout << "ListNode(int x)" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
//实例化一个对象构造函数、析构函数自动调用
ListNode node(1);
//new调用构造函数、delete调用析构函数
ListNode* p = new ListNode(2);
delete p;
//显示调用构造函数、析构函数
ListNode* pt = (ListNode*)malloc(sizeof(ListNode));//malloc申请ListNode大小的空间。
new(pt)ListNode(3);//调用 void* operator new(size_t size ,void* where) 直接返回where;
对指定地点的空间调用构造函数,让他成为一个真正的类。
pt->~ListNode();// 显示调用析构函数清除空间里的资源
free(pt); // 再释放空间
return 0;
}
class Test{
public :
Test(int t = 0)
:_t(t)
{
cout << "Test()" <~Test();//一般情况都不自己调用,只有new 调用构造函数之后 才会 调用析构
free(pt); // 再释放空间
return 0;
注意:
Test *pt1 =new Test;
new(pt) Test;
两个new调用的 operator new 不同。
定位 new 表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用 new 的定义表达式进行显示调构造函数进行初始化。
这里举一个好理解的 —— 复制一份 a数组到另一块空间 pb
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A()" << this << endl;
}
A(const A& a)
{
cout << "A(const A& a)" << this << endl;
}
A& operator=(const A& a)
{
cout << "A& operator=(const A& a)" << this << endl;
return *this;
}
~A()
{
cout << "~A()" << this << endl;
}
private:
int _a;
};
int main()
{
//构造+赋值
A a[5];
A* pb = new A[5];
for(int i = 0; i < 5; i++)
{
pb[i] = a[i];
}
delete []pb;
//拷贝构造
A* pd = (A*)malloc(sizeof(A) * 5);
for(int i = 0; i < 5; i++)
{
new(pd + i)A(a[i]);
}
for(int i = 0; i < 5; i++)
{
(pd+i)->~A();
}
return 0;
}
由此可见,使用定位 new 表达式的效率更高
malloc/free 和 new/delete 的共同点是:
都是从堆上申请空间,并且需要用户手动释放。
不同的地方是:这里主要从特点和用法、底层原理 (本质区别)、处理错误的方式三个方面进行说明
malloc 和 free是函数,new 和 delete 是操作符
malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可
malloc 的返回值为 void*, 在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常
申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理
什么是内存泄漏:
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
具体的说,从我们的几个区域角度看,除了堆之外其它区域的内存是不需要我们管的 —— 栈、数据段、代码段;所以更具体的说内存泄漏就是在堆上申请了空间,我们不用这块空间了,且它没有释放,就存在内存泄漏,因为你不用了,也没有还给系统,别人也用不了。通俗点说:内存泄漏的本质就是站着茅坑不拉屎。
内存泄漏的危害:
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
int main()
{
char* p = new char[1024 * 1024 * 1024];//1个G
return 0;
}
说明
当我们调试起来就可以打开任务管理器看到我们的程序:
上面的程序存在内存泄漏,一次泄漏 1G,但是多次泄漏,对我们的系统也没有什么影响 ?
一个程序正常结束后,会把映射的内存都会释放掉, 所以上面的程序,我们虽然没有主动释放,但是进程结束也会释放掉,对于进程和线程相关的知识,会在 Linux 内系统的学习。
那内存泄漏好像也没啥事,因为进程正常结束都会释放,其实不然,如下场景:
少数情况:进程没有正常结束 ——僵尸进程,就可能存在资源没释放
多数情况:长期运行的服务器程序,比如王者荣耀的后台服务 (只有升级时才会停,且都是半夜),服务器每次一运行就是两三个月,如果每天内存泄漏一点,可能上线才一个月,服务器就越来越慢了
其它情况:物联网设备,如扫地机器人、冰箱等,它们的内存很小,它们就更经不起内存泄漏的折腾了,所以它们在设计的时候是绝不允许内存泄漏的
正常停机升级这是正常的流程,意外挂掉在公司叫事故,比如你去了一家公司,你把公司的服务器搞崩了,那么可能你们组的年终奖都无了,甚至严重的还会被开了。
对 C++ 而言,我们需要主动释放内存;对 JAVA 而言,我们不需要主动释放内存,因为 JAVA 后台有垃圾回收器,接管了内存释放,当然接管也会付出一些代价,而 C++ 是一个极度关注性能的语言。
C/C++ 程序中一般我们关心两种方面的内存泄漏:
堆内存泄漏 (Heap leak)
堆内存指的是程序执行中依据须要分配通过 malloc / calloc / realloc / new 等从堆中分配的一块内存,用完后必须通过调用相应的 free 或者 delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生 Heap Leak。
系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。
在VS下,可以使用windows操作系统提供的_CrtDumpMemoryLeaks()函数进行简单检测,该函数只报出了大概泄漏了多少个字节,没有其他更准确的信息。
int main(){
int* p = new int[10];
//将该函数放在main函数之后,每次程序退出的时候就会检测是否存在内存泄漏
_CrtDumpMemoryLeaks();
return 0;
}
如何检测内存泄漏:
在 linux下内存泄漏检测:Linux 下几款内存泄漏检测工具
在 windows 下使用第三方工具:VLD 工具说明
其它工具:内存泄漏工具比较
智能指针
内存泄漏检测工具
内存泄漏非常常见,解决方案分为两种:
事前预防型:如智能指针等
事后查错型:如泄漏检测工具
32 位程序在虚拟进程地址空间只有 4G,还有 1G 是供内核使用的,况且还有其它的,怎么能申请 4G;但如果是 64 位程序,可以认为这个虚拟进程空间我们是用不完的,因为它是 264 个字节。
因为我的机器是 64 位的,当然可以支持 32 位、64 位,所以我们这里切换成 64 位即可:
//vs2013:默认是按照32位方式编译的
int main(){
cout << sizeof(int*) << endl; //指针大小
void* p = new char[0xfffffffful]; //4G
cout << "new:" << p << endl;
return 0;
}
32位机器下
64位机器下
看到这里如果感觉对你有帮助的话,希望大佬一键三连 !