首页 > 系统相关 >【C语言】动态内存管理

【C语言】动态内存管理

时间:2024-05-31 12:32:37浏览次数:21  
标签:10 malloc 管理 int realloc free C语言 动态内存 空间

前言

为什么要有动态内存分配?

可以回想一下目前为止,我们想要向内存申请一块空间存放数据,有哪些手段呢

目前我们遇到的方式主要有两种

一、创建一个变量。比如我们存放一个整型数据,就创建一个整型变量。(也就是申请4个字节)我们创建一个变量,存放了一个数据。

int n = 2077;

二、如果我们要存放多个数据呢?这就是我们的数组,连续存储一组相同类型数据。比如下面这里申请了4个整型,也就是16个字节的空间。

int arr[4]={2,0,7,7};

但是,这两种存储方式是有缺陷的:

我们能否将n的4个字节空间扩大到10个字节、将arr的16个字节扩大到40个字节?(缩小也是同理)这是做不到的。这些空间开辟好就固定了,大小是不能变化的

那么,不能变大变小,在实际解决问题过程中就会存在缺陷

所以,C语言引入了动态内存开辟,让程序员可以自己申请和释放空间,想扩大缩小都可以,就比较灵活了。

这样的过程是动态的,顾名思义。

而实现动态内存管理的,就是四个函数:malloc 、free 、calloc、 realloc。

那么接下来就是这四个函数的介绍。

malloc函数

对于记忆一个函数名称,理解它的字母本身的含义是非常重要的:

m代表着memory,内存。alloc是allocate,分配、划拨。也就是开辟内存的意思。

头文件是stdlib.h。

参数size就是我们要开辟多大的内存块,单位是字节

返回值类型是void*,malloc帮我们开辟完空间,交给我们的方式就是把这个空间的起始地址交给我们。

注意,开辟空间的大小不是我们想多大就可以多大的。如果我们要开辟的空间过大,可能就会开辟失败。而开辟失败,这个函数就会返回空指针。

所以这个函数的返回应该有两种情况:起始地址或空指针。

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

我们无需自己计算,而是用sizeof的方式来计算10个整型的大小;同时,我们既然希望是把这块空间用于存放整型,我们就通过强制类型转换,最终用一个整型指针来接收这块空间的起始地址。

而根据我们刚才所说,malloc开辟空间可能有两种情况,所以我们要对空指针的情况进行检查

检查空指针情况
int main()
{
    int* p = (int*)malloc(10*sizeof(int));
    if(p = NULL)
    {
        perror("malloc");//perror请看下文简介
        return 1;//返回0代表正常情况,非0值代表异常
    }
    return 0;
}
perror函数

这个函数能将错误对应的错误信息直接打印出来,原型是void perror(const char* str);它会先打印str指向的字符串,然后帮你加上":"和一个空格,再打印错误信息。

malloc申请空间的使用

言归正传,malloc申请的也是一块连续的空间,所以我们可以将其视作数组一般,在我们申请的40个字节里,存上10个整型:

int main()
{
    int* p = (int*)malloc(10*sizeof(int));
    if(p = NULL)
    {
        perror("malloc");//perror请看下文简介
        return 1;//返回0代表正常情况,非0值代表异常
    }

    int i = 0;
    for(i = 0;i < 10;i++)
    {
        *(p + i)=i + 1;//将1~10赋给10个整型的空间
    }    

    return 0;
}

但是我们要清楚,这样开辟的空间与数组开辟的空间的区别:
1.动态内存的大小是可以调整的。

2.开辟空间的位置不同。

看这张图,是否还记得内存分为这几个区域。

可以看到我们的数组是放在栈区的,而动态内存,也就是我们malloc free calloc realloc操作的空间是在堆区。 

最后还要注意的一点是,如果在使用malloc时,参数size的大小为0,那么malloc的行为是未定义的,取决于编译器。

free函数

上面我们做的是申请空间的动作,但其实在申请和使用完之后是需要释放空间的

在C语言中,free函数就是专门用来做动态内存的释放和回收的。

可以看到,头文件是stdlib.h,原型是void free(void* ptr);

返回类型为void,无需返回值。

参数是void*类型的指针,可以将任意类型地址的空间释放。

所以在刚才我们的代码中,只需在使用完空间后写上一句free(p);就能释放了。

free调用后是把这块空间还给操作系统了,但是p仍然存放着这块空间的地址

此时我们的p就变成了野指针p指向的空间不属于当前程序,但还是能找到这个空间)。

就像你将房子租给一个房客,在他退租后仍然拥有这个房子的钥匙能够访问这个空间一样,十分危险。

所以我们要收走这个“钥匙”,也就是p=NULL;

free(p);
p=NULL;
为什么要free

此时我们还能看到数组与malloc申请空间的另一个不同。

请看数组的生命周期的情况:

int main()
{
	int arr1[10];

	{
		int arr2[5];
	}//出了这里,arr2生命周期就结束了
	
	return 0;
}//出了这里arr1生命周期就结束了

注意,局部变量(数组)是进入范围创建,出了范围就自动销毁了,也就是将这块空间还给操作系统了。这种空间的生命周期的维护是自动的。但malloc申请的空间就不一样,需要手动(代码方式)释放内存。

如果不释放的话,程序结束时也会被操作系统自动回收

似乎不释放也会由操作系统“擦屁股”,但是在我们不使用这块空间后可能还有很多代码,而我们没有释放这块空间,这块空间既不用也不释放,有点浪费空间了。所以我们不用时应该释放。

而且,在后面我们想要释放的时候可能找不到这块空间了。 

使用free有两种要注意的情况:

1.如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。

2.如果参数ptr是NULL指针,则函数什么事都不做。

比如ptr是指向数组的指针时,不是动态开辟的,这是不能用free释放的。数组在栈上,而free负责的是堆区的空间。

小结

根据上面的讲解,malloc是开辟一块空间,free是释放一块空间,它俩最好成对使用

calloc函数

 calloc也是用来动态内存分配的。

返回类型仍然是void*,而有两个参数。

 

可以看到,这个函数是开辟空间并且初始化为0。 

总结就是这个函数为你开辟num个size大小的空间,把起始地址返回,同时会将内存内容初始化为0。

比如我们现在申请10个整型大小的空间(下面的malloc用于对比):

int* p1 = (int*)calloc(10,sizeof(int));
int* p2 = (int*)malloc(10*sizeof(int));

同样的,calloc开辟空间也可能失败,也会返回NULL,所以我们同样需要对NULL情况进行检查:

if(p == NULL)
{
    perror("calloc");
    return 1;
}

如果检查不是NULL,代表开辟成功,那么这块空间就可以使用。为了体现calloc开辟的特性,我们就把这块空间的值进行打印:

int i = 0;
for(i=0;i<10;i++)
{
    printf("%d ",p[i]);//*(p+i)
} 
free(p);//使用完也要释放
p=NULL;//别忘了,否则p变为野指针

使用完要记得释放空间和把p置为NULL。

可以看到,打印的结果就是10个0,说明calloc申请来的空间确实会被初始化为0。

malloc没有初始化的动作,率略高一些。 

realloc函数

re是再的意思,所以realloc是调整空间的意思。我们前面说了动态内存开辟来的空间可以变大变小,而变大变小依靠的就是realloc函数。

原型是void* realloc(void* ptr,size_t size);

我们再看看参数:

 

可以看到,ptr指向的是被malloc calloc realloc先前开辟过的空间。(所以realloc是调整)

size就是你需要调整为的空间大小。 

返回的是调整之后的内存起始位置。

现在我们可以在刚才calloc开辟空间的基础上进行调整,将其变大为20个整型的空间:

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        perror("calloc");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", p[i]);
    }

    p=(int*)realloc(p, 20 * sizeof(int));//有问题

    free(p);
    p = NULL;
}

但这样写,是有问题的,我们不能直接用p来接收realloc的返回值。

在知道为什么之前,我们需要先清楚realloc到底是怎么扩大空间的:

realloc函数在申请空间时,有两种情况:

一、后面后续的空间尚未分配:那么realloc返回旧的地址(也就是参数部分传入的地址)。

二、后面后续的空间已被分配:

  1.realloc会在内存的堆区中找一块新的空间,这块空间的大小就是我们想要的新的大小(也就是realloc第二个参数size的大小);

  2.会将旧的数据,拷贝到新的空间;

  3.会释放旧的空间(不用手动free了);

  4.最后,返回新的地址。

特殊情况:也有可能扩容失败,返回NULL。

因为有这种特殊情况的存在,所以我们直接将realloc的返回值赋给p不是很好的做法,因为如果开辟失败p变为NULL,我们原本p指向空间的数据也找不到了。 

int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        perror("calloc");
        return 1;
    }
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        printf("%d ", p[i]);
    }
    //p=(int*)realloc(p, 20 * sizeof(int));有问题
    int* ptr = (int*)realloc(p, 20 * sizeof(int));
    if (ptr != NULL)
    {
        p = ptr;//如果ptr不是空指针,就再赋给p
    }
    free(p);
    p = NULL;
}

注意,realloc第一个参数ptr不能乱传,必须是动态开辟的起始地址,不能传动态开辟空间的中间某个位置,也不能传非动态开辟的空间地址。

到此,本篇博客的内容就全部结束了,祝阅读愉快^_^

标签:10,malloc,管理,int,realloc,free,C语言,动态内存,空间
From: https://blog.csdn.net/2301_82135086/article/details/139335976

相关文章