C语言 动态内存管理:动态内存函数介绍,常见的动态内存错误,柔性数组

动态内存管理目录:


为什么会有动态内存管理呢

我们在日常使用中,创建一个数组,一个变量时都会开辟空间
如:

	int a;  //在栈上开辟一个四字节的空间
	char str[5];   //在栈上开辟一个五字节的连续的空间

但是上面这种开辟空间的方法都具有一个特点

1. 空间开辟的大小是固定的,无法修改
2. 声明数组的时候必须指定长度

但是当我们在实际使用中,因为适用不同的情况,我们的空间应该是可以随时修改来应对所有需求的,但是上面的那种方法是不能实现的,所以我们就需要动态内存管理了。

动态内存函数的介绍

malloc和free
void* malloc (size_t size);

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

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

在我们开辟一段空间后,我们也必须要将它释放掉,而C语言同样有这个函数

void free(void* ptr);

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

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

但是,free函数并不是完全销毁这段空间,而是将这段空间的使用权收回,它可能会分配给其他东西,也可能一直放在那里不用,如果打个比方的话:我们申请一个空间,就相当于是拿到了一间房子的钥匙,等到我们使用权结束后,我们并没有把房子给销毁,而是归还房子的钥匙,但是在归还钥匙之前,我们还是能够进入这个房间,所以如果仅仅使用free,可能还不够安全,所以应该采用下面的形式

	int* arr = (int*)malloc(sizeof(int) * 5);
	free(arr);
	arr = NULL;
calloc
void* calloc (size_t num, size_t size);
  • 函数的功能是为num个大小为size的元素开辟一段空间,并把空间中的所有字节初始化为0
  • 与malloc基本一样,只是calloc会将空间的所有字节初始化为0

在上面有提到过,我们之所以选择动态内存管理,就是因为它可以让我们随心所欲的更改我们需要的空间的大小,那这个时候,我们就要用到realloc这个函数

realloc
void* realloc (void* ptr, size_t size);
  • ptr是要调整的内存地址
  • size是调整后的大小
  • 返回值为调整之后的内存起始地址
  • 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间

但是这个函数在使用的时候会有两种情况

  1. 原有空间之后有足够大的空间
    当情况1时,我们拓展空间时会直接在原有空间后面追加一段空间

  2. 原有空间之后没有足够大的空间

当情况2时,因为后面的空间无法存放,我们就需要在堆上面找到一个足够的,连续的空间,将我们之前的数据拷贝过去,并且返回新的空间的地址


常见的动态内存错误

  1. 对NULL指针的解引用操作
void test()
{
	int* p = (int*)malloc(INT_MAX / 4); //申请一段巨大的空间,申请失败所以返回NULL
	*p = 20; //对NULL指针进行解引用操作
	free(p);
}

因为当我们内存申请失败的时候,malloc函数会返回一个NULL指针,如果我们对NULL指针进行解引用操作,就会产生错误,所以我们应该对malloc的返回值进行检查,确保内存申请成功。

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

void test()
{
	int a = 10;
	int* p = &a;
	free(p);
}

对于这段函数,我们在编译运行时可能不会报错,但是我们在进行调试的时候就可以看到错误。
在这里插入图片描述
因为free函数不能对非动态开辟的内存进行操作

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

void test()
{
   int* p = (int*)malloc(100);
   p++;
   free(p);
}

在这里插入图片描述

这段代码也是,在编译的时候没有任何报错,但是在运行的时候就产生了错误。因为我们让指针往前移动,指向的不再是动态内存的起始位置,但是free函数必须要对动态内存的起始地址进行操作,而我们的指针p已经不再指向动态内存的起始地址了,所以产生了错误

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

void test()
{
   int* p = (int*)malloc(100);
   free(p);
   free(p);
}

![在这里插入图片描述](https://img-blog.csdnimg.cn/20191127222215487.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM1NDIzMTU0,size_16,color_FFFFFF,t_70
我们重复free的时候,在编译阶段只会给出一个警告,但是在运行的时候就会报错。警告的提示是使用未初始化的内存p,因为我们第一次free的时候已经将p给释放了,所以重复free就会产生这样的结果

拓展

void GetMemory(char* p)
{
   p = (char*)malloc(100);
}
void test()
{
   char* str = NULL;
   GetMemory(str);
   strcpy(str, "hello world");
   printf(str);
}

这段代码,我们乍一看是没有错误的,编译也没有报错没有警告,但是在我们运行的时候就完全没有作用。

要了解这一点,我们就首先得知道栈帧的概念。
当我们在调用一个函数的时候,我们会在栈上开辟一个栈帧,然后再函数结束的时候,释放这个栈帧,而这个GetMemory调用时我们传入的p就是在这个栈帧中作用,如果我们想让它真的操作这个p,就应该传这个p的地址,即一个二级指针

修改后:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void test()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
}

在这里插入图片描述
这样修改后就可以成功了

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void test()
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "hello");
		printf(str);
	}
}

在这里插入图片描述
这里是可以运行的,这里就是我刚刚讲解的将str置为空指针的问题,因为我们free了之后这个指针就变成了一个野指针,它可能可以访问原来的空间,也有可能不行。

char* GetMemory()
{
	char p[] = "hello world";
	return p;
}
void test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

在这里插入图片描述
这里就是考察我们对于指针和数组的理解,在之前的指针那一章节中我就提到过

char* str1 = "hello world"; //指向常量区的这段字符串
char strl2[11] = "hello world";  //在栈上开辟一段内存空间,拷贝常量区的这段字符串

当我们在一个栈帧中,如果是str1的形式,我们返回的其实是在常量区中这段字符串的地址,而str2的方式就是其在这个栈帧中开辟的这段内存的地址,但随着栈帧的销毁,所以我们返回的其实是一段乱码
在这里插入图片描述

char* GetMemory()
{
	char *p = "hello world";
	return p;
}
void test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}

这样才能得到我们想要的答案


柔性数组

C语言中还存在这样一个鲜为人知的东西,在一个结构中,最后一个允许是位置大小的数组,这就叫做柔性数组成员,它的使用如下

typedef struct st_type
{
	int i;
	int a[0]; //柔性数组成员      有些编译器不能在下标中写入0
}type_a;
  • 结构中的柔性数组成员前面必须有至少一个其他成员
  • sizeof返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
柔性数组的使用

与结构同时分配内存

int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
	
	p->i = 100;
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}

	free(p);
}

先给结构体分配空间,再给柔性数组分配空间

typedef struct st_type
{
	int i;
	int *a; //柔性数组成员      有些编译器不能在下标中写入0
}type_a;
int main()
{
	int i = 0;
	type_a* p = (type_a*)malloc(sizeof(type_a));
	p->i = 100;
	p->a = (int*)malloc(p->i * sizeof(int));
	for (i = 0; i < 100; i++)
	{
		p->a[i] = i;
	}
	free(p->a);
	p->a = NULL;
	free(p);
	p = NULL;
}

两段代码作用相同,但是第一个只需要释放一次空间,而第二个要释放两次。但是第二种方法也是存在好处的,就是它的的访问速度会比第一种要快。

发布了60 篇原创文章 · 获赞 78 · 访问量 6322

猜你喜欢

转载自blog.csdn.net/qq_35423154/article/details/103283679