• C语言动态内存管理


    为什么存在动态内存

    C语言学到这里的时候,我们掌握的内存开辟有二种

    int a = 10;
    int arr[10] = {0};
    
    • 1
    • 2
    • 第一种是是直接在栈空间上开辟空间
    • 第二种是以数组的形式在栈空间上开辟连续的空间

    但是上面的二种方法有二个特点:

    1. 开辟的空间大小都是固定的,不能更改
    2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配

    但是有时我们在程序还没运行时,不太清楚要多少空间
    如果开辟的空间过小,那程序会越界
    如果开辟的空间过大,那也会造成空间的浪费
    这时后就会使用到动态内存函数,因为动态内存函数是在堆区创建的
    栈区创建的空间大小都是固定的,是不可以更改的
    在这里插入图片描述

    动态内存函数的介绍

    动态内存函数的头文件是

    melloc

    melloc 函数的参数:

    void* malloc(sizt_t size);
    size_t size 参数,指定要开辟多少个字节的空间,是以字节为单位的
    
    • 1
    • 2

    这个函数的作用是向内存申请一块空间,并返回指向这块空间的地址

    • 如果开辟成功,返回的是指向这块空间的指针
    • 如果开辟失败,返回的是 NULL
    • malloc 的返回值是 void*,所以malloc 函数并不知道要开辟什么类型的空间,所以要自行的决定
    • 如果malloc 的参数 size 是 0 ,这种行为是标准为定义的,最后的结果取决于编译器

    malloc 函数的使用:

    #include   //动态内存管理对应头文件
    
    int main()
    {
         // 用 malloc 开辟 40 个字节的空间,然后把空间的起始地址返回给 p
    	int* p = (int*)malloc(40);
    	//malloc申请空间可能会失败,所以要进行判断
    	//申请失败:打印错误信息并退出
    	if (p == NULL)
    	{
    		printf("%s\n", strerror(errno));
            return 1;
    	}
    	int i = 0;
    	// 打印
    	for (i = 0; i < 10; i++)
    	{
    		p[i] = i;
    		printf("%d ", p[i]);
    	}
    	free(p); ///释放动态内存开辟的空间
    	p = NULL;//将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

    free

    free 函数的参数:

    void free(void* ptr)
    
    • 1

    free 函数是专门用来做动态内存的释放和回收的

    • 如果 free函数的参数是 ptr 指向的空间并不是动态内存函数开辟的,那 free 是不能进行释放的
    • 如果 free 函数是 NULL ,那 free 是什么都不做
    • free 是用于动态内存函数的释放(malloc、calloc、realloc);

    calloc

    calloc 函数参数:

    void* calloc(size_t num,  size_t size);
    
    • 1
    • calloc 第一个参数是要开辟多少空间
    • calloc 第二个参数是开辟是什么类型的空间
    • calloc 函数在开辟好空间后会自动把所有初始化为0

    calloc 函数的使用:
    callocmalloc 的差别:
    malloc 开辟好空间是不会初始化的,而calloc 是开辟好空间后会自动把所有初始化为0

    #include   //动态内存管理对应头文件
    
    int main()
    {   // 用 calloc 开辟 10 个空间,大小为 int
    	int* p = (int*)calloc(10,sizeof(int));
    	if (p == NULL)
    	{   //打印错误信息
    		printf("%s\n", perror);
    		return 1;
    	}
    	int i = 0;
    	for (i = 0; i < 10; 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

    代码结果:
    在这里插入图片描述

    realloc

    realloc 函数参数:

    void* realloc(void* ptr, size_t size);
    
    • 1

    realloc 函数是调整通过动态内存开辟的空间

    • realloc 函数的第一个参数是调整的空间的起始地址
    • realloc 函数的第二个参数是 未增加空间的大小+想增加的大小的总空间大小
      比如:我开辟了 10 个int 类型的空间,不够用想要在增加 10 个空间
      realloc 是以字节为单位的, 原本空间是 10*4 = 40 个字节,如果想要在增加 10 个int 类型的空间
      40 + 40 = 80 个字节,第二个参数就要写 80

    realloc 函数的使用:

    int main()
    {    // 使用 malloc 开辟 5 个为 int 的空间
    	int* p = (int*)malloc(5 * sizeof(int));
    	if (p == NULL)
    	{   // 判断
    		printf("%s\n", perror);
    		return 1;
    	}
          // 使用 realloc 在扩容 5 个空间,总空间大小是 10
          //防止p指向realloc开辟失败的空间时,丢失原来空间地址的情况,
          //所以使用临时变量接受 realloc 的返回值
    	int* ptr = realloc(p, 10 * sizeof(int));
    	if (ptr == NULL)
    	{   //判断
    		printf("%s\n", perror);
    		return 1;
    	}
    	// 成功后在赋值给 p
    	p = ptr;
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		p[i] = i;
    		printf("%d ", p[i]);
    	}
    	free(p);//释放
    	p = NULL;//将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
    • 29

    代码结果:

    在这里插入图片描述

    使用 realloc 时还需要注意realloc在调整内存空间的时候存在两种情况:

    • realloc 的工作原理第一种情况:
      realloc 进行扩容时,原来后面的空间不足以进行扩容
      realloc 会找一块 扩容后总空间的大小,然后把原空间的数据拷贝过去,返回的是新空间的地址
      旧的空间是会自动销毁
      在这里插入图片描述

    • realloc 第二种情况:
      realloc 进行扩容时后面的空间充足,是直接在后面开辟空间
      在这里插入图片描述

    常见的动态内存错误

    1.对 NULL 的解引用

    malloc、calloc、realloc这些函数向内存申请空间是有可能会失败的,申请失败函数就会返回空指针,如果我们不对函数的返回值进行判断,而直接对其解引用的话,就会造成程序崩溃;

    在这里插入图片描述

    解决办法:在使用动态内存管理函数申请动态内存时,一定要记得用if进行检查函数的返回值是否为空。

    2.对动态开辟空间的越界访问

    动态内存开辟的空间是不能随意越界的,动态内存开辟的空间也是有限的

    在这里插入图片描述

    3.对非动态内存进行free释放

    free 函数是专门用于释放动态开辟的空间的
    如果对非动态开辟的空间进行 free 操作,会造成程序崩溃

    在这里插入图片描述

    4.使用free释放一块动态内存的一部分

    free 必须是使用 起始地址 释放
    当我们成功开辟一块动态空间并将它交由一个指针变量来管理时,我们可能会在后面的程序中让该指针变量自增,从而让其不再指向该动态空间的起始位置,而是指向中间位置或者结尾,这时我们在对其进行 free 进行释放,也会导致程序崩溃,因为free函数必须释放一整块动态内存,而不能释放它的一部分。

    int main()
    {
    	int* p = (int*)malloc(10 * sizeof(int));
    	if (p == NULL)
    	{
    		return 1;
    	}
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		*p = i;
    		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

    运行结果:

    在这里插入图片描述
    解决办法:将申请的动态内存交由两个指针变量进行管理,其中一个用于各种操作,另外一个用于记录空间的起始地址

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

    我们在写程序的时候可能在程序中的某一位置已经对动态内存进行释放了,但是随后写代码中,我们可能忘记了而重复对一块动态内存进行释放

    int main()
    {
    	int* p = (int*)malloc(100);
    	if (p == NULL)
    	{
    		return 1;
    	}
    	free(p);
    	// p = NULL;
    
    	//.......
    
    	free(p);//重复释放
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    解决办法:进行 free 释放后,把指针置成空指针(NULL),这样即使后面重复释放,free(NULL) 也没有任何影响

    6.动态内存忘记释放(内存泄漏)

    内存泄漏是:用动态内存函数申请空间使用完毕后,没有进行回收(free

    void test()
    {
    	//这种开辟空间 和 malloc 功能是一样的
    	int* p = (int*)realloc(NULL, 40);
    	int a = 0;
    	scanf("%d", &a);
    	if (a == 5)
    		return;
    	free(p);
    	p = NULL;
    }
    int main()
    {
    	test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 如果 a 大于5,函数提前结束,没有机会进行释放,就会导致内存泄漏

    经典笔试题练习

    笔试题一

    void GetMemory(char* p)
    {
    	p = (char*)malloc(100);
    }
    void test(void)
    {
    	char* str = NULL;
    	GetMemory(str);
    	strcpy(str, "hello world");
    	printf(str);  //将str的首地址传给printf函数,可行
    }
    int main()
    {
    	test();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    题解:

    • 在test函数中调用GetMemory函数时,传的是形参,p只是str的一份临时拷贝,所以对 p 的修改不会影响 str
    • GetMemory函数里p 进行申请空间使用后,并没有进行释放,会导致内存泄漏
    • p 是形参没有对str 进行修改,所以str还是NULL,所以在解引用时程序会崩溃

    笔试题二

    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
    • 在函数GetMemory()内部创建临时变量,他的作用范围只在在该函数内存
    • 虽然GetMemory()返回了 p 的地址,但是 p 以经销毁了,str 就是野指针

    笔试题三

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 程序中对动态内存使用完后没有 进行free,造成了内存泄漏

    笔试题四

    void Test(void)
    {
    	char* str = (char*)malloc(100);
    	strcpy(str, "hello");
    	free(str);
    	if (str != NULL)
    	{
    		strcpy(str, "world");
    		printf(str);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • free掉动态开辟的内存之后没有把相应的指针变量置空,导致if条件成立,造成野指针问题。
  • 相关阅读:
    C语言 cortex-A7核UART总线实验
    MinGW相关错误
    Java-I/O输入输出
    JAVA中的replace、replaceAll、replaceFirst方法的区别和使用
    html5——CSS基础选择器
    SQL Server 2014主从数据库订阅和发布(SQLserver复制)超详细
    编译内核模块生成ko驱动文件
    游戏开发团队配置与协作流程
    【c++】运算符重载实例
    杠杆思维和时间管理
  • 原文地址:https://blog.csdn.net/m0_66483195/article/details/125859977