C语言三种开辟堆内存方式
c语言用3个函数进行动态内存分配。
malloc申请一段连续的堆空间并返回首地址,不能初始化内存空间。
calloc会将分配到的空间每一位初始化为0,因此效率稍低。
realloc给一个已经分配了地址的指针重新分配空间,realloc 扩大空间时,很有可能原有空间后的余留空间不够,需要进行三步操作,开辟一块新的空间,拷贝原空间数据到新空间,释放原空间。
最后都必须使用free对申请的空间进行释放。
malloc底层实现,本质
测试环境vs2010,Debug版本
F11进入malloc函数,查看c库是怎么实现的。不断调用F11发现库函数对malloc进行了多次封装,最后进入dbheap.c文件进行真正的堆空间申请。
首先找到一句堆上锁的语句
_mlock(_HEAP_LOCK);
之后都是检测堆校验,检测块类型,重点是计算blocksize,这里nsize是用户真正需要创建的空间大小,后面一个参数宏定义为4,第一个参数是一个结构体。假定用户nsize = 4执行完这一句blockSize=40.
blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNoMansLandSize;
接下来系统用40去真正的申请一段堆空间。
pHead = (_CrtMemBlockHeader *)_heap_alloc_base(blockSize);
那么额外的结构体和一个4字节的空间有什么用呢?
结构体是用来管理和标记这块内存空间的,多分配了4字节用来隔离数据区。
/* fill in gap before and after real block */
memset((void *)pHead->gap, _bNoMansLandFill, nNoMansLandSize);
memset((void *)(pbData(pHead) + nSize), _bNoMansLandFill, nNoMansLandSize);
/* fill data with silly value (but non-zero) */
memset((void *)pbData(pHead), _bCleanLandFill, nSize);
RTCCALLBACK(_RTC_FuncCheckSet_hook,(1));
retval=(void *)pbData(pHead);
库函数对用户申请的空间前后用0xfd填充,真正的用户数据区用0xcd填充。像是一个栅栏一样把用户数据区隔离起来。
现在可以回答一个问题了,free如何知道该释放多少空间?
在前面的结构体中有一个位置记录了动态分配内存的大小。
在Release版本实际分配的内存等于请求的内存大小,现在我们知道了在Debug版本系统实际申请的堆空间要大于我们申请的空间,因此在做链表节点申请时如果单个节点太小,会造成资源浪费的问题。
C++的动态内存管理
C++中通过new和delete运算符进行动态内存管理。
new和delete配套使用,如果用new申请了自定义类型对象Date(有动态申请空间),却使用free,那么就有可能造成内存泄露。
class Date{
public:
Date()
:_day(10)
{
_array = new int[20];//内存泄露
}
~Date(){
delete[] _array;
cout<<"~Date"<<endl;
}
private:
int _day;
int* _array;
};
malloc和free, new和delete
malloc/free是c库函数,new/delete是c++的操作符
malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理(清理成员)
malloc/free需要手动计算类型大小且返回值会void*,new/delete 可自己计算类型的大小,返回对应类型的指针
深入探究new和delete所做的工作
以下为vs2010的库源码
new:
调用 operator new对malloc的封装,如果申请失败了就再次申请
// try to allocate size bytes
void *p;
while ((p = malloc(count)) == 0)
if (_callnewh(count) == 0)
{ // report no memory
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
之后在已经申请的空间上调用构造函数
delete:
调用析构对象的析构函数,之后调用operator delete
同样的在源码文件找到了operator delete,是对free函数的封装
void operator delete( void * p )
{
RTCCALLBACK(_RTC_Free_hook, (p, 0));
free( p );
}
new[]:
void *__CRTDECL operator new[](size_t count) _THROW1(std::bad_alloc)
{ // try to allocate count bytes for an array
return (operator new(count));
}
1.这里调用的函数count为4+实际申请空间,多申请4字节用来保存对象的个数,之后调用operator new()。
2.在申请的空间上调用构造函数
3.对象个数放在空间前4字节
4.空间首地址偏移4,返回用户空间首地址
delete[]:
- 取出对象个数N
- 调用N次析构函数
- 释放空间调用 operator delete [] (指针向前偏移4再调用operator delete)
对于一个显式定义了析构函数的Test来说,以下方式释放空间会有问题。
void test()
{
Test* p1 = (Test*)malloc(sizeof(Test));
Test* p2 = (Test*)malloc(sizeof(Test));
Test* p3 = new Test;
Test* p4 = new Test;
Test* p5 = new Test[10];
Test* p6 = new Test[10];
delete p1;
// delete[] p2; //崩溃
free(p3); //没有调析构,可能会内存泄漏
// delete[] p4; //崩溃
// free(p5); //释放的空间比申请的小4字节,崩溃
delete p6; //只调用一次析构函数,可能内存泄漏
}
定位new
上文提到如果用new申请链表节点,可知在每次实际的申请内存会比预期的大,这样使用空间的效率就变的低了。若果预先分配一大段空间,然后每次从里面取出一块来使用就好了。在c++中通过定位new来实现。
#include <iostream>
#include <new>
const int chunk = 16;
class Foo
{
public:
int val()
{
return _val;
}
Foo()
{
_val = 0;
}
private: int _val;
};
// 预分配内存 但没有Foo对象
char *buf = new char[ sizeof(Foo) * chunk ];
int main() {
// 在buf中创建一个Foo对象
Foo *pb = new (buf) Foo;
// 检查一个对象是否被放在buf中
if (pb->val() == 0)
cout << "new expression worked!" << endl;
delete[] buf;
return 0;
}