在讲动态内存管理之前呢,我们需要对内存有一个更深层次的了解。一般在c语言中,称呼内存为c程序地址空间,因为内存很复杂,有很多不是c中的知识,称呼C程序地址空间更为贴切。
C程序地址空间
你怎么就知道向上、向下增长呢?来看图
如果你平时打印地址仔细观察的话,会发现在栈区上同一个变量的地址每次运行后都是不一样的。比如上面的那张图,我在打印一次,地址是不一样的。其它区的上变量的地址也可能改变,在修改动作过大的时候才会。
栈区上这个现象会很明显,那你知道为什么吗?
这种现象叫栈随机化,意思就是说变量的地址是随机的,不会固定的,这是编译器对变量的一种保护机制,如果知道了变量的地址,是可以对变量进行篡改的,如果一个黑客,知道变量的地址,就可以根据这个地址去篡改一些东西,这是非常危险的。
为什么存在?
1.堆区空间足够大
技术层面上,普通的空间申请都是在栈区和全局,全局区尽量少用。但是栈区的空间有限,如果需要很大空间的话,只能在堆区。
在堆区上申请,完全是可以的。
2.堆区空间大小更为灵活
数组中大小必须固定了(不考虑变长数组),如果在使用过程中空间不够了,只能回到定义数组的地方去换一个更大的空间,而使用堆区,它的空间大小是可以随时更改的这是和
realloc
函数有关,这就对顺序表就很有帮助了。
顺序表不会用数组的,用起来太挫了,因为空间的限制,开辟大了又浪费,开辟小了又不够。管理起来是很不方便的。
通过动态内存函数在堆区申请的空间,这些空间的地址都是连续的,并且在使用过程中是可以扩容或者缩小的,这是数组不具备的。因为空间地址是连续的,因此是可以在堆区上去实现顺序表。
具体是如何实现顺序表,以及如何扩容的,可以去看看这篇博客通讯录的实现,这里不仅包括了顺序表的实现和扩容,还包括了本章另一个重点柔性数组。
动态内存函数
头文件
#include<stdlib.h>
malloc
malloc
函数功能:向堆区申请size
个字节大小的空间。
返回值:是这块空间的起始地址,因为不知道具体的类型所以返回的是void*
,具体情况根据使用者来定,潜台词是需要强制类型转换。如果开辟失败了,返回一个NULL
。
这里可以规范一下代码风格,可以参照我写的,也可以不用。
大小:单个元素的空间大小*元素的个数
这样逻辑会比较清晰,另外if
语句进行的条件判断,最好写,不要怕麻烦,能够提醒自己这里会存在开辟空间失败的情况,免得以后在这里吃亏。
可以看看开辟失败的情况:
可以去验证一下,开辟的空间是不是连续的。
每个地址都是相差一个4
个字节,因为一个int
类型的元素占4
个字节的空间大小,可以换成char
类型的去打印,那结果一定是连续的。
free
在C程序地址空间那块有说到有借有还再借不难,不还存在风险,在生活中嘛,不还钱风险挺大的,在C中不还空间,会造成一个很大的问题-----内存泄漏。等下可以给大家演示一下。
free
函数功能:释放堆区开辟的空间。
只能传堆区空间的起始地址。
free的注意事项
内存泄漏
堆区申请的空间,必须释放,否则会发生内存泄漏。
内存泄漏:
可以看到一直在吃内存,但是吃到一定程度上,不会在消耗内存了,这是因为存在一些保护机制。
这是一个很直观的方法去看内存泄漏,对现在的你来说可能没什么影响,但是如果你是一名程序猿,在工作中写了个类似这样的程序,那会有很大的问题的,比如电脑上的杀毒软件,你说它里面能存在内存泄漏嘛?又或是操作系统,服务器程序,这些程序,你说它能有内存泄漏吗?是不能存在的。
一个疑问,程序关闭后,内存泄漏对电脑有影响吗?答案是没有影响,如果你安装的程序存在内存泄漏,你每次只要关机重启,电脑又可以使用了,但过一会又会死机,内存泄漏是根本原因。
没有free和free另外的细节
在学了动态内存函数之后,如果你学到了数据结构,比如说顺序表,存在越界的情况,有
free
和无free
,编译器检测的效果是不一样的。
越界情况是在free
过程中去检测的,free
本身的报错是地址传错,下面图片展示一下。
释放非堆区的地址空间:
越界情况:
无
free
,有很明显的越界行为,但是没有报错。
那有
free
了呢
这是我单独拿出来讲,很容易知道是这里出错了,但是真到了自己写代码的时候,如果是数组,越界是不会报错的,如果使用动态内存管理,越界+free报错后,你可能都不知道是哪错了,就比如在实现顺序表的增删查改等功能的时候。
不可对堆区的空间多次释放
写在同一个文件中,犯这中错误的概率很低,但是如果是多文件中呢?
这里可以举一个通讯录中会写出多次释放的一个例子,写着写着,可能自己都不会记得,在哪释放过了。
通讯录
释放后要对指针置为NULL,避免野指针
比如
free(p)
,p指针仍然指向那块堆区的空间,你不置为NULL
,如果在free(p)
下面还有代码,再次对p
进行解引那是错误的,因此要置为NULL
free(NULL)会有影响吗
并不会有任何影响。
calloc
calloc
函数功能:向堆区开辟空间,并作初始化。
size_t num:
元素的个数
size_t size:
元素占空间大小
返回值:同样是开辟空间的起始地址,需要根据实际情况去强制类型转换,开辟失败同样会返回一个NULL
和这种写法表达的意思一样
同样写的时候注意规范,也要加上条件判断,时刻提醒自己存在开辟失败的情况。
观察初始化:
realloc
前三个还好,这个函数很重要,也存在很多细节,也可以举在通讯录中的例子。
realloc
函数功能:对已经存在堆区的空间进行调整,可以扩容,也可以缩小,或者是开辟空间。
void* ptr:
存在两种形式:①NULL
②已经存在堆区的起始地址,对于①这种去调用realloc
,此时realloc
功能和malloc
一样,根据size
去开辟空间。(这个地址是任意类型的,只要在堆区就行)
size_t size:
原有空间大小+调整的空间大小,比如说原来已经有4个字节的空间,现在扩容2个字节的空间,那么size
是6个字节的空间。
返回值:也是堆区空间的起始地址,可能和ptr
原指向的地址空间不一样,待会会详细说,正是因为这个,所以有坑。如果开辟空间失败也会返回NULL
①参数为
NULL
,功能和malloc
一样
realloc重点–扩容问题
原有空间后面不存在足够空间的情况:
这种现象是随机的,在调用这个函数的时候要特别注意。
再拿通讯录中的例子说明。
在写增加联系人的时候,需要判断容量是否满了,满了需要进行扩容,在这里设计的时候,如果将扩容封装成函数,如果是用柔性数组设计的通讯录,这个扩容函数,要么带返回值,返回这个新的地址,要么是在传参的时候传一级指针的地址,用二级指针去接收。
另一种形式设计的通讯录就不用这样设计,主要原因是函数调用的问题,这里不在过多描述,更详细的要去博客中了解。
只记住realloc
可能会更改地址,在传参的时候要注意。
部分截图
原有空间后面存在足够大的空间的情况:
在用
realloc
时,代码风格同样也要规范。
刚刚就说过了,如果realloc
开辟失败,会返回NULL
。可以想一下,如果开辟失败,指针c
去接收了这个NULL
,那原来堆区的空间不就找不到了吗?因此需要一个新的指针去接收返回值,再去判断开辟释放成功,开辟成功则交给指针指针c
去维护。最好是用同一个指针c
去维护堆区的空间,指针p
只是一个中转站。
避免踩坑
除了
free
那提到的一些注意事项,还有下面的。
重提野指针
在详解数组和指针的那些知识中有详细介绍,上面也略微提到了,这里还是需要强调一下。
使用动态内存函数是必须和指针打交道的,对于指针的规范使用是很有必要的。
在对指针初始化的时候,如果指针没有明确指向,需要置为NULL
不可跳过起始位置去释放堆区的空间
free
释放的必须是起始位置,不能只释放其中一部分。什么时候会犯这种错误呢?在对指针进行自增操作的时候。
也需要去规范,一般都是写成数组的形式去遍历堆区的空间中的元素
动态内存管理更深层次的理解
内存实际申请的空间真的和我们申请的空间一样大吗?
释放之前
释放之后,观察内存发现释放的空间比我们开辟的空间要多,换句话说,在开辟空间的时候,实际上内存申请的空间比我们申请的空间大,程序猿使用的是程序猿申请的空间,那些多出来了的空间是用来干嘛的呢?
多出来的空间是用来记录一些信息,记录什么信息呢?比如实际申请出来多大的空间,还有其它属性的信息。记录实际开辟空间大小,在
free
的时候就可以根据这个地址去释放多大的空间,一般实际开辟的空间和编译器有关。
free()
到底释放了什么?
free:张三和如花的故事
释放后,指针仍然指向堆区的空间,但是不能访问。
free
究竟释放了什么?释放的本质是解除指针和对应堆空间的关系,虽然指针保留了这个地址,但是现在没有权力去访问它了。
举一个很生动的例子:如花和张三分手了,但是张三很痴情,仍然记得如花,但这个时候张三还能和如花手拉手、每天打电话吗?不能了,如花会说骚扰,然后…这应该很形象了吧φ(゜▽゜*)♪
柔性数组
柔性数组和结构体有关。
结构中的柔性数组成员前面必须至少一个其他成员。柔性数组数组存在下面两种形式有0和无0的。
struct S
{
int a;
char data[];
//char data[0];
};
sizeof 返回结构体的大小不包括柔性数组的内存。
不记得的可以看看结构体的大小
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。
一般先给结构体分配好空间,再给柔性数组分配空间
以通讯录中的为例
伪代码如下:
typedef struct PeopleInfo
{
char name[20];
int age;
char tele[12];
char address[30];
}People;
typedef struct Contacts
{
int count;//当前
People data[1000];
}Contacts;
Contacts* Con = (Contacts*)malloc(sizeof(Contacts) + sizeof(People) * X);
分配空间后是下面这个样子的,通过数组名data和下标索引就可以对柔性数组进行操作了。
结束语
把结构体和动态内存管理都掌握了,可以去实现小项目通讯录,多写才能熟能生巧,才能深刻理解。那篇博客写的比较全,单链表、顺序表的和柔性数组的形式都有,可以参考下。
转载:https://blog.csdn.net/m0_64212811/article/details/127594168