动态内存管理

动态内存管理

在开始这个知识点的总结前,我要先强调两个非常重要的点。

  1. 所有的内存申请都要进行返回值判断。
  2. 申请完,如果这段内存不再使用后,要调用free()函数进行释放。

为什么要进行动态内存分配?

我们现在已经掌握的内存开辟方式有:

int val = 7; //在栈上开辟4个字节
char arr[10] = {0}; //在栈空间上开辟10个字节连续的空间

但是上面两种开辟空间的方法有两个特点:

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

但是有的时候,我们的需求并不满足于上面的情况:

1. 有时候我们需要的空间大小需要在程序运行的时候才能知道。

2. 需要大块内存的时候,就要使用动态内存开辟。

动态内存函数的介绍

malloc和free

动态开辟内存都是在堆上开辟,由程序员亲自开辟,亲自释放,如果不释放,可能会造成内存泄漏问题,严重可能会造成宕机。

内存泄漏会随着进程的退出而结束,会归还内存。

但是这只是一种机制,在使用完开辟的内存后,释放内存还是非常非常非常重要滴!

在C语言里,malloc 的原型是:

void *malloc(size_t size);

由这里我们就可以知道,malloc是函数,既然是函数就有可能存在调用失败,所以必须检查返回值,如果调用失败,则返回值为NULL。

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定一定要做检查
  • 返回值的类型是void *,所以malloc函数并不知带开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数size为0,malloc的行为是标准未定义的,取决于编译器。

虽然malloc开辟的空间是在堆上开辟的,但是指向它的指针是在栈上开辟的。

既然有开辟内存的函数,那么就会有释放内存的函数。

在C语言里free函数的原型为:

void free(void *ptr);

free函数的参数是malloc函数开辟内存后的返回值。

free函数用来释放动态开辟的内存。

  • 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的。
  • 如果参数ptr是NULL指针,那么函数什么事都不做。

malloc和free都声明在stdlib.h头文件中。举个例子

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>

/*
** 本程序为动态内存管理专题课件代码练习
** 郭文峰
** 2018/11/26
*/

int main(void)
{
	int n = 0;
	scanf("%d", &n);
	int *ptr = (int *)malloc(n * sizeof(int));

	if (!ptr)
	{
		printf("Can't get memory.");
		exit(EXIT_FAILURE);
	}

	int i = 0;
	for (; i < n; i++)
	{
		ptr[i] = 0;
	}

	free(ptr);
	ptr = NULL;//这一步是否重要???

	system("pause");
	return 0;
}

那么最后的那一步ptr = NULL是否重要呢??

答案是:非常重要,在free之后,指针内容是不变的,但是此时指针已无法访问堆空间。这样的后果就是会造成野指针

在这里插入图片描述
在这里可以看到,在对指针进行free之后,指针的值依然是一个地址,只是这个地址已经无法访问刚刚创建的堆空间。但是若不对其进行ptr = NULL操作的话,就会造成野指针。

calloc

c语言里还提供了一个函数叫calloc,calloc函数也用来动态内存分配,原型如下:

void *calloc(size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0 。
  • 与函数malloc的区别只是在于calloc函数会在返回地址之前把申请的空间的每个字节初始化为全0 。

举个例子:

#define _CRT_SECURE_NO_WARNINGS 1

#include <stdio.h>
#include <Windows.h>

/*
** 本程序为动态内存管理专题课件代码练习
** 郭文峰
** 2018/11/26
*/

int main(void)
{
	int *p = calloc(10, sizeof(int));

	if (!p)
	{
		printf("Can't get memory!\n");
		exit(EXIT_FAILURE);
	}

	free(p);
	p = NULL;
	system("pause");
	return 0;
}

在这里插入图片描述
因此,如果我们需要对开辟的内存进行初始化,可以使用calloc函数。

realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时我们会发现申请的内存有点小或者有点大了,我们需要重新调整内存的大小,这时realloc函数就完美的可以解决这个问题。

realloc函数的原型为:

void *realloc(void *ptr, size_t size);
  • ptr是要调整的内存地址。
  • size是调整之后的大小。
  • 返回值为调整之后的内存起始位置。
  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
    • 情况1:原有空间之后有足够大的空间,那么此时扩大空间或缩小空间之后,返回的地址都不会改变。
    • 情况2: 原有空间之后没有足够大的空间用来调整新的需求,所以该函数会将这个所有的数据复制到一个新的足够大能满足新的需求的空间,并返回新的地址。

但是在使用realloc的时候需要注意一点,那就是realloc到底是可以直接在原有的内存地址直接开辟,还是需要新定义一个指针,如果返回值不为空,在将新的指针赋值给老指针??

int *ptr = (int *)malloc(100);

	if (!ptr)
	{
		printf("ERROR!\n");
		exit(EXIT_FAILURE);
	}

	//情况一
	ptr = realloc(ptr, 1000);
	if (!ptr)
	{
		printf("ERROR!\n");
		exit(EXIT_FAILURE);
	}

	free(ptr);
	ptr = NULL;
	//情况二
	int *p = realloc(ptr, 1000);
	if (!p)
	{
		printf("ERROR!\n");
		exit(EXIT_FAILURE);
	}

	ptr = p;

	free(ptr);
	ptr = NULL;

结论:情况二是一种安全的行为,因为,如果realloc申请内存失败,则情况一会将原内存的内容都丢失。

柔性数组

柔性数组(flexible array)这个概念并不常见,但是它确实存在。C99中,结构中最后一个元素允许是位置大小的数组,这就叫做柔性数组成员。

例如:

struct FleArr
{
	int i;
	char a[0];//柔性数组成员
    //也可以写成
    //char a[];
};

柔性数组是为了解决结构体内需要变长数组的需求。

结构体内最后一个元素为数组且数组的有且仅有0个成员的数组,为柔性数组。

柔性数组的特点:

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

不管柔性数组有无元素,柔性数组的大小都不算进结构体大小。

例如:

struct FleArr
{
	int i;
	char a[0];//柔性数组成员
};
printf("%d\n", sizeof(struct FleArr));

在这里插入图片描述

柔性数组的使用

	int i = 0;
	struct FleArr *p = (struct FleArr*)malloc(sizeof(struct FleArr), 100 \
	* sizeof(char));

	if (!p)
	{
		exit(EXIT_FAILURE);
	}

	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = 'a';
	}

	free(p);
	p = NULL;
	

在开辟空间的时候,就相当于先创建一个结构体大小的空间,在给柔性数组成员a创建了一个100个字符型元素的连续空间。

柔性数组的优势

	typedef struct st_type
	{
 		int i;
 		int *p_a;
	}type_a;
	
	type_a *p = malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int *)malloc(p->i*sizeof(int));
	
	for(i=0; i<100; i++)
	{
	 p->p_a[i] = i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;

如果不采用柔性数组的形式,我们需要在一个结构体内创建一个变长的数组,就需要再次动态分配内存,这样就需要动态分配两次内存,再释放的时候也需要释放两次。

这样的复杂度,在简单的代码里体现不出来,如果再一个多文件的代码里,自己就会被扰乱,不知道自己到底有没有创建,或者有没有释放。

综上:柔性数组的优势有两点:

  1. 方便内存的释放。

  2. 有利于访问速度。

    连续的内存有益于提高访问速度,也有益于减少内存碎片。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值