C语言 :学习动态内存分配

C语言动态分配

为什么存在内存分配?

在声明数组的时候,必须用一个编译时常量来指定数组的长度,但是数组的长度往往在编译的时候才能确定,这样可能会有遇到下面这两种情况:
1.有可能数组长度不够。
2.可能会浪费大量的空间。

例如:如果存在一个通讯录,通讯录的大小事先已经确定(1000),对于一些人可能只会使用100个单位,
对于一类人,可能会认为1000个单位不够使用

我们可以使用动态内存分配来解决这个问题,动态内存可以随时开辟一段空间,并且可以随时根据要求去调整该空间的大小。
不过需要注意:但是动态内存分配只能调整动态开辟出来的空间大小,并不能随意调整其他空间的大小,因为动态内存分配的空间和其他变量分配的空间不属于同一块空间

在这里插入图片描述

动态内存函数的介绍

对内存的动态分配是通过系统提供的库函数来实现的,主要有malloc,calloc,free,realloc这4个函数

malloc

该函数的头文件:stdlib.h

该函数的原型

void* malloc (size_t size);
//void类型的返回值类型利于我们返回任意类型的指针
//size_t 单位是字节,意味着我们会申请出一块以字节为单位的空间

注意

  • 该函数的作用是在内存中的动态内存区分配一个长度为size的连续空间。

  • 如果开辟空间成功,会返回出这个新空间的首字节地址。

  • 如果开辟空间失败,函数会返回一个空指针。

下面我们来使用一次该函数(我们使用指针来接收返回值):

#include<stdio.h>
#include<stdlib.h>
int main()
{
    
    
	//开辟十个整形的空间
	int * p = (int *)malloc(40);
    //应该尽量强制转换成接收指针的类型,这样更规范。
	return 0;
}

由于我们不知道这个空间是否开辟成功,所以我们需要判断返回的指针是否是空指针。

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
    
    
	//开辟十个整形的空间
	int * p = (int *)malloc(40);
	if(NULL == p)
    {
    
    
        printf("%s\n",strerror(errno));//使用sttrerror 函数可以打印出开辟空间错误的原因。
        return 0//如果开辟空间失败就退出。
    }
    return 0;
}

在空间开辟成功后,我们就可以访问该空间了(我们可以使用指针来访问)

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
    
    
	//开辟十个整形的空间
	int * p = (int *)malloc(40);
	if (NULL == p)
	{
    
    
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		*(p + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d\t", *(p+i));
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

上面的代码段最后我们使用了free函数,使用该函数的目的是释放掉刚才所申请的空间。下面详细的介绍该函数的使用:

free

该函数的原型是:

void free (void* ptr);

该函数的参数是指向由动态内存开辟的空间(包括malloccallocrealloc开辟)的指针

free函数的作用原理是:

使用该函数后,参数的那一个指针指向空间的操作(访问)权限归还给操作系统,我们不能再次使用该空间

free函数的使用

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
    
    
	//开辟十个整形的空间
	int * p = (int *)malloc(40);
    //尽量使用强制类型转换
	if(NULL == p)
    {
    
    
        printf("%s\n",strerror(errno));
        return 0;
    }
    //使用这段空间
    //...
    //释放这段空间
    free(p);
    p = NULL;
    return 0;
}
使用后将指针赋为NULL

在实际操作中,我们只使用free函数将该空间释放是不够的。分析该段代码:我们在申请空间的时候创建了一个指针变量用来接收返回的地址,但是在释放了动态空间后,该指针仍然指向那一块空间。这会造成非法访问(因为我们已经没有权限再次使用那块空间),所以我们在释放空间后需要将该指针变量赋为空指针,这样更安全。

注意:
每次使用完动态开辟的空间后都需要将该空间释放。如果没有释放,那么那一块空间将会在程序结束的时候才会释放(程序没有结束的时候该空间就不能有其他的作用)。

calloc

我们已经知道malloc函数可以开辟空间,其实不止这一个函数,我们还有两个函数也可以开辟空间,其中一个是calloc

该函数的使用方法和malloc不同

该函数的原型是:

void* calloc (size_t num,size_t size);
//第一个参数是 开辟元素的个数
//第二个参数是 每一个元素的大小

除了参数不同外,该函数还有一个特点:在开辟空间后会将这个空间全部初始化为0,然后才返回该空间的起始地址

运用一次calloc函数
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
    
    
	//开辟十个整形的空间
	int * p = (int *)calloc(10,sizeof(int));
	if (NULL == p)
	{
    
    
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int i = 0;
    for (i = 0; i < 10; i++)
	{
    
    
		printf("%d\t", *(p+i));
	}//观察初值
    printf("\n");
	for (i = 0; i < 10; i++)
	{
    
    
		*(p + i) = i;
	}//重新赋值
    
	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d\t", *(p+i));
	}//观察赋值后的情况
    
	//释放
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

我们可以看到在观察初值的时候,每一个值都是0,这也验证了前面说的calloc函数会在开辟好空间后将整个空间全部初始化为0.

realloc

该函数的原型

void* realloc(void* ptr, size_t size);
//第一个参数是一个指针,指向由动态内存开辟出来的空间的起始地址
//如果第一个参数是空指针,那么该函数的使用方法和malloc相同
//第二个参数 要访问的空间的大小

该函数的特点

  • 该函数既可以开辟动态空间(第一个参数为NULL),也可以调整动态空间(第一个参数不为NULL).
当第一个参数为空指针时:

使用方法和malloc一样:开辟成功的话会返回新空间的首地址,开辟失败会返回空指针

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
    
    
	//开辟十个整形的空间
	int* p = (int*)realloc(NULL, sizeof(int) *10);
	if (NULL == p)
	{
    
    
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
    
    
		*(p + i) = i;
	}//重新赋值

	for (i = 0; i < 10; i++)
	{
    
    
		printf("%d\t", *(p + i));
	}//观察赋值后的情况
	//释放

	free(p);
	p = NULL;
	return 0;
}
当第一个参数不为空指针的时候:

可以调整前面申请的空间的大小:

#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>
int main()
{
    
    
	//开辟十个整形的空间
	int* p = (int*)malloc( sizeof(int) *10);//第一次申请空间
	if (NULL == p)
	{
    
    
		printf("%s\n", strerror(errno));
		return 0;
	}
	//使用
	int i = 0;
	int* ptr = realloc(p, sizeof(int) * 20);//利用realloc将前面申请的空间扩大一倍
	if (NULL == ptr)
	{
    
    
		printf("%s\n", strerror(errno));
		return 0;
	}
	p = ptr;

	for (i = 0; i < 20; i++)
	{
    
    
		*(p + i) = i;
	}
	for (i = 0; i < 20; i++)
	{
    
    
		printf("%d\t", *(p + i));
	}

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

我们注意到在使用realloc函数的时候我们仍然会用一个指针去接收返回值的,不过,为什么我们不直接用第一次申请空间的指针区接收这个返回值?
我们需要先来了解realloc函数使用时会遇到的几种情况:

情况1:第一次申请的空间后面有足够的空间,该函数就在这段空间后面开辟新的空间,

在这里插入图片描述

情况2:第一次申请的空间后面没有足够的空间,该函数会在一个足够的位置创建

在这里插入图片描述

情况3:没有满足条件的内存空间,就会返回一个空指针

在这里插入图片描述

现在我们就能理解为什么我们在使用realloc函数区调整一个空间的时候会去利用一个新的指针变量:

如果我们两次使用同一个指针变量,那么当使用realloc函数返回空指针的时候,我们的指针变量就被赋值为NULL,
无法再指向我们原来的数据,我们也无法再找到之前的数据

常见的动态内存分配错误

对空指针的解引用操作
#include<stdio.h>
#include<limits.h>
#include<stdlib.h>
int main()
{
    
    
	int* p = malloc(INT_MAX);
	int i = 0; for (i = 0; i < 10; i++)
	{
    
    
		*(p + i) = i;
	}
	return 0;
}//这段代码向内存申请了一段很长的连续空间,内存无法相应该需求,就会返回空指针,此时我们利用空指针去访问空间按,会造成非法访问。

所以我们应该在申请空间后判断是否为空指针,利用这一个代码段

int* p = malloc(INT_MAX);
if(NULL == p)
{
    
    
    printf("%s",strerror(errno));//出现问题还可以打印出问题的原因在结束程序。
    return 0;
}
对动态开辟空间的越界访问

我们在申请空间之后应该清楚的知道该空间有多大,不能访问我们申请的空间之外的内容

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
    char * p = (char *)malloc(10);
    if(NULL == p)
    {
    
    
   		printf("%s",strerror(errno));//出现问题还可以打印出问题的原因在结束程序。
        return 0;
    }
    
    int i = 0;
    for(i = 0; i <= 10; i++)//在访问*(p+10)的时候会造成内存非法访问
    {
    
    
        *(p+i) = 'a'+i;
    }
    for(i = 0; i <= 10; i++)
    {
    
    
        printf("%c ",*(p+i));
    }
    free(p);
    p = NULL;
    return 0;
}
对非动态内存使用free释放

free只能释放用动态内存开辟的空间

int main()
{
    
    
    int a = 10;
    int p = &a;
    .
    .
    .
    free(p);
    p = NULL;//使用的时候应该注意不能将普通变量free 
    return 0;
}

使用free去释放动态内存开辟空间的一部分

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
    char * p = (char *)malloc(40);
    if(NULL == p)
    {
    
    
   		printf("%s",strerror(errno));
        return 0;
    }
    
	//将前五个数据初始化为1 2 3 4 5
    int i = 0;
    for(i = 0; i <5;i++)
    {
    
    
        *p = i+1;
        p++;
    }
    
    free(p);//这时候指针p不在指向开辟的空间的起始位置
    p = NULL;
    return 0;
}

这是无法实现的,在释放动态开辟空间的时候,只能释放整个空间(free参数只能时前面开辟的空间的起始地址);

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

第一种:在p 赋为空指针后再次释放,不会报错

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
    char * p = (char *)malloc(40);
    if(NULL == p)
    {
    
    
   		printf("%s",strerror(errno));
        return 0;
    }
    //...
    //...
    
    free(p); 
  	p = NULL;
    free(p);

    return 0;
}

第二种:连续释放了两次指针p(中间没有将p赋值为空指针) ,会出现错误。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
    
    
    char * p = (char *)malloc(40);
    if(NULL == p)
    {
    
    
   		printf("%s",strerror(errno));
        return 0;
    }
    //...
    free(p); 
    //...
    free(p);
    return 0;
}

所以我们如果每次释放完该控件,就去将指针赋值为空指针,就不会出现这个问题

忘记释放动态空间

我们申请的空间如果不能及时释放,那么该空间就只会在该程序结束的时候才会被自动释放,在此之前,这块空间无法再被利用,会造成内存泄漏。内存泄漏会增加程序的体积,可能会导致系统或者程序崩溃。

柔性数组

在C99中,结构的最后一个元素允许时未知大小的数组,这就叫做柔性数组成员

#include<stdio.h>
struct S{
    
    
  int n;
  int arr[];//也可以这样使用int arr[0];
};
//结构的最后一个成员时数组,并且这个数组没有指定大小,这个成员就是柔性数组成员
 int main()
 {
    
    
     return 0;
 }

柔性数组的特点

  1. 结构中柔性数组成员前面至少应该还有一个其他成员

  2. 柔性数组大小不纳入结构体大小的计算

在这里插入图片描述

包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预取大小

一个如何开辟包含柔性数组成员的结构

在这里插入图片描述

我们得到了这样一块动态内存空间,前面了解到我们可以通过realloc 函数去调整动态内存分配的空间,所以我们可以使用realloc函数来动态调整柔性数组的大小

记住:在使用完后仍然需要释放动态分配的空间

柔性数组的优势
  • 有利于释放

  • 访问速度更快

猜你喜欢

转载自blog.csdn.net/cainiaochufa2021/article/details/123162900