0


C语言进阶——动态内存管理

🌳🌲🌱本文已收录至:C语言——梦想系列

更多知识尽在此专栏中!

🎉🎉🎉欢迎点赞、收藏、关注 🎉🎉🎉


🌳前言

** C/C++中的内存区域大体可划分为这三个部分:栈区、堆区以及静态区,这三块区域比较重要。比如我们的 main 函数就是在栈上开辟的空间,当然我们使用的一般变量也都是存储在栈区上的,但是栈区空间有限,不能存储较大的数据,此时我们会通过动态内存管理来为这些“大数据”在堆上开辟空间供其使用,用完后记得释放内存就好了,除了储存“大数据”外,在堆区上开辟的空间还可以随意改变其大小(扩大或缩小都可以)。由此可见动态内存开辟的实用性,要想实现动态内存开辟也不难,只需要跟着本文一步一步学习就好了!**


🌳正文

C语言中的动态内存开辟函数有三个:malloccallocrealloc,有开辟就要有释放,一般在使用以上三个函数时,都会配套使用一个 free 来进行内存释放。除了介绍这几个函数外,我还会介绍一下C99标准中的柔性数组,因为它也会用到动态内存管理。

🌲一、malloc

🌱声明

malloc,是我们要学习的第一个内存开辟函数,它的作用是向堆区申请一块目标大小的连续空间,如果申请成功,会返回这块空间的首地址,失败则返回空指针(NULL)当我们申请内存后,一般会对返回的指针进行判断,如果是空指针,就得结束程序(因为此时已经申请失败,再继续运行就会出错),虽然现在的空间都比较大,几乎不会出现申请失败的情况,但最好还是加一个判断,确保万无一失嘛,判断这个操作适用于所有动态内存申请函数。


malloc标准格式

可以看到** malloc 格式还是比较简单的,只需要传递大小,然后准备好指针接收返回值就行了,当然我们在使用时会在此基础上进行完善,比如对返回值进行强制类型转换、传递的字节数通过sizeof(类型)数量得出、对返回指针进行判断等*

//malloc 使用方法
int main()
{
    int* p = (int*)malloc(sizeof(int) * 5);//申请五个int型的空间
    if (p == NULL)
    {
        printf("申请失败!\n");
        return 1;//结束程序
    }

    //申请成功
    //……使用……
    //释放
    free(p);
    p = NULL;//需要置空,避免野指针
    return 0;
}

🌱使用

在有的题目中,会涉及到大量的数据,此时需要足够大的空间,此时在栈区上申请会出错,毕竟栈区空间有限,但如果改在堆区上申请,就会合适且轻松。

//malloc 的实际使用
int main()
{
    int* p = (int*)malloc(sizeof(int) * 10000);//申请40000字节的空间
    if (p == NULL)
        return 1;//这里我们直接结束程序就好了
    int i = 0;
    for (i = 0; i < 10000; i++)
        *(p + i) = i;
    printf("测试完成,无任何报错\n");
    free(p);
    p = NULL;
    return 0;
}

🌱注意

注意

  • 1.malloc 申请后要对其返回值进行强制类型转换
  • 2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了
  • 3.使用前要判断,使用时不要越界,使用后要释放,释放函数马上介绍
  • 4.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器
  • 5.申请要合理,不要无限申请,这样会造成严重的后果,比如下面这个例子

🌱补充例子

因为申请的内存来自于我们的电脑,如果将申请空间这个操作放在一个死循环中,电脑内存就会被申请满,从而导致电脑运行奔溃,然后就会蓝屏(x64环境下会蓝屏,x86环境下有保护)

//补充示例
//注意:尝试前确保数据已保存
int main()
{
    //死循环,不断申请
    while (1)
    {
        int* p = (int*)malloc(sizeof(int) * 100);
        //申请完还不释放
    }
    return 0;
}

造成这种现象有两个原因:1.无限申请空间 2.申请的空间不释放。x64环境内存分配更激进,运行一会内存就爆了,然后就会蓝屏(我已经试过了),如果想玩玩记得保存好数据,举出这个例子就是想让大家记住这两个重要的点:要合理、要释放,避免发生意外情况。

🌲二、free

🌱声明

free 就是我们用来释放已申请内存的工具,有申请就要有释放,所以** free** 一般都是和动态内存申请函数配套使用,可根据实际情况进行释放,但也不能随意释放。free 的形式就非常简单了,只需要在其中放入指向待释放空间的指针即可。


free标准格式

** free** 用起来也很简单,就是对已申请且用完的空间进行释放,值得注意的是:free****释放的空间必须是已申请的空间,释放完后要将指向这块空间的指针置空

//free 使用方法
int main()
{
    int* p = (int*)malloc(sizeof(int));//向堆区申请1个整型的空间
    if (p == NULL)
        return 1;//申请失败的情况

    char* ptr = "123";//在栈区开辟的空间
    free(ptr);//非法释放,会报错
    ptr = NULL;

    free(p);//合理释放
    p = NULL;//置空,避免野指针
    return 0;
}

非栈区申请的空间,不能释放,这点还是很好理解的,避免张冠李戴嘛。

🌱使用

这里我们就沿用之前 malloc 无限申请空间的例子,说明** free** 释放空间是真实存在的。

//free 实际运用
int main()
{
    //死循环,不断申请
    while (1)
    {
        int* p = (int*)malloc(sizeof(int) * 100);
        free(p);//申请完后释放
        p = NULL;//相当于没申请
    }
    return 0;
}

当然 free 的例子得配合适合的程序才能体现其价值,这里我们就使用无限申请空间简单验证下就好了,free 虽方便,但也有使用注意事项

🌱注意

注意

  • 1.free 的对象必须是已申请的堆区空间
  • 2.free 完后要对目标指针手动置空
  • 3.不可对同一对象进行连续free

🌲三、calloc

🌱声明

** calloc,跟 malloc 很像,功能也差不多,都是向栈区申请一块目标空间,不过 calloc 有个小升级,就是 calloc 完后,它会帮忙把申请的空间初始化为0,这样就不至于申请空间中存放的都是随机数了。malloc** + memset 也可以实现这一功能,但奈何别人** calloc** 优秀,将二者的功能合二为一了。


calloc标准格式

calloc 无非就是参数部分比 malloc 多了一个参数(其实相当于没多,因为 calloc 中的两个参数,在 malloc 中被我们手动乘为一个参数了),calloc 在使用时也跟 malloc 一致,都是返回目标空间的首地址,都需要进行判断,保证不会得到一个空指针,当然肯定也少不了释放

//calloc 使用方法
int main()
{
    int* p = (int*)calloc(10, sizeof(int));//申请10个整型大小的空间
    if (p == NULL)
        return 1;

    int i = 0;
    for (i = 0; i < 10; i++)
    {
        if (i < 5)
            *(p + i) = i;//测试是否初始化为0
        printf("%d ", *(p + i));
    }

    free(p);//释放
    p = NULL;//置空
    return 0;
}

🌱使用

calloc 可以用于需要动态内存开辟,且开辟空间要全部初始化为0的情况,这里我想到了一个题目:小乐乐与序列,题目大概意思就是将序列去重后排序并输出,这里的解题思路是:找到与数列中的数值对应的下标(这里的下标是指申请空间中对于首地址的偏移量),再将其对应的值改为1(改的是申请空间的值),即使有重复的数字,也都只会改一次,而如果是没有出现的数字,就默认为0(根据值来判断,如果出现过,不管是否重复,都为1)。最后再弄个循环,从1下标处开始判断(题目要求,不会出现元素0),如果对应值为1,说明此处留下过标记,输出此时的下标就行了。这样一来我们就得到了一个去重且排好序的序列,可以看出我们有个硬性要求:申请空间默认为0,此时我们的 calloc 就可以派上用场了


题目详情

//BC118 小乐乐与序列
//calloc 实际运用
#include <stdio.h>
#include<stdlib.h>

int main() 
{
    int n = 0;
    scanf("%d", &n);
    int* pa = (int*)calloc(n + 1, sizeof(int));//申请n+1大小的空间
    if (pa == NULL)
        return 1;
    int i = 0;
    int m = 0;
    for (i = 0; i < n; i++)
    {
        scanf("%d", &m);
        *(pa + m) = 1;
    }
    for (i = 1; i <= n; i++)
    {
        if (*(pa + i) == 1)
        {
            printf("%d ", i);
        }
    }
    free(pa);//释放
    pa = NULL;//置空
    return 0;
}

这种解法本身就很妙,再配合上 calloc 默认初始化为0的特性,就更妙了。

🌱注意

注意

  • 1.calloc 申请后要对其返回值进行强制类型转换
  • 2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了
  • 3.使用前要判断,使用时不要越界,使用后要释放
  • 4.申请要合理,不要无限申请,这样会造成严重的后果
  • 5.calloc 会将申请的空间初始化为0
  • 6.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器

🌲四、realloc

🌱声明

英语中的 re 有重复、再次的意思,因此 realloc 作用是对已开辟的空间进行扩容(再申请),可以推测出** realloc 需要两个参数:待扩容空间地址、扩容后的大小。如果给 realloc 的第一个参数传递为一个空指针,那么此时的 realloc 就相当于 malloc** ,仅仅是申请了一块空间。

** realloc** 在扩容时有两种情况:1.后续空间足够大,且能够与已开辟好的空间(这里简称目标空间)相连,直接开辟就行了 2.后续空间不足,此时 realloc 会往后寻找一片足够大的空间,开辟好后会将目标空间中的元素搬过来,然后会对其旧的空间进行释放,这样就相当于增容了。当然 realloc 也需要判断、释放、置空

//realloc 使用方法
//情况1,后续空间足够
int main()
{
    int* p = (int*)malloc(sizeof(int) * 5);//只申请了五个整型大小的空间
    if (p == NULL)
        return 1;
    int i = 0;
    int* ptr = (int*)realloc(p, sizeof(int) * 10);//扩容为十个整型大小的空间
    if (ptr == NULL)
        return 1;

    free(ptr);//释放
    ptr = p = NULL;//置空
    return 0;
}

//realloc 使用方法
//情况2,后续空间不足
int main()
{
    int* p = (int*)malloc(sizeof(int) * 5);//只申请了五个整型大小的空间
    if (p == NULL)
        return 1;
    int i = 0;
    int* ptr = (int*)realloc(p, sizeof(int) * 100);//扩容为一百个整型大小的空间
    if (ptr == NULL)
        return 1;

    free(ptr);//释放
    ptr = p = NULL;//置空
    return 0;
}

🌱使用

realloc 可以用于需要二次申请(扩容)的场景,比如在顺序表中,如果下标等于容量,就需要扩容以确保首地址不被改变。当然因为顺序表是属于后面的知识,所以这里就用一个简单例子说明一下扩容的实际场景。

//realloc 的实际使用
int main()
{
    int* p = (int*)malloc(sizeof(int) * 5);//先申请五个整型大小的空间
    if (p == NULL)
        return 1;
    int* ptr = (int*)realloc(p, sizeof(int) * 10);//再扩容为十个整型大小
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        *(p + i) = i;
        printf("%d ", *(p + i));
    }
    free(ptr);//释放
    //free(p);//释放 ptr 就够了,因为 p 包含于 ptr
    ptr = p = NULL;//都要置空
    return 0;
}

🌱注意

注意

  • 1.realloc 申请后要对其返回值进行强制类型转换
  • 2.申请空间的大小不必自己进行计算,通过 sizeof 配合目标数量就好了,realloc 申请的空间大小至少要大于原空间大小,不然没意义
  • 3.使用前要判断,使用时不要越界,使用后要释放
  • 4.申请要合理,不要无限申请,这样会造成严重的后果
  • *5.realloc 对参数1传递空指针*,等价于 **malloc
  • 6.申请空间时,不要申请0字节大小的空间,这是标准未定义的行为,具体实现操作取决于编译器

🌲五、小结

不难发现这几个动态内存管理都有相似之处,比如需要对返回地址进行判断、使用完后对开辟空间进行释放等。于是我们可以把动态内存开辟的常见错误总结为以下几点:

  • 1.对空指针进行解引用(开辟后没有进行判断)
  • 2.对开辟空间的越界访问(使用空间与开辟空间不匹配)
  • 3.对非动态内存开辟的空间进行释放(比如在栈区上开辟的空间是不能释放的)
  • 4.释放空间与申请空间不匹配(跟第2点很像,使用这些空间时要注意!)
  • 5.对同一块动态开辟空间进行多次释放(开辟的空间只能释放一次,释放过后的空间不能再释放)
  • 6.动态开辟的空间忘记释放(内存泄漏问题,这个问题比较严重)
  • **7.通过传值函数调用开辟空间(形参的改变并不会影响实参,此时通过函数开辟的空间处于无人认领的情况,而主函数中释放的空间也并非在堆区上开辟的空间) **

关于以上错误的详情可以参考这篇文章:常见的动态内存的错误 和 柔性数组

🌲六、动态内存开辟笔试题

** 下面是几道比较经典的动态内存开辟笔试题,看完这些题后我们对动态内存的理解能提升一个层次!**

** 题目出自经典书籍《高质量C/C++编程》**

🌱第一题

请问运行Test 函数会有什么样的结果?

//第一题
void GetMemory(char* p)
{
    p = (char*)malloc(100);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(str);
    strcpy(str, "hello world");
    printf(str);
}
int main()
{
    Test();
    return 0;
}

第一题中的主要错误是对空指针的解引用,出现空指针的原因:给 GetMemory 函数传值,然后再进行动态内存开辟,无法对实参 str 造成影响,相当于此时的 str 仍然是一个空指针,对空指针解引用是非法的。当然此题还有其他错误,下面来看看详解:

1.传值调用,即使成功开辟空间,也不会对实参 str 造成影响

2.没有对函数 GetMemory 中的开辟情况进行判断

3.对空指针 str 的解引用(strcpy 会对其进行解引用)

4.没有对开辟的内存进行释放(显然此时只能在 GetMemory 中释放)

🪴纠正方案

将上面的错误逐个解决就好了,下面来看看纠正后的代码:

//第一题
void GetMemory(char** p)
{
    *p = (char*)malloc(100);
    if (*p == NULL)
        exit(-1);//申请失败就直接结束程序
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str);//传址调用
    strcpy(str, "hello world");//合法解引用
    printf(str);//这种打印方法是合法的

    free(str);//释放
    str = NULL;//置空
}
int main()
{
    Test();
    return 0;
}

🌱第二题

请问运行Test 函数会有什么样的结果?

//第二题
char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    Test();
    return 0;
}

第二题中的主要错误是使用已经回收的空间,GetMemory 中的 p 作为数组首元素地址,同时也是一个位于函数中的局部变量,生命周期仅仅在函数内部 ,函数结束后的指针 p 指向空间已被回收。此时即使得到了 p 指向空间的地址,也无法打印出之前的值。换句话说,此时的指针 str 指向空间是一块全是随机值的空间,强制打印会得到一串乱码

🪴纠正方案

将数据存放在静态区中,这样在函数 Test 中也能使用了。

至于为什么不直接在堆上申请,使用完后释放?原因很简单,如果想把数据存储在堆区上,需要挨个存入,之后才能正常释放,就拿字符串 "hello world" 来说,需要一个字母一个字母的存,如果直接让指针 p 指向字符串常量 "hello world" 的话,也能达到打印的效果。但释放就不行了,因为 p 此时指向的是只读数据区(非堆区)

//第二题
char* GetMemory(void)
{
    static char p[] = "hello world";//存放在静态区中
    return p;
}
void Test(void)
{
    char* str = NULL;
    str = GetMemory();
    printf(str);
}
int main()
{
    Test();
    return 0;
}

🌱第三题

请问运行Test 函数会有什么样的结果?

//第三题
void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
}
int main()
{
    Test();
    return 0;
}

第三题中的主要错误是没有对已开辟的空间进行释放,这样会造成内存泄漏;其次就是没有对开辟的空间进行判断。短期来看这段代码并没有大问题,但如果此段代码日夜不停的运行,不断申请空间,却又不释放,长此以往内存就泄漏了,是个比较严重的问题

🪴纠正方案

在申请空间后进行判断,使用完内存后记得释放就行了。

//第三题
void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
    if (*p == NULL)
        exit(-1);//申请失败
}
void Test(void)
{
    char* str = NULL;
    GetMemory(&str, 100);
    strcpy(str, "hello");
    printf(str);
    free(str);//释放
    str = NULL;//置空
}
int main()
{
    Test();
    return 0;
}

🌱第四题

请问运行Test 函数会有什么样的结果?

//第四题
void Test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");
    free(str);
    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
int main()
{
    Test();
    return 0;
}

第四题主要问题是**将 **str *释放后,仍然对其进行操作(野指针),这会导致难以预料的后果;另一个小问题就是*没有对开辟的空间进行判断。当然 free 语句把 ptr 指向空间释放后,其中的内容会变成随机值,实际上 str != NULL 这条语句是不会起作用的

🪴纠正方案

将释放后置空、申请后判断的语句加上就行了

//第四题
void Test(void)
{
    char* str = (char*)malloc(100);
    if (str == NULL)
        return 1;//申请失败

    strcpy(str, "hello");
    free(str);//释放
    str = NULL;//置空

    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
int main()
{
    Test();
    return 0;
}

🌲七、C/C++中的内存区域划分

我们都知道,C++ 是 C语言 的超集,因此二者在内存区域划分基本一致。主函数、局部变量、返回地址等占用空间小的数据是存放在栈区上的;而占用空间大或程序员指定存放的数据是存放在堆区上的;全局变量、静态数据等则是存放在静态区(数据段)中。

  • 1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返****回地址等。
  • 2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分****配方式类似于链表。
  • 3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  • **4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。 **

🌲八、柔性数组

🌱声明

柔性数组(flexible array),这是个出现在C99标准中的新特性,其表现形式为数组作为结构体中最后一个成员,且数组前面至少有一个其他成员。

//柔性数组的形式
struct Test
{
    int a;
    char b;
    int arr[];//这就是柔性数组
};
int main()
{
    printf("包含柔性数组结构体的大小:%d\n", sizeof(struct Test));
    return 0;
}

可以看到,在计算包含柔性数组结构体的大小时,并未包含此数组的大小,说明此结构体中的最后一个成员(柔性数组)的大小是可控的,而要让大小可控,就需要用到我们前面介绍的动态内存管理函数,这也正是柔性数组****柔的原因。

🌱使用

那么柔性数组该怎么使用呢?一起来看看下面这个例子吧

此时结构体中的柔性数组获得了100个整型大小的空间,可以随意使用,如果觉得不够用了,还可以通过** realloc **再次扩容

//柔性数组的使用
struct Test
{
    int i;
    int arr[];//柔性数组
}T1;
int main()
{
    struct Test* p = (struct Test*)malloc(sizeof(struct Test) + sizeof(int) * 100);//获取100个整型大小的空间
    if (p == NULL)
        return 1;
    T1.i = 100;
    int i = 0;
    for (i = 0; i < 100; i++)
    {
        T1.arr[i] = i;
        printf("%d ", T1.arr[i]);
    }
    free(p);//释放
    p = NULL;//置空
    return 0;
}

🌱注意

注意

  • 1.柔性数组前至少要有一个其他成员
  • 2.sizeof 计算结构体大小时,并不会包含柔性数组的大小
  • 3.在对柔性数组进行空间分配时,一定要包含结构体本来的大小
  • **4.柔性数组是C99中的新特征,部分编译器可能不支持 **

🌱模拟实现柔性数组

既然我们拥有众多动态内存管理神器,能否直接通过对一个指针指向空间的再次申请来模拟实现****柔性数组呢?答案是可以的,不过会有些麻烦:

//模拟实现柔性数组
struct Test
{
    int i;
    int* p;
}T1;
int main()
{
    struct Test* ptr = (struct Test*)malloc(sizeof(T1));//先在堆上开辟空间
    if (ptr == NULL)
        return 1;
    T1.p = (int*)malloc(sizeof(T1) + sizeof(int) * 100);
    if (T1.p == NULL)
        return 1;
    T1.i = 100;
    int i = 0;
    for (i = 0; i < 100; i++)
    {
        T1.p[i] = i;
        printf("%d ", T1.p[i]);
    }
    //需要释放两次
    free(T1.p);
    T1.p = NULL;
    free(ptr);
    ptr = NULL;
    return 0;
}

光是动态内存申请和内存释放就需要操作两次,而且还有很多隐藏问题,而这些问题在柔性数组中可以得到避免

🌱柔性数组的优势

既然柔性数组是作为一个C语言的新特征而出现的,那么其设计者在设计语法的时候肯定考虑到了上面的问题,于是才会出现这么个新特征。

优势

  • 1.不易于产生内存碎片,有益于提高访问速度
  • 2.方便内存释放(只需要释放一次)

🌳总结

** 以上就是关于C语言中动态内存管理的全部内容了,我们从 malloc 开始,到柔性数组结束,学习了多种动态内存开辟的方式,还了解C/C++中的内存区域划分。这样我们以后在编写程序的时候,就可以不用把数据全都存放在栈区了,可以往堆区中存,毕竟那儿空间大;还可以通过函数灵活使用堆区中的空间,我想这正是C语言灵活强大的原因之一吧。能力越大,责任越大,我们在每次使用完开辟出的空间后,都要对其进行释放,不要引发内存泄漏这样的严重问题。总而言之,我们可以去尝试使用动态内存管理函数了!**

** 如果你觉得本文写的还不错的话,期待留下一个小小的赞👍,你的支持是我分享的最大动力!**

** 如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正**

相关文章推荐

**常见的动态内存的错误和柔性数组 **

C语言进阶——字符串函数&&内存函数

C语言进阶——指针进阶

标签: c语言 c++ 库函数

本文转载自: https://blog.csdn.net/weixin_61437787/article/details/127110324
版权归原作者 Yohifo 所有, 如有侵权,请联系我们删除。

“C语言进阶&mdash;&mdash;动态内存管理”的评论:

还没有评论