动态内存管理

1、内存:

      内存又叫内存储器,在冯诺依曼体系结构中,外设不能和CPU直接进行数据交流,外设可以和内存进行输入和输出,内存可以和CPU(运算器和控制器)进行数据交流。程序运行是要加载到内存中才能执行,变量需要申请内存才能被执行。申请了资源(内存)就要释放资源,不进行释放,CPU占有量会越来越大,其他进程就申请不到资源,就会造成内存泄露,对于普通的电脑来说,最终的结果就是关机,但对于大型服务器来说,服务器挂掉的影响是非常巨大的,会造成很大的损失,所以对内存的动态管理,也就是正确的申请和释放内存,是非常重要的。

内存的划分


  • 栈: 又称为堆栈,用于存放程序临时创建的局部变量,函数参数值,由编译器自动分配和释放。栈上的内容只在当前函数范围内有效,函数在被调用时,函数的参数和变量会被压入栈中,函数的的返回值也会被压入栈中,函数调用完成后,栈中的内容会自动进行销毁。栈就相当于一个交换临时数据的地方。其特点是效率高但是空间大小有限。
  • 共享区: 用于存放栈和堆中的公共数据。
  • 堆: 堆用于存放程序运行中被动态分配的内存。一般由程序员进行分配和释放,若程序员没哟释放,程序结束后由操作系统进行回收。使用malloc,realloc,calloc系列函数和new操作符进行动态申请,新分配的内存就会被动态添加到堆上。使用free和delete函数释放内存。堆的空间比较大,可动态扩张和缩减。其特点是使用灵活,空间范围大。
  • 静态全局区:又叫数据段, 用于保存全局变量和静态变量(static变量,包括stati全局变量和static局部变量)。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域。静态全局区的内容生命周期随程序。
  • 代码段: 又叫常量区,存放可执行的代码和常量。

2、动态申请内存

malloc、realloc、calloc的区别和联系:

在C语言中使用malloc/realloc/calloc在堆上进行动态申请内存,使用free进行动态释放内存,动态申请的内存在free前一直存在。

  • malloc:函数原型:void *malloc( size_t size );  函数参数:size 字节数
功能:在内存中开辟一块连续的空间,size为开辟的空间的长度,函数返回这块空间的首地址。
  • realloc:函数原型:void *realloc( void *memblock, size_t size );   函数参数:memblock为指向以前分配的内存块
指针, size为重新开辟的大小(以字节为单位)

功能:realloc函数更改已分配内存块的大小,memblock指向原内存块的首地址。与malloc的区别是,如果memblock为NULL,此时realloc和malloc的作用相同,相当于调用了malloc,开辟size大小的空间。如果memblock不为空,则memblock指向以前调用calloc、malloc或realloc时返回的指针,也就是memblock指向一个已经分配好的地址,那么使用的就是realloc本身,重新开辟size大小的空间。

  • calloc:函数原型:void *calloc( size_t num, size_t size );  函数参数:num:元素的个数   size:每个元素的字节数

功能:calloc函数为有num个元素的数组分配内存,和malloc相似,也是开辟一块连续的内存。每个元素的大小为size,每个元素被初始化为0。

区别:

<1> malloc函数不能初始化分配的空间。如果malloc分配的空间原来没有被分诶过,则每一位有可能为0,但如果这一块内存原来被分配过,那里面就可能是随机值。所以使用malloc开辟空间的时候,要进行初始化。一般使用memset初始化空间。如果不进行初始化,在多次开辟之后就有可能出现错误。

<2> calloc会将分配的内存中的每一位都初始化为0。如果为字符型或整型类数组分配空间,则这些元素会被初始化为0;如果是指针类,则会被初始化为NULL。

<3> malloc向系统申请size个字节的空间,函数返回的是这个空间的首地址,类型为void*,void*表示未确定的类型,void*可以被转换成任意类型的指针。

<4> realloc可以对给定的指针所指向的内存进行扩大或缩小,无论扩大还是缩小,原有内存的内容将保持不变,对于缩小后的空间,被缩小的部分空间的数据会丢失。realloc调整后的内存和原来的内存有可能不是同一地址,取决于要开辟的大小。如果堆上现存的数据后面的字节足够,则会返回这片内存的首地址,也就是和原来的内存块的地址相同。但如果后面的字节数不够,就会使用堆上第一个有足够大小的自由块,然后将现存的数据拷贝到那个自由块,原来的内存块会被释放,在这个过程中,数据会被移动。

malloc的使用:

int *p = (int*)malloc(sizeof(int)*100);

malloc函数返回一个void*类型的指针,内存分配成功后,malloc函数返回这块内存的首地址。但由于返回的是void*类型,所以必须要强转成你需要的类型,也就是说申请的这块内存可以存储任何类型的数据,但你要为这块内存指定它要存储的数据类型。

在上述例子中,在堆上分配了100个整型字节的内存,返回这块内存的首地址,并把该地址强制装换成int*类型后赋值给指针变量p。只能使用指针变量p来操作这块内存,这块内存本身并没有名字,对它的访问时匿名访问。


3、动态释放内存

在C语言中,与malloc/realloc/calloc对应的操作是free,释放内存。

函数原型:void free( void *memblock );   参数:memblock指向要释放的内存的首地址。例如:free(p);

功能:free函数要斩断指针变量与这块内存的关系。例如,malloc函数开辟的这块内存的访问权是属于p的,只能通过p来访问这一块内存,而free就是斩断这块内存和p之间的关系。p指针指向的那块内存本身并没有发生变化,只是指针p对这块内存已经没有访问权了,不能对内存块进行操作。而那块内存中的数据也没有被改变,只是没有方法去访问或修改其中的数据了。

malloc和free是对应的,malloc后就要进行free,只malloc不进行free,内存会越来越少,就会出现内存泄露,最终机子会挂掉。如果malloc了两次单只free了一次,就会出现内存泄露;如果malloc了一个free了两次,就会出错,原因在于第一次使用free后,malloc所开辟的空间就已经释放掉了,第二次在使用free就已经没有内存可以释放了,这种对内存的误操作就有可能会导致程序崩溃。

free掉内存后,为什么一定要把指针置空?

原因在于free后只是切断了指针和内存的关系,但指针本身依旧指向一个地址,只是这个地址不能再被访问罢了,如果不把指针置空,那么指针就会变成一个野指针(又叫悬垂指针),当再次使用该指针时,实际上该指针已经指向了一块非法的地址,对该地址进行访问就是非法访问,有可能会造成意想不到的后果,所以free后,要养成free后将指针置空的好习惯。


4、使用new和delete进行内存管理

在C++中使用new进行内存申请,使用delete进行内存释放。C++是兼容C的,已经有了malloc和free进行动态内存管理,为什么C++中还要定义new和delete操作符来进行动态内存管理呢?原因就在于C++中多了类和对象,new和delete可以分别调用其构造函数和析构函数来进行初始化和清理。

malloc/free和new/delete的区别:
  • 它们都是动态内存管理的入口。
  • malloc/free是C/C++标准库函数, new/delete是C++操作符。
  • malloc/free只是进行动态分配和释放内存,而new/delete不仅会动态分配和释放空间还会调用构造函数和析构函数进行初始化和清理工作。
  • malloc/free需要手动计算类型大小并且会返回void*,new/delete可以自己计算类型的大小,并且返回对应类型的指针。
operator new/operator delete 和 operator new[]/operator delete[] 的区别:
  • operator new/operator delete和operator new[]/operator delete[] 的用法和malloc相同,它们的底层其实调用的是malloc和free。是malloc和free的一层封装。
  • 它们只是进行分配和释放内存。
  • operator new[] 和 operator delete[] 其实是对数组进行内存分配和释放。
new/delete和new[]/delete[]:

new其实就是new operator(new操作符)。

new做的两件事:

  • 调用operator new 分配空间
  • 调用构造函数进行初始化

delete其实就是delete operator(delete操作符)。

delete做的两件事:

  • 调用operator delete 释放空间
  • 调用析构函数进行清理

new[]是对数组内存分配。

new[N]做的两件事:

  • 调用operator new 分配空间
  • 调用N次构造函数分别初始化每个对象

delete[]要做的两件事:

  • 调用N次析构函数清理对象
  • 调用operator delete 释放空间

注意:

<1> new/delete 、new[]/delete[]必须搭配使用。

<2> delete一个void指针,只是释放了内存,因为没有类型信息也没有办法让编译器知道要调用哪个析构函数;delete一个NULL指针,其实它什么都没有做,因而是安全的。


5、实现NEW_ARRAY/DELETE_ARRAY宏、模拟new[]/delete[]申请和释放数组

模拟实现new[]

#define NEW_ARRAY(PTR, TYPE, N)         \
do{
    PTR = (TYPE*)operator new(sizeof(TYPE)*N + 4);   \
    (*(int*)PTR) = N;                   \
    PTR = (TYPE*)((char*)PTR + 4));    \
    for (size_t i = 0; i < N; ++i)      \
        new(PTR + i)TYPE;               \
}while(false);                          \

代码解析:

  • PTR是指向开辟的空间的地址,开辟类型为TYPE的N个空间,也就是TYPE*N个字节,用4个字节去存放数组元素的个数,所以要加4。
  • 用开辟的空间的前四个字节存放数组元素的个数(int),所以将指向强转为int*,再解引用,使前四个字节的内容为N。
  • 使PTR指向四个字节之后的地址,也就是真正存放数组元素的空间。
  • 对数组中的元素进行初始化。


模拟实现delete[]

#define DELETE_ARRAY(PTR,TYPE)     \
do{
    size_t N = *((int*)PTR - 1);   \
    for (size_t i = 0; i < N; ++i)   \
        PTR[i].~TYPE();             \
    PTR = (TYPE*)((char*)PTR -4);    \
    operator delete(PTR);            \
}while(false);

  • 取出数组元素的个数:将PTR强转为int*后减1,也就是指针向前移动了4个字节,此时指向该空间的最开始,解引用后得到数组元素的个数N。
  • 释放空间:采用for循环,再调用~TYPE()析构函数,释放数组中的每个元素的空间。
  • 释放开始的四个字节的空间:PTR强转为字符型指针后减4,就指向了存放数组个数的那四个字节,强转为TYPE*后赋给PTR。
  • 采用operator delete()释放空间。


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值