【C语言】动态内存管理

文章目录

在前面的指针学习中我们已经对地址,内存有了一定的了解了。前面也说过地址就相当于是门牌号,内存就相当于是一块空间相当于一栋宿舍楼,一栋宿舍楼里有不同的房间可能有些是4人间或8人间,而计算机也一样计算机就是讲一块大内存分成如干个小的内存单元。回忆完了内存我们就来说说为什么要有动态内存分配:

一,为什么要有动态内存分配

首先我们目前已经掌握的内存开辟方式有如下2种:

c 复制代码
int a=20;
char arr[20]={0};

这两种空间的开辟方式有两个共同点:

  1. 他们都是在栈区开辟的,开辟的大小是固定的。
  2. 数组在开辟空间的时候大小确定后就不能够变化。

但有些时候我们数组的大小在程序运行的时候才知道,比如果我们之前在创建数组时数组的大小定小了那后面也没法改。要解决这一问题C语言就引入了动态内存管理,能够让程序员更加灵活的申请和释放空间,所以才要有动态内存分配。

要明白动态内存分配我们就要先理解掌握有关动态内存分配的几个函数,比如malloc,calloc,realloc,free这几个函数,下面我们一一来介绍:

二,malloc函数

要认识一个函数我们首先就要来看这个函数的参数,返回值是什么样的,功能是什么?
void* malloc (size_t size);

1.首先这个函数的功能是一个开辟空间的函数

2.其次该函数的参数类型为size_t类型 传入的是所开辟空间的大小

3.接着是返回值 返回的是一个void*的指针
·如果开辟空间成功,则返回的就是一个指向你所开辟好的空间的指针(首地址)

·如果开辟空间失败,则没有一个指针指向一个空间所以返回NULL(空指针) ,因此在使用malloc函数的时候一定要检查malloc函数是否开辟空间成功!
·还有一点就是如果参数 size 为0,malloc的行为是标准是未定义的,这取决于编译器。

这时可能会有人问:为什么返回的是void*的指针呢?

1,首先由于该函数不会提前知道你要开辟什么类型的空间的,所以使用void*的指针来接收并返回,因为void*的指针能够接收任意类型的数据这点我们之前也讲过,但还有一点要注意就是void*的指针是不能直接进行解引用操作的,需要强转后才能解引用!

了解了上面的知识后我们就来使用一下这个函数:

在使用之前不要忘了要包含<stdlib.h>这个头文件,因为malloc这个函数是在这里边定义的。

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	//int *p=(int*)malloc(10*sizeof(int))
	int* p = (int*)malloc(40);//将malloc函数的返回值强转为 int*类型赋给指针变量p
	if (*p == NULL)//开辟空间失败就返回错误信息 这一步不能少!
	{
		perror("malloc");//perror是一个打印错误信息的函数
		return 1;
	}
	//开辟了空间后 使用
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = 10;
		//p[i] = i;
	}
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *p);
	}
	return 0;
}


通过结果不难看出我们开辟的空间成功了,并且可以正常使用。但假如开辟失败了我们却没有检查malloc函数是否开辟空间成功会怎么样呢?

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	//int *p=(int*)malloc(10*sizeof(int))
	int* p = (int*)malloc(INT_MAX*1000);//将malloc函数的返回值强转为 int*类型赋给指针变量p
	/*if (p == NULL)
	{
		perror("malloc");//如果为空就打印错误信息
		return 1;
	}*/
	//开辟了空间后 使用
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = 10;
		//p[i] = i;
	}
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *p);
	}
	return 0;
}

结果就是什么也没有,因为INT_MAX*1000是一个非常非常大的数字,内存是没有这么大的空间的此时开辟空间失败了,所以malloc返回的是空指针,而对空指针去解引用会导致未定义行为,可能会导致程序崩溃或产生意外结果。

空指针指向的内存地址是无效的,解引用空指针意味着试图访问该地址所存储的数据,这在大多数情况下会导致程序运行错误。因此,在编程中应该避免对空指针进行解引用操作,可以在解引用前检查指针是否为空。

如果我们想知道为什么开辟空间失败的话,加上if语句的内容使用perror打印错误信息,他就会告诉我们没有足够的空间可以开辟。

创建好了空间使用完后总不能不回收吧,如果计算机只申请空间而不释放回收掉的话就会变得非常卡顿,就如同如果我们电脑的C盘爆满就会变得很卡一样,所以我们需要释放空间这就用到了free函数

三,free函数

为了解决回收空间的问题C语言提供了一个专门用来做动态内存的释放和回收的函数,函数原型如下:
void free (void* ptr);

  1. 首先参数使用一个void*的指针来接收传入的参数,使用viod*的指针方便接收任意数据类型的数据。 ·如果参数ptr指向的空间不是由动态内存函数开辟的话,那么free函数的行为是未定义的。 ·如果传入的值是NULL,则函数什么事情都不做,因为没有空间需要释放。
  2. 其次返回值是void 也就是不会反回任何东西。

那怎么使用呢还是以上面的代码作为例子:
在使用之前要包含头文件<stdilb.h>

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	//int *p=(int*)malloc(10*sizeof(int))
	int* p = (int*)malloc(INT_MAX*1000);//将malloc函数的返回值强转为 int*类型赋给指针变量p
	/*if (p == NULL)
	{
		perror("malloc");//如果为空就打印错误信息
		return 1;
	}*/
	//开辟了空间后 使用
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i) = 10;
		//p[i] = i;
	}
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *p);
	}
	 free(p);//通过free(p)就是主动释放p的空间 假如我们不主动释放等程序运行结束也会被操作系统回收。
	 p=NULL;//p指向的空间被释放 此时p就是野指针 所以要置为NULL空指针 否则等再次使用的时候就会出现非法访问
	return 0;
}

在使用free函数时有一点需要注意就是 :当指向动态内存开辟的指针被释放后,该指针指指向的内存就已经不存在了所以此时该指针就变成了野指针;在指针篇我们讲过指针指向的空间被回收就会造成野指针,使用野指针就会造成非法访问所以我们要避免。

四,calloc函数

同样C语言还提供了一个能够动态内存开辟的函数calloc函数,函数原型如下:
void* calloc (size_t num, size_t size);

  1. 首先有两个参数,第一个参数为size_t类型的num,及传入的是元素个数,第二个参数也是size_t类型的size即传入的是单个元素的大小。
    · 所以该函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0(这一点与malloc函数不同)。
  2. 返回值为void*类型,这一点与malloc函数类似

c 复制代码
#include<stdio.h>
#include<stdlib.h>
int main()
{	
	int *p=(int*)malloc(10*sizeof(int));
	int *pp=(int*)malloc(10,sizeof(int));
	int i=0;
	for(i=0;i<10;i++)
	{
		printf("%d ",*(p+i));
	}
	for(i=0;i<10;i++)
	{
		printf("%d ",*(pp+i));
	}
	return 0;
}


我们可以很直观的看到malloc开辟空间后打印出的值是随机值,calloc函数开辟的空间打印出的全为0,即已经初始化了。我们再调试起来让大家看看他们的区别:

五,realloc函数

有了上面的malloc和calloc函数我们就可以跟灵活的开辟内存空间,但是假如我们觉得开辟的空间不够大或者太大了,想要做出一些调整怎么做呢?realloc函数就是解决这一问题的。

该函数的函数原型如下:
void* realloc (void* ptr, size_t size);

  1. 首先该函数有两个参数,第一个参数为void*类型的ptr,用于接收需要调整内存的地址,第二个参数为size_t类型的size 即想要扩展或缩小内存的大小;这里的大小是总的大小。
  2. 返回值为void*类型 返回的是调整之后的内存的起始地址。
    对于第二个参数错误有个例子如下:
    注意:realloc函数调整内存空间存在两种情况
    1. 如果调整的空间不是太大,原有空间之后有足够大的空间;则在原空间后再加入一段内存,原来空间的数据不发生变化。
    2. 如果扩展的空间非常大,原有空间之后没有足够大的空间;则realloc会在堆区重新找一个合适大小的连续的空间来使用并返回新空间的首地址。

举个例子你就明白了:

c 复制代码
#include<stdio.h>
#include<stdilb.h>
int main()
{
	int* p = (int*)malloc(20);//在堆区开辟5个整型的空间
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//前5个元素存5
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		*(p + i) = 5;
	}
	//让空间变成10个整型的空间
	p = (int*)realloc(p, 10 * sizeof(int));//扩展到10个整型的空间
		if (p != NULL)
		{
			//将扩展后的5个元素改成10
			for (i = 5;i < 10;i++)
			{
				*(p + i) = 10;
			}
		}
	//打印
	for (i = 0;i < 10;i++)
	{
		printf("%d ", p[i]);
	}
	free(p);
	p = NULL;
	return 0;
}


通过运行结果不难看出realloc成功扩展了一块新的空间,但是哪种开辟方式呢?我们画图分析可能的情况:

要判断是那种情况其实很简单我们只需要看一下指针变量p所存的地址发生改变了没有就知道了,我们调试起来让大家看看:

当程序走到第7行时,我们调试看p的地址为0x0000001cb8cffcc8


当程序走到21行时,说明空间已经扩展完成了,此时p的地址与未扩展前的地址一样由此我们可以知道是第一种情况。

如果想要得到第二种情况的话我们可以适当的将追加空间的大小变得更大一些,这里就不再演示了有兴趣的读者可以自行去尝试。

说完了如何使用这些动态内存开辟的函数我们就来看看一些动态内存的错误,能够让我们在使用的时候尽量避免这些错误。

六,常见动态内存的错误

1,对空指针NULL进行解引用操作

我们直接看例子:

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test()
{ 
 	int *p = (int *)malloc(INT_MAX/4); 
 	*p = 20;
 	free(p); 
 	p=NULL;
}
int main()
{
	test();
	return 0;
}

这段代码首先在堆区开辟一个大小为INT_MAX/4的空间,但是开辟后没有判断p是否为空指针,即没有判断是否开辟空间成功。
如果开辟失败返回空指针而*p = 20;这种对空指针的解引用就会让程序报错。

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

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test() 
{ 
	int i = 0; 
	int *p = (int *)malloc(10*sizeof(int)); 
	if(NULL == p) 
	{ 
		perror("malloc");
		return 1; 
	}
	for(i=0; i<=10; i++) 
	{ 
		*(p+i) = i;
	}
	free(p); 
	p=NULL:
}
int main()
{
	test();
	return 0;
}

这段代码首先使用malloc函数在堆区开辟一个40个字节大小的空间,即10个整型的空间,但是我们看到for循环中i0开始,结束条件是i<=10这样的结果是循环了11次,多出来的一个整型就造成了非法访问。

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

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test()
{ 
	int a = 10; 
	int *p = &a; 
	free(p);
	p=NULL;
}
int main()
{
	test();
	return 0;
}

看到这段代码我们首先要明确一个点就是我们之前创建的普通变量也好,局部变量等都是在内存中的栈区来创建的,而free函数管理的是内存中堆区的空间,这种跨区域的能力是free函数不具备的。所以不能使用free函数去释放栈区的空间;即对非动态开辟内存不能使用free函数进行释放。

4,使用free释放⼀块动态开辟内存的⼀部分

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test() 
{ 
	int *p = (int *)malloc(40); 
	int i=0;
	if(p!=NULL)
	{
		for(i=0;i<5;i++)
		{
			*p=i;
			p++;
		}
	}
	free(p);
	p=NULL;
}
int main()
{	
	test();
	return 0;
}

上面的代码中在使用for循环完毕后,我们开辟的空间中前5个函数都已经被赋了值,但这时我们使用free函数去释放p就会报错,原因是p所指向的地址发生了变化,经过for循环已经指向了第5个空间的地址(malloc函数开辟的空间是连续的),所以这时候去释放只能释放前5个空间内存后面5个不能被释放所以报错。

5,对同⼀块动态内存多次释放

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test() 
{ 
	int *p = (int *)malloc(40); 
	int i=0;
	if(p!=NULL)
	{
		for(i=0;i<10;i++)
		{
			*(p+i)=i;
		}
	}
	free(p);
	free(p);
	p=NULL;
}
int main()
{	
	test();
	return 0;
}

还用上面的代码修改一下不让p指向的位置发生变化,如果我们重复的对p使用free函数释放就会报错,道理很简单,对释放过一次不存在的空间再释放显然编译器是找不到该释放的空间的所以报错。

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

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void test() 
{ 
	int *p = (int *)malloc(100); 
	if(NULL != p) 
	{ 
		*p = 20; 
	} 
}
int main() 
{ 
	test(); 
	while(1); 
}

如标题,如果我们使用完了空间不去释放,久而久之内存很快就会被我们用完,而这些未被释放的内存不能在被使用就相当于内存泄漏了。

所以切记:动态开辟的空间⼀定要释放,并且正确释放。

下面来几道动态内存的经典笔试题

七,动态内存经典笔试题分析

1,题目一

c 复制代码
#include<stdio.h>
#include<stdlib.h>
void GetMemory(char *p) 
{ 
	p = (char *)malloc(100); 
}
void Test(void) 
{ 
	char *str = NULL; 
	GetMemory(str);
 	strcpy(str, "hello world"); 
 	printf(str); 
 	free(str);
 	str=NULL;
}
int main()
{
	test();
	return 0;
}

以上代码的分析我们画图展示:

2,题目二

c 复制代码
#include<stdio.h>
#include<stdlib.h>
char *GetMemory(void) 
{ 
	char p[] = "hello world"; 
	return p; 
}
void Test(void) 
{
 	char *str = NULL; 
 	str = GetMemory(); 
	 printf(str); 
}
int main()
{
	Test();
	return 0;
}

代码分析如下:

结果如下:

3,题目3

c 复制代码
#include<stdio.h>
#include<stdlib.h>
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;
}

代码分析如下:

结果如下:

4,题目4

c 复制代码
#include<stdio.h>
#include<stdlib.h>
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;
}

首先按照正常的分析我们看到在malloc函数开辟完空间后就使用字符串复制函数,将"hello"复制到str中,接着free释放str的内存空间,空间释放后此时str就已经变成了NULL即空指针了所以后面的if语句的判断就没有任何意义了。

但当我们将结果打印出来以后会有疑问为什么是world,前面str已经变成了NULL空指针了吗?

当使用free释放str的时候str变成了空指针不假,但要注意这时后的str就是野指针了,对野指针进行使用就是非法访问内存了,所以最好的解决办法就是在free函数后加上str=NULL及时将str置为空指针。

以上就是有关动态内存有关的笔试题就介绍完了,下面我们介绍柔性数组。

八,柔性数组

可能你没有听过柔性数组,但它确实是存在的,在C99 中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

那什么呢是柔性数组呢?

在结构体中最后一个成员是数组,且该数组是未知大小的数组,那么这个数组就叫柔性数组。

例如我们定义一个结构体

c 复制代码
struct type
{
	int i;
	int a[];//这就叫柔性数组成员
}

1,柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。因为柔性数组为最后一个成员
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
  3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

对于第二点我们举个例子:

c 复制代码
 struct stu
{
	int i;
	int arr[];
}stu;
int main()
{
	printf("%zd\n", sizeof(stu));
	return 0;
}

2,柔性数组的使用

柔性数组一般配合malloc函数来使用我们给出代码

c 复制代码
#include<stdio.h>
#include<stdlib.h>
struct s
{
	int n;
	int arr[];
};
int main()
{
	//为n和arr数组开辟空间
	struct s*ps=(struct s*)malloc(sizeof(int)+5*sizeof(int));//5*sizeof(int)就是在给数组分配空间
	if(ps==NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功就使用
	ps->n=10;
	int i=0;
	for(i=0;i<5;i++)
	{
		ps->arr[i]=i;
	}
	//空间不够想扩展空间 扩展10个字节
	struct s*ptr=(struct s*)realloc(ps,sizeof(int)+10*sizeof(int));
	if(ptr !=NULL)
	{
		ps=ptr;
		ptr=NULL;
	}
	//使用完释放
	free(ps);
	return 0;
}

我们给出代码的分析:

当然给柔性数组使用realloc函数的时候也可能是第二种情况,即重新开辟空间的情况。

3,柔性数组的优势

我们同样模仿柔性数组设置一个在堆区分配空间的数组,代码如下:

c 复制代码
#include<stdio.h>
#include<stdlib.h>
struct s
{
	int n;
	int* arr;//使用指针方便调整大小
};
int main()
{
	struct s* ps=(struct s*)malloc(sizeof(int));
	if(ps==NULL)
	{
		perror("malloc1");
		return 1;
	}
	ps->n=10;
	ps->arr=(int*)malloc(5*sizeof(int));//给数组开辟空间
	if(ps->arr==NULL)
	{
		perror("malloc2");
		return 1;
	}
	//开辟完使用
	int i=0;
	for(i=0;i<5;i++)
	{
		ps->arr[i]=i;
	}
	//内存不够想将数组扩容到10个整型
	struct s*ptr=(struct s*)realloc(ps->arr,sizeof(int)+5*sizeof(int));
	if(ptr==NULL)
	{
		perror("realloc");
		return 1;
	}
	//释放内存 很重要一定先释放(*ps).arr 或ps->arr
	free(ps->arr);
	ps->arr=NULL;
	free(ps);
	ps=NULL;
	return 0;
}

我们给出代码分析:

总结:
对比使用柔性数组和不使用柔性数组相信当你看完上面这两个例子之后就能判断出使用柔性数组的优势,在第二种情况中,释放内存尤为要小心,因为是额外给数组分配的空间。而使用柔性数组最初的ps指向的就是指向数组的空间,无论后面怎么扩展在释放的时候只需要释放ps就可以了。

所以使用柔性数组有两个好处:

  1. 第一个好处是:方便内存释放
  2. 第二个好处是:这样有利于访问速度

有关柔心数组的介绍就到这里,下面再给一张图让你更清晰的了解动态内存:

九,C/C++中程序内存区域划分

我们给出每个区的解释:

  1. 栈区:是一种内存区域,用于存储函数的局部变量、函数参数、函数返回地址等数据。
  2. 堆区:堆区是一种内存区域,用于动态分配内存空间,存储程序运行时动态创建的数据结构和对象。
  3. 数据段(静态区):静态区是程序运行时用来存储静态变量和常量的内存区域。
  4. 代码段:代码段是存放程序执行代码的内存区域。

以上就是本章的全部内容啦!本篇文章内容超级多请各位读者按需阅读噢!

最后感谢能够看到这里的读者,如果我的文章能够帮到你那我甚是荣幸,文章有任何问题都欢迎指出!制作不易还望给一个免费的三连,你们的支持就是我最大的动力!

相关推荐
闪电麦坤951 分钟前
数据结构:递归的种类(Types of Recursion)
数据结构·算法
ascarl201011 分钟前
准确--k8s cgroup问题排查
java·开发语言
Gyoku Mint1 小时前
机器学习×第二卷:概念下篇——她不再只是模仿,而是开始决定怎么靠近你
人工智能·python·算法·机器学习·pandas·ai编程·matplotlib
纪元A梦1 小时前
分布式拜占庭容错算法——PBFT算法深度解析
java·分布式·算法
fpcc1 小时前
跟我学c++中级篇——理解类型推导和C++不同版本的支持
开发语言·c++
莱茵菜苗1 小时前
Python打卡训练营day46——2025.06.06
开发语言·python
爱学习的小道长1 小时前
Python 构建法律DeepSeek RAG
开发语言·python
px不是xp1 小时前
山东大学算法设计与分析复习笔记
笔记·算法·贪心算法·动态规划·图搜索算法
luojiaao2 小时前
【Python工具开发】k3q_arxml 简单但是非常好用的arxml编辑器,可以称为arxml杀手包
开发语言·python·编辑器