1. 为什么要有动态内存分配
当我们用之前的办法创建一个变量或者数组的时候,当时创建了多大的空间,后来就只能用多大的空间。但是很多情况下对于空间的需求不一定是固定的,有时我们需要的空间大小在程序运行的时候才能知道,那数组在编译时开辟的空间大小就不一定满足需要用到的空间大小了。因此C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,这样就比较灵活了
2. malloc 和 free
2.1 malloc
void* malloc (size_t size);
malloc函数用于在堆区中开辟一块连续可用的空间,并返回指向这块空间的指针。参数size的含义是指开辟这块空间的大小
官网资料:malloc - C++ Reference (cplusplus.com)
如果开辟成功,则返回一个指向开辟好空间的指针
如果开辟失败,则返回一个空指针,因此malloc函数的返回值一定要检查
返回值的类型是 void* 所以在使用的时候要指定malloc函数开辟的空间的类型
如果参数size的值是0,malloc函数的行为是C语言标准中未定义的,具体行为取决于编译器
2.2 free
void free (void* ptr);
free函数是专门用来释放动态内存的,其参数ptr是指向动态内存的指针,注意是动态内存
官网资料:free - C++ Reference
如果参数ptr指向的空间不是动态开辟的,那free的行为就是标准中未定义的//具体行为取决于编译器
如果参数ptr是空指针,则函数什么事都不做
使用这些动态内存操作函数都要引用头文件stdlib.h
2.3 实战
下面我们尝试使用一下这两个函数
这段代码中展示了用malloc函数创建动态内存实现类似数组效果的功能。这里有3点值得注意,第一,molloc函数在使用的时候将其返回值强制转换成了int*型,这样再赋值给p,否则void*类型是不能进行加减操作的。第二,我检验了一下p是否是空指针,如果是空指针说明空间开辟失败了,然后将失败的错误码信息打印出来。第三,再使用完这块动态内存后要记得free释放,同时将指针变量p的值设为空,以便下次使用。
可能你会好奇如果malloc空间创建真的失败了会怎样,下面我们尝试一下
这段代码我让malloc创建一块巨大无比的空间,于是它创建失败了,错误码打印出来的信息是Not enough space,然后程序return,返回了一个代码1
3. calloc 和 realloc
3.1 calloc
void* calloc (size_t num, size_t size);
calloc的作用和malloc一样,都是开辟一块动态内存,参数num是指要开辟几块连续的空间,size是指每块空间的大小是多少,其返回值是这一整块空间的起始地址
与malloc和realloc不同的是calloc会将开辟的内容都初始化成0,而那两个函数并不会初始化其开辟空间的内容
我们尝试用calloc开辟一块空间
3.2 realloc
realloc函数的出现是真正的让动态内存管理更加的灵活,前两个函数其实还是开辟了一块定死大小的空间,而realloc不一样,它可以随意改变设定好的那块空间的大小,但是要注意只能改变堆区中动态内存的大小,像栈区中的那些变量是不能操作的
void* realloc (void* ptr, size_t size);
参数ptr是要调整的动态内存地址,size是从新调整后的大小,返回值一般是调整后的内存起始位置
为什么说是一般呢,事实上realloc的返回值分3种情况
情况1:空间调整失败,返回空指针NULL
情况2:要调整的空间后面有足够的空间可以扩容,那么realloc会直接扩容这块空间,并返回这块空间的地址
情况3:要调整的空间后面有其他已经被使用了的空间,没有足够的空间可以扩容,那么realloc会再在堆区找一块空间开辟相应的大小,并将原空间中的内容全部复制到新空间中去,然后释放原来的空间,最后返回新的空间的地址
基于三种情况我们在接收realloc的返回值的时候要小心
这段代码中我创建了一个指针变量ptr用于临时接收realloc的返回值,这样做的意义是防止realloc创建空间失败,返回了一个NULL,如果此时用p去接收NULL的话,那么malloc创建的那块空间都找不到了,也永远无法释放了,这种情况被称作内存泄露的问题
如果ptr不是空指针,就将它赋值给p然后继续用,如果是空指针就打印错误信息
事实上realloc还可以当作malloc用
就像这样,当realloc的第一个参数是空指针NULL的时候realloc就会在堆区随机找一块空间开辟,这与malloc的实现逻辑是完全一样的
4. 常见的动态内存的错误
4.1 对NULL指针的解引用操作
如果不判断p是不是空指针就盲目的对它进行操作,这是错误的操作
4.2 对动态开辟空间的越界访问
越界访问之后程序会直接崩溃,并回馈堆区(HEAP)越界访问的信息
4.3 对非动态开辟的内存空间使用free释放
这段代码中我将malloc创建的动态内存地址赋值给p之后又写了好多代码,但是在这其中我写昏头了,将a的地址又赋值给了p,最后用free去操作p,这样就让free尝试释放了一个非动态开辟的内存空间,最终结果就是程序崩溃报错
在这里p的内容被篡改了之后malloc创建的那块空间就像之前那段代码一样,又内存泄露了,这里我想补充一点,malloc calloc realloc申请的空间,如果不主动释放,即使出了使用它们的作用域也不会销毁的,它们申请的空间的释放方式只有两种
1.free主动释放
2.直到程序运行结束,才由操作系统回收。但是像有些服务器,它的程序要一直运行着,那么这些内存就真的永久泄露了
4.4 使用free释放一块动态开辟内存的一部分
这段代码是说我在后续写代码的途中不小心将p的值增大了1,这样它就指向了动态开辟内存的一部分,但是这样运行的话就会崩溃报错,因为free中的参数必须指向整块动态内存
所以说我们在使用动态内存的时候尽量不要改变指向它起始位置的指针的大小
4.5 对同一块动态内存多次释放
这段代码是说我在后续写的时候我已经将p指向的动态内存释放了,但是我给忘了,于是又释放了一次,这样的话程序也会崩溃报错
当然这也跟书写习惯有很大关系,如果我在第一次释放的时候就把p设为NULL了那第二次释放的时候参数就是个空指针,free什么都不会做的,程序也会照常执行下去
4.6 动态开辟内存忘记释放
动态开辟的内存没有释放的情况我们之前已经看了好几例了,这段代码展示一下即使我没有忘记写释放动态内存,但是因为test函数提前返回了(a > 0),导致的内存泄露问题,这里我在主程序中设置了让它一直它运行的死循环,因此随着内存被逐渐吞噬,这个程序最后还是会崩溃的
所以说我们在写代码的时候一定要头脑清晰,不要出现内存泄露的问题
5. 柔性数组
在C99中,结构体的最后一个元素允许是未知大小的数组,这就叫做 柔性数组 成员。
下面我们创建一个柔性数组试试
可能有些编译器不认这么写,那就试试把数组中间写个0,像这样 arr[0]
5.1 柔性数组特点
结构体中柔性数组前面必须至少有一个成员
用sizeof计算结构体大小时不包括柔性数组的内存大小
包含柔性数组成员的结构体用malloc函数进行动态内存分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的大小
5.2 柔性数组的使用
下面我们尝试使用一个柔性数组
当然,如果突然发现这个柔性数组的大小不够用了,我们还可以使用realloc来拓展它
在这里我还要分享另一种创建结构体的方案,以实现类似柔性数组的功能
最后在释放空间的时候是有讲究的,要先把arr指向的空间释放掉,在释放p,否则就找不到arr指向的那块空间了
这两种方法各有利弊,第一种方柔性数组的方案方便内存的释放,一次free就可以解决问题。第二种方案的好处在于有利于访问速度,利于减少内存碎片
6. 总结C/C++中程序内存区域的划分
1.栈区(stack):函数内部的局部变量在这里创建,函数执行结束之后这些储存单元会自动释放
2.堆区(heap):一般由程序员释放,如果程序员不释放,程序结束的时候由OS(操作系统)回收
3.数据段(静态区)(static):存放全局变量、静态数据。程序结束后由系统释放
4.代码段:存放函数体、类成员函数和全局函数的二进制代码