栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C语言动态内存管理详解

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C语言动态内存管理详解

动态内存管理 为什么存在动态内存分配
  • 我们已经掌握的内存开辟方式有:

    int i = 20;   //在栈空间开辟4个字节
    char arr[10] = { 0 };  //在栈空间开辟10个字节的连续空间
    
  • 但上述的开辟空间的方式有两个特点:

    • 空间开辟的大小是固定的
    • 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
  • 但是对于空间的需求,不仅仅是上述的情况。有时我们需要的空间大小在程序运行的时候才能知道,那数组在编译时开辟空间的方式 就不能满足了,所以这个时候只能试试动态内存开辟了

动态内存函数的介绍

为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态地分配内存空间,也可把不再使用的空间回收待用,为有效的利用内存资源提供了手段

分配内存空间函数 malloc()
  • 调用形式:void* malloc (size_t size);
  • 功能:分配内存块
    • 在内存的动态存储区中开辟一块长度为“ size ”字节的连续可以的空间,并返回指向这块空间的指针。
    • 函数的返回值为该区域的首地址(指向开辟好的空间的首地址)
    • 如果开辟空间失败,则函数的返回值为一个NULL指针,因此malloc()的返回值一定要做检查
  • 返回值的类型是void*,所以malloc()函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定,所以需要我们将void*强制类型转换为我们需要的指针类型
  • “size”是一个无符号数,如果参数size为0,则malloc()函数的行为标准为定义的,取决于编译器(size_t表示一个无符号整型)
释放内存空间函数 free()
  • 调用形式:void free (void* ptr);
  • 功能:
    • 释放 ptr 所指向的一块内存空间,ptr 是一个任意类型的指针变量,它指向的被释放区域的首地址。
    • 被释放区域应是由malloc()和calloc()函数所分配的区域
  • 如果参数ptr指向的空间不是动态开辟的,那free()函数的行为是未定义的
  • 如果参数ptr是NULL指针,则函数什么也不做
  • 使用free() 函数将内存释放之后,不会将ptr这个指针置为空指针,所以为了非法访问内存,需要我们手动将ptr置为空指针
    即:ptr = NULL;
  • free()函数和malloc()、calloc()函数成对出现

例:

#include 
#include 
#include 
int main()
{
	struct student      //定义了结构 student
	{
		int num;
		char name[20];
		char sex;
		float score;
	}*ps;  //定义了结构指针变量ps
	ps = (struct student*)malloc(sizeof(struct student));//分配了一块student类型的内存空间,并把首地址赋给ps,使ps指向该区域,注意使用malloc()函数的时候应该将void*强制类型转换为我们需要的指针类型(struct student*)
	ps->num = 102;
	strcpy(ps->name, "张三");
	ps->sex = 'M';
	ps->score = 67.5;    //以ps为结构指针变量对各个成员赋值
	printf("num = %dnname = %sn", ps->num, ps->name);
	printf("sex = %cnscore = %fn", ps->sex, ps->score);
    free(ps);  //用free()函数释放ps指向的内存空间
    ps = NULL; //将ps这个指针置为空指针,避免非法访问
    return 0;
}//整个程序包含了申请内存空间,使用内存空间,释放内存空间3个步骤,实现了存储空间的内存分配
分配内存空间函数 calloc()
  • 调用形式:void* calloc (size_t num, size_t size);

  • 功能:为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0

  • calloc()和 malloc()的区别在于:calloc()`函数会在返回地址之前把开辟的连续区域的每个字节初始化为0

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-woPEf0L5-1659065555159)(C:Users何锦AppDataRoamingTyporatypora-user-imagesimage-20220728161809391.png)]

调整内存空间函数 realloc()

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

有时候我们发现开辟的内存区域或大或小,会对内存的大小做灵活的调整,这时候就会用到realloc()函数

  • 调用形式:void* realloc (void* ptr, size_t size);

  • ptr是需要调整的内存地址,是一个任意类型的指针变量;size为调整之后新的大小;返回值为调整之后的内存起始位置

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移到新的空间

  • realloc()函数在调整内存空间时存在两种情况:

    • 原有空间之后有足够大的空间,要扩展内存就直接在原有内存之后直接追加空间,函数返回旧的地址
    • 原有空间之后没有足够大的空间,那么就会在堆空间上另找一个合适大小的连续空间来使用,原来的空间就会被释放掉,函数返回的是一个新的地址
  • 值得注意的是:对于返回的地址,我们是用原来的指针变量来接收还是另外定义一个指针变量来接收?

    • 如果realloc()函数在堆区找不到合适的空间来调整大小,就会返回空指针NULL,这样原来的指针ptr就会丢失

    • 所以最好的办法就是定义一个临时的指针变量ptr1

      int* ptr1 =realloc(ptr, 20);  //定义一个临时指针变量ptr1
      if(ptr1 != NULL)  //判断是否调整成功
      {
          ptr = ptr1    
      }
      
  • realloc()函数和malloc()函数:

    int* p = (int*)realloc(NULL,40);
    int* p = (int*)malloc(40);//这两段代码意义一致
    
常见的动态内存错误
  • 对NULL指针的解引用操作

    #include 
    int main()
    {
    	int* p = (int*)malloc(1000000000);
        //开辟的空间过大导致开辟失败,malloc()函数返回一个空指针,所以对malloc()函数的返回值要进行NULL判断,
    	int i = 0;
    	for (i = 0; i < 10; i++)
    	{
    		*(p + i) = i;  //p是一个空指针,会导致非法访问
    	}
    	printf("%dn", *(p+i));
        free(p);
        p = NULL;
    	return 0;
    }//编译结果:-842150451  明显不对
    
  • 对动态开辟空间的越界访问

    #include 
    #include 
    int main()
    {
    	int i = 0;
    	int* p = (int*)malloc(10 * sizeof(int));
    	if (p != NULL)
    	{
    		for (i = 0; i <= 10; i++)  
    		{
    			*(p + 1) = i;  //当 i 是10的时候越界访问
    		}
    		free(p);
    		p = NULL;
    	}
    	return 0;
    }//编译结果:无结果
    
  • 对非动态内存使用free()释放

    int main()
    {
    	int a;
    	int* p = &a;  
    	free(p);  //free()函数只能释放位于堆区的动态内存,而不能释放位于栈区的非动态内存函数
    	return 0;
    }
    
  • 使用free()释放一块动态开辟内存的一部分

    int main()
    {
    	int* p = malloc(10 * sizeof(int));
    	if (p == NULL)
    	{
    		return 1;
    	}
    	int i = 0;
    	for (i = 0; i < 5; i++)
    	{
    		*p++ = i;  
    	}  //经过循环之后,p指向的不再是原来的位置,找不到原来p所指向的位置了
    	free(p);  //free()函数只释放了动态内存的一部分
    	p = NULL;
    	return 0;
    }
    
  • 对同一块动态内存多次释放

    int main()
    {
    	int* p = (int*)malloc(100);
    	free(p);
    	free(p);  //重复释放
    	return 0;
    }
    
  • 动态开辟内存忘记释放(内存泄漏)

    void test()
    {
        int* p = (int*)malloc(100);
        if(P != NULL)
        {
            *p = 20;
        }//没有释放不再使用的动态开辟的空间会造成内存泄漏
    }
    int main()
    {
        test();
        return 0;
    }
    

    动态内存回收的两种方式:free()函数回收;程序结束回收。

几道经典题目

例1:

#include 
#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;
}  //编译不成功
  • str传给GetMemory函数是值传递,GetMemory函数的形参p是str的一份临时拷贝,在GetMemory函数内部动态申请空间的地址放在p中,不会影响外面的str,所以GetMemory函数返回之后,str依然是空指针;
  • 当GetMemory函数返回之后,形参p销毁,使得动态开辟的100个字节存在内存泄漏,无法释放

例2:

#include 
char* GetMemory(void)
{
     char p[] = "hello world";
     return p;
}
void Test(void)
{
     char* str = NULL;
     str = GetMemory();
     printf(str);
}
int main()
{
    Test();
    return 0;
}
  • GetMemoy函数内部创建的数组是在栈区上创建的,一旦出了函数,p数组的空间就还给了操作系统,那么函数返回的地址是没有实际意义的,如果通过返回的地址去访问内存就是非法访问内存
柔性数组
  • C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员

    • struct stu
      {
          int i;
          int a[];  //在有些编译器下可以写成  int a[0];
      }stu1;
      
  • 柔性数组的特点

    • 结构中的柔性数组成员前面必须至少一个其他成员

    • sizeof返回的这种结构大小不包括柔性数组的大小

      struct stu
      {
          int i;
          int a[];
      }stu1;
      int main()
      {
          printf("%d", sizeof(type_a));
          return 0;
      }  //编译结果:4
      
    • 柔性数组的大小是未知的

    • 包含柔性数组成员的结构用malloc()函数进行内存动态分配时,分配的内存应该大于结构的大小,以适应柔性数组的预期大小

      struct stu
      {
          int i;
          int a[];
      }stu1;
      int main()
      {
          //希望柔性数组为10个整型的大小
          struct stu* ps = (struct stu*)malloc(sizeof(stu1) + 10 * sizeof(int));
          ps->i = 10;
          int j = 0;
          for (j = 0; j < 10; j++)
          {
              ps->a[j] = j;
          }
          //增加空间
          struct stu* str = (struct stu*)realloc(ps, sizeof(stu1) + sizeof(int) * 10);
          if (str != NULL)
          {
              ps = str;
          }
          //使用
          //释放
          free(ps);
          return 0;
      }
      
  • 柔性数组的优势

    • 首先我们看一段代码:

      struct stu
      {
      	int i;
      	int* pa;
      }stu1;
      int main()
      {
      	struct stu* ps = (struct stu*)malloc(sizeof(struct stu));
      	if (ps == NULL)
      	{
      		return 1;
      	}
      	ps->i = 10;
      	ps->pa = (int*)malloc(10 * (sizeof(int)));
      	if (ps->pa == NULL)
      	{
      		return 1;
      	}
      	int j = 0;
      	for (j = 0; j < 10; j++)
      	{
      		ps->pa[j] = j;
      	}
      	//增加
      	int* str = (int*)realloc(ps->pa, sizeof(int) * 20);
      	if (str != NULL)
      	{
      		ps = str;
      	}
      	//释放
      	free(ps->pa);
      	ps->pa = NULL;
      	free(ps);
      	ps = NULL;
      	return 0;
      }
      

      我们发现,上面这一段代码完全可以替代柔性数组的功能,但哪种方法好呢?

    • 柔性数组的优势:

      • 方便内存释放:如果我们的代码时在一个给别人使用的函数中,在里面做了二次内存分配,并把整个结构体返回,那么free()函数释放结构体,但结构体内部的成员也需要释放,而使用柔性数组后,只需要一次free()函数即可释放内存
      • 有利于访问速度
      • 有利于减少内存碎片
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1029773.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号