C语言进阶第九课 --------动态内存管理

作者前言

🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂
​🎂 作者介绍: 🎂🎂
🎂 🎉🎉🎉🎉🎉🎉🎉 🎂
🎂作者id:老秦包你会, 🎂
简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂
喜欢学习C语言和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂
🎂个人主页::小小页面🎂
🎂gitee页面:秦大大🎂
🎂🎂🎂🎂🎂🎂🎂🎂
🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂


动态内存的意义

在我们空间的开辟有定义变量

#include<stdio.h>
int main()
{
	int a = 0;
	char arr[] = "sdsd";
	printf("%p\n", &a);
	printf("%p\n", arr);

	return 0;
}

甚至还有局部变量都会向内存申请空间,但是这些空间不擦长久,或者很难随意操作这些空间进行销毁,或者不能随意扩大空间
但是上述的开辟空间的方式有两个特点:

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

动态内存函数的介绍

malloc

在这里插入图片描述
这个函数的动态内存开辟函数,
size_t是一个无符号整数类型,表示需要分配的内存大小(以字节为单位)。malloc函数会在堆上分配一块指定大小的内存,并返回指向该内存块的指针。如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = (int*)malloc(sizeof(int) * INT_MAX);
	if (arr == NULL)
	{
		perror("malloc:");
		return 1;
	}
	return 0;
}

可能有一些人会利用malloc申请0个字节,这种做法本身没有错误,但是这个就是脱裤子放屁多此一举
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
在这里插入图片描述

free

有心细的小可爱就会发现,当我们程序运行结束就会释放出开辟的动态内存(被动),如果我们要自己释放就要用到free函数
在这里插入图片描述
free是C语言中用于释放动态分配内存的函数。当我们使用malloc、calloc等函数在程序运行期间动态申请内存时,需要在使用完毕后使用free函数将其释放回系统,以便其他程序或进程可以使用。

其中,ptr是指向需要释放的内存块的指针。使用free函数可以将之前通过malloc、calloc等函数动态分配的内存块释放回系统,以避免内存泄漏和浪费。

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

	return 0;
}

需要注意的是,在使用free函数释放内存块时需要注意以下几点:

只能释放之前通过malloc、calloc等函数动态分配的内存块,不能释放栈上的局部变量和全局变量等静态分配的内存;
不能重复释放同一块内存,否则会导致程序崩溃;
释放内存后,应该将指针设置为NULL,以避免出现野指针问题。

calloc

在这里插入图片描述
calloc是C语言中用于动态分配内存并进行初始化的函数。与malloc函数相似,calloc也用于在程序运行时动态分配内存,但与malloc不同的是,calloc会将分配的内存块初始化为零,而malloc不会
其中,num表示需要分配的元素个数,size表示每个元素的大小。calloc函数会分配num个大小为size的连续内存块,并将其初始化为零。如果分配成功,则返回指向分配内存块的指针;如果分配失败,则返回NULL。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = (int*)calloc(10, sizeof(int));
	if (arr == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		arr[i] = i;

	}
	free(arr);
	arr = NULL;


	return 0;
}

realloc

realloc函数的出现让动态内存管理更加灵活,前面的malloc和calloc申请的空间无法避免申请空间过大或者过小,realloc函数就可以做到动态开辟内存的大小进行调整

realloc是C语言中用于重新分配内存的函数。在程序运行期间,我们有时需要重新分配内存块的大小,可以使用realloc函数来实现。realloc函数可以将之前分配的内存块的大小调整为新的大小,并返回指向新内存块的指针。
在这里插入图片描述
ptr是指向之前分配的内存块的指针,size是需要重新分配的内存块的大小。
如果重新分配成功,则返回指向新内存块的指针;如果分配失败,则返回NULL。需要注意的是,如果新的大小小于原来的大小,则realloc函数会截断原来的内存块;如果新的大小大于原来的大小,则realloc函数会在原来的内存块后面分配新的内存块。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = (int*)malloc(sizeof(int) * 10);
	int* arr1 = (int*)calloc(20, sizeof(int));
	if (arr == NULL)
	{
		perror("malloc");
	}
	if (arr1 == NULL)
	{
		perror("calloc");
	}
	//调整空间

	int *arr3 = (int*)realloc(arr, 2000 * sizeof(int));
	if (arr3 != NULL)
	{
		arr = arr3;
	}
	free(arr);
	free(arr1);
	arr = NULL;
	arr1 = NULL;
	arr3 = NULL;

	return 0;
}

realloc函数开辟的空间情况分两种
第一种:
在这里插入图片描述
之前malloc开辟空间的后面刚好满足扩大后的空间大小就会在原来的地址处重新扩大,这种情况返回的新地址就会和旧地址的值是一样的
在这里插入图片描述

第二种:

之前malloc开辟的空间的后面不能满足扩大后的空间大小,就会放弃在原来的空间扩大,反而会重新找一块能满足条件的内存进行开辟,一次性开辟好,会将旧的空间的数据就会拷贝到新的空间,旧的空间就会销毁
在这里插入图片描述

常见的动态内存错误

对NULL指针的解引用操作

这种情况的出现是因为没有对返回值进行判断造成的

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = malloc(sizeof(int) * INT_MAX);
	int i = 0;
	for (i = 0; i < INT_MAX; i++)
	{
		arr[i] = 1;
	}
	return 0;
}

如上面代码一样,没有判断是否创建成功,直接解引用操作就会报错
在这里插入图片描述

对动态内存的越界访问

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

这种情况就和我们创建数组,越界访问是一样的

对非动态开辟的内存进行free释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int a = 0;
	int* p = &a;
	free(p);
	p = NULL;
	return 0;
}

这种情况就是懵了

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

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = calloc(10, sizeof(int));
	int i = 0; 
	for (i = 0; i < 5; i++)
	{
		*arr = i;
		arr++;
	}
	free(arr);
	arr = NULL;
	return 0;
}

在这里插入图片描述

对一块空间多次释放

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
int main()
{
	int* arr = (int*)malloc(sizeof(int) * 10);
	if (arr == NULL)
	{
		perror("malloc");
		return 1;
	}
	free(arr);
	free(arr);
	return 0;
}

解决这个错误我们可以养成一个习惯,只要释放过的空间,对应的变量全部赋值为NULL,因为free释放NULL不会有啥错误

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

include <stdio.h>
#include <stdlib.h>
#include <limits.h>
void Other()
{
	int* arr = (int*)malloc(sizeof(int) * 10);
}
int main()
{
	Other();
	while (1)
	{
		;
	}
	return 0;
}

这样很容易造成空间不足,我们要记住谁申请,谁释放,如果没有释放交接任务也要告诉别人释放

经典笔试题

第一题

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

上面这些代码主要的错误就是当我们调用Test函数时,也会调用到 GetMemory函数,而传参传递的时变量str的值,而不是地址,最后str的值还是NULL,还造成了内存泄漏对NULL解引用就会报错,如果把 GetMemory函数更改为 GetMemory(char* *p ),然后传递str的地址
更改后:

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

	Test();
	return 0;
}

还要注意的printf函数的使用没有错,是可以这样写的,这个写法只针对字符串
第二题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();

	return 0;
}

这里的错误是GetMemory函数在调用结束后,p的空间销毁掉了,返回的值也就成了野指针
第三题

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.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);
}
int main()
{
	Test();
	return 0;
}

这道题主要是少了free释放,内存泄漏了
第四题

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

这里的错误就是野指针,在我们释放了动态开辟的内存,str还是指向那块地址,这样就会非法访问内存,这里我们要注意释放后一定要赋值NULL

C/C++中程序内存区域分布

在这里插入图片描述
其中代码段是用来存储代码翻译成的二进制指令只读常量(字符串常量),里面的数据不可更改
在这里插入图片描述
这个图就可以很明了的知道动态内存开辟的空间是在堆区,局部变量和形式参数的空间是在栈区m全局变量和静态变量的空间是在数据段(静态区)
还要注意的是函数的空间也是在栈上创建的,函数栈帧的创建和销毁前面我已经写过,可以去看看
C/C++程序内存分配的几个区域:

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结
    束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是
    分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返
    回地址等。
  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分
    配方式类似于链表。
  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

柔性数组

这个概念是在C99引入的,之前是没有的
形成柔性数组的条件:

  1. 要在结构体中
  2. 是结构体的最后一个成员,并且这个数组是一个未知大小的数组(变长数组)

写法:

struct S
	{
		int a;
		int arr[];//柔性数组
	}a1;
	struct S1
	{
		int a;
		int arr[0];//柔性数组
	}a2;

代码中 的两个结构体属于两种写法,

柔性数组的特点

  1. 结构中的柔性数组成员前面必须至少一个其他成员。
  2. sizeof 返回的这种结构大小不包括柔性数组的内存。
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{
	int a;
	int arr[0];
};
int main()
{
	
	printf("%d", sizeof(struct S1);
	
	return 0;
}

在这里插入图片描述
可以看到sizeof返回的大小不包含柔性数组,假设结构体只有一个成员,而这个成员是柔性数组就会发现这个结构体的大小是0


3. 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{
	int a;
	int arr[0];
};
int main()
{
	struct S1* p = (struct S1*)malloc(sizeof(int) + 20);
	if (p == NULL)
	{
		perror("malloc");
		return;
	}
	p->a = 10;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		p->arr[i] = i;
	}
	free(p);
	p = NULL;
	return 0;
}

其实我们可以不使用柔性数组,我们也可以模拟出来

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include<string.h>
struct S1
{
	int a;
	int arr[0];
};
struct S2
{
	int a;
	int* arr;
};
int main()
{
	/*struct S1* p = (struct S1*)malloc(sizeof(int) + 20);
	if (p == NULL)
	{
		perror("malloc");
		return;
	}
	p->a = 10;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		p->arr[i] = i;
	}
	free(p);
	p = NULL;*/
	struct S2 a2;
	a2.a = 10;
	a2.arr = (int*)malloc(sizeof(int) * 4);
	if (a2.arr == NULL)
	{
		perror("malloc");
		return;
	}
	printf("%d", sizeof(struct S2));
	free(a2.arr);
	a2.arr = NULL;
	return 0;
}

使用这种方法虽然可以代替柔性数组,但是使用free的次数会增多,而使用柔性数组只需使用一次free

当我们使用malloc开辟的数次越多,产生的内存碎片就会越多,内存的利用率就会下降

使用柔性数组的好处

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

总结

在这里介绍了动态内存的开辟、常见错误柔性数组,有不懂的小可爱可以私聊

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

老秦包你会

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值