小言_互联网的博客

C动态内存管理|有张三和如花的故事你心动了吗

318人阅读  评论(0)

在讲动态内存管理之前呢,我们需要对内存有一个更深层次的了解。一般在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个字节的空间,那么size6个字节的空间
返回值:也是堆区空间的起始地址,可能和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
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场