C语言动态内存管理

在了解到动态内存分配这块儿的知识之前,大家或许也和我一样,掌握的内存开辟空间只有创建变量创建数组 。或许大家平时使用这类方法时也并没有觉得有什么不妥,甚至觉得很方便好用。但其实当我们在编程这条路上走的更远的时候就会发现,使用数组开辟空间也是有非常多的坏处的而为了避免这些坏处,就引出了我们今天要学习的内容:动态内存管理~

一、什么是动态内存?

① 为什么要有动态内存分配

首先让我们回忆一下创建变量创建数组的方式开辟内存格式:

cs 复制代码
int main()
{
	int a;//在栈空间上开辟四个字节
	char str[20];//在栈空间上开辟10个字节连续空间
	return 0;
}

同时让我们思考一下,这样的开辟内存方法的缺点是什么呢?主要是以下两点:

空间开辟大小是固定的。

• 数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。

• 有时需要的空间大小在程序运行过程中才能得知,而使用数组无法做到灵活调整空间。

而为了避免这些缺点,并且为了使程序员能够灵活的调整空间,就出现了动态内存分配 ~使用这种方法就能让我们自己申请和释放内存了,非常的方便。

② 动态内存分配的好处

首先我们看一下,这个使用数组开辟空间而导致的错误:

cs 复制代码
int main()
{
	int arr[300000];
	return 0;
}

而出错的原因就是溢出了,是"栈溢出"。而接下来我们尝试一下用动态内存分配方法开辟相同大小的空间。

cs 复制代码
int main()
{
	int* arr = (int*)malloc(sizeof(int) * 300000);
	printf("YES\n");
	return 0;
}

此时我们会发现,使用这种方法便不会出现报错情况。 这是因为动态内存分配与使用数组内存分配,开辟出的内存空间并不在一个区域内:而这三个区域的大小又各有不同,所以使用动态内存分配不会导致溢出,而栈区导致了溢出。

栈区大小:2M或1M。
函数内申请的变量、数组,是在栈(stack)中申请的一段连续的空间。

静态区大小:2G.
全局变量,全局数组,静态数组(static)大小为2G。

堆区大小:视内存而定,可以开很大
malloc、new的空间,则是开在堆(heap)的一段不连续的空间,理论上则是硬盘大小。

可以看到栈区的空间是很小的,所以这也是动态内存分配的一大好处。

而后续介绍的动态内存分配函数中也会提到它的其他好处,比如灵活修改内存大小。

++(以下函数都需要头文件stdlib.h)++

二、malloc函数

malloc函数的作用是向内存申请一块连续的空间,并返回指向这段空间的指针。

malloc函数接收的参数是一个size_t(无符号整型) 的变量,参数size代表的是申请分配的内存大小,单位为字节

因为想要创建什么类型的变量取决于程序员 ,所以malloc函数的返回类型需要做到灵活,所以是void*类型

当然~malloc归根结底也还是申请内存的函数,只要申请空间就需要占用空间,只要占用空间就避免不了可能会申请失败

++• 如果开辟成功,则返回一个++++指向开辟好空间的指针++++++

++• 如果开辟失败,则返回一个++++NULL++++指针,++++因此malloc的返回值一定要做检查++++++

++• 返回值的类型是 void* 所以++++malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定++++++

++• 如果参数 size 为 0(开辟0个字节),malloc的行为是标准是未定义的,取决于编译器。++

让我们写一段代码来具体的学习一下malloc函数的使用吧~

cs 复制代码
int main()
{
	int* p = (int*)malloc(sizeof(int) * 10);//开辟大小为40字节的空间
	if (p == NULL)
	{
		perror("malloc");//打印错误信息
		return 1;//异常返回
	}
	return 0;
}

当我们运行这段代码时,很顺利的就成功了,而当我们将10改成100,1000,甚至更多呢这次就没能如愿的分配出这么多的空间了,原因就是:not enough space(空间不足)。

接下来我们尝试一下:使用动态内存分配模拟实现数组。

cs 复制代码
int main()
{
	int* p = (int*)malloc(sizeof(int) * 10);
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
		printf("%d\n", *(p + i));
	}
	return 0;
}

这样就成功模拟出来啦~怎么样,其实还是挺简单的吧。

三、free函数

在动态内存中,malloc函数的作用是开辟空间,那么自然也要有释放空间的函数。free的作用就是释放动态内存分配函数所开辟的空间。

free接收的参数类型为void*,ptr所指代的就是动态开辟出的空间的指针。

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

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

++(需要注意的是:free的作用仅仅是将开辟的空间释放,而并不是置空,所以为了防止误访问已释放的空间,最好再加一步置空操作~)++

函数的使用:

cs 复制代码
int main()
{
	int* p = (int*)malloc(sizeof(int) * 100);//开辟100个sizeof(int)大小的空间
	for (int i = 1; i <= 100; i++)
	{
		*(p + i - 1) = i;//为100个空间赋值
		printf("%4d", *(p + i - 1));
		if (i % 10 == 0)
			printf("\n");
	}
	free(p);//释放空间
	p = NULL;//空间置空
	return 0;
}

可能有人就会觉得,这个释放空间的作用到底为何呢?因为当我们使用完以后,自然而然的退出就好了,为何要多此一举的去释放它呢?实则不然,free的作用非常之重要,动态内存分配最大的优点就在于灵活

如果使用数组开辟空间,++当题目变量过多++ ,++需要多个数组存储数据时++ ,用完一个数组后无法像动态内存一样,能够使用free去释放空间,从而导致使用完毕的数组浪费空间,甚至可能导致溢出。而动态内存分配的优点也体现于此,当我们使用完一块空间后可以灵活的将它释放掉,并使用新的空间进行接下来的操作,这就大大的节省了不必要的空间~

如果使用空间过多,而每次使用完后并不即使将空间释放,那么就会造成++内存泄漏:++

cs 复制代码
int main()
{
	int i = 0;
	int* arr[100000];//储存每一次开辟的空间
	for (i = 0; i < 100000; i++)
	{
		arr[i] = (int*)malloc(sizeof(int) * 10000);//每次开辟10000*4大小的空间
        //free(arr[i]);     //我故意的...
		if (arr[i] == NULL)
		{
			printf("%d\n", i);//如果失败则打印开辟多少次时失败
			perror("arr[i]");//如果开辟失败则返回错误信息
			break;
		}
	}
	return 0;
}

代码解读:我们使用一个指针数组来存储每一次使用malloc开辟出的空间,并且每次开辟都不释放,当开辟次数达到一个限度,使得没有足够的空间开辟下一次时,便会报错并退出运行。 我们可以看到,如果使用完后不进行释放,那么在开辟空间的多重积累下,即便拥有的空间再多也会可能发生溢出而当我们将被注释掉的free函数解除注释后,此代码就能完美无误的彻底运行结束

四、calloc函数

calloc函数和malloc函数其实大同小异,都是用来开辟动态内存空间的。只不过它们的区别在于:① malloc函数开辟出空间后并未初始化,而calloc函数开辟出空间后会自动初始化为0。

② calloc函数接收的参数为两个,num代表元素的个数,size代表每个元素的大小。

两者区别的验证:

cs 复制代码
int main()
{
	int* arr1 = (int*)malloc(10 * sizeof(int));
	int* arr2 = (int*)calloc(10, sizeof(int));
	printf("malloc开辟:\n");
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", arr1[i]);
	}
	printf("\n");
	printf("calloc开辟:\n");
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", arr2[i]);
	}
	free(arr1);
    free(arr2);
	arr1 = NULL;
    arr2 = NULL;
	return 0;
}

总而言之,如果我们想开辟存储数字数据的空间,那么calloc无疑是更加合适的人选~

五、realloc函数

realloc函数的作用是:重新调整之前使用malloc或calloc所开辟的空间大小。

其中两个参数:

① void* ptr:代表的是想要改变大小的空间的指针(必须是之前用malloc或calloc开辟出的动态内存空间才可以)

② size_t size:此块动态内存改变后的大小。

realloc函数的使用:

cs 复制代码
int main()
{
	int* arr = (int*)calloc(5, sizeof(int));//开辟出大小为5*sizeof(int)的空间
	for (int i = 0; i < 10; i++)
	{
		if (arr[i] != 0)//判断此处空间是否被开辟(初始化)
		{
			arr = (int*)realloc(arr, (i + 1) * sizeof(int));//开辟新空间,用于储存新数据
		}
		arr[i] = i;
		printf("%d ", arr[i]);
	}
	free(arr);
	arr = NULL;
	return 0;
}

(如果不使用realloc函数进行内存调整,就会提醒"错误出现在创建对象时内存分配")使用realloc函数就成功的,真正的做到了灵活控制开辟空间大小~

当然,realloc也可能出现空间调整失败的情况,而使用realloc改变内存空间大体有以下三种情况

① 当原空间后有足够空间时,直接在原空间后追加新空间。

② 当原空间后没有足够空间时,选取一块新的空间进行开辟,然后将原空间的数据复制到新选取的空间后释放原空间,最后返回新空间的地址。 ③ 找不到空间 了解了三种情况之后,让我们再看看应对这三种情况,改进之后的代码

cs 复制代码
int main()
{
	int* arr = (int*)calloc(5, sizeof(int));//开辟出大小为5*sizeof(int)的空间
	if (arr == NULL)
	{
		perror("arr");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		if (arr[i] != 0)//判断此处空间是否被开辟(初始化)
		{
			//创建新变量接收扩大后的空间,防止返回NULL
			int* arr1 = (int*)realloc(arr, (i + 1) * sizeof(int));//开辟新空间,用于储存新数据
			if (arr1 != NULL)//判断,若不为NULL则将新指针给原指针
				arr = arr1;
		}
		arr[i] = i;
		printf("%d ", arr[i]);
	}
	free(arr);
	arr = NULL;
	return 0;
}

这样就非常安全啦~非常滴严谨

**六、**常见的动态内存的错误

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

cs 复制代码
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (arr == NULL)
	{
		perror("arr");
		return 1;
	}
	free(p);
	free(p);
	return 0;
}

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

cs 复制代码
int main()
{
	int i = 0;
	int* arr[100000];
	for (i = 0; i < 100000; i++)
	{
		arr[i] = (int*)malloc(sizeof(int) * 10000);
		if (arr[i] == NULL)
		{
			printf("%d\n", i);
			perror("arr[i]");
			break;
		}
	}
	return 0;
}

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

cs 复制代码
int main()
{
	int* arr = (int*)calloc(10 , sizeof(int));
	if (arr == NULL)
	{
		perror("arr");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*arr = i;
		printf("%d ", *arr);
		arr++;
	}
	//虽然能够成功打印0-9,但arr指向发生了变化
	free(arr);//而free要求的是:指向动态开辟内存的指针,所以会错误
	arr = NULL;
	return 0;
}

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

cs 复制代码
int main()
{
	int* arr = (int*)malloc(10 * sizeof(int));
	if (arr == NULL)
	{
		perror("arr");
		return 1;
	}
	for (int i = 0; i <= 10; i++)
	{
		*(arr + i) = i;
		printf("%d ", *(arr + i));
	}
	free(arr);
	arr = NULL;
	return 0;
}

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

cs 复制代码
int main()
{
	int arr[10];
	free(arr);
	return 0;
}

⑥ 对NULL指针的解引用操作

cs 复制代码
int main()
{
	int* arr = (int*)malloc(INT_MAX);
	*arr = 10;
	free(arr);
	return 0;
}

七**、动态内存管理习题**

习题一:

以下代码运行后会是怎样的结果?

cs 复制代码
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;
}

答案:出错了 T A T

解析: GetMemory函数的作用顾名思义,就是用来开辟空间的,但是此空间在函数结束后同时也被释放了,于是str仍然还是指向NULL,而NULL无法访问!!!故报错。

改进方案:

cs 复制代码
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的地址作为参数传过去,同时将函数的参数改成二级指针,这样我们改变的就是地址,而地址既被改变,就不会被释放,所以就能够正常的拷贝hello world啦~

注意:还有一个隐含的错误就是,使用malloc开辟动态内存空间后,并没有使用free释放内存!!!非常重要!!!

习题二:

运行这段代码,能够成功的打印hello world吗?

cs 复制代码
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

答案:烫烫烫烫烫!!! T A T 解析: 注意:在函数中定义的值和改变的值是局部变量,虽可以返回地址,但里面的值会随函数结束而被销毁。

习题三:

此段代码运行后,结果是什么?

cs 复制代码
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;
}

答案: 将前两题研究明白后,这题也就比较简单啦~其实就和习题一的改良后差不多,只是参数发生了些许变化。因为使用地址接收创建的空间,所以不会销毁,故hello是能够成功打印的,但是有一处错误就是:可能会出现内存泄漏!使用malloc开辟动态内存空间后并没有使用free释放!可不要马虎了~

那么关于动态内存管理的知识,就为大家分享到这里啦~如果有什么讲的不明白,或者有出现错误的地方,还请大家在评论区多多指出,我也会虚心改进的!!那么我们下期再见啦~

相关推荐
zwjapple10 分钟前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five11 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
前端每日三省13 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
凡人的AI工具箱26 分钟前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang
chnming198742 分钟前
STL关联式容器之map
开发语言·c++
进击的六角龙44 分钟前
深入浅出:使用Python调用API实现智能天气预报
开发语言·python
檀越剑指大厂44 分钟前
【Python系列】浅析 Python 中的字典更新与应用场景
开发语言·python
湫ccc1 小时前
Python简介以及解释器安装(保姆级教学)
开发语言·python
程序伍六七1 小时前
day16
开发语言·c++
wkj0011 小时前
php操作redis
开发语言·redis·php