一、动态内存管理存在的真相
对于内存的开辟方式我们目前掌握的方式有两种:
int val = 58 //在栈空间开辟四个字节
char arr[20] = { 0 }; //在栈空间上开辟20个字节的连续空间
其实我们很清楚这两种开辟方式都有限制条件,比如:
①.空间开辟的大小是固定的
②.数组在申明的时候,必须指定数组的长度,他所需要的内存在编译的时候进行分配
但在某些情况下我们对空间的要求不仅仅只是上述情况.我们有时候并不知道我们所需要的空间大小,只有在程序运行的时候才能知道空间所需的具体大小
这个时候数组的编译时开辟空间就不能满足了,这时候就轮到动态内存开辟闪亮登场了!
二、动态内存函数的介绍
1、malloc和free
我们先来看malloc,这是C语言提供的一个动态内存开辟的函数,一般形式如下:
void* malloc(size_t size);
这个函数会向内存申请连续可用的空间,并返回一块连续可用的空间,并会犯指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数size是0,malloc的行为是标准为未定义的,取决于编译器。
说完了malloc我们来看看另外一个函数free,看这个名字就知道这个函数有释放的作用,它就是专门用来做内存动态的释放和回收的,函数的原型如下:
void free(void* ptr);
在通过free函数来使用动态开辟的空间是有两个注意事项:
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为就是未定义的。
- 如果参数ptr是NULL指针,则函数什么事都不做
**NOTE:**malloc和free都声明在stdlib.h头文件中
我们来看一个最简单的例子:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int i = 0;
scanf("%d", &a);
int arr[i] = {0};
int* tmp = NULL;
tmp = (int*)malloc(num*sizeof(int));
if(NULL != tmp) //判空操作
{
int n = 0;
for(n = 0; n < i, n++)
{
*(tmp + i) = 0;
}
}
free(tmp); //释放tmp所指向的动态内存
tmp = NULL;
return 0;
}
2.calloc
calloc可能有些人很少见到,但这也是C语言为我们提供的一个用来动态内存分配的函数,原型如下:
void* calloc(size_t num, size_t size);
- 该函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数malloc的区别只在于calloc会在返回地址之前把申请的空间每个字节初始化为全0。
这里我们就不举例说明了,通过参数大家就可以自己在编译器进行简单的使用
所以在我们对申请的内存空间的内容进行初始化时,使用calloc这个函数我们就可以很方便的完成。
3.realloc
上面我们了解开辟动态内存,释放动态内存,初始化内存的函数,那么可能有人就会问了,当我们在使用动态内存的时候如果发现我们之前开辟的空间过大或者过小的时候,我们有没有办法对其进行修改?当然是可以的,在这里我们就要请出realloc这个函数了
- realloc函数的出现让动态内存管理变得更加灵活
- 有时候我们会发现我们之前申请的空间太小,这时候我们又会觉得申请的空间过大了,那为了我们可以有一个合理的内存空间,我们会对内存的大小做灵活的调整,那么realloc函数就可以充当这个角色。
函数的原型如下:
void* realloc(void* ptr, size_t size);
-
ptr是要调整的内存地址
-
size调整之后新大小
-
返回值为调整之后的内存起始位置
-
这个函数调整内存空间大小的基础上,还可以将原来内存中的数据移动到新的空间
-
realloc的调整存在两种情况
- 原有空间之后有足够大的空间
- 原有空间之后没有足够大的空间
看下面这张图:
情况①
当后面有足够空间的时候,要扩展内存就直接在原有内存之后直接追加空间,原来空间的数据不发生变化。
情况②
党后面没有足够空间的时候,我们扩展采取的方法是:在堆空间上另外找一个合适大小的连续空间来使用,但这样函数返回的就是一个新的内存地址
**NOTE:**正是有了这两种情况所以我们在使用realloc函数的时候需要额外注意一下
三、常见的动态内存错误
在这里我只大致列举了几种常见的错误,不会举出详细的例子,后续另外一篇博客对此进行详细的解释。
1.对NULL指针的解引用操作
空指针解引用是C/程序中较为普遍的内存缺陷类型。当指针指向无效的地址并且对其引用时。可能产生不可预见的错误,导师软件系统崩溃。空指针引用缺陷可能导致系统崩溃、拒绝服务诸多严重后果 。
2.对动态开辟空间的越界访问
假如我们在一个程序中使用malloc函数开辟了20个字节(也就是5个int类型大小的空间),但是在使用时程序是拿到开辟空间的地址向后访问10个字节的空间的,这就造成了越界访问,最后使程序崩溃 。所以我们在写代码的时候一定要考虑到越界访问的问题。
3.对非动态开辟内存使用free释放
这一点就不用过多赘述了,实在是一个非常。。。。抽象的操作。
4.使用free释放一块动态开辟内存的一部分
我们在使用free释放所开辟动态空间时,传入的指针一定要指向该空间的起始位置,不然会造成free函数无法造成释放空间。
5.对同一块动态内存多次释放
我们在编写代码时可能写着写着用函数free对同一块动态空间多次释放了,这也会造成程序运行出错,
其实避免这个错误也十分简单,我们在释放完一个空间时立即把传入的指针置为空就可以避开此错误。
6.动态开辟内存忘记释放(内存泄漏)
我们在写代码的时候很容易忘记释放我们不再使用的动态内存,虽然程序结束后会自动释放,但是这样并不利于程序的运行,会导致内存泄漏的问题。
编程时也一定要注意不要忘记释放自己开辟的动态内存的空间(否则这种情况越多当程序运行时堆区的占用越多,最终会导致内存不足)。
四、柔性数组
柔性数组这个概念大部分人可能不太熟悉,但它确实是存在的。在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组的成员
比如下面这一块代码:
typedef struct Soft_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
有些编译器可能会报错无法编译,这个时候我们将a[0]改成a[ ]即可。
一、柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的小大,以此使用柔性数组的预期大小。
比如下面这一块代码:
typedef struct Soft_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a)); //输出的是4
二、柔性数组的使用
int i = 0;
type_a *p = (type_a*)malloc(sizeof(type_a)+50*sizeof(int));
p->i = 100;
for(i=0; i<100; i++)
{
p->a[i] = i;
}
free(p);
这样的柔性成员a,就相当于获得了50个整形元素的连续空间
三、柔性数组的优势
- 方便内存释放
试想一下,如果我们的代码是在一个给别人使用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
- 有利于提高访问速度
这一点其实比较牵强,毕竟当我们跑不了的时候也要用偏移量的加法来寻找地址,但是从理论上来说连续的内存还是有益于提高访问速度,也有利于减少内存碎片。
户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
- 有利于提高访问速度
这一点其实比较牵强,毕竟当我们跑不了的时候也要用偏移量的加法来寻找地址,但是从理论上来说连续的内存还是有益于提高访问速度,也有利于减少内存碎片。