C++基础——动态内存分配的总结

C中的动态内存分配

malloc函数

  函数原型为void *malloc(unsigned int size);

在内存的动态存储区中分配一块长度为"size" 字节的连续区域。函数的返回值为该区域的首地址。 “类型说明符”表示把该区域用于何种数据类型。(类型说明符*)表示把返回值强制转换为该类型指针。“size”是一个无符号数。例如: pc=(char *) malloc (100); 表示分配100个字节的内存空间,并强制转换为字符数组类型,函数的返回值为指向该字符数组的指针, 把该指针赋予指针变量pc。若size超出可用空间,则返回空指针值NULL。

calloc 函数

函数原型为void *calloc(unsigned int num, unsigned int size)

  按所给数据个数和每个数据所占字节数开辟存储空间。其中num为数据个数,size为每个数据所占字节数,故开辟的总字节数为num*size。函数返回该存储区的起始地址。calloc函数与malloc 函数的区别仅在于一次可以分配n块区域。例如: ps=(struct stu*) calloc(2,sizeof (struct stu)); 其中的sizeof(struct stu)是求stu的结构长度。因此该语句的意思是:按stu的长度分配2块连续区域,强制转换为stu类型,并把其首地址赋予指针变量ps。

realloc函数

函数原型为void *realloc(void *ptr, unsigned int size)

*ptr指向的要改变的空间地址,如果size大小的连续的空间,那么就返回*ptr,如果没有size大小的空间,那么就需要重新申请新的连续空间,把之前空间的数据拷贝到新的空间,释放以前的空间的数据,返回新的地址。

free函数

函数原型为void free(void *ptr)

  将以前开辟的某内存空间释放。函数原型为 void free(void *ptr)其中ptr为存放待释放空间起始地址的指针变量,函数无返回值。应注意:ptr所指向的空间必须是前述函数所开辟的。例如free((void *)p1);将上例开辟的16个字节释放。可简写为free(p1);由系统自动进行类型转换。

#define SIZE 100

struct MyStruct
{
	int i;
	char a;
	double d;
};

char* a = (char*)malloc(SIZE * sizeof(char));

realloc(a, SIZE * 2 * sizeof(char));
free(a);

MyStruct* b = (MyStruct*)calloc(5, sizeof(MyStruct));
free(b);

malloc和free的实现原理

在C语言中只能通过malloc()和其派生的函数进行动态的申请内存,而实现的根本是通过系统调用实现的(在linux下是通过sbrk()系统调用实现);malloc()是从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。

malloc()在运行期动态分配内存,free()释放由其分配的内存。malloc()在分配用户传入的大小的时候,还分配的一个相关的用于管理的额外内存,不过,用户是看不到的,所以:实际的大小 = 管理空间 + 用户空间。

此外,堆中的内存块总是成块分配的,并不是申请多少字节,就拿出多少个字节的内存来提供使用。堆中内存块的大小通常与内存对齐有关(8Byte(for 32bit system)或16Byte(for 64bit system)。

在linux系统下面一个程序的堆的管理是通过内存块进行管理的,也就是将堆分成了很多大小不一的内存块。这些块怎么管理(比如怎么查询块的大小,怎么查询块是否正在被程序使用,怎么知道这个块的地址),为了解决内存块的管理所以要设计一个管理内存块的数据结构,详细的数据结构如下:

/**内存控制块数据结构,用于管理所有的内存块
* is_available: 标志着该块是否可用。1表示可用,0表示不可用
* size: 该块的大小
**/
struct mem_control_block {
    int is_available;
    int size;
};

有了管理内存块的数据结构,那么在内存中堆的组织形式也好理解了,也就是堆是由很多内存块组成的,所以有了如下的示意图:

增加内存后进程的堆

综合上面的知识,可以很容易想到malloc()实现的大体思路。首先挨个检查堆中的内存是否可用,如果可用那么大小是否能满足需求,要是都满足的话就直接用。当遍历了堆中的所有内存块时,要是没有能满足需求的块时就只能通过系统调用向操作系统申请新的内存,然后将新的内存添加到堆中。思路很简单,malloc()实现流程图如下所示:

malloc()实现流程图

malloc的实现源码如下:

/**内存控制块数据结构,用于管理所有的内存块
* is_available: 标志着该块是否可用。1表示可用,0表示不可用
* size: 该块的大小
**/
struct mem_control_block {
    int is_available;
    int size;
};

/**在实现malloc时要用到linux下的全局变量
*managed_memory_start:该指针指向进程的堆底,也就是堆中的第一个内存块
*last_valid_address:该指针指向进程的堆顶,也就是堆中最后一个内存块的末地址
**/
void *managed_memory_start;
void *last_valid_address;

/**malloc()功能是动态的分配一块满足参数要求的内存块
*numbytes:该参数表明要申请多大的内存空间
*返回值:函数执行结束后将返回满足参数要求的内存块首地址,要是没有分配成功则返回NULL
**/
void *malloc(size_t numbytes) {
    //游标,指向当前的内存块
    void *current_location;
    //保存当前内存块的内存控制结构
    struct mem_control_block *current_location_mcb;
    //保存满足条件的内存块的地址用于函数返回
    void *memory_location;
    memory_location = NULL;
    //计算内存块的实际大小,也就是函数参数指定的大小+内存控制块的大小
    numbytes = numbytes + sizeof(struct mem_control_block);
    //利用全局变量得到堆中的第一个内存块的地址
    current_location = managed_memory_start;

    //对堆中的内存块进行遍历,找合适的内存块
    while (current_location != last_valid_address) //检查是否遍历到堆顶了
    {
        //取得当前内存块的内存控制结构
        current_location_mcb = (struct mem_control_block*)current_location;
        //判断该块是否可用
        if (current_location_mcb->is_available)
            //检查该块大小是否满足
            if (current_location_mcb->size >= numbytes)
            {
                //满足的块将其标志为不可用
                current_location_mcb->is_available = 0;
                //得到该块的地址,结束遍历
                memory_location = current_location;
                break;
            }
        //取得下一个内存块
        current_location = current_location + current_location_mcb->size;
    }

    //在堆中已有的内存块中没有找到满足条件的内存块时执行下面的函数
    if (!memory_location)
    {
        //向操作系统申请新的内存块
        if (sbrk(numbytes) == -1)
            return NULL;//申请失败,说明系统没有可用内存
        memory_location = last_valid_address;
        last_valid_address = last_valid_address + numbytes;
        current_location_mcb = (struct mem_control_block)memory_location;
        current_location_mcb->is_available = 0;
        current_location_mcb->size = numbytes;
    }
    //到此已经得到所要的内存块,现在要做的是越过内存控制块返回内存块的首地址
    memory_location = memory_location + sizeof(struct mem_control_block);
    return memory_location;
}

free的实现源码如下:

/**free()功能是将参数指向的内存块进行释放
*firstbyte:要释放的内存块首地址
*返回值:空
**/
void free(void *firstbyte)
{
    struct mem_control_block *mcb;
    //取得该块的内存控制块的首地址
    mcb = firstbyte - sizeof(struct mem_control_block);
    //将该块标志设为可用
    mcb->is_available = 1;
    return;
}

C++中的动态内存分配

new、new[]和delete、delete[]的实现原理

在C++ Primer书中有提到说: new/delete的表达式与标准库函数同名了,所以系统并没有重载new或delete表达式。new/delete真正的实现其实是依赖下面这几个内存管理接口的。c++中称之为“placement版”内存管理接口:

void * operator new (size_t size);  
void operator delete (size_t size);

void * operator new [](size_t size);  
void operator delete[] (size_t size);

operator new(size_t size)的工作流程如下(可以发现这个函数是调用malloc来分配内存):

new T的原理:

  1. 调用void * operator new (size_t size)函数申请T类型大小的堆内存空间
  2. 调用 T 的构造构造函数,完成对象的构造 

delete的原理:

  1. 调用对象的析构函数,完成对象中的资源释放
  2. 调用void operator delete (size_t size) 函数释放对象的空间

 

对于对象数组的内存布局(如:new T[10]; )不同于单一的对象内存布局,更确切的说,对象数据的内存通常还包含”数据大小的记录“,以便delete知道需要调用多少次析构函数,它的布局可能是这样:

new T[N]的原理:

  1. 调用 void * operator new [](sizeof(T)*N + 4)函数,在内部实际是调用void * operator new (size_t size)函数完成N个独享的空间申请
  2. 将空间的前四个字节填充对象的个数
  3. 在申请的空间上执行N次构造函数

delete[] 的原理:

  1. 从一个对象数组的前四个字节中获取对象的个数N
  2. 调用N个对象的析构函数,释放资源
  3. 调用void operator delete[] (size_t size);函数释放N个对象的空间,函数内部通过调用void operator delete (size_t size) 函数释放单个对象的空间。

new和malloc的区别

属性:new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持c。

参数:使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

分配失败:new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

自定义类型: new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现);malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

重载:C++允许重载new/delete操作符,特别的,布局new的就不需要为对象分配内存,而是指定了一个地址作为内存起始区域,new在这段内存上为对象调用构造函数完成初始化工作,并返回此地址。而malloc不允许重载。

内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

 

 

以上参考:

 

  • 5
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值