前情提要
本章节就进入C语言的核心:深度剖析C语言动态内存管理
接下来我们即将进入一个全新的空间,对代码有一个全新的视角~
以下的内容一定会让你对C语言有一个颠覆性的认识哦!!!
以下内容干货满满,跟上步伐吧~
作者介绍:
作者: 热爱编程不起眼的小人物
作者的Gitee:代码仓库
系列文章推荐:
《刷题特辑》—实现由小白至入门者的学习记录
实现Strcpy函数 - 通过函数发现 “程序之美” | 不断优化、优化、再优化~
【C语言】数据在内存中的存储_ [进阶篇_复习专用]
【C语言】字符函数&字符串函数&内存函数(上)[进阶篇_复习专用]
【C语言】字符函数&字符串函数&内存函数(下)[进阶篇_复习专用]
【C语言】自定义类型(结构体类型+枚举类型+联合体类型)[进阶篇_ 复习专用]
我和大家一样都是初次踏入这个美妙的“元”宇宙 希望在输出知识的同时,也能与大家共同进步、无限进步
导航小助手
- 本章重点
- 一.动态内存分配
- Ⅰ.为什么存在动态内存分配
- 二.动态内存函数
- Ⅰ.malloc和free
- Ⅱ.free函数
- Ⅲ.malloc和free的使用
- Ⅳ.calloc函数
- Ⅴ.realloc函数
- Ⅵ.总结
- 三.常见的动态内存错误
- Ⅰ.对NULL指针的解引用操作
- Ⅱ.对动态开辟空间的越界访问
- Ⅲ.对非动态开辟内存使用free释放
- Ⅳ.使用free释放一块动态开辟内存的一部分or忘记释放
- Ⅴ.对同一块动态内存多次释放
- Ⅳ.经典的笔试题
- Ⅰ.试题一
- Ⅱ.试题二
- Ⅲ.试题三
- Ⅳ.试题四
- Ⅳ.内存布局
- Ⅴ.柔性数组
- Ⅰ.柔性数组的概念
- Ⅱ.柔性数组的特点
- Ⅲ.柔性数组的使用
- Ⅳ.柔性数组的优势
- Ⅴ.总结
- 总结
动态内存分配
动态内存函数
常见错误&经典笔试题
柔性数组
在我们已知的C语言对于内存空间的开辟有一个方法:申请栈区开辟开辟
如下:
int a = 20;
//在栈空间上开辟四个字节
char arr[10] = {0};
//在栈空间上开辟10个字节的连续空间
❗特别注意:
对于上述的开辟空间的方式有两个特点:
1️⃣空间开辟大小是固定
的
2️⃣ 数组在申明的时候,必须指定
数组的长度,它所需要的内存在编译时分配
✨综上:
这种方式开辟的空间不够灵活
有时候可能需要在程序运行的时候才知道所需的空间,那这种空间的开辟方式就不能满足了
➡️以下,便引出我们的动态内存
开辟啦~
void* malloc (size_t size);
malloc
函数的作用: 向内存的堆区
申请开辟一块连续可用
的空间
函数的参数: 想要申请的空间大小【单位:字节
】
函数的返回值: 返回的是指向这块空间起始地址的指针,指针类型为void*
➡️函数工作原理:
malloc
开辟空间成功
,则返回一个指向开辟好空间起始地址
的指针malloc
开辟空间失败
,则返回一个NULL
指针❗有了以上了解,我们可以得出三点:
1️⃣malloc
函数开辟的空间是在堆区
上开辟的,且是连续
的,这也就是为什么可以类似于访问数组成员的方式使用这块空间
2️⃣malloc
开辟成功时返回类型为void*
,是因为malloc
函数并不知道开辟空间的类型,所以具体类型在使用的时候自己来决定,去强制转换成什么样的类型
3️⃣malloc
开辟失败时返回值为NULL
,所以我们需要对返回值做出检查,以防非法访问内存
的现象出现
特别注意:
malloc
函数不允许开辟0
字节的空间,因为这个属于标准未定义
,是无意义的✨函数存储的位置:
接下来,我们再介绍一下free
函数,便可以开始使用动态内存开辟
啦~
void free (void* ptr);
free
函数的作用: 用来做动态内存的释放
和回收
函数的参数: 输入指向想要释放的空间的起始地址的指针
函数的返回类型: void*
➡️函数工作原理:
❗有了以上了解,我们可以得出如下结论:
➡️想要释放指针所指向的那块空间,指针必须是指向空间的起始地址
,否则会产生如下问题:
若指针丢失了开辟的空间的起始地址【Eg:指向的是空间中的其它地址……】,释放此空间时会造成内存泄露
(即释放的空间不干净)
若没有对开辟的空间进行及时的释放,则也会造成 内存泄露
的问题,那这块空间就会一直占用着内存,浪费空间
特别注意:
1️⃣如果参数
指向的空间不是动态开辟的,那free函数的行为是未定义的
2️⃣如果参数
是NULL
指针,则函数什么事都不做
✨综上: 对于动态开辟的空间,使用完后记得及时释放
~
示例如下:
#include
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
,以防止非法访问内存
特别注意:
堆区
的空间也不是无限制开辟的,是有限的
void* calloc (size_t num, size_t size);
calloc
函数的作用: 与malloc
函数一样,用来动态内存分配
函数的参数:
num
为想要开辟地空间个数
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;
}
void* realloc (void* ptr, size_t size);
realloc
函数的作用: 再原有动态内存开辟的空间基础上,实现对动态开辟内存大小的调整
函数的参数:
ptr
为要调整动态内存开辟的内存地址
size
为调整之后的新大小
函数的返回值: 返回的是调整之后
指向这块空间起始地址的指针,指针类型为void*
➡️函数工作原理:
1️⃣realloc
函数的出现让动态内存管理更加灵活
2️⃣realloc
函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新
的空间
❗综上,我们可以得出点:
realloc
在调整内存空间的是存在两种
情况:
realloc
便会将扩大的空间在原有空间的基础上,向后追加空间,原来空间的数据不发生变化释放
】❗特别注意:
极度不建议拿原有的指针去接收新调整过的空间
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);
}
❗会造成非法访问内存
的问题
✨为了防止这种问题出现,应对谨慎对照着使用空间
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
❗该行为是未定义
的
✨为避免,可简单理解为free
与动态内存开辟
来搭配使用即可
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函数的形参p
是str
的一份临时拷贝,所以在函数内部申请的空间的返回值并没有真正赋值给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 中,结构中的
最后一个元素
允许是未知大小
的数组,这就叫做柔性数组
成员
可表示为:
struct S
{
int n;
//其余成员变量
//...
int arr[]; //大小是未知的
};
struct S
{
int n;
//其余成员变量
//...
int arr[0]; //大小是未知的
};
❗特别注意:
1️⃣结构中的柔性数组成员
前面必须至少一个
其他成员
2️⃣sizeof
返回的这种结构大小不包括
柔性数组的内存大小
3️⃣包含柔性数组成员
的结构用malloc
函数进行内存的动态分配,并且分配的内存应该大于
结构体的大小,以适应柔性数组的预期大小
Eg:
typedef struct st_type
{
int b;
int a[0];//柔性数组成员
}type_a;
int main()
{
printf("%d\n", sizeof(type_a));
//空间大小为:4字节
return 0;
}
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
的实现有两个好处
:
1️⃣方便内存释放:
代码1
可直接由一个指针释放整个结构体
但代码2
需要释放两次:一个是释放结构体
,另外一个释放成员变量指针
【且需要按顺序释放,先释放内部
,再释放外部
】
2️⃣这样有利于访问速度:
✨综上:就是柔性数组的内容啦~
➡️相信大家对动态内存开辟
有不一样的看法了吧
综上,我们基本了解了C语言中的 “动态内存管理” 的知识啦~~
恭喜你的内功又双叒叕得到了提高!!!
感谢你们的阅读
后续还会继续更新,欢迎持续关注哟~
如果有错误❌,欢迎指正呀
✨如果觉得收获满满,可以点点赞支持一下哟~✨