动态内存管理详细介绍

Posted 捕获一只小肚皮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态内存管理详细介绍相关的知识,希望对你有一定的参考价值。

1.为什么存在动态内存分配?

目前为止,我们已经掌握的内存开辟方式有两个,且知道在内存中,我们主要使用的是 栈区,堆区,静态区.其中三者的作用分别如下:

(1)静态存储区:主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。

(2)栈区:存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束

(3)堆区:程序员自己申请一块任意大小的内存—也叫动态内存分配。这块内存会一直存在知道程序员释放掉。C语言中,用malloc or new动态地申请内存,用free or delete释放内存。良好习惯:若申请的动态内存不再使用,要及时释放掉,否则会造成内存泄露

示例:

第一:创建局部变量,函数调用

void print()
{
    char str[] = "I'm your father";
    printf("%s",str);
}

int main()
{
    int a = 0;   
    
    print();
    return 0;
}

例如上面,就是创建了局部变量 str和a,以及进行了函数调用.而他们是在 栈区使用内存

第二: 全局变量,常量等

int b = 20;
int main()
{
    const int c = 10;
    return 0;
}

例如上面,就是创建了 全局变量b,常量c, 而他们是在 静态区使用


但是上述的内存开辟方式却有两个特点:

  1. 空间开辟大小是固定的.

  2. 数组在声明的时候,必须指定数组长度,且此后长度固定,无法修改.它所需要的内存在编译时进行分配


而最后剩下的一种----- 堆区,便是我们此篇文章的核心与主角.

**引言:**有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时才开辟空间的方式就不能满足,这时候就只能试试动态存开辟,即使用 堆区内存

比如下面的例子:

struct info
{
    char name[20];
    int age;
};

int main()
{
    int n = 0;
    scanf("%d",&n);
    struct info arr[50];
    return 0;
}

上面的结构体 struct info是一个人(学生)的信息(姓名,年龄).下面创建了一个大小为50的数组arr用来存各个学生的信息.

但是我们难免会碰到一些情形,比如一些学生因为特殊原因不来上学,就不会记录信息.或者因为生源太好,名额超出50.那么最开始设置的50个名额,就不会够用,或者空间浪费.因此我们便需要对数组长度进行动态调整.

2.各种动态内存函数介绍

2.1 malloc

官方声明:void* malloc (size_t size)

  • size 需要开辟空间的字节大小
  • void* 开辟的空间的地址

2.1.1声明与解释

作用: 开辟一块数组空间,返回地址.

使用:

  • 如果开辟成功,则返回一个指向开辟成功的空间的指针

  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值必须检查

  • 返回值类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

  • 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。

2.1.2 使用

例子:

  1. 向内存申请10个整形空间
int* p = (int *)malloc(sizeof(int) * 10);

可以看见size的实参是 sizeof(int) * 10

最后返回时,指针void*被转化为整型指针.

  1. 在申请的时候可能也会申请失败,比如:
#include<stdio.h>
#include <stdlib.h>
int main()
{
    double* p = (double*)malloc(sizeof(double) * 1000000000000);
    if (p == NULL)
        perror("申请失败原因:");
    return 0;
}

结果:

image-20210518095118789

  1. 使用空间
#include<stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
        for (int i = 0; i < 10; i++)
        {
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    return 0;
}

image-20210518100110813

上面已经介绍了如何在 堆区申请空间,判断申请是否成功,如何使用malloc .

但是我们最开始就说了,堆区申请的空间是 程序员自己手动释放,而放眼看前面,我们释放了吗??

并没有,因此,引出了下面的函数 free


2.2 free

上述已经学会了使用 malloc ,现在就应该还回去我们所申请的空间了.那怎么还回去呢? 答案是 使用 free函数

官方文档: void free(void *memblock);

memblock 需要释放的空间的地址

使用:

#include<stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
        for (int i = 0; i < 10; i++)
        {
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    free(p);    // 成功释放
    return 0;
}

上面已经成功释放了,但是还有个小问题,比如我们在free后面和最开始申请空间语句后面加一条语句 printf("%p\\n",p);

image-20210518102455618

会发现,申请的空间虽然被释放,但是p的值并没有改变,仍然记住了,此时p成了野指针,及其危险,所以我们需要在free(p)后面加上 p = NULL;

比如:

#include<stdio.h>
#include <stdlib.h>
int main()
{
    int* p = (int*)malloc(sizeof(int) * 10);
    if (p == NULL)
        perror("申请失败原因:");
    else
    {
        for (int i = 0; i < 10; i++)
        {
            p[i] = i;
            printf("%d ",p[i]);
        }
    }
    free(p);    // 成功释放
    p = NULL;    //置为空
    return 0;
}

助记:

此时的p就相当于男女朋友中的男生,当他们分手了以后,男生还记得女生的所有信息(p仍然记住了原地址) ,女生为了防止自己以后不被骚扰,就给了男的当头一棒,让男的失忆(p = NULL)

总结:

malloc,free,NULL 需要成对使用.

free函数用来释放的是动态开辟的内存,如果参数是指向非动态开辟的内存,那么free函数未定义.

如果free的参数是空指针NULL,则free函数什么也不做


2.3 calloc

calloc函数的用法与malloc很像,但是callocmalloc函数多了一个功能---------可以在开辟空间时候,给数组每个位置初始化为0

官方文档: void* calloc (size_t num , size_t size);

num ------>>> 数组长度

size------>>> 数组元素类型的字节大小

开辟成功返回数组地址,不够返回NULL

示例:

#include <stdio.h>
#include <stdlib.h>
int main()
{
 	int* p = (int* )calloc(10,sizeof(int));  //申请空间
    if(p == NULL)                           //检测是否开辟失败
        perror("错误原因:");
    else
        for(int i = 0;i<10;i++)            //打印数组每个值
            printf("%d ",p[i]);
    free(p);                               //释放空间
    p = NULL;                              //置为空指针
    return 0;
}

image-20210518141307237


2.4 realloc (王者出场,真正的调整数组长度)

前面我们介绍了malloccalloc,但是好像都是在申请一块空间,并没有与我们开头介绍的数组长度可变有半毛钱关系,于是顺应我们的要求,王者老大哥---------realloc出场.

realloc的出现使内存管理更加灵活,可以调整动态内存开辟的大小.

官方文档: void* realloc (void* memblock,size_t size)

  1. memblock 已经开辟的内存块的地址

  2. size 新的空间大小

  3. 开辟成功就会返回开辟的空间地址

  4. 失败就会返回NULL

  5. realloc的返回值需要用一个新的变量接收

示例:

#include <stdio.h>
#include <stdlib.h>
int main()
{
 	int* p = (int* )malloc(sizeof(int)*10);  //申请空间
    if(p == NULL)                           //检测是否开辟失败
        perror("错误原因:");
    else
        for(int i = 0;i<10;i++)            //打印数组每个值
            printf("%2d ",p[i] = i);
    printf("\\n");
    
    
    //调整空间
	int* p1 = (int*) realloc(p, sizeof(int) * 20);
    if(p1 == NULL)              //检测开辟是否成
        perror("错误原因:");
    else
    {
        p = p1;      //重新交给p管理申请的空间
        for(int i = 10;i<20;i++)
            printf("%d ",p[i] = i);
    }
    
    
    free(p);                               //释放空间
    p = NULL;                              //置为空指针
    return 0;
}

image-20210518143140221

可以看见,之前malloc只开辟了10个长度,但是现在却可以有20个空间.

2.4.1 realloc的空间开辟方式

但是看了上面的使用示例,大家是否有什么疑问呢? 比如:

  • 为什么一定要用新的指针变量来接收realloc

  • 后面为什么又重新交给原来的指针变量p

  • 释放空间时候,为什么只需要释放p,而不用管p1了.

在回答这几个问题之前我们需要知道realloc开辟空间的方式.以下面的图为例

image-20210518144945933

在堆区,空间占有比较混乱,如图中所示malloc已经成功开辟了一块空间,但是可以清晰的看到,在malloc开辟的地方和右上角其他占用地区中间还有一块小缝隙.在下方的两块其他占用区中间也有一块空白的地方.

reallocmalloc申请的空间进行调整时候,会首先自动检测新的需要开辟的空间,如果直接跟在mallo后是否够用,比如下图:

image-20210518145638318
  1. 比如此时realloc还需要三个整型长度----------realloc (p, sizeof(int)*13); 检测到malloc后面还剩下4可用,于是便直接在其后追加三个空间,然后依然返回p的地址.

  2. 而倘若realloc还需要8个整型长度----------------realloc(p,sizeof(int)*18); 检测到malloc后面只有4个空间,并不够用,于是便再另起山头,在下面看到了一个可以开辟20整型长度的空地,于是便在这里重新开辟 18个整型空间,开辟好后,便把原malloc开辟空间内容拷贝一份放到新开辟的18个空间.然后返回新开辟的空间地址,返回时候,便同时自动释放掉原来malloc开辟的空间

  3. 倘若上面两种都开辟失败,就会返回NULL

因此,这就是我们为何说,一定要用新的指针接收realloc的返回值,因为如果开辟失败,非但没有得到新的空间,还返回了一个NULL给原来管理malloc开辟的空间的变量p,导致p无法再管理malloc. -------------这种危险写法p = (int*)realloc(p,sizeof(int)*18);

而如果开辟成功了,再重新把新的地址交给p管理,p就有两种可能.

  • 第一,仍然接管p原来空间(现在增大了)
  • 第二,接管了新的空间.

会发现,无论上面哪种情况,最后释放p时,都会完全释放两块空间(因为新开辟时候,会自动释放原malloc开辟空间).


3.常见的动态内存错误

示例1

int main()
{
     int *p = (int *)malloc(INT_MAX/4);
     for(int i = 0;i<10;i++)
         *(p+i) = i;
     free(p);
    return 0;
}

上面的例子有什么错误??

答案:

  1. 没有判断是否成功开辟空间,如果开辟失败p将会是NULL,那么后面的*(p+i) = i;就是在对 NULL指针进行解引用操作
  2. p最后未置为NULL,是不安全的,正如博主一开始写的男女朋友关系那个例子

示例2

int main()
{
    int i = 0;
    int *p = (int *)malloc(10*sizeof(int));
    if(NULL == p)
    {
    	perror("开辟失败原因:");
        return -1;
    }
    for(i=0; i<=10; i++)
    {
    	*(p+i) = i;
    }
    free(p);
    p = NULL;
    return 0;
}

上面的例子有什么错误??

答案:

数组越界,10个空间的数组,索引最大只能9.

示例3

int main()
{
    int a = 10;
    int *p = &a;
    free(p);
    p = NULL;
    return 0;
}

上面的例子有什么错误??

答案:

free前面已经讲到只能对管理堆区的内存进行释放,而这里的p管理的是栈区

小插曲:


| 之前忘记讲了,我们知道realloc是对动态开辟的内存进行管理,但是我们如果把realloc的第一个参数给一个NULL |

| 那么,此时的realloc就完全是个malloc , 比如 realloc(NULL, sizeof(int)*10) 等价于 malloc(sizeof(int) * 10)|


示例4

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(sizeof(int)*20);
	if (p != NULL)
	{
		for (int i = 0; i < 10; i++)
			*p++ = i;
	}
	free(p);
	p = NULL;
	return 0;
}

上面的例子有什么错误??

答案:

并没有完全释放堆区内存,下图进行解释

image-20210518155936487

在执行循环后,free释放的只是后面橙色的空间,黄色空间并没有释放.

示例5

int main()
{
    int *p = (int *)malloc(100);
    if(p == NULL) 
        perror("错误原因");
     else
     {
         for(int i= 0;i<25;i++)
             printf("%d ",p[i] = i);
     }
    free(p);
    ......  //省略几万行代码
    ......  //省略几万行代码  
    free(p);
    return 0;
}

上面的例子有什么错误??

答案:

多次释放同一块内存,原因是当代码功能过多时候,我们会忘记前面是否释放过,而导致重复释放.

第一次释放后,空间已经没有了,但是p还记忆着在堆区的原地址,再次释放,如果此时该地址其他东西被占用,将会导致内存管理出错,或者程序挂掉.

防止重复释放的解决办法: 每次释放后记得加 p = NULL,free对空指针NULL不做处理

示例6

int main()   
{
    while(1)
    {
        malloc(1);
    }
    return 0;
}

上面的例子有什么错误??

答案:

在疯狂的开辟空间,并没有还回去,导致自己不用了,别人也不能用(内存泄露).

这会导致整个计算机最后崩溃.

4.几个经典笔试题

题目一:

void GetMemory(char* p)
{
     p = (char *)malloc(100);
}

void Test(void)
{
     char *str = NULL;
     GetMemory(str);
     strcpy(str, "hello world");
     printf(str);
}

会出现什么结果???

答案:

程序会崩溃.原因:

GetMemory属于传值调用,不是传址调用.
即p虽然获取了堆区申请地址,可以进行管理,但由于GetMemory函数结束后,p的生命周期也就结束了.管理不了该空间,且下面用的还是str,不是p
最后,我们字符串拷贝的第一个参数是str,传值调用时,str的值并没有改变,仍然是NULL.
那么最后指针str还是一个空指针,便不能进行拷贝.程序出错.

其次,在函数GetMemory内部并没有释放内存.

怎么修改??

第一种改法:

void GetMemory(char* * p)  //修改形参
{
     *p = (char *)malloc(100);
}

void Test(void)
{
    char *str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
    
    free(str);//释放内存
    str = NULL;
}

第二种改法:

char * GetMemory(char* p)
{
    p = (char *)malloc(100);
    return p;       //直接返回值.
}

void Test(void)
{
     char* str = NULL;
     str = GetMemory(str);  //用str接收
     strcpy(str, "hello world");
     printf(str);
}

题目二:

char *GetMemory(void)
{
     char p[] = "hello world";
     return p;
}

void Test(void)
{
     char *str = NULL;
     str = GetMemory();
     printf(str);
}

答案:

烫烫烫烫烫烫烫烫烫…

解析:

GetMemory函数内部的局部变量的地址是在栈区,当GetMemory函数调用结束,字符数组p的生命周期也就结束,里面的内容也不复存在.而其却返回了p的地址,因为不复存在,系统便给未初始化的栈空间一个中文GB2312编码中0xccdd,正好对应

怎么修改?

第一种改法:

char *GetMemory(void)
{
     static char p[] = "hello world";  //变为静态变量,延长生命周期
     return p;
}

void Test(void)
{以上是关于动态内存管理详细介绍的主要内容,如果未能解决你的问题,请参考以下文章

超详细的C进阶教程!动态内存管理

从结构体内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理

解析PHP中的内存管理,PHP动态分配和释放内存

C++动态申请内存,new和delete详细介绍以及举例

C++动态申请内存,new和delete详细介绍以及举例

详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++