一.为什么要存在动态内存分配:
下图是不同类型数据在内存中的分配:
上述的开辟空间的⽅式有两个特点:
•
空间开辟⼤⼩是固定的。
•
数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间⼤⼩在程序运⾏的时候才能知
道,那数组的编译时开辟空间的⽅式就不能满⾜了
像上述代码中,i是机器在内存中的栈区开辟了4个字节,arr[20]是机器在内存中的栈区开辟了连续的20个字节存放。而这些大小已经被机器限制死了,无法去改变,这就很有局限性,而如果存在动态内存分配程序员⾃⼰可以申请和释放空间,就⽐较灵活了。
二.动态内存分配的相关函数:
这些动态内存分配的函数使用时必须包括头文件<stdlib.h>
1.malloc:
使用时必须包括头文件<stdlib.h>
这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。
•
如果开辟成功,则返回⼀个指向开辟好空间的指针。
•
如果开辟失败,则返回⼀个
NULL
指针,因此malloc的返回值⼀定要做检查。
•
返回值的类型是
void*
,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃
⼰来决定。
•
如果参数
size
为0,malloc的⾏为是标准是未定义的,取决于编译器
当这个程序运行结束时,这申请的空间会返还给操作系统。
这时申请的内存空间太大,系统没有足够的空间。
2.free:
free函数⽤来释放动态开辟的内存。
•
如果参数
ptr
指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
•
如果参数
ptr
是NULL指针,则函数什么事都不做。
上面写的代码不严谨,既没有释放申请的内存空间也没有在申请之后判断申请的空间指向的指针是不是空指针。
所以更加严谨的写法:
所以总的来说malloc和free要一起出现这样才使得程序更加严谨,不会出错。
3.calloc:
•
函数的功能是为
num
个⼤⼩为
size
的元素开辟⼀块空间,并且把空间的每个字节初始化为0。
•
与函数
malloc
的区别只在于
calloc
会在返回地址之前把申请的空间的每个字节初始化为全 0
由这两个运行结果就可以看出来malloc与calloc的区别。
也可在调试内存中看出calloc将数组10个元素全部初始化为0.
所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务
4.realloc:
•
realloc函数的出现让动态内存管理更加灵活。
•
有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc
函数就可以做到对动态开辟内存⼤
⼩的调整。
•
ptr
是要调整的内存地址
•
size
调整之后新⼤⼩
•
返回值为调整之后的内存起始位置。
•
这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。
•
realloc在调整内存空间的是存在两种情况:
◦
情况1:原有空间之后有⾜够⼤的空间
◦
情况2:原有空间之后没有⾜够⼤的空间
综上两种情况下,在使用realloc时,需要注意一下,用一个临时的指针变量来存放扩展完的内存空间的地址:
当给realloc传第一个参数时如果传NULL时此时的作用就跟malloc一样,会开辟一个新的内存空间。
三.在开辟动态内存空间时容易犯的错误:
1.对NULL指针的解引用操作:
因为没有提前进行判断p是否为空指针,所以导致调试时出现了异常。
所以要对其做一个判断:
此时用assert函数对其进行断言或者用if语句判断:
2.对动态开辟空间的越界访问:
这跟数组非常相似。
3.对非动态开辟内存使用free释放:
当去调试的时候你会发现:
压根没有这块内存空间去释放:
运行的时候也会卡死
4.使⽤free释放⼀块动态开辟内存的⼀部分:
在前面的代码中我们利用循环去给开辟的内存空间去赋值:
这是一种循环的写法,还有一种:
虽然这两段代码都可以实现对其赋值但是,在代码二中p的指向不断移位,结束之后就不再是起始位置,而内存释放时要的是开辟的空间的起始位置。
5.对同⼀块动态内存多次释放:
但是如果你每次释放完内存时,都将指针置为空,此时也能正常运行。
6.动态开辟内存忘记释放(内存泄漏):
所以当申请完内存空间想到的第一步就是用完要释放申请的内存空间。
四.动态内存经典笔试题分析:
1.
图解:
正确的写法1:
正确写法二:
2.返回栈空间指针的问题:
不能返回临时变量的地址。
可能打印出来,也可能打印不出来。
图解:
3.
没有释放开放的内存空间,没有free,造成内存泄漏
4.
这个题目要我们记住的也是free完之后要将其置为空指针。
五.柔性数组:
C99 中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
根据这句话可以知道,要想使用柔性数组,则:
1.必须在结构体中而且要是最后一个结构体成员。
2.最后一个成员是数组,并且是没有指定这个数组的大小。
1.柔性数组的写法:
2.柔性数组的特点:
• 结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
• sizeof 返回的这种结构⼤⼩不包括柔性数组的内存。
•
包含柔性数组成员的结构⽤malloc ()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期大小。
sizeof计算时不会包含柔性数组的大小。而且这就是为什么柔性数组前面必须要有其他的结构体成员,因为如果没有的话sizeof计算不出大小。
3.柔性数组的使用:
这里能使用malloc开辟空间所以也能使用realloc来开辟空间,因为前面的成员的内存空间是固定的,所以当用realloc去调整大小时,是后面数组的大小在不断变化,所以才称为柔性数组。这也就是为什么柔性数组要放在结构体成员最后。
调整柔性数组的空间大下:
法一:
法二:先开辟一个内存空间给结构体中的成员,再单独给结构体成员中的柔性数组再单独开一个内存空间:
上述 法
1
和 法
2
可以完成同样的功能,但是 ⽅法
1
的实现有两个好处:
第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤
⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能
指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返
回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度.
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你
跑不了要⽤做偏移量的加法来寻址)