c++动态内存管理深入理解

C语言三种开辟堆内存方式

c语言用3个函数进行动态内存分配。

malloc申请一段连续的堆空间并返回首地址,不能初始化内存空间。

calloc会将分配到的空间每一位初始化为0,因此效率稍低。

realloc给一个已经分配了地址的指针重新分配空间,realloc 扩大空间时,很有可能原有空间后的余留空间不够,需要进行三步操作,开辟一块新的空间,拷贝原空间数据到新空间,释放原空间。

最后都必须使用free对申请的空间进行释放。

malloc底层实现,本质

测试环境vs2010,Debug版本

F11进入malloc函数,查看c库是怎么实现的。不断调用F11发现库函数对malloc进行了多次封装,最后进入dbheap.c文件进行真正的堆空间申请。

首先找到一句堆上锁的语句

_mlock(_HEAP_LOCK);

扫描二维码关注公众号,回复: 2281712 查看本文章

之后都是检测堆校验,检测块类型,重点是计算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[]:

  1. 取出对象个数N
  2. 调用N次析构函数
  3. 释放空间调用 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;
}

猜你喜欢

转载自blog.csdn.net/hanzheng6602/article/details/80752363