动态内存管理/开辟/分配

一、为什么会存在动态内存分配

创建变量的本质是向内存申请空间,我们已经掌握的申请内存空间的方式有两种:

int main()
{
	//1.在栈空间上申请4个字节的内存空间
	int a = 10;
	//2.在栈空间上申请一块40个字节的连续空间
	int arr[10] = { 0 };
	return 0;
}

上述开辟空间的方式有个问题,当申请的内存空间不够用了或是用了一部分还剩了一部分空间,空间会自动变大或变小吗?很明显是不会的,说明上述两种情况开辟空间的大小是固定的,而且,数组在声明的时候,必须指定数组的长度,数组的空间一旦确定了,那么它的大小是不能调整的,同理,变长数组也是这样的。

C语言提供了动态内存管理,那么为什么会存在动态内存分配呢?

  1. 变量和数组的方式不够灵活。
  2. 使用动态内存分配可以自己来维护内存的使用生命周期。

二、malloc和free

1.malloc

malloc()是C语言提供的一个动态内存开辟的函数。

在这里插入图片描述
函数原型如下:

void* malloc(size_t size);

意思是malloc()会开辟一块大小为size个字节的连续可用的内存空间,空间开辟成功的话,会返回一个指向这块空间的起始位置的指针;开辟失败,会返回一个空指针,所以函数的返回值一定要检查好。若参数size为0,malloc()的行为是标准未定义的,取决于编译器。

2.free

C语言提供了一个函数free(),是专门用来做动态内存的释放和回收的,函数原型如下:

void free(void* ptr);

在这里插入图片描述
free()函数的参数必须是指向动态内存开辟的空间。
free()函数的使用是主动释放内存空间后,这块空间就还给了操作系统,操作系统可以把这块空间用到别处。这个过程就像你去图书馆借书,看完了还书,那么这本书还可以借给别人看一样。
程序退出的时候,即使没用free(),操作系统也会主动收回这块内存空间。

例子: 申请40个字节的空间,用来存放10个整数。

使用动态内存分配都需要这三个步骤:申请内存、使用内存、释放内存。
“使用内存”部分,这块连续空间就像数组一样,可以像访问数组一样,使用指针访问这块空间。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请内存
	int num = 10;
	int* p = (int*)malloc(num * sizeof(int));//(int*)是强制类型转换成int*类型
	if (p == NULL)
	{
		perror("malloc");//失败的话,会打印:malloc:xxxxxx
		return 1;//失败返回
	}
	//使用内存
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;//p始终指向开辟内存的起始地址,这样p指向的就是一整块内存,最后释放的就是这一整块内存
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放内存
	free(p);//释放空间这一刻p不知道指向了哪里,此时p就是野指针
	p = NULL;//所以要给p赋值为NULL指针
	return 0;//正常返回
}

在这里插入图片描述
在这里插入图片描述

开辟空间失败的例子,p已经是空指针了:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
所以需要对返回值做检查。

观察释放动态内存前后动态内存空间的内容的变化:
在这里插入图片描述

在这里插入图片描述

三、calloc和realloc

1.calloc

在这里插入图片描述

函数原型如下:

void* calloc (size_t num, size_t size);

例子:

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述
若使用malloc()
在这里插入图片描述
对之前的图进行一个补充:
在这里插入图片描述

2.realloc

函数原型如下:

void* realloc (void* ptr, size_t size);

realloc()函数有两个功能:

  1. 调整内存空间的大小。
  2. 申请内存空间。当ptr为NULL,就和malloc()函数的功能一样了。

在这里插入图片描述

realloc()函数在调整内存空间的大小时有两种情况:

情况一: 原有空间之后有足够大的空间。直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况二: 原有空间之后没有足够大的空间。扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用,将旧空间的数据拷贝到新空间,旧的空间会自动还给操作系统,这样函数返回的是⼀个新的内存地址。

在这里插入图片描述
realloc()函数在调整内存空间的大小时会调整失败的,那么函数返回的是NULL指针。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	//申请内存
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//使用内存
	int i = 0;
	for (i = 0; i < 5; ++i)
	{
		*(p + i) = i + 1;
		printf("%d ", *(p + i));
	}
	//内存不够用了:想打印出6到10,所以要调整内存空间
	int* ptr = (int*)realloc(p, 10 * sizeof(int));
	//realloc()可能调整失败 - 返回NULL指针。直接赋值给p会与前面的p !=NULL矛盾
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
		p = ptr;
	//使用调整后的内存空间,这块空间会把旧空间的数据拷贝过来
	for (i = 5; i < 10; i++)
	{
		*(p + i) = i + 1;
		printf("%d ", *(p + i));
	}
	//释放内存
	free(p);
	p = NULL;
	return 0;
}

使用realloc()函数前:
在这里插入图片描述
使用realloc()函数后:
在这里插入图片描述

realloc()函数申请内存空间:

#include <stdlib.h>
int main()
{
	int* p = (int*)realloc(NULL, 10 * sizeof(int));
	//相当于int* p = (int*)malloc(10 * sizeof(int));
	return 0;
}

四、常见的动态内存的错误

1.对NULL指针的解引用操作

void test()
{
 	int *p = (int *)malloc(INT_MAX/4);//malloc()开辟失败会返回NULL指针
 	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
	p = NULL;
}

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

void test()
{
	int i = 0;
	int* p = (int*)malloc(10 * sizeof(int));
	if (NULL == p)
	{
		exit(EXIT_FAILURE);
	}
	for (i = 0; i <= 10; i++)
	{
		*(p + i) = i;//当i是10的时候越界访问
	}
	free(p);
	p = NULL;
}

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

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

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

void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
	p = NULL;
}

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

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}

改进:

void test()
{
	int* p = (int*)malloc(100);
	free(p);
	p = NULL;
	free(p);//free()的参数是NULL,则free()函数什么都不会做
}

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

void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。

五、动态内存经典笔试题分析

题目一

请问运行Test函数会有什么样的结果?

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

在这里插入图片描述
strcpy(str, "hello world");是对NULL指针的解引用操作符,这时就是非法访问内存了,程序会崩溃的。

代码改进:

#include <stdio.h>
#include <string.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);//相当于printf("%s\n", str);或printf("hello world");
	//str是存放常量字符串的首元素的地址的,常量字符串与数组一样都是连续的空间,知道了首元素的地址,就会知道整个数组的内容
	//释放内存空间
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

这样看函数参数有点多余,进一步优化:

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

在这里插入图片描述

最终是指针str指向这块开辟空间:
在这里插入图片描述

题目二

请问运行Test函数会有什么样的结果?

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

结果为:
在这里插入图片描述
进行了画图分析后,理应打印出hello world,但是结果并没有,这里其实是返回栈空间地址的问题。
在这里插入图片描述
在《深入理解指针(2)》中讲解了野指针成因,当中有一条就是指针指向的空间释放了,上述代码就是这个意思,数组所在的空间出了函数,这一块的空间就被回收了,即str是野指针。

题目三

请问运行Test函数会有什么样的结果?

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

会造成内存泄漏,原因是没有对动态内存进行释放。
代码改进:

#include <stdio.h>
#include <string.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);
	//释放内存
	free(str);
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

在这里插入图片描述

题目四

请问运行Test函数会有什么样的结果?

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

代码改进:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	if (str == NULL)
	{
		perror("malloc");
		return ;
	}
	strcpy(str, "hello");
	free(str);//100B的空间释放了,这时str就是野指针
	str = NULL;
	if (str != NULL)//假
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

在这里插入图片描述

六、柔性数组(flexible array)

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

例如:

struct S1
{
	int i;
	int a[0];//柔性数组成员
};
struct S2
{
	int n;
	int a[];//柔性数组成员
};

1.柔性数组的特点

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

例如:

#include <stdio.h>
struct S
{
	int i;
	int a[0];//柔性数组成员
};
int main()
{
	printf("%zd\n", sizeof(struct S));
	return 0;
}

在这里插入图片描述
可以发现:sizeof()返回的结构大小中不包括柔性数组的内存。

2.柔性数组的使用

代码1:

#include <stdlib.h>
#include <stdio.h>
struct S
{
	int n;
	int a[];
};
int main()
{
	struct S* ptr = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr->n = 100;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ptr->a[i] = i + 1;
		printf("%d ", ptr->a[i]);
	}
	//若觉得数组a的空间不够用了,还可以动态调整
	struct S* ps = realloc(ptr, sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("realloc");
		return 1;
	}
	ptr = ps;
	for (i = 10; i < 20; i++)
	{
		ptr->a[i] = i + 1;
		printf("%d ", ptr->a[i]);
	}
	free(ptr);
	ptr = NULL;
	return 0;
}

在这里插入图片描述
在这里插入图片描述

3.柔性数组的优势

代码2:

#include <stdio.h>
#include <stdlib.h>
struct S
{
	int n;
	int* a;
};
int main()
{
	struct S* ptr = (struct S*)malloc(sizeof(struct S));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr->n = 100;
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr->a = p;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ptr->a[i] = i + 1;
		printf("%d ", ptr->a[i]);
	}
	//空间不够了,扩容
	int* p2 = (int*)realloc(ptr->a, 20 * sizeof(int));
	if (p2 == NULL)
	{
		perror("realloc");
		return 1;
	}
	ptr->a = p2;
	for (i = 10; i < 20; i++)
	{
		ptr->a[i] = i + 1;
		printf("%d ", ptr->a[i]);
	}
	free(ptr->a);
	ptr->a = NULL;
	free(ptr);//若先free掉ptr,那么会找不到ptr->a的
	ptr = NULL;
	return 0;
}

在这里插入图片描述

在这里插入图片描述
上述代码1和代码2可以完成同样的功能,但是代码1的实现有两个好处:

  1. 方便内存释放
  2. 有利于访问速度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值