学习内存管理可以防止内存泄漏、提高程序的效率,更加深刻的理解底层原理。
说明:
- 栈:又称作堆栈,是非静态局部变量、函数参数、返回值、函数空间存放的地方。对于栈来说是由编译器自动管理,无需我们来手动管理。栈的特点是向下生长,也就是先使用高地址空间,再往下使用低地址空间。
- 内存映射段:是一种操作系统中的内存管理技术,它将文件的内容映射到进程的地址空间中,使得文件的内容可以直接在内存中进行读写操作,而不需要通过文件系统进行磁盘I/O操作。内存映射段可以提高文件访问的效率,并且可以简化对文件的操作。在Unix和类Unix系统中,内存映射段通常使用mmap系统调用来实现。
- 堆:用于程序运行时动态分配内存,编译器不会释放该内存,需要我们手动释放,不然会造成内存泄漏。和栈相反,堆是向上生长的。而且堆不能静态分配只能动态分配。
- 数据段:用于存放全局变量和静态变量。这也说明了为什么这两种变量的生命周期和普通变量不一样,因为存储的地方不同。
- 代码段:用于存放可执行代码(含了程序的指令和函数等可执行代码)和只读常量(例如字符串常量)。
C语言是通过malloc、realloc、calloc、free等函数来动态管理内存的。但C++引入了对象的概念,C语言这种管理方式对于一些情况处理不了,于是C++引入了两个操作符:new、delete来动态管理内存。
int main()
{
//非初始化
int* ptr1 = new int;
int* ptr2 = new int[10];
//初始化
int* ptr3 = new int(100);
int* ptr4 = new int[5]{ 1,2,3,4,5 };
delete ptr1;
delete[] ptr2;
delete ptr3;
delete[] ptr4;
return 0;
}
说明:
- 如果申请或释放单个空间new/delete后面加类型,如果申请连续的空间的话new/delete后面加类型[ 个数 ]。
- 如果初始化单个空间的话再类型后面加(初始化的数值),如果要初始化连续的空间的话,使用{一系列数值,逗号分割 }。
- 注意一定要匹配使用(new/delete new[]/delete[]),不然会造成不可知的后果。
我们来看一下用C语言的malloc、free和C++的new、delete对于自定义类型的动态管理的差别。
class A
{
public:
A(int b)
:_a(new int[4]{1,2,3,4})
,b(10)
{
cout << "调用了构造函数" << endl;
}
~A()
{
delete[] _a;
cout << "调用了析构函数" << endl;
}
private:
int* _a;
int b;
};
int main()
{
A* a1 = (A*)malloc(sizeof(A));
A* a2 = new A(10);
free(a1);
delete a2;
return 0;
}
通过调试和打印结果我们可以看出这两者之间的差别了,也就是new、delete除了会分配、释放空间还会去调用自定义类型的构造函数和析构函数。 而malloc、free只能分配、释放空间。
总结:
- 对于内置类型来说,malloc和new,free和delete没有什么区别。
- 对于自定义类型来说new等于malloc加上构造函数,free等于free加上析构函数。
- malloc失败,返回空指针。new失败,抛异常。
new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator
delete是系统提供的全局函数,new在底层调用operator
new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
我们来看一下源码稍微理解一下。
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 new这个函数只是把malloc这个函数包装了一下,如果malloc失败,就会抛出异常。
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 delete 最终是通过free来释放空间的。
(1) new的原理
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
(2)delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
(3)new[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数
(4)delete[N]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
我们知道构造函数是不能由我们显示的调用的,是由编译器自动调用的,但是用定位new表达式可以实现显示的调用构造函数达到初始化对象的目的。
使用格式:
new (place_address) type或者new (place_address)type(initializer-list) place_address必须是一个指针,initializer-list是类型的初始化列表
class A
{
public:
A(int a, int b, int c)
:_a(a)
,_b(b)
,_c(c)
{}
void Print()
{
cout << _a << " " << _b << " " << _c << endl;
}
private:
int _a;
int _b;
int _c;
};
int main()
{
A* a = (A*)malloc(sizeof(A));
//定位new
new(a)A(1, 2, 3);
a->Print();
return 0;
}
可能会有很多人会疑惑这有什么用?有点多此一举了,不如直接new,只是我们现在知道的太少了。
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。