本期介绍🍖
主要介绍:C语言中一些大家熟知知识点中的盲区,这是第八期,主讲动态内存管理。
文章目录
- 1. 为什么会存在动态内存
- 2. 动态内存管理库函数
- 2.1 malloc函数
- 2.2 calloc函数
- 2.3 realloc函数
- 2.4 free函数
- 3. 内存泄漏&内存池
- 4. 常见的动态内存错误
- 4.1 对NULL指针解引用操作
- 4.2 对动态开辟空间越界访问
- 4.3 对非动态开辟内存使用free释放
- 4.4 使用free释放动态开辟内存的一部分
- 4.5 对同一块动态内存多次释放
- 4.6 动态开辟内存忘记释放(内存泄漏)
- 5. 柔性数组
- 5.1 什么是柔性数组
- 5.2 柔性数组的特点
- 5.3 柔性数组的应用
- 6. C/C++中程序内存区域划分
1. 为什么会存在动态内存
在学习动态内存管理前,已知的两种开辟内存空间的方式:1.int a;
(类型+变量名的方式),2.int arr[10];
(数组的开辟方式)。但这两种开辟方式有局限性,一旦开辟就无法再更改开辟空间的大小了。
举个例子:创建有一个有100个元素的结构体数组,数组每个元素都是一个人的信息。假如仅需要存放3个人的信息,对于这个结构体数组来说,剩余的97个空间不就浪费了嘛。同理假如需要存放120个人的信息,对于该数组来说是放不下的,又不能对其大小进行修改。
可见这两种对于空间的开辟方式是不灵活的,那么是否存在能自主管理空间的内存开辟方式?有了这种开辟方式,开辟的内存空间能够想大就大想小就小。C语言提供了这种方式:动态内存管理。在C语言中是通过下面这些库函数函数来实现的,malloc()
、calloc()
、realloc()
、free()
。值得注意的是,在使用这些库函数前需要引用头文件<stdlib.h>
。
2. 动态内存管理库函数
2.1 malloc函数
malloc()
是一个动态内存开辟函数。这个函数会向内存申请一块大小为size
的连续空间(单位:字节),并返回一个指向该空间起始位置处的地址(指针的类为void*
)。函数声明如下所示:
void* malloc(size_t size);
值得注意,如果参数size
为0
,这种malloc()
行为是标准未定义的,取决于编译器。如果malloc()
开辟内存空间失败,会直接返回NULL
,因此malloc()
的返回值一定要做检查。举例如下所示:
#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)malloc(sizeof(char) * 10);if(pc == NULL){perror("malloc");return 1;}//注意:在C语言中有一个习惯,return 0;表示正常返回,return 1;表示异常返回。//执行下面的代码//...return 0;
}
2.2 calloc函数
calloc()
是一个动态内存开辟函数。这个函数会向内存申请一块大小为num * size
的连续空间(单位:字节),然后把这块申请空间的每个字节初始化为0,最后返回一个指向该空间起始位置处的地址(指针的类为void*
)。函数声明如下所示:
void* calloc(size_t num, size_t size);
与malloc()
函数相同的是,calloc()
函数如果参数num*size
的结果为0
,这种calloc()
行为是标准未定义的,取决于编译器;如果calloc()
开辟内存空间失败,会直接返回NULL
,因此calloc()
的返回值一定要做检查。与malloc()
函数不同的是,calloc()
函数申请完空间后,会将空间全部初始化为0再返回。举例验证如下:
#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)calloc(20, sizeof(char));if (pc == NULL){perror("malloc");return 1;}//执行下面的代码//...return 0;
}
2.3 realloc函数
realloc()
函数是一个能够调整动态开辟内存大小的函数,也可以动态开辟空间,只是参数需要为NULL
。有时大家会发现过去申请的空间太小了,有时又会觉得申请的空间太大了。为了能够更有效的使用内存,就需要对申请空间的大小做灵活的调整,这时就需要用realloc()
函数了。函数类型声明如下:
void* realloc(void* ptr, size_t size);
ptr
:指向将要被调整大小的空间size
:调整后新的大小(单位:字节)
realloc()
函数调整内存空间时存在两种情况:
- 原有空间后有足够大的空间
在原空间的基础上直接向后追加空间,原空间内的数据不发生任何变化。调整前与调整后动态开辟内存的起始位置不发生任何变化,如下图所示:
- 原有空间后没有足够大的空间
向后寻找一个能够存放的下调整后大小的连续空间,然后将原空间中的数据拷贝到新空间中,最后将新开辟空间的起始地址返回。值得注意,realloc在开辟完新空间后,会自动释放掉原先的那块空间,不需要手动释放。如下图所示:
值得注意,若realloc()
函数调整大小成功,则会返回调整后内存空间起始位置的地址(类型为void*
);若调整失败,直接返回NULL
。如果直接用维护原空间的指针ptr
来接收调整后新空间的地址,必然会存在一种情况:当调整失败后,指向原空间的指针ptr
被赋值为NULL
(也就是说再也找不到原先那块动态开辟的内存空间了,也释放不掉那块空间,导致内存泄漏)。
所以一般使用realloc()
函数,会创建一个中间变量来接收函数的返回值。然后对其进行判断,如果不为NULL
再将其赋值给ptr
。举例如下所示:
#include<stdio.h>
#include<stdlib.h>
int main()
{//动态开辟空间char* pc = (char*)malloc(20 * sizeof(char));if (pc == NULL){perror("malloc");return 1;}//调整空间大小char* ptr = (char*)realloc(pc, 10);if (ptr != NULL){pc = ptr;}//执行下面的代码//...return 0;
}
2.4 free函数
free()
是专门用来回收或者说释放,动态内存开辟的空间的函数。其参数ptr
为那块动态开辟空间的起始地址。函数类型声明如下所示:
void free( void* ptr );
关于free()
函数,值得注意以下三点:
- 使用
free()
释放ptr
维护的那块空间时,free()
是不会将ptr
顺手置为NULL
#include<stdio.h>
#include<stdlib.h>
int main()
{char* pc = (char*)malloc(10 * sizeof(char));if (pc == NULL){perror("malloc");return 1;}//执行下面的代码//...free(pc);return 0;
}
由上图可知,ptr
指向的那块空间确实被释放了(也就是还给操作系统了),但是ptr
仍然指向那个位置,故此时的ptr
为野指针。所有大家应该养成一个习惯,在free()
释放完一块空间后,就将指针赋为NULL
。
- 如果参数
ptr
指向的空间不是动态开辟的,那free()
函数的行为是未定义的
- 如果参数
ptr
是NULL
指针,则函数什么事都不做
3. 内存泄漏&内存池
内存泄漏通俗点来说,就是你向内存申请了一块空间,用完后不还,别人想用还用不到,如果你一直不还,那么这块空间不就相当于不存在了,也就是理论上泄漏掉了。内存泄漏的影响:系统内存浪费、程序运行缓慢、甚至系统崩溃等严重后果。
纠正一件事,如果malloc()
申请空间后没有使用free()
释放,这种情况并不是内存泄漏。因为当程序结束时,操作系统会自动回收那块空间。除非那个程序一直运行不停下来,那么这时该程序就算是内存泄漏了。代码如下所示:
#include<stdio.h>
#include<stdlib.h>int main()
{int* ptr = (int*)malloc(sizeof(int) * 10);if (ptr == NULL){perror("malloc");return 1;}int i = 0;for (i = 0; i < 10; i++){*(ptr + i) = i + 1;}return 0;
}
值得注意,动态内存的开辟是在堆区上的。而在堆区上开辟空间,空间之间是有间隙(内存碎片)。如果在内存中频繁的使用malloc()
、calloc()
、realloc
开辟空间,就会使得内存中存在非常多的内存碎片,如果这些内存碎片在之后没能有效的利用的话,就会导致内存利用率和效率的下降。
而想要解决上述问题,就需要引用一个新概念内存池。那什么是内存池呢?内存池就是一种通过程序来维护内存空间的方法。是怎么实现的呢?首先我们会向内存申请一块相对来说能够满足我们当前需求的空间,然后程序内部用内存池的方式来维护这块空间,如此就不用应频繁的申请而打扰操作系统了,且解决了内存碎片的问题。
4. 常见的动态内存错误
4.1 对NULL指针解引用操作
由于使用malloc
或calloc
函数后没有对返回值进行判断所导致的。当malloc
或calloc
向内存申请空间失败后,会直接返回NULL
。如果没有对其进行判断直接上手使用,自然就会伴随着一定的风险。举个例子:
void test()
{int *p = (int *)malloc(INT_MAX/4);*p = 20;//如果p的值是NULL,就会有问题free(p);p = NULL;return 0;
}
4.2 对动态开辟空间越界访问
之前在学习数组、指针时经常会编写一些越界访问的程序,而如今对于动态开辟出来的空间自然也会出现,由于操作不挡而导致的越界访问(所谓的越界访问,就是访问的过程中访问了不属于我们的空间)。举例如下:
void main()
{int i = 0;int *p = (int *)malloc(10*sizeof(int));if(NULL == p){printf("%s\n", strerror(errno));return 1;}for(i=0; i<=10; i++){*(p+i) = i;//当i是10的时候越界访问}free(p);p = NULL;return 0;
}
4.3 对非动态开辟内存使用free释放
总是有些人喜欢一些奇奇怪怪的事,就譬如拿用来释放动态开辟空间的free
函数去释放静态开辟的空间,操作系统会立即报错。如下所示:
int main()
{int arr[10] = { 0 };int* p = arr;free(p);//是否可行?return 0;
}
4.4 使用free释放动态开辟内存的一部分
有时在使用维护动态开辟空间的指针来编写代码时,会在无意中移动这个指针,当想要用该指针释放动态内存时,该指针已然不指向该内存的起始位置,如此不就造成了部分内存释放了嘛。举个例子:
int main()
{int* ptr = (int*)malloc(40);if (ptr == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 5; i++){ptr++;}free(ptr);//ptr不再指向动态内存的起始位置ptr = NULL;return 0;
}
对于动态内存的部分释放操作系统是不被允许的,free()
必须是指向动态开辟空间的起始地址,这一点一定要注意了。
4.5 对同一块动态内存多次释放
int main()
{int* ptr = (int*)malloc(40);//...free(ptr);//...free(ptr);//重复释放return 0;
}
在C语言中同一块动态内存是不允许被多次释放的,只能被释放一次,不然就报错。为了避免这种情况,我们其实可以在每次free
释放完一块空间后立马将维护这块空间的指针置为NULL,这样下一次无意中再次对其释放就不会报错了,因为free(NULL);
是不做任何操作的。
4.6 动态开辟内存忘记释放(内存泄漏)
在不考虑程序结束时操作系统自动回收内存的情况下,下面的代码会导致内存泄漏。
void test()
{int* ptr = (int*)malloc(40);if (ptr == NULL){return ptr;}
}int main()
{test();return 0;
}
5. 柔性数组
5.1 什么是柔性数组
柔性数组是一种动态可变的数组,通过结构体实现,在C99标准底下支持的一种语法。想要使用柔性数组需要满足3个条件:
- 柔性数组只能存在于结构体内,且必须是结构体最后一个成员
- 柔性数组成员前,至少存在一个其他成员
- 数组的大小未定义
声明一个柔性数组,如下所示:
typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;
有些编译器会报错无法编译,可以改成如下形式:
typedef struct st_type
{int i;int a[];//柔性数组成员
}type_a;
5.2 柔性数组的特点
- 使用
sizeof
计算包含柔性数组的结构体的大小,得出的结果不包含柔性数组的大侠
- 包含柔性数组成员的结构体要用
malloc
函数来进行内存的动态分配,并且分配的内存应大于结构体的大小,以适应柔性数组的预期大小。如下所示:
#include<stdio.h>
#include<stdlib.h>typedef struct st_type
{int i;int a[0];//柔性数组成员
}type_a;int main()
{type_a* ptr = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));if (ptr == NULL){perror("malloc");return 1;}ptr->i = 100;int i = 0;for (i = 0; i < 100; i++){ptr->a[i] = i + 1;}free(ptr);ptr = NULL;return 0;
}
柔性数组就是以这种内存空间不断的变化,来使得整个数组拥有了动态的性能,某种意义上相当于该数组柔软可变的,所以称为柔性数组。内存布局如下所示:
&emps; 值得注意,若直接用这种包含柔性数组的结构体类型来创建变量,那么该结构变量相当于只有一个成员i
。原因是该类型结构体的大小不包含柔性数组,故在像内存申请空间时,自然也不会获得柔性数组的空间,所以该结构体变量相当于就只有一个成员i
。
5.3 柔性数组的应用
其实在实际应用中,完全可以用柔性数组结构体代替动态结构体,如下所示:
可以看出,使用柔性数组实现的结构体,一整个都是在堆区上开辟的,而且是一块连续的空间。而用结构体中包含指向动态内存的指针的方法,是分为两块不同的区域来开辟的,结构体是在栈区上申请的,指针维护的那块空间是在堆区上申请的。相比较这两种写法,柔性数组的好处是:有利于访问速度。
6. C/C++中程序内存区域划分
1. 内核空间:用户无法对这块空间进行读写,该空间是专门用来跑操作系统的。
2. 栈区:在调用函数时,函数调用空间、函数内局部变量、函数参数的存储单元都是在栈区上创建的,函数调用结束时这些存储单元自动被释放。
3. 堆区:一般由程序员自主的动态内存开辟和释放,若程序员不释放,程序结束时可能由OS回收。动态分配方式类似于链表。
4. 数据段:就是之前所说的静态区,主要用于存放全局数据、静态数据。
5. 代码段:用于存放代码经过编译链接后的二进制可执行程序和只读常量。
实际上普通的的局部变量是由放栈区分配的空间,而栈区的特点是在上面创建的变量出了作用域就自动销毁。而对于用static修饰的变量存放到了数据段(静态区),数据段的特点是在上面创建的变量,直到程序结束才会销毁,故生命周期长。而在堆区上开辟的空间特点是能够自主的调整其大小。
这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。