C语言——动态内存管理
创始人
2024-11-16 06:37:41
0

引言

在编程中,动态内存管理是一个至关重要的概念,它对于开发高效、灵活且可扩展的程序至关重要。接下来我们就来学习一下C语言——动态内存管理的知识。

为什么需要动态内存分配

我们现在已经掌握了两种内存开辟的方法:

#include int main() { 	int val = 20;         //在栈空间上开辟四个字节 	char arr[10] = { 0 }; //在栈空间上开辟10个字节的连续空间 	return 0; }

上述的开辟内存的方法为静态开辟内存。

这种方法存在两个缺点:

(1)空间开辟的大小是固定的。

(2)数组在声明的时候,必须要指定数组的长度,数组的长度一旦确定就不能修改。

有时候我们可能会遇到:需要的空间大小不能确定的情况,此时使用静态开辟内存就无法解决问题。

为了解决这类问题,C语言引进了动态内存开辟,也就是我们今天要重点学习的内容。

动态内存

与动态内存相关的函数声明均在中。

1.perror函数

在学习动态内存开辟函数和释放函数之前,我们可以先学习一下perror函数。

perror是C语言标准库中的一个函数,用于错误处理和调试

perror包含在头文件中。

函数声明为:void perror ( const char * str );

        str – 这是 C 字符串,包含了一个自定义消息,将显示在原本的错误消息之前。

作用:把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str,后跟一个冒号,然后是一个空格。

返回值:无返回值。

2.动态内存开辟函数

2.1 malloc

(1)malloc的用法

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

函数原型为:

void* malloc (size_t size);

参数:

size 是一个无符号整数,指定了要分配的内存块的大小(以字节为单位)。如果参数 size 为0,那么malloc的行为是未被定义的,取决于编译器。

返回值:

返回一个指向分配的内存块的指针。返回值为void*,所以malloc函数并不知道开辟空间的类型,具体需要根据使用者的需求去决定。

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

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

(2)malloc的使用

我们可以将malloc和perror结合使用:

#include    #include  int main() { 	int* arr = (int*)malloc(sizeof(int) * 5);     //开辟5个大小为整型的空间     //返回类型强制转换为(int*) 	if (arr == NULL) 	{ 		perror("malloc fail:");//如果开辟失败则打印错误信息 		return 1; 	} 	//存储数据 	for (int i = 0; i < 5; i++) 	{ 		arr[i] = i; 	} 	//打印数据 	for (int i = 0; i < 5; i++) 	{ 		printf("%d ", arr[i]); 	}  	return 0; }

输出结果为:

2.2 calloc

(1)calloc的用法

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

函数原型为:

void* calloc (size_t num, size_t size);

参数:

num:指定要分配的元素数量。

size:指定每个元素的大小。

返回值:

返回一个指向分配的内存块的指针。返回值为void*

但与malloc函数不同的是:calloc会将申请的空间中的每个字节都初始化为0

由此我们得知:calloc不仅分配内存,还负责初始化内存,这是它与malloc的一个重要区别。

(2)calloc的使用
#include    #include  int main() { 	int* arr = (int*)calloc(5, sizeof(int)); 	//开辟5个大小为整型的空间 	//返回类型强制转换为(int*) 	if (arr == NULL) 	{ 		perror("calloc fail:");//打印错误信息 		return 1; 	} 	return 0; }

我们通过监视窗口来观察一下:

2.3 realloc

(1)realloc的用法

realloc函数用于重新分配之前已经分配的内存块的大小。

函数原型为:

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

参数:

ptr:指向已分配的内存块的指针。如果ptr为NULL,则realloc的行为与malloc相同,分配一块新的内存空间。

size:新的内存块的大小(以字节为单位)。

返回值:

该函数返回一个指针 ,指向重新分配大小的内存。如果请求失败,则返回 NULL。

这个函数在调整内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

realloc在调整内存空间之后存在两种情况:

情况1:本地扩容

原有空间之后有足够大的空间,直接在原有内存之后直接追加空间原有的数据不发生变化。

情况2:异地扩容

原有空间之后没有足够大的空间时,在堆空间上另外找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址

(2)realloc的使用

由于在扩展空间时会出现两种情况,因此我们使用realloc的时候就需要注意,避免出错。

int main() { 	int* arr = (int*)malloc(sizeof(int) * 10); 	//开辟十个大小为整型的空间 	//返回类型强转为int* 	if (arr == NULL)//如果开辟失败 	{ 		perror("malloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	//继续新增空间 	int* tmp = (int*)realloc(arr, sizeof(int) * 15); 	//不用arr是为了防止开辟失败,被至为NULL 	if (tmp == NULL)//如果开辟失败 	{ 		perror("realloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	arr = tmp; 	return 0; }

如果新增内存较小时一般是在原有基础上新增空间。两者地址相同

如果我们把开辟的空间比较大:

int* tmp = (int*)realloc(arr, sizeof(int) * 150);

新增内存较大时则会重新开辟一段空间,将原来的空间释放。两者地址不同。

3.动态内存释放函数

1.free函数

(1)free的用法

C语言提供了另外一个函数free,专门用于做动态内存的释放的回收。

函数原型为:

void free (void* ptr);

参数:

ptr: 指针指向一个要释放内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果传递的参数是一个空指针,则不会执行任何动作。

作用:

释放之前调用 calloc、malloc 或 realloc 所分配的内存空间。

返回值:

该函数不返回任何值。

(2)free的使用

代码如下:

int main() { 	int* arr = (int*)malloc(sizeof(int) * 10); 	//开辟十个大小为整型的空间 	//返回类型强转为int* 	if (arr == NULL)//如果开辟失败 	{ 		perror("malloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	//继续新增空间 	int* tmp = (int*)realloc(arr, sizeof(int) * 150); 	//不用arr是为了防止开辟失败,被至为NULL 	if (tmp == NULL)//如果开辟失败 	{ 		perror("realloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	arr = tmp; 	free(arr); 	arr = NULL; 	return 0; }

常见的内存分布

C语言中的内存分布主要可以分为以下几个区域,这些区域由操作系统进行管理,用于存储不同类型的数据和代码。

1.栈区:用于存储函数内的局部变量、函数参数以及函数调用的返回地址等信息。

2.堆区:用于存储程序运行时动态分配的内存。

3.静态区:用于存储全局变量和静态变量(包括静态局部变量和全局静态变量)。

常见的动态内存的错误

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

void test() { 	int* p = (int*)malloc(INT_MAX / 4); 	*p = 20; //如果p的值是NULL,就会有问题 	free(p); }

1.INT_MAX是一个宏定义,他表示整型的最大值,值为2147483647

2.malloc申请的空间太大导致内存开辟失败,失败返回NULL

3.系统无法访问到NULL指针指向的地址,这时编译器就会报警告

我们要对代码进行修改:

void test() {     int* p = (int*)malloc(INT_MAX / 4);     if (NULL == p)     {         perror("malloc fail: ");//打印错误信息         return 1;     }     *p = 20;     free(p);     p = NULL; }

这就体现出判断空指针的重要性了

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

void test() { 	int i = 0; 	int* p = (int*)malloc(10 * sizeof(int)); 	if (NULL == p) 	{ 		perror("malloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	for (i = 0; i <= 10; i++) 	{ 		*(p + i) = i; //当i是10的时候越界访问 	} 	free(p);     p=NULL; }

malloc开辟的空间只有10个,但是for循环循环了十一次,这时就会越界访问。

修改方法:

void test() { 	int i = 0; 	int* p = (int*)malloc(10 * sizeof(int)); 	if (NULL == p) 	{ 		perror("malloc fail: ");//打印错误信息 		return 1;//直接返回 	} 	for (i = 0; i < 10; i++) 	{ 		*(p + i) = i;  	} 	free(p); 	p = NULL; } 

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

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

1.free函数只能释放有动态内存开辟在堆上的空间。

2.p开辟的空间在静态区,free函数无法释放。

修改方法:

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

动态内存开辟的空间不需要释放

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

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

1.p++跳过了一个整型大小的空间。

2.free函数释放p只会释放当前位置开始之后的空间,这导致有一个整型大小的空间未被释放,导致内存泄漏。

修改方法:

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

我们要注意不能随意改变p指向的位置,开辟多少内存就释放多少

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

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

1.p已经被释放归还给操作系统,但是此时p还指向该内存,是一个野指针。

2.再次释放p就会出现内存出错问题。

修改方法:

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

6.动态开辟内存没有释放

void test() { 	int* p = (int*)malloc(100); 	if (NULL != p) 	{ 		*p = 20; 	}//内存泄漏 } 

忘记释放不再使用的动态开辟的空间会导致内存泄漏

修改方法:

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

动态开辟的内存一定要释放,并且正确释放

动态内存经典笔试题分析

1.题目一

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

我们来分析一下:

1.空间非法访问:

使用传值调用时,形参只是实参的临时拷贝,对形参的改变无法影响实参,这时str仍是空指针,而strcpy拷贝会对空指针进行解引用操作,对NULL指针解引用会出错。

2.空间内存泄漏:

在GetMemory()函数内部动态申请了100字节的空间,因为p随着函数结束而被销毁,所以已经再也找不到该空间,会造成内存泄漏。

修改方法:

1.我们要使用传址调用,str本身是指针变量,传递指针变量的地址需要使用二级指针。

2.释放申请的空间

代码如下:

void GetMemory(char** p) { 	*p = (char*)malloc(100); }   void Test(void) { 	char* str = NULL; 	GetMemory(&str); 	strcpy(str, "hello world"); 	printf("%s\n",str); 	free(str); 	str = NULL; }

2.题目二

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

我们来分析一下:

等GetMemory函数返回后,使用str访问p数组,这是非法访问:局部变量离开作用域就会返还给操作系统,此时str就会变成野指针。

修改方法:

我们可以把p的值放在静态区,可以使用static或者常量字符串。

代码如下:

const char* GetMemory1(void) { 	const char* p = "hello world"; 	return p; } char* GetMemory2(void) { 	static char p[] = "hello world"; 	return p; }  void Test(void)  {     const char* str = NULL;      str = GetMemory1();     printf("%s\n", str);     str = GetMemory2();     printf("%s\n", str); }

3.题目三

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

分析:

又是十分经典的问题:内存没有释放。

修改方法:

把开辟的空间及时释放。

代码如下:

void Test(void) { 	char* str = NULL; 	GetMemory(&str, 100); 	strcpy(str, "hello"); 	printf("%s\n",str); 	free(str); 	str = NULL; }

4.题目四

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

分析:

str所开辟的空间已经归还给了操作系统,这时再将world拷贝进str就会出错。

修改方法:

归还内存之后随手将其值为NULL指针。

代码如下:

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

柔性数组

1.什么是柔性数组

在C99中,结构体中的最后一个元素允许是位置大小的数组,这就是【柔性数组】成员

举个例子:

typedef struct st_type { 	int i; 	int a[0];	//柔性数组成员 }type_a;

有些编译器会报错,可以改成:

typedef struct st_type { 	int i; 	int a[];	//柔性数组成员 }type_a;

2.柔性数组的特点

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

例如:

typedef struct st_type { 	int i; 	int a[0];	//柔性数组成员 }type_a;  int main() { 	printf("%d\n", sizeof(type_a)); 	return 0; }

输出结果为:4

3.柔性数组的使用

// 代码1 typedef struct st_type  {     int i;     int a[];  } type_a;  int main()  {     int i = 0;     type_a* p = malloc(sizeof(type_a) + 100 * sizeof(int));      if (p == NULL)      {         return;      }     p->i = 100;     for (i = 0; i < 100; i++)      {         p->a[i] = i;      }     free(p);      return 0; } 

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。

4.柔性数组的优势

我们来看这段代码:

// 代码2 typedef struct st_type  {     int i;     int* p_a; } type_a;  int main()  {     type_a* p = (type_a*)malloc(sizeof(type_a));     if (p == NULL)     {         perror("malloc fail:");         return;     }     p->i = 100;     p->p_a = (int*)malloc(p->i * sizeof(int));     if (p->p_a == NULL)     {         perror("malloc fail:");         return;     }     for (int i = 0; i < 100; i++)     {         p->p_a[i] = i;     }      free(p->p_a);     p->p_a = NULL;     p = NULL;     return 0; } 

代码1 和 代码2 都能实现同样的功能,但是 代码1 的实现有两个好处:

1.方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,容易造成内存泄漏。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

2.有利于访问速度

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

结束语

求点赞收藏加关注!!!十分感谢各位的支持!!!

相关内容

热门资讯

终结者2PC服务器究竟指的是什... 终结者2 PC服务器是指用于运行《终结者2》这款电子游戏的电脑服务器。这种服务器通常配置较高,能够处...
当DNS服务器未响应时,我们该... DNS服务器未响应意味着您的设备无法连接到互联网的域名系统(DNS)服务器,这通常是由于网络连接问题...
服务器持续运行背后的驱动因素是... 服务器正在运行中表示该服务器的操作系统和相关服务已经启动并处于工作状态。这通常意味着服务器可以处理请...
服务器在机房中的作用与电脑的哪... 机房服务器在功能上相当于电脑中的CPU和内存,是处理数据和存储信息的核心部件。它负责执行程序、处理请...
如何通过微信登录秀米账号? 用微信登录秀米账号是指通过微信这一第三方平台,使用微信账号信息来快速注册或登录到秀米这个平台上。这通...
物理应用服务器的确切位置在哪里... 物理应用服务器地址是指用于托管物理应用的服务器的网络位置。这个地址通常由IP地址或域名表示,使得用户...
如何在国内选择性价比较高的服务... 国内服务器价格因配置、服务商和需求而异,一般中等配置如双核CPU、4GB内存、50GB硬盘的云服务器...
如何将纸张巧妙地连接在一起? 纸张通常通过粘合剂(如胶水、胶带或订书钉)连接在一起。在装订书籍时,还可能使用线装或胶装的方法。文件...
为什么发送到手机的文档会出现缺... 可能是由于文档格式不兼容或者手机屏幕尺寸限制导致的。建议尝试使用不同的应用程序或软件打开文档,或者将...
服务器主机发出巨大噪音,是什么... 服务器主机发出响声可能是由于风扇故障、硬盘损坏或电源问题。建议检查风扇是否正常运转,硬盘是否有异常声...