0


动态内存管理

动态内存管理

前言

记录动态内存管理的那些操作以及对于柔性数组的认识!

一、 为什么会存在动内存管理?

举个例子:

就比如一个简单的通讯录(静态版本)我们一开始是不是就会明确给出通讯录的大小,但是空间总有用完的一天啊,空间用完了,但是我们还想往里面存入数据(又不能删除前面的数据),这时候就会陷入一个很尴尬的境地,像上述这样,一开始就把能用的空间给写死的情况,我们称作“静态内存”,当然为了解决这个问题,与之对应的就是动态内存,顾名思义,,内存是动态的,我们想要多大的内存就可以向内存申请(当然不能大的离谱,必须在合法的范围之内,不然会开辟失败的),当我们的空间不够的时候,我们又可以向内存多申请一点,总之不够就申请,当然我们申请的内存,如果最后不用了,就应该归还给操作系统,防止内存泄漏!!!当然C语言实现这些操作主要靠4个函数:
malloc、calloc、realloc、free
下面咱们来详细介绍一下这几个函数:

1、malloc

首先我们通过https://cplusplus.com/
这个网站来了解一下malloc

malloc介绍:
在这里插入图片描述
void*malloc(size_t size);
参数介绍:
在这里插入图片描述
返回值介绍:
在这里插入图片描述

malloc函数用于开辟空间,其参数是表示需要开辟的空间的大小(单位字节),然后饭后首字节的地址;
那么问题来了,malloc是向哪里开辟空间呢?
我们都知道内存有着这么几个区:
在这里插入图片描述
简单示意图:
我们知道局部变量等的开辟的都是存在栈区,静态变量、全局变量是存储在静态区的;而我们自动像编译器申请的空间是在堆区开辟的,堆区是系统提供给我们程序员自由操控的区域;
下面我们来看看malloc函数的使用:

#include<stdio.h>#include<stdlib.h>intmain(){int* tmp =(int*)malloc(sizeof(int)*10);if(tmp ==NULL)return1;for(int i =0; i <10; i++){
        tmp[i]= i;}for(int i =0; i <10; i++){printf("%d\n",tmp[i]);}return0;}

前面说了malloc函数是一个动态内存函数,可以开辟任何类型的空间,就比如现在,我们向堆区申请了10个int大小的空间,也就是40个字节,最后返回一个指向这40个字节的首地址,由于返回的是void*的指针,可能存在=左右两边类型不兼容的情况,我们最好加上对应类型的指针的强制类型转换;

malloc给我们开辟的空间是没有经过初始化的,是需要我们自己手动去初始化的:
在这里插入图片描述
所以我们需要自己手动去初始化,第一个for循环就是类似初始化操作;
同时我们也需要注意malloc函数也有可能开辟失败(一次性开辟的空间太大了),而返回空指针,为了避免对空指针解引用,我们应该对malloc返回的指针进行判断,以保证malloc返回的指针的合法性!!!(这也是if语句的作用):
在这里插入图片描述
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

2、calloc

刚才我们讲完了malloc函数,现在我们来讲一讲calloc函数,其实calloc函数的功能与malloc差不多,唯一的区别就是一个是自动挡一个是手动挡;什么意思?
我们上面说了,malloc是只管开辟空间,不管空间的初始化嘛,这需要我们自己手动去初始化;但是对于calloc函数来说,它不仅会帮我们开辟空间,还会帮我们对这块空间进行初始化(以字节为单位进行初始化),相当于事实malloc+memset函数的结合了;
既然calloc是手动挡,那么它的参数必然也和malloc不一样了:

void* calloc (size_t num, size_t size);
calloc函数介绍:
在这里插入图片描述
参数介绍:
在这里插入图片描述
num表示你要开辟多少个这样的空间,size表示像这样的空间一个占多大的空间(数据类型的大小)
返回值介绍:
在这里插入图片描述
虽然是自动挡,但是我们不能定义初始化的值,calloc开辟的空间默认用0来初始化!!!
举个例子:
在这里插入图片描述
这块空间全是0!!!
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。

3、realloc

realloc叫做扩容函数,主要是用来调整堆区申请的空间不够的情况,比如我用malloc开辟了10个空间,但是这10个空间被我用完了,我还想要一些空间,这是我们就可以利用realloc函数来实现这一操作了,当然我们不能过分,就是我们不能要求realloc帮我们扩一个很大的空间出来,我们要求扩容的空间必须合法,否者realloc会扩容失败;当然realloc扩容有两种情况:

1、原数据后面还有空间并且满足这一次的扩容:
在这里插入图片描述
同时这次返回的首元素地址也会和原来的首元素指针一样:
在这里插入图片描述
我们可以发现的确是这样

2、原数据后面虽然还有空间,但是确不能满足我们扩容的大小
在这里插入图片描述
在这里插入图片描述
3、上面两种情况都不满足,就是没有找到合适的扩容的空间,则realloc会返回NULL,表示扩容失败,当然原来我们所拥有的空间不会被释放:
在这里插入图片描述
没有报错,说明,当我们扩容失败时,并不会释放原来的空间;
4、当然realloc也可以进行缩小,对原数据进行缩小,同时释放掉不用的空间;
在这里插入图片描述
我们可以看到当我们再次访问6号元素下标的时候,编译器会报警告,告诉我们非法访问了;
在这里插入图片描述
当我们取消调整,在对下标为6的位置进行访问,编译器就不会在给我们报越界访问的警告了;

说了这么多,我们还没具体讲讲realloc的用法;

realloc函数介绍:
在这里插入图片描述
参数介绍:
在这里插入图片描述
如果我们给realloc传一个NULL指针过去,此时realloc相当于malloc;
返回值:
在这里插入图片描述

4、free

我们前文说了,我们malloc、calloc、realloc所开辟的空间,都是在堆区申请的,当我们不在使用这一片空间的时候,我们应该归还给操作系统,防止内存泄漏!!!

free函数介绍:
在这里插入图片描述
参数;
在这里插入图片描述
返回值;
在这里插入图片描述
具体应用:
在这里插入图片描述
这种格式是对于堆区开辟的空间释放的最漂亮的处理方式!!!
在这里插入图片描述
我们可以看到如果传给free的不是首元素地址,编译器会报错;

free函数用来释放动态开辟的内存:
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做

5、free为什么能一块空间?

我们都知道free的参数是待释放空间的首地址,但是问题是,我们明明只传给了它一个首地址,它(free)怎么知道我们要释放多大的空间呢?
这就涉及到malloc函数的实现了,简单俩说,malloc函数实际上所开辟的空间是大于我们实际需要的空间的,它会多开辟一些空间,用来存储这些信息,就比如在windows平台下,malloc所返回的地址第前四个字节,就存储着malloc开辟的空间的大小的信息:
画个图:
在这里插入图片描述

二、常见的动态内存错误

1、对NULL指针的解引用操作

voidtest(){int*p =(int*)malloc(INT_MAX/4);*p =20;//如果p的值是NULL,就会有问题free(p);}

分析:
我们知道malloc函数并不一定会成功帮我们开辟空间,如果我们需要都空间开辟的太大,malloc函数是会返回空指针的,也就是说p有可能是空指针,而*p=20;就是在对空指针进行解引用,对空指针解引用,这是编译器不能容忍的,它会直接给你报错;当然如果p不是空指针,那么程序是不会有问题,但是为了避免这种情况的出现,我们最好对p指针进行判断一下;(只要我们开辟的空间不是太大,malloc都能满足我们的要求)
比如像这样:
在这里插入图片描述

2、对动态开辟空间的越界访问

voidtest(){int i =0;int*p =(int*)malloc(10*sizeof(int));if(NULL== p){exit(EXIT_FAILURE);}for(i=0; i<=10; i++){*(p+i)= i;//当i是10的时候越界访问}free(p);}

我们可以看到,我们之开辟了10个int的空间,下标也就是0~9,但是我们的佛如循环去让我们访问到了下标为10的位置,这是万万不行的,因为我们并没有申请下标为10的位置,这块空间自然就不属于我们,我们自然就不能对其进行访问;如果我们硬要访问就属于非法访问内存了,一般的编译器会报警告,严格一点的编译器就会直接报错;
在这里插入图片描述

3、对非动态开辟内存使用free释放

voidtest(){int a =10;int*p =&a;free(p);//ok?}

分析:
free是释放堆区开辟的空间,对于不是堆区的空间,用free进行释放,free是没有定义的,对这种情况的具体处理行为取决于编译器;
在这里插入图片描述
编译器直接崩溃了;

4、使用free释放一块动态开辟内存的一部分

voidtest(){int*p =(int*)malloc(100);
p++;free(p);//p不再指向动态内存的起始位置}

分析:free只能从起始位置开始释放,不能从中途释放;举个例子:就比如我借了100给你,但是最后你只还我50,这是我不能容忍的!同样我们像编译器开辟了10字节空间,现在我们只还5字节给编译器,编译器绝对不能容忍,直接就给你报错:
在这里插入图片描述

5、对同一块动态内存多次释放

voidtest(){int*p =(int*)malloc(100);free(p);free(p);//重复释放}

分析:
对于我们向malloc申请的空间我们自然有对其进行读和写的操作,同时我们还可以对我们使用过的空间进行释放,释放过后空间就不再属于我们了,我们也就无权对它进行任何操作,这块空间会被操作系统拿去使用和存数据,如果我们在对这块空间进行重复使用,是想让操作系统也使用不了这块空间?还是直接将该空间里面的数据直接抹掉,让程序崩溃?这都不合适,因此我们不能对同一块空间进行多次释放:
在这里插入图片描述

6、动态开辟内存忘记释放(内存泄漏)

voidtest(){int*p =(int*)malloc(100);if(NULL!= p){*p =20;}}intmain(){test();while(1);}

分析:
我们必需对堆区申请的空间进行手动释放,即用既释放;尽管我们不手动释放,在程序结束后自动进行释放?但是这个前提是程序要能结束?但是有些程序是结束不了的啊!比如操作系统:只要你开机就会一直运行,从不会结束,如果像这样的话,操作系统存在内存系统,内存就会不断的被吃掉,电脑也会越来越卡,试问,这样的操作系统你还爱吗?这肯定不行,因此为了养成良好的编程习惯,对于malloc开辟的空间我们不用了,记得手动释放一下;!!!!

三、几个经典的笔试题

题目1

voidGetMemory(char*p){
p =(char*)malloc(100);}voidTest(void){char*str =NULL;GetMemory(str);strcpy(str,"hello world");printf(str);}intmain(){Test();return0;}

分析:程序会崩溃,同时存在内存泄漏的问题;
我们可以看到题目的意思是想让str保存malloc出来的空间的地址,但是str保存的了吗?GetMemory函数属于值传递了,p和str属于同类型的两个单独的空间,p空间的改变并不会影响str,所以str里面还是存的空指针;而我们strcpy对NULL解引用,这是不被编译器容忍的,直接就给你崩溃掉;,同时我们在GetMemory函数结束后再也无法找到malloc出来的空间的地址了,同时我们也就无法对于该空间释放,该程序也就存在着内存泄漏的风险!!!
在这里插入图片描述
程序直接崩溃!!!

题目2

char*GetMemory(void){char p[]="hello world";return p;}voidTest(void){char*str =NULL;
str =GetMemory();printf(str);}

分析:
该问题属于非法访问了,我的p数组是在GetMemory函数的栈帧里面建立的,我GetMemory函数调完了,GetMemory的栈帧也就被销毁了,里面所用到的空间也会被回收给操作系统,但是我们将该位置的地址饭回来了,我么在通过该地址再去访问该空间,抱歉,不行,这块空间已经不在属于我们,我们如果强行访问就属于非法访问了,但是如果我们真去非法访问,我们可以读取里面存的数据吗?答案是可以的,为什么?因为编译器只是回收了这块空间,并没有销毁该空间里面存的数据,理论上我们是可以通过返回的地址取处里面存放的内容的,同时我们可以验证一下:
在这里插入图片描述
我们可以清晰的看到str所指向的内容的确没有被修改;我们来看看程序运行:
在这里插入图片描述
我们可以看到程序运行出来是一串乱码,这是为什么?和我们的预期不符啊!这里主要涉及到函数的栈帧:
在这里插入图片描述
我们知道在GetMemory函数结束后,它的栈帧会被释放掉,这是str所指向空间内容没变。但是紧接着我们调用了printf,printf也是一个函数啊,他也会建立栈帧,而而且是在原来GetMemory函数建立过的基础上建立,这样就会导致str所指向的空间也就属于printf函数的东西了,printf函数自然也就可以对其进行操作,并且改变里面的内容,自然str所指空间原来的内容,就会被printf的函数栈帧所覆盖掉,自然我们看到的输出出来的就是乱码;
在C++中如果利用cout输出语句输出就会正常输出Hello Word;

题目3

voidGetMemory(char**p,int num){*p =(char*)malloc(num);}voidTest(void){char*str =NULL;GetMemory(&str,100);strcpy(str,"hello");printf(str);}

分析:咋一眼看过去没有什么问题,但是实际上该代码存在着潜在问题,存在内存泄漏,对于堆区开辟的空间,我们没有对其进行手动释放,就目前这个程序还好,程序结束编译器会自动帮我们释放,但是如果是那种不结束的程序呢?那是不是内存一直被开辟,从未归还,内存总有被耗干的那一天,到时候程序会崩溃,带给用户的体验也就不好:
在这里插入图片描述

题目4

voidTest(void){char*str =(char*)malloc(100);strcpy(str,"hello");free(str);if(str !=NULL){strcpy(str,"world");printf(str);}}

我们可以看到该程序对于防止内存泄漏这一块做得很到位,但是free释放的过早了,同时最好将str置空;首先str被提前释放了,同时str也没置空,str变成了一个野指针,指向的那块空间也就不属于我们了,但是我们现在还去对其进行强行访问,就会造成非法访问,宽松一点的编译器还好,会给你报警告,但是严格一点的编译器会直接给你报错;

四、C/C++程序的内存开辟

在这里插入图片描述
对于当前我们只需主要关注:栈区、堆区、数据段、代码段;

栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等.
堆区:一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统)回收 。分配方式类似于链表。
数据段:(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码。

五、柔性数组

1、柔性数组定义

什么是柔性数组,柔性数组的概念是在C99标准中提出来的,在C99中它允许结构体最后一个成员是不指定大小的数组或者数组元素为0的数组,该成员被称为[柔性数组]成员;
例如:

typedefstructst_type//一种柔性数组的表示方式{int i;int a[];//柔性数组成员}type_a;typedefstructst_type//另一种柔性数组的表示方式{int i;int a[0];//柔性数组成员}type_a;

以上两种方式都是柔性数组的正确表达方式!!!;

2、柔性数组的特点

1、结构体中的柔性数组成员必须为最后一个元素,并且柔性数组成员前面必须至少有一个成员变量;
2、sizeof求这种结构体的大小,并不会计算柔性数组的空间(因为我们没有给该成员分配大小),只会计算具有固定大小的成员变量的大小,同时内存对齐依旧存在;
在这里插入图片描述
3、如果我们想要使用柔性数组,我们就需要手动给柔性数组开辟空间,这里也就需要和内存开辟函数配合着使用(我们不能在栈区上建立结构体因为在栈区上结构体的大小是固定的,编译器不会为柔性数组开辟空间,因此我们只能在堆区上手动为柔性数组开辟空间),同时我们要开辟大于结构体大小的空间才能使用柔性数组(注意等于也是不行的!!!),比如上面的结构体不是求出来为4字节嘛,那我们就利用malloc开辟8个字节,然后编译器就会从这8个字节里面先拿出4个字节来给i使用,然后剩下的就全是柔性数组的的空间(比如这剩下的4字节就是柔性数组的,这时候柔性数组就可以存一个int型元素);
比如:在这里插入图片描述
当然既然是柔性数组,那么重点体现在这个“柔”字上面,这个柔字就说明,对于柔性数组的大小我们是可以进行调节的,比如现在我为柔性数组开辟了一个空间,不够用,我现在要给它开辟10个空间,那我们就要realloc来调整呗!
比如:
在这里插入图片描述
这个“柔”字,就体现这里,我们没有把数组的大小写死,对于数组的大小我们可以实现动态分配!!

但是呢?有的人又提出了柔性数组的替代品

structS{int i;int*a;//用来控制动态开辟的数组}

你看我们写成这样,似乎也能完成柔性数组所能完成的工作:

在这里插入图片描述
那么问题来了,既然利用一个指针就可以代替柔性数组,并且也能完成柔性数组所能完成的工作,那么为什么会存在柔性数组?
主要从两方面来论证:

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给
用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你
不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好
了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正
你跑不了要用做偏移量的加法来寻址),但是又有益于提高内存空间的利用率倒是真的:
在这里插入图片描述

标签: c++ c语言

本文转载自: https://blog.csdn.net/qq_62106937/article/details/127002065
版权归原作者 南猿北者 所有, 如有侵权,请联系我们删除。

“动态内存管理”的评论:

还没有评论