• 【C】动态内存管理 malloc calloc relloc free 函数详解



    🙈个人主页 对de起日子
    👉系列专栏【C语言–大佬之路】
    🎈今日心语:一些小痛苦,小烦恼,小挫折,像一只手掌,看上去很小,但如果放不下,总是拉近来看,放在眼前,搁在心头,就会遮住你人生的整个晴空。


    【C】动态内存管理


    本章重点

    • 为什么存在动态内存分配
    • 动态内存函数的介绍
      • malloc
      • free
      • calloc
      • realloc
    • 常见的动态内存错误
    • 几个经典的笔试题

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

    我们已经掌握的内存开辟方式有:

    #include
    int main()
    {
    	int num = 10;//向内存申请了4个字节的空间
    	int arr[10];//向内存申请了40个字节的空间
    
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这种内存开辟,如果开辟多了,那么内存空间就会浪费

    但是上述的开辟空间的方式有两个特点:
    1. 空间开辟大小是固定的。
    2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

    但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组编译时开辟空间的方式就不能满足了,这时候就只能试试动态存开辟了。


    2.动态内存函数的介绍

    2.1 malloc和free

    malloc函数特点

    C语言提供了一个动态内存开辟的函数malloc

    void* malloc (size_t size);
    
    • 1

    这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

    • 如果开辟成功,则返回一个指向开辟好空间的指针
    • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
    • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
    malloc返回值的检查
    #include//malloc
    #include//errno
    #include//streror
    int main()
    {
        // 返回值的类型是void,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
    	//存放10个字节,用指针来维护,而如果是void类型,向后移动几个字节是不能确定的,所以一般不这样写,
        //通常要进行强制类型转换,这样如果对指针进行++,或者解引用操作,就知道指针向后移动几个字节和取几个字节的空间。
    	int* p =(int*)malloc(INT_MAX);// INT_MAX是有符号整数最大值
        //检查
    	if (p == NULL)//如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    	{
    		printf("%s\n", strerror(errno));/*打印错误信息*/
    		return 1;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    运行结果:

    最好还是将开辟的空间释放掉,这时我们就要搭配下面这个函数进行空间的释放:

    空间释放函数free

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

    void free (void* ptr);
    
    • 1

    free函数用来释放动态开辟的内存。

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

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

      malloc和free都声明在stdlib.h头文件中。

    malloc以及后面的calloc 必须和free成对出现,不然会造成内存泄露

    示例:

    #include//malloc
    #include//errno
    #include//streror
    int main()
    {
    	//void* p = malloc(40);//向内存申请了40个空间
    	
    	int* p = (int*)malloc(40);
    	int* ptr = p;//若不进行此步,后面的free(p);是错误的,因为p本来指向的是空间的起始位置,但是前面的循环使p指向了后半部分空间,使空间不能完全释放
    	//检查
    	if (p == NULL)//如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
    	{
    		printf("%s\n", strerror(errno));/*打印错误信息*/
    		return 1;
    	}
    	//使用
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*ptr = i;
    		ptr++;
    	}
    	//释放
    	free(p);
    	p = NULL;//为了避免通过p非法访问已经释放的空间,这里将p置为空指针
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    进行调试,监视内存,我们可以清楚地看到free释放内存空间,并将p置为空的效果:

    2.2 calloc

    C语言还提供了一个函数叫calloccalloc函数也用来动态内存分配。原型如下:

    void* calloc (size_t num, size_t size);
    
    • 1
    • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
    • 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
    • malloc以及calloc 必须和free成对出现,不然会造成内存泄露

    示例:

    #include//perror
    #include//calloc
    //calloc函数
    int main()
    {
    	//40个字节-10个整型
    	//malloc(40)
    	int* p = (int*)calloc(10, sizeof(int));
    	//检查
    	if (p == NULL)
    	{
    		perror("calloc");
    		return 1;
    	}
    	int i = 0;
    	for (i = 1; i < 10; i++)
    	{
    		*(p + 1) = i;
    	}
    	//释放
    	free(p);
    	p = NULL;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    调试结果如图,

    可以理解为calloc = malloc+(memset将开辟的空间初始化为0)。

    如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。


    2.3 realloc

    • realloc函数的出现让动态内存管理更加灵活。
    • 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。
      函数原型如下:
    void* realloc (void* ptr, size_t size);
    
    • 1
    • ptr是要调整的内存地址
    • size 调整之后新大小
    • 返回值为调整之后的内存起始位置。
    • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
    • realloc在调整内存空间的是存在两种情况:
      • 情况1:原有空间之后有足够大的空间
      • 情况2:原有空间之后没有足够大的空间

    情况1
    当是情况1 的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
    情况2
    当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存址。
    由于上述的两种情况,realloc函数的使用就要注意一些。

    示例:

    #include//realloc
    int main()
    {
    	int* p = (int*)malloc(40);
    	//检查
    	if (p == NULL)
    	
    		return 1;
    	//使用
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    	*(p + i) = i;
    	}
    	//
    	for (i = 0; i < 10; i++)
    	{
    		printf("%d ", *(p + i));
    	}
    
    	//增加空间
    	int* ptr = (int*)realloc(p, 80);//将p开辟的空间改为80个字节
    	//当realloc开辟失败的时候,返回的是NULL,所以也需要检查
    	if (ptr != NULL)
    	{
    		p = ptr;//为了方便管理,下面还使用p,引入ptr
    		ptr = NULL;
    	}
    
    	//使用
    	for (i = 10; i < 20; i++)
    	{
    		*(p+i) = i;
    	}
    
    	//释放
    	free(p);
    	p = NULL;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    realloc(NULL, 40);等价于malloc(40);


    3.常见的动态内存错误

    3.1 对NULL指针的解引用操作

    #include
    #include//malloc
    int main()
    {
        int *p = (int *)malloc(INT_MAX);//当内存开辟失败时,malloc会返回NULL
        *p = 20;//如果p的值是NULL,就会有问题
        free(p);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    改进:

    #include//perror
    #include//malloc
    #include//INT_MAX
    int main()
    {
        int* p = (int*)malloc(INT_MAX);//当内存开辟失败时,malloc会返回NULL
        if (p == NULL)
        {
            perror("malloc");
            return 1;
        }
        else
        {
            *p = 20;//如果p的值是NULL,就会有问题
    
        }
        free(p);
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    3.2 动态开辟空间的越界访问

    // 越界访问
    #include
    #include//malloc
    int main()
    {
        int* p = (int*)malloc(20);//开辟20个字节的空间,相当于5个int
        if (p == NULL)
            return 1;
        //使用
        int i = 0;
        for (i = 0; i < 20; i++)//越界访问了第5个int元素(下标为4)后面的空间
        {
            *(p + i) = i;
            //p[i] = =i;
        }
        for (i = 0; i < 20; i++)
        {
            printf("%d ", p[i]);
        }
        //释放
        free(p);
        p = NULL;
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这里虽然代码可以运行,但是会有错误警告

    改进:

    直接将for循环中的20改为5即可


    3.3 对非动态开辟内存使用free释放

    int main()
    {
    	int num = 10;//num是非动态开辟内存
    	int* p = &num;
    
    	//……
    
    	free(p);
    	p = NULL;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面代码对非动态开辟内存使用free释放,这时编译器就会报错:


    3.4 使用free释放一块动态开辟内存的一部分

    //使用free释放一块动态开辟内存的一部分
    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    		return 1;
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		//*(p + i) = i;
    		*p = i;
    		p++;//当循环了五次后,p指向了中间的元素,而不是起始位置
    	}
    	//在释放时p指向的不再是动态内存开辟的起始位置
    
    	free(p);
    	p = NULL;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    这时编译器会报错:

    动态内存空间必须从起始位置释放,不然是释放不了的。

    3.5 对同一块动态内存多次释放

    int main()
    {
    	int* p = (int*)malloc(40);
    	if (p == NULL)
    		return 1;
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		*(p + i) = i;
    		
    	}
    	//释放
    
    	free(p);
    	///p = NULL;
    
    
    	free(p);//第二次释放同一块内存空间,err,
    	        //但如果前面的p置为空,程序可以正常运行
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    多次释放,而且不置空报错:


    3.6 动态开辟内存忘记释放(内存泄漏)

    //函数会返回动态开辟空间的地址,记得在使用之后返回
    int* get_memory()
    {
    	int* p = (int*)malloc(40);
    	//……
    	return p;
    }
    
    int main()
    {
    	int* ptr = get_memory();
    	//使用
    
    	//释放,调用时很可能忘记释放
    	free(ptr);
    	ptr = NULL;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    忘记释放不再使用的动态开辟的空间会造成内存泄漏。
    切记:
    动态开辟的空间一定要释放,并且正确释放。

    4.几个经典的笔试题

    4.1 题目1:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请问这个函数有什么错误?

    注意:printf(str);这种写法是正确的。

    主要错误如下:

    1.改变形参p,str依然是NULL,strcpy无法将”hello world”拷贝到空指针指向的地址,所以会访问出错。

    2.malloc开辟的动态内存空间需要进行free释放。

    代码改进:

    4.2 题目2:

    char* GetMemory(void)
    {
        char p[] = "hello world";
        return p;
    }
    void Test(void)
    {
        char* str = NULL;
        str = GetMemory();
        printf(str);
    }
    
    int main()
    {
        Test();
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    请问这个函数有什么错误?

    而上图中第二个代码的写法虽然是错误的,但是在运行后可能会得到10,这时只要略作修改就得不到原来得值,如下,我们添加了输出项,对应的输出结果如下图:

    究其原因,涉及到函数栈帧的部分知识:

    4.3 题目3:

    void GetMemory(char **p, int num)
    {
        *p = (char *)malloc(num);
    }
    void Test(void)
    {
        char *str = NULL;
        GetMemory(&str, 100);
        strcpy(str, "hello");
        printf(str);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    请问这个函数有什么错误? 通过前面的学习,我们应该可以很快地找出错误

    错误:

    malloc函数开辟了内存空间,但是却没有释放,造成了内存泄露地问题。
    这时,我们只需在后面加上
    free(str);
    str = NULL;即可,
    改进代码如下:

    void GetMemory(char** p, int num)
    {
        *p = (char*)malloc(num);
    }
    void Test(void)
    {
        char* str = NULL;
        GetMemory(&str, 100);
        strcpy(str, "hello");
        printf(str);
    
        free(str);
        str = NULL;
    }
    
    int main()
    {
        Test();
        return 0;    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    4.4 题目4:

    void Test(void)
    {
        char* str = (char*)malloc(100);
        strcpy(str, "hello");
        free(str);//free释放开辟的动态内存空间,而不置空
        if (str != NULL)//str为真
        {
            //str所指向的地址不属于当前程序,是野指针,这里是非法访问
            strcpy(str, "world");
            printf(str);
        }
    }
    int main()
    {
        Test();
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    该代码中free函数释放了malloc开辟的动态内存空间,但是没有将指针置空,导致后面调用时出现了野指针导致了非法访问。

    所以一个好的代码习惯是在释放动态内存空间后,将这个空间的指针置为空。


    5. C/C++程序的内存开辟

    C/C++程序内存分配的几个区域:

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

    有了这幅图,我们就可以更好的理解之前介绍的static关键字修饰局部变量的例子了

    实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。
    
    
    但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才销毁
    所以生命周期变长。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结语:

    这里我们关于动态内存管理的内容就介绍完了,
    文章中某些内容我们之前有介绍,所以只是一笔带过,还请谅解。
    希望以上内容对大家有所帮助👀,如有不足望指出🙏

    在这里插入图片描述


    加油!!

  • 相关阅读:
    CI/CD笔记.Gitlab系列.`gitlab-ci.yml`中的头部关键字
    三分钟查询出快递单号物流延误更新的
    公司刚来的00后真卷,上班还没2年,跳到我们公司起薪20k....
    听说你java基础很好?这些能答对几个?附上最全java必学知识点
    centos7安装部署ElasticSearch
    java CAS详解(深入源码剖析)
    Docker - 安装
    【c ++ primer 笔记】第 14章 重载运算符
    MySQL(3)索引实践一
    Java版本企业工程项目管理系统源码+spring cloud 系统管理+java 系统设置+二次开发
  • 原文地址:https://blog.csdn.net/qq_72069067/article/details/127715913