C语言动态内存管理详解

举报
YIN_尹 发表于 2023/08/07 20:09:33 2023/08/07
【摘要】 这篇文章,我们一起来学习C语言中的动态内存管理!!!1.为什么存在动态内存分配我们先来想一下,我们现在掌握的开辟内存的方式是什么:是不是就是直接创建一个变量或者数组,然后操作系统给我们分配空间:int main(){ int val = 20;//在栈空间上开辟4个字节 int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间 return 0;}大家...

这篇文章,我们一起来学习C语言中的动态内存管理!!!


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

我们先来想一下,我们现在掌握的开辟内存的方式是什么:


是不是就是直接创建一个变量或者数组,然后操作系统给我们分配空间:

int main()
{
    int val = 20;//在栈空间上开辟4个字节
    int arr[10] = { 0 };//在栈空间上开辟40个字节的连续空间
    return 0;
}

大家思考一下这样的方式有没有什么弊端:


我们这样定义一个数组int arr[10],开辟的空间大小是固定的。

int arr[10]就只能存的下10个整型,我们想多存一个都不行。

我们想存11个整型,用int arr[10]这个数组就不行了,除非我们再定义一个数组。

其次:数组在声明的时候,需要指定数组的长度,它所需要的内存在编译时分配。

但是,对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道。

那这时候,这样开辟空间的方式就不行了。

这个时候就需要动态开辟内存空间了。


那怎么实现动开辟内存呢?


C语言给提供了一些函数使得我们可以实现对内存的动态开辟。


2.动态内存函数的介绍

接下来我们就来一起学习一下这些函数:


2.1 malloc

看一下它的参数:


void* malloc (size_t size);

那它是用来干嘛的呢?

5a268f78dcca431e8e634500175eb4cf.png

接下来再来给大家详细解释一下:

  1. 参数size_t size接收我们想要开辟的内存空间的大小,单位是字节,返回指向该内存块开头的指针。
int main()
{
    void* p = malloc(40);
    return 0;
}

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

malloc给我们返回的指针类型是void*,但我们知道void*是不能直接解引用的,注意使用时要先转换为我们需要的指针类型。

比如我们想再申请的空间里放整数,就应该这样搞:

int* p = (int*)malloc(40);

然后,我们就可以往里面放整型数据了。

当然,你想用来放其他数据,就转换成其它相应的类型。


如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

当然用malloc开辟空间也有可能开辟失败,当请求失败的时候,它将会返回空指针(NULL)。

我们知道空指针是不能直接解引用的。

所以,对于malloc的返回值,使用之前,我们一定要检查一下。

如果为空,那就是失败了,就不能使用了。

那什么时候又可能失败呢,比如当我们开辟的空间特别大的时候,就有可能失败返回空指针。

如果开辟失败我们可以做一个相应处理,打印一下错误信息,然后return一下,让程序结束。

    int* p = (int*)malloc(40);
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }

函数strerror我们在之前的文章里介绍过。

当然我们也可以断言一下:

assert(p);

如果不为空,那就是开辟成功了。

开辟成功,我们就可以使用了。

举个例子,我们现在就在上面开辟好的P指向的40字节的空间里放一些整型数据。

    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }

40个字节,我们可以放10个整型,0到9。

我们也可以通过内存观察一下:

使用前:

cb6f689d9b4c4de09407ae3befde6f4c.png

这里再给大家提一点:

我们发现开辟好的空间里面放的这些其实是一些随机值

这也是malloc的一个特性:

  1. 新分配的内存块的内容不做初始化,仅保留不确定的值。

使用后:

f1855d9bf03240138b723d1d9e5ae557.png

如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

此时malloc的行为是标准是未定义的,取决于编译器。

所以我们尽量不要这样试,况且这样做也没什么意义,申请一个大小为0的空间?


那申请的空间使用完之后,我们是不是什么都不用管了呢?


不是的,对于像malloc这些函数动态开辟的内存,使用完之后我们是需要将这些空间释放掉的,不及时释放,有可能会造成内存泄漏。


那怎么释放呢?


2.2 free

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的。


接下来我们就来一起学习一下函数free:


它的参数是这样的:

1e3e9664ca02486e9c595a2f8eba5952.png

怎么用呢?

  1. 参数void* ptr接收一个指针,这个指针指向我们使用malloc这些动态开辟内存函数分配的内存块,无返回值。

比如,上面例子中的指针P:

    int* p = (int*)malloc(20);
    /*if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }*/
    assert(p);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }

在上述循环的过程中,p 的指向并没有发生改变,还是指向分配的内存块的起始地址,所以我们就可以这样做:

free(p);

这样,就把malloc申请的空间释放掉了。

那释放掉之后,是不是就万事大吉了呢?

不,我们还应该做一件事情:

p置空

p = NULL;

为什么要这样做呢?


大家想一下,我们现在虽然已经把p指向的那块空间给释放掉了。

但是,p是不是还保存着那块空间的地址啊。

那么一个指针指向了一块被释放掉的空间,那它是不是一个典型的野指针啊。

要知道如果对一个野指针解引用那程序就会出错的。


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

也就是说参数 ptr 指向的空间必须是动态开辟的。

如果指向其它的空间,那么free函数会怎么处理是标准未定义的。


比如:

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

你写一个这样的代码,肯定是不行的,因为p指向的空间不是动态开辟的。

这里的num是一个局部变量,要知道局部变量是保存在栈区的,再来复习一下:

409f755863064d49ab692f30c4423794.png

而我们这些动态开辟的内存,是堆区分配的。

  1. 如果参数 ptr 是NULL指针,则函数不执行任何操作。
    像这样:
    int* p = NULL;
    free(p);

函数不执行任何操作。

2.3 calloc

C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。

我们一起来学习一下:

15f49e9365bb4b61acf7726e55950dc3.png

函数calloc 有两个参数,无返回值,那它的作用是什么呢?这两个参数分别接收什么呢?


函数的功能是为 num 个大小为 size 的元素开辟一块空间,同样返回指向该内存块开头的指针,类型为(void*)

参数size_t num接收我们想要分配空间的元素个数;

size_t size接收每个元素的大小,单位为字节。


那我们就可以这样用:

int main()
{
    int* p = (int*)calloc(10,sizeof(int));
    /*if (p == NULL)
    {
        printf("%s\n", strerror(errno));
        return 1;
    }*/
    assert(p);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    free(p);
    p = NULL;
    return 0;
}

当然calloc分配的空间使用完也应该使用free释放并将指向空间起始地址的指针置空。


与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

对于malloc 来说,它不会对开辟好的空间初始化,里面放的是随机值。

但是,calloc 会把申请的空间的每个字节都初始化为0。


就拿上面那段代码,我们来调式看一下:

b01b1eccb36a45498781bc8b65ac8ed6.png

  1. 和malloc 一样,calloc 函数如果开辟内存块失败,则返回空指针void*。

所以对于calloc 的返回值,我们也有必要做一下检查,判断是否为空指针。

10f083ec3313480b98dad989daa9bd47.png和malloc一样,如果参数size_t size为0,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

标准未定义的,取决于编译器。

总的来说,malloc和calloc 区别不大:


1. calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,而malloc不会,里面放的是随机值。

2. 它们的参数不同。


2.4 realloc

realloc函数的出现让动态内存管理更加灵活。


有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们就要对开辟的内存的大小做出灵活的调整。

那 realloc 函数就可以做到对动态开辟的内存大小进行灵活的调整。


一起来学习一下:

eaa63407552e43199c49b9823c980811.png

两个参数分别接收什么呢?


void* ptr接收一个指针,该指针指向我们想要调整大小的内存块,当然这块内存块也应该是我们之前动态开辟的空间。

size_t size接收我们想要为内存块调整的新大小,以字节为单位。


返回值又是什么呢?


返回指向重新分配的内存块的指针


举个例子吧,我们再来看一段上面的代码:

int main()
{
    int* p = (int*)malloc(40);
    assert(p);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    free(p);
    p = NULL;
    return 0;
}

还是这段代码:

我们使用malloc申请了40个字节空间,放了10个整型。

那假设我们现在想再放10个整型,那原来的空间就不够用了,那我们现在就可以使用realloc 进行扩容。

怎么搞呢?这样写:

    int* p = (int*)malloc(40);
    assert(p);
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
    }
    int* ptr = (int*)realloc(p, 80);
    if (ptr != NULL)
    {
        p = ptr;
        ptr = NULL;
    }
    //使用
    free(p);
    p = NULL;

变成这样,我们再中间又加了一些代码。


我们看到上面代码中我们扩容后返回的指针赋给指针变量ptr ,那为什么不直接给p呢?

因为,realloc开辟空间也有可能会失败的,它失败同样返回空指针。

所以我们先赋给ptr ,然后判断一下,不为空,再赋给p,让p继续管理扩容后的空间。

然后,不使用ptr ,最好将其也置空。


然后,没什么问题,我们就可以使用扩容后的空间了。


但是,在扩容的时候,又存在存在两种情况:


原地扩

什么时候是原地扩呢?

就还拿刚才的例子来说:

int* ptr = (int*)realloc(p, 80);

p原来指向的空间是40个字节,现在我们想要使用realloc将p指向的空间扩容为80个字节。

那这时realloc就会从原空间向后看,如果后面有足够大的空间能够再增加40个字节,那么realloc就会在原地向后扩容40个字节,使得p指向的空间变为80字节。

0ff107a384cc4043851a80fbb929f401.png

当然这样realloc返回的地址还是原来p指向的地址。


异地扩

那什么时候异地扩呢?

假设现在还是相把p指向的空间扩容为80个字节。

但是,原空间后面没有足够大的空间,那这时候怎么办?

这时候:

realloc会在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址,不再指向原空间。

而且:

realloc会将原空间的数据拷贝到新空间,并会将旧空间释放掉。然后返回指向该内存块起始地址的指针。

比如:

int* p = (int*)realloc(NULL, 40);

那这句代码就相当于:

int* p = (int*)malloc(40);

以上就是对这4个动态内存函数的介绍,它们包含的头文件都是#include

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。