【C语言】动态内存管理 [进阶篇_ 复习专用]

💛 前情提要💛

本章节就进入C语言的核心:深度剖析C语言动态内存管理

接下来我们即将进入一个全新的空间,对代码有一个全新的视角~

以下的内容一定会让你对C语言有一个颠覆性的认识哦!!!

以下内容干货满满,跟上步伐吧~


作者介绍:

🎓 作者: 热爱编程不起眼的小人物🐐
🔎作者的Gitee:代码仓库
📌系列文章推荐:

  1. 《刷题特辑》—实现由小白至入门者的学习记录🥰

  2. 实现Strcpy函数 - 通过函数发现 “程序之美” | 不断优化、优化、再优化~

  3. 【C语言】数据在内存中的存储_ [进阶篇_复习专用]

  4. 【C语言】字符函数&字符串函数&内存函数(上)[进阶篇_复习专用]

  5. 【C语言】字符函数&字符串函数&内存函数(下)[进阶篇_复习专用]

  6. 【C语言】自定义类型(结构体类型+枚举类型+联合体类型)[进阶篇_ 复习专用]

📒我和大家一样都是初次踏入这个美妙的“元”宇宙🌏 希望在输出知识的同时,也能与大家共同进步、无限进步🌟



💡本章重点

  • 动态内存分配

  • 动态内存函数

  • 常见错误&经典笔试题

  • 柔性数组


🍞一.动态内存分配


🥐Ⅰ.为什么存在动态内存分配

💡在我们已知的C语言对于内存空间的开辟有一个方法:申请栈区开辟开辟

👉如下:

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

特别注意:

对于上述的开辟空间的方式有两个特点:

  • 1️⃣空间开辟大小是固定

  • 2️⃣ 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配

综上:

  • 这种方式开辟的空间不够灵活

  • 有时候可能需要在程序运行的时候才知道所需的空间,那这种空间的开辟方式就不能满足了

➡️以下,便引出我们的动态内存开辟啦~


🍞二.动态内存函数


🥐Ⅰ.malloc和free

这里是引用

void* malloc (size_t  size);

💡malloc函数的作用: 向内存的堆区申请开辟一块连续可用的空间

  • 函数的参数: 想要申请的空间大小【单位:字节

  • 函数的返回值: 返回的是指向这块空间起始地址的指针,指针类型为void*

➡️函数工作原理:

  • 如果malloc 开辟空间成功,则返回一个指向开辟好空间起始地址的指针
  • 如果malloc开辟空间失败,则返回一个NULL指针

有了以上了解,我们可以得出三点:

  • 1️⃣malloc函数开辟的空间是在堆区上开辟的,且是连续的,这也就是为什么可以类似于访问数组成员的方式使用这块空间

  • 2️⃣malloc开辟成功时返回类型为void*,是因为malloc函数并不知道开辟空间的类型,所以具体类型在使用的时候自己来决定,去强制转换成什么样的类型

  • 3️⃣malloc开辟失败时返回值为NULL,所以我们需要对返回值做出检查,以防非法访问内存的现象出现

💥特别注意:

  • malloc函数不允许开辟0字节的空间,因为这个属于标准未定义,是无意义的

函数存储的位置:

这里是引用

接下来,我们再介绍一下free函数,便可以开始使用动态内存开辟啦~


🥐Ⅱ.free函数

这里是引用

void free (void* ptr);

💡free函数的作用: 用来做动态内存的释放回收

  • 函数的参数: 输入指向想要释放的空间的起始地址的指针

  • 函数的返回类型: void*

➡️函数工作原理:

  • 将传入的指针所指向的那块空间释放掉

有了以上了解,我们可以得出如下结论:

  • ➡️想要释放指针所指向的那块空间,指针必须是指向空间的起始地址,否则会产生如下问题:

    1. 若指针丢失了开辟的空间的起始地址【Eg:指向的是空间中的其它地址……】,释放此空间时会造成内存泄露(即释放的空间不干净)

    2. 若没有对开辟的空间进行及时的释放,则也会造成 内存泄露的问题,那这块空间就会一直占用着内存,浪费空间

💥特别注意:

  • 1️⃣如果参数指向的空间不是动态开辟的,那free函数的行为是未定义的

  • 2️⃣如果参数NULL指针,则函数什么事都不做

综上: 对于动态开辟的空间,使用完后记得及时释放~


🥐Ⅲ.malloc和free的使用

💡示例如下:

#include <stdio.h>

int main()
{
	//假设开辟10个整型的空间
	//10*sizeof(int)
	
	//动态内存开辟
	int* ptr = (int*)malloc(10 *sizeof(int));
	//判断ptr指针是否为空
	if(ptr == NULL)
	{
		perror("main");
		return 1;
		//直接结束程序
	}
	else
	{
		//使用
		int i = 0;
		for(i=0; i<10; i++)
		{
		//初始化开辟的内存为0
			*(ptr+i) = 0;
		}
	}
	
	free(ptr);//释放ptr所指向的动态内存
	
	ptr = NULL;//是否有必要?
	
	return 0;
}

❗由上,同学们觉得下面这行代码有必要吗?

ptr = NULL;//是否有必要?

➡️答案是:有必要的,因为:

  • 当这个指针所指向的空间被释放后,我们应该主动的将这个指针置为NULL,以防止非法访问内存

💥特别注意:

这里是引用

👉堆区的空间也不是无限制开辟的,是有限的


🥐Ⅳ.calloc函数

这里是引用

void* calloc (size_t num, size_t size);

💡calloc函数的作用:malloc函数一样,用来动态内存分配

  • 函数的参数:

    1. num为想要开辟地空间个数

    2. size为想要开辟的单个空间的大小【单位:字节

  • 函数的返回值: 返回的是指向这块空间起始地址的指针,指针类型为void*

➡️函数工作原理:

  • 1️⃣与malloc一样,且传参的时候比malloc函数更加具体,将两个参数分开来填写

  • 2️⃣而且会将开辟好的这块空间全部初始化为0

综上,我们可以得出:

  • 与函数malloc的区别只在于calloc会在返回地址之前,把申请的空间的每个字节初始化为全0

👉举个例子:

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

	if (p == NULL)
	{
		return 1;
	}

	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + 1));
	}

	free(p);

	p = NULL;

	return 0;
}

这里是引用


🥐Ⅴ.realloc函数

这里是引用

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

💡realloc函数的作用: 再原有动态内存开辟的空间基础上,实现对动态开辟内存大小的调整

  • 函数的参数:

    1. ptr为要调整动态内存开辟的内存地址

    2. size为调整之后的新大小

  • 函数的返回值: 返回的是调整之后指向这块空间起始地址的指针,指针类型为void*

➡️函数工作原理:

  • 1️⃣realloc函数的出现让动态内存管理更加灵活

  • 2️⃣realloc函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到的空间

综上,我们可以得出点:

realloc在调整内存空间的是存在两种情况:

  • 原有空间之后有足够大的空间:realloc便会将扩大的空间在原有空间的基础上,向后追加空间,原来空间的数据不发生变化

在这里插入图片描述

  • 原有空间之后没有足够大的空间: 在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。【原有的数据会存储在新的空间上,且原有的空间会被释放

这里是引用

特别注意:

  • 极度不建议拿原有的指针去接收新调整过的空间

    • 因为开辟失败,返回的是NULL指针,赋给原有指针的话,会把原有的空间地址给覆盖,那原来那块空间便会丢失,找不到 ,会造成内存泄漏的问题

🥯Ⅵ.总结

✨综上:就是开辟动态内存函数啦~


🍞三.常见的动态内存错误


🥐Ⅰ.对NULL指针的解引用操作

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

❗会造成非法访问内存的问题

✨为了防止这种问题出现,应对动态内存开辟后的返回值作判断先


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

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

❗会造成非法访问内存的问题

✨为了防止这种问题出现,应对谨慎对照着使用空间


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

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

❗该行为是未定义

✨为避免,可简单理解为free动态内存开辟来搭配使用即可


🥐Ⅳ.使用free释放一块动态开辟内存的一部分or忘记释放

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

❗该行为会造成释放不完全,形成内存泄露的问题

✨所以未避免这种问题,应避免对指针的地址进行操作


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

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

❗会造成程序报错

重复释放的操作就完全没意义啦~


🍞Ⅳ.经典的笔试题


🥐Ⅰ.试题一

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}

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

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

💡以上程序共犯如下错误:

  • 1️⃣str传给GetMemory函数的时候,是值传递,所以GetMemory函数的形参pstr的一份临时拷贝,所以在函数内部申请的空间的返回值并没有真正赋值给str

  • 2️⃣因为str还是NULL,所以strcpy会执行失败

  • 3️⃣当GetMemory函数返回之后,形参p销毁,使得动态内存开辟的100个字节的空间存在内存泄露,无法释放

为避免这种错误,可以:

  • 在传参的时候传&str,然后形参部分用二级指针接收即可

🥐Ⅱ.试题二

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

void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

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

💡以上程序共犯如下错误:

  • GetMemory函数内部创建的数组是在栈区上创建的出了函数,结束函数时,p数组的空间就还给操作系统,返回的地址是没有意义的,如果通过返回的地址去访问内存,就是非法访问内存

🥐Ⅲ.试题三

void GetMemory(char **p, int num)
{
	*p = (char *)malloc(num);
}

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

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

💡以上程序共犯如下错误:

  • 没有对内存开辟的空间进行释放

为避免这种错误,可以:

  • 对申请的空间进行释放,并手动对指针置为NULL

🥐Ⅳ.试题四

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

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

💡以上程序共犯如下错误:

  • 内存被提前释放,造成非法访问内存的情况

为避免这种错误,可以:

  • 释放完内存后,对指针认为置为NULL,这样就访问不到了

🍞Ⅳ.内存布局

这里是引用

1. 栈区(stack):

  • 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。
  • 栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  • 栈区主要存放运行函数而分配的局部变量函数参数返回数据返回地址等。

2. 堆区(heap):

  • 一般由程序员分配释放; 若程序员不释放,程序结束时可能由OS回收 。
  • 分配方式类似于链表

3. 数据段(静态区)(static):

  • 存放全局变量静态数据
  • 程序结束后由系统释放

4. 代码段:

  • 存放函数体类成员函数全局函数)的二进制代码

🍞Ⅴ.柔性数组


🥐Ⅰ.柔性数组的概念

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

💡可表示为:

  • 1️⃣
struct S
{
	int n;
	//其余成员变量
	//...
	int arr[]; //大小是未知的
};
  • 2️⃣
struct S
{
	int n; 
	//其余成员变量
	//...
	int arr[0]; //大小是未知的
};


🥐Ⅱ.柔性数组的特点

特别注意:

  • 1️⃣结构中的柔性数组成员前面必须至少一个其他成员

  • 2️⃣sizeof返回的这种结构大小不包括柔性数组的内存大小

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

👉Eg:

  • 1️⃣
typedef struct st_type
{
	int b;
	int a[0];//柔性数组成员
}type_a;

int main()
{
	printf("%d\n", sizeof(type_a));
	//空间大小为:4字节
	return 0;
}
  • 2️⃣
typedef struct st_type
{
	char d;
	int b;
	int a[0];//柔性数组成员
}type_a;

int main()
{
	printf("%d\n", sizeof(type_a));
	//空间大小为:8字节
	return 0;
}

💡由上述的两个例子我们得出:

  • 柔性数组成员外的其余成员变量,都遵循着结构体的内存规则:内存对齐

  • 即使给了柔性数组成员空间大小,也依然不会计算其空间


🥐Ⅲ.柔性数组的使用

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

int main()
{
	//开辟
	type_a* str = 
(type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
	
	//使用
	//...

	//释放
	free(str);
	str = NULL;

	return 0;
}

由上述我们可得知:

  • malloc所开辟的空间是专门为柔性数组成员申请的,其中:
    • 1️⃣malloc里的+号前面的是给其余成员变量开辟空间的【遵循内存对齐,所以为了避免计算直接用sizeof

    • 2️⃣+号后面的是给柔性数组成员开辟的空间【不用遵循内存对齐,所以可以直接按需开辟

综上: 就给柔性数组成员开辟了100个整型元素的连续空间,这也是为什么柔性数组使得结构体更加灵活啦~


🥐 Ⅳ.柔性数组的优势

👉同学们可以比较以下两组代码,看看有什么不同:

1️⃣柔性数组这里是引用

2️⃣指针模拟柔性数组
这里是引用

我们不难发现: 上述 代码1代码2可以完成同样的功能,但是方法1的实现有两个好处

  • 1️⃣方便内存释放

    • 代码1可直接由一个指针释放整个结构体

    • 代码2需要释放两次:一个是释放结构体,另外一个释放成员变量指针

    • 【且需要按顺序释放,先释放内部,再释放外部

  • 2️⃣这样有利于访问速度:

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

🥯Ⅴ.总结

✨综上:就是柔性数组的内容啦~

➡️相信大家对动态内存开辟有不一样的看法了吧🧡


🫓总结

综上,我们基本了解了C语言中的 “动态内存管理” 🍭 的知识啦~~

恭喜你的内功又双叒叕得到了提高!!!

感谢你们的阅读😆

后续还会继续更新💓,欢迎持续关注📌哟~

💫如果有错误❌,欢迎指正呀💫

✨如果觉得收获满满,可以点点赞👍支持一下哟~✨
在这里插入图片描述

  • 115
    点赞
  • 137
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 127
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Dream-Y.ocean

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

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

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

打赏作者

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

抵扣说明:

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

余额充值