C&C++内存管理
c/c++内存分布
变量的存储位置
- globalVar 存储在 数据段中,全局变量
- staticGlobalVar 存储在数据段,静态变量
- staticVar 存储在数据段,静态变量
- localVar ,num1,char2,*char2,pChar3存储在栈中,局部变量
- *pChar3 存储在代码段中
- ptr1 存储在栈中,这只是一个局部变量的变量名
- *ptr1 存储在堆中,代表着malloc开辟出来的数据,在堆中
说明:
- 栈又叫做堆栈,一般存储非静态的局部变量/返回值/函数参数等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。可以使用系统接口创建共享内存,做进程间通信的功能。
- 堆用于程序运行时动态内存分配,堆是可以向上增长的。
- 数据段,存储全局数据和静态数据。
- 代码段,存储可执行的代码/只读常量。
C语言中动态内存管理方式
int* p1 = (int*) malloc(sizeof(int));
free(p1);
int* p2 = (int*)calloc(4, sizeof (int));
int* p3 = (int*)realloc(p2, sizeof(int)*10);
malloc,calloc,realloc都是在堆中直接开辟指定大小的空间,但是三者的区别在于:
- malloc函数会直接在堆中开辟指定类型大小的空间,但对空间中的原数据不做任何操作。
- calloc函数会在开辟空间的基础上,把对应空间的内容都初始化为0。
- realloc函数会在原数组的基础上,向后扩容指定大小的空间。在扩容的时候,系统会先在原数组的后面找相应大小的空间,如果有连续的空间,就直接在原数组后扩容。如果原数组后没有响应大小的空间,就在堆中找一块连续的空间,来存储扩容后的数组,两种情况都会返回扩容后数组的首地址。
- free函数会释放malloc开辟出来的空间,函数内部会判断NULL指针,free(NULL)不会报错。
C++内存管理方式
在C++中,我们用C语言的内存管理方式也可以正常使用,毕竟C++兼容C语言。那么C++在这里做了哪些升华呢?
在C语言中,我们为了申请空间,往往需要malloc这种写一行代码,C++在这里加入了new和delete操作符进行动态内存管理。
void Test()
{
//1个int型数据
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
free(p1);
p1 = nullptr;
delete p2;
p2 = nullptr;
//10个int型数据的数组
int* p3 = (int*)malloc(sizeof(int)* 10);
int* p4 = new int[10];
free(p3);
delete[] p4;
//申请一个int对象,并且初始化为10
int* p5 = new int(10);
delete p5;
}
注意
- 申请和释放单个元素的空间,使用new和delete操作符;申请和释放连续的空间,使用new[],delete[]。
- 在申请自定义类型空间的时候,new会调用构造函数,delete会调用析构函数,而malloc与free不会。
- 我们调用delete和free之后,一定要给原数据的空间置NULL,负责在下次申请空间的时候,可能会出现该位置有数据的情况。
operator new与operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
首先我们需要确认的一点就是,operator new与operator delete函数不是类中的运算符重载函数,而是系统的自定义的一种函数。
C++底层的实现
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// 尝试分配大小字节
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// 检测没有这块内存
// 如果申请内存失败了,这里会抛出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); /* 阻止其他线程 */
__TRY
/* 获取指向内存块头的指针 */
pHead = pHdr(pUserData);
/* 验证块类型 */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg(pUserData, pHead->nBlockUse);
__FINALLY
_munlock(_HEAP_LOCK); /* 其他线程释放 */
__END_TRY_FINALLY
return;
}
//free的实现
#define free(p) _free_dbg(p,_NORMAL_BLOCK);
operator new实际上也是用malloc来申请空间的,如果malloc申请成功就直接返回,否则执行用户提供的空间不足时的应对措施。如果用户提供该措施就继续申请,否则就会抛出异常。
在出现异常的同时,会直接从出现异常的地方跳转到处理异常的函数处,不会执行出现异常处后面的代码。
try
{
Test();//申请空间的函数
}
catch (exception& e)
{
//如果申请失败,调用我们自定义的处理函数
cout << e.what() << endl();
}
operator delete内部是使用free来释放空间的。
operator new与operator delete的类专属重载
struct ListNode
{
ListNode* _next;
ListNode* _prev;
int _data;
void* operator new(size_t n)
{
void* p = nullptr;
p = allocator<ListNode>().allocate(1);
cout << "memory pool allocate" << endl;
return p;
}
void operator delete(void* p)
{
allocator<ListNode>().deallocate((ListNode*)p, 1);
cout << "memory pool deallocate" << endl;
}
};
operator new和operator delect的内部其实还是使用malloc来向内存中申请空间。如果我们写一个链表,在我们需要频繁申请节点的时候,调用operator new向内存中申请空间就显得有点慢。
我们就可以写一个自定义的operator new函数,在这个函数中我们可以直接向内存池申请空间,就会比较快一点,提高效率。
new和delete的实现原理
- 内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
- 自定义类型
new的实现方式
- operator new 函数来申请空间
- 在申请的空间上执行构造函数,完成对象的构造
delete的实现方式
- 在空间上执行析构函数
- operator delete函数来释放对象的空间。
new T[N]的实现方式
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象愧疚的申请。
- 在申请的空间上执行N次构造函数。
delete[] 的实现方式
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理。
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
处理内存的方式 | 区别 |
---|---|
malloc,free | 函数失败后,返回nullptr |
operator new,operator delete | 失败后会抛异常 |
new,delete | 自定义类型调用构造函数和析构函数 |
定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
- 使用格式
new(place_address)typedef;
new(place_address)type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表。
- 使用场景
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化
class A
{
public:
A(int data = 10)
: _data(data)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _data;
};
void Test()
{
// pt现在指向的只不过是与A对象相同大小的一段空间,
//还不能算是一个对象,因为构造函数没有执行
A* pt1 = (A*)malloc(sizeof(A));
new(pt1)A; // 注意:如果A类的构造函数有参数时,此处需要传参
pt1->~A();
free(pt1);
A* pt2 = (A*)operator new(sizeof(A));
new(pt2)A(10); // 注意:如果A类的构造函数有参数时,此处需要传参
pt2->~A();
operator delete(pt2);
}
申请空间失败的两种情况:
- 一次需要申请的空间太大,大于堆栈的总空间,就不可以申请得到。
- 经过连续的申请许多空间,虚拟地址空间中已经没有连续的该大小的空间。
内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费
内存泄漏再通俗一点来说,拿链表来举一个例子,我们在释放节点的时候,没有用free函数把每一个结点都释放以便,那么这些节点虽然已经没有用了,但是他还是在内存中站的位置,要是节点比较多,内存就会被这些无用数据占满,影响性能。
- 内存泄漏的危害
长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死
内存泄漏的分类
- 堆内存泄漏(Heap leak)
堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。 - 系统资源泄漏
指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定
在windows下如何检测内存泄漏
- 下载vld
链接:https://pan.baidu.com/s/18XLJQ9mWvpGbG9c6IAtDuQ
提取码:k318 - 使用
vld的安装就是一路next就好了,中间也可以改一下安装路径
在安装完毕之后,在vs2013(我是vs2013)版的项目属性中,查看是否有这些路径,没有就手工加上,具体路径看你安装的地方了,以上是我的配置。
然后输入以下代码,点击运行
#include<vld.h>
#include <iostream>
int main()
{
int* pbuf = new int[10];
return 0;
}
出现这样的提示就说明安装成功了,我们的代码出现了内存泄漏。
如何一次在堆中申请4G内存
// 将程序编译成x64的进程,运行下面的程序
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}
32进程下,他的字节数是 2 的32次方G --> 4G
64位进程下,他的字节数是2 的34次方G --> 大约16G