1.为什么存在动态内存分配
2.动态内存函数的介绍
1.malloc |
2.free |
3.calloc |
4.realloc |
3.常见的动态内存错误
4.柔性数组
1.为什么会有动态内存分配
在没有掌握动态内存分配之前我们会的开辟内存空间的手段:
1.创建变量 --- 开辟的是静态内存空间
2.创建数组 --- 开辟的还是静态内存空间
静态内存空间的创建虽然省事,但是会很容易导致内存空间的浪费或者是内存空间不足
为了更好的管理和分配内存空间,让内存空间“活”起来,我们引入了动态内存分配这一个操作
动态内存函数的介绍
内存空间中的分区有:堆区(用来进行动态内存开辟的),栈区(常用来放临时变量,如局部变量(包括在函数中开辟的局部数组)和函数形参,空间不大)和静态区(又称为数据段,放的是全局变量和静态变量)
而用来进行动态内存开辟的函数:
malloc , calloc ,free , realloc
所开辟的动态内存空间一定在堆区上
一.malloc和free函数
#include <stdlib>
void* malloc(size_t size)
allocates memory blocks --- 开辟内存块
size --- bytes to allocate --- 要开辟的字节数 --- malloc函数是一个字节一个字节开辟内存空间的
void* ---
1.如果malloc在堆区中成功开辟了内存空间的话,它就会返回承接组成这个内存空间的首个内存单元的地址的指针,这个指针的类型是void*(void*是无类型指针,能够指向任何数据,但不具有步长)
void*无类型指针不能够进行直接操作,如解引用,++,--等,但是我们可以通过强制类型转换来实现上述操作
2.如果没有成功开辟的话就会返回一个空指针NULL(也可以看作是 0 )
malloc函数返回一个无类型指针,我们把这个指针强制类型转换后,再将其赋值给对应类型的指针后,就可以得到一个符合我们预期类型的指针了。
得到预期类型的指针后我们还要去做判断,如果指针中存的是NULL(0),即指针是一个空指针的话,就说明malloc函数开辟内存空间失败
我们可以打印出错误信息 --- 用perror函数 --- 其括号内是我们自定义的,在冒号前的输出内容,它会在冒号后输出对应的错误信息
如果成功了再进行对应的指针操作。
在我们用完动态内存开辟的空间之后,我们为了减少空间的浪费,需要我们自己利用 free 函数
去释放空间 --- 即将空间归还给操作系统
回收格式:
在函数用完后,调用free函数 --- free(装有组成开辟的内存空间的首内存单元的地址的指针a --- 通过它可以把整个动态内存空间释放)
有了开辟又有了释放,这样的话我们就能够高效的利用我们的内存空间,减少了空间的浪费
开辟的动态内存空间是我们借的,用完之后是要还回去
注意!!! 当我们把内存空间归还给操作系统之后,装有组成我们开辟的动态内存单元的首内存空间的地址的指针a没有发生改变,也就是说,指针a依然在指向我们之前开辟的动态内存空间
但是,空间已经被释放,它已经被我们归还给了操作系统,指针是指不到它想指的对象的,这样的话这个指针就成为了野指针,会导致指针非法访问,使程序出问题为了避免这种情况,
我们要在释放完空间之后再将之战a置为空指针
使用完malloc函数开辟的动态内存空间之后,一定要用free函数将空间释放,避免内存空间的浪费
开辟了空间就要在用完后释放空间 ,用了malloc就要用free来擦屁股
二.calloc函数
#include <stdlib>
void* calloc(size_t num , size_t size)
// Allocates an arry in memory with elements initialized to 0
//开辟一块动态数组空间,且其中的元素都被初始化为0
num --- 元素的个数
size --- 每个元素的大小 ,单位是字节
要注意的是:
我们开辟的这块动态数组空间中的元素的类型是无类型元素,如果想让其变为我们想要的类型的话,需要我们:
将指向它的无类型指针强制类型转换为对应类型的指针,这样当我们解引用的时候,计算机会自动将指针指向的元素也强制类型转换为对应类型的元素之后再被解引用出来
calloc函数也是通过free函数来释放的
free函数中放的是组成我们开辟的动态内存空间的首内存单元的地址
calloc函数与malloc函数的区别:
1.calloc函数能够实现malloc函数的功能,而malloc函数无法实现calloc函数的功能:
calloc(10 , sizeof(int)) == malloc(10*sizeof(int))
2.calloc函数能够将自己开辟的内存空间都初始化为0,而malloc函数则不行,malloc开辟的内存空间由于没有初始化,所以其内存的值都是随机值
要注意的是:
calloc函数返回的也是一个空指针,这个空指针指向的是calloc函数开辟的动态数组的首元素的地址,如果我们想要获得对应类型的元素和数组的话,就需要我们将这个空指针强制类型转换为对应类型的指针,并将其传给对应类型的指针变量中。
为什么我们需要一个对应类型的指针变量来承接强制类型转换后的空指针呢?强制类型转换之后直接用不行吗?
答案函数不行的。这是因为强制类型转换的局部有效性 --- 强制类型转换只能在使用它的语句中有效,到下一个语句时它就不生效了。
为了能使强制类型转换之后的结果一直存着,我们需要建立一个变量并将强制类型转换之后的结果copy一份赋值给他,这样的话哪怕在语句结束之后,我们依然可以保留强制类型转换的结果。
三.realloc函数 --- 调整已开辟的动态内存空间的大小
#include <stdlib>
void* realloc(void* ptr , size_t size)
malloc ,calloc函数是开辟动态内存空间,是动态的骨肉,而realloc函数则是动态管理开辟的动态内存空间,是动态的灵魂 , free函数则是动态释放空间,是动态的终点
ptr --- 是组成要调整的动态内存空间的首内存单元的地址
size --- 调整之后的新大小(以字节为单位)
realloc的返回指是一个无类型指针,这个无类型指针指向的是内存空间大小变化后的新的动态内存空间(存的数据是组成该空间的首内存单元的地址)
情况1 --- 减小已开辟的动态内存空间大小 --- size < 原大小
首先:size的大小要小于已开辟的动态内存空间的大小,然后realloc动态减小的原理:
对于减小而言,realloc的处理方式是在堆区重新开辟size个字节大小的动态内存空间,然后再将存在ptr指向的动态内存空间中的数据拷贝到大小变化后的新的动态内存空间
注意!!!由于是将旧内存空间a中的数据直接拷贝到新内存空间b中,所以存在a中的数据大小不能大于b的内存空间的大小,否则会导致非法内存访问
情况2 --- 增大已开辟的动态内存空间大小 --- size > 原大小
首先realloc函数会自动计算出要增大多少个字节的空间
然后realloc函数会先尝试在ptr指向的旧的内存空间的后面增加新的内存空间
如果!!!!
1.ptr指向的内存空间后面的空间足够的话(指从后面开始剩余内存单元个数 >= 要新增的空间且在增到要求空间大小前都不会遇到已被占据的内存单元/内存空间时,realloc函数就会直接ptr指向的内存空间后面开辟新的内存空间)
开辟完后会将ptr指针强制类型转换为无类型指针后返回
2.ptr指向的内存空间后面的空间不足够的话
realloc函数会在内存空间中重新开辟一个size个内存单元大小的动态内存空间,然后将ptr指针指向的数据拷贝到新的动态内存空间中,并将这个新的动态内存空间的地址(组成它的首内存单元的地址)返回
注意:这种情况下旧的内存空间在realloc拷贝完就没用了,所以我们需要在拷贝完后将其手动释放(free)避免空间的浪费
3.如果1和2都不能够成功增大内存空间的大小的话,就表明realloc函数找不到合适的空间来进行增大操作,此时realloc函数就会返回一个空指针 NULL .
最后:我们要创建一个新的指针变量来承接reelloc函数的返回值,这样的话就不会污染我们存储最开始开辟的动态内存空间的地址的指针 --- 这样就能保证无论如何我们都能找到最开始开辟的动态内存空间
最最后,realloc函数在单独使用的时候也能够实现malloc函数的功能
如:
当我们给realloc函数传一个空指针且给定size的时候,realloc函数会在堆区中自动开辟对应大小的动态内存空间 --- 并将承载该空间的地址(组成该内存空间的首内存单元的地址)的指针返回(指针是无类型指针,和malloc一样)
断言判断为假 --- 直接报错,不再执行断言下的语句,为真的时候才执行下面的语句
Ps : 在进行
s[i] >= 65&& s[i] <= 90 || s[i]>=97&&s[i]<=122
这样子的长判断时:
我们的想法是: 先把或||左右两边的与&&做判断,然后再进行或判断
但是!!!别人在看我们的代码的时候有可能看错优先级以至于判断出错
为了避免这种情况,我们要把我们想先执行的操作用括号括起来,将优先级显性表示,方便我们看代码!!!
即有:
(s[i] >= 65&& s[i] <= 90) || (s[i]>=97&&s[i]<=122)
else if后面也要跟条件!!!!
使用了动态内存开辟函数之后一定要用free函数释放空间,给动态内存空间一个终点
如果不手动free(释放)的话,主函数main结束的时候,计算机也会自动自动free掉我们开辟的动态内存空间
关于结构体知识的补充:
1.当我们通过点操作符和箭头操作符访问结构体的不同类型的成员时:
如果访问的是数组:这两个操作符的操作结果都是:
一个承接着该数组首元素地址的指针(相当于数组名) --- 注意!指针是变量,是可以改的
如果不是数组的话,则是承接着:
将指向成员变量的指针成员变量解引用后的结果(注意:通过给指针解引用后的结果(给*p赋值)赋值,可以直接修改指针指向的内存空间中的数据)
3.注意!!在结构体类型中无法对成员变量进行初始化,只能在结构体类型外可通过memset函数(也可以用其它的方法)初始化
四.常见的动态内存错误
1.对空指针进行解引用
首先我们要知道:
1.空指针无法解引用
2.空指针也不具有步长,不能够++/--
3.利用动态内存函数malloc , realloc , calloc函数开辟动态内存空间时 ,可能会由于内存空间不足而开辟失败,而在开辟失败的时候动态内存函数会返回一个空指针
综上,如果我们开辟完动态内存空间后,没有对动态内存函数返回的指针进行判断,以此来分辨它是不是一个空指针而直接用这个指针进行解引用操作的话 ——
就可能导致对空指针解引用操作的情况出现(动态内存开辟失败时),进而使得程序报错,所以当我们接收了返回值之后,第一件事就是对指针进行判断,看它是不是一个空指针!!
2.对已开辟的动态内存空间越界访问
动态内存空间也是有限的空间,咱可不能越界访问啊!!
3.对非动态内存使用free函数
free函数只能够对动态开辟的内存进行释放(在堆区上开辟的内存空间),而不能对非动态开辟的内存(栈区上开辟的空间)进行释放。
ps:放在栈区上的内容有函数局部变量(数组,int,double....等等),函数形参....等等
4.使用free函数释放一块已开辟的动态内存空间的一部分
用free释放动态内存空间的时候,要释放的话必须全释放掉,不能一次只释放一部分
注意:free函数会将你给的指针指向的动态内存空间及其后的所有动态内存空间释放掉
free函数释放的时候必须将所有动态内存空间释放掉,不能一次只释放一部分
5.对同一块动态内存空间进行多次释放
一块动态内存空间只能释放一次,不能释放多次
ps:如果传给free函数的参数是空指针的话,free函数什么都不会做
6.忘记释放动态开辟的空间
动态开辟的空间有两种释放的方式:
1.主动free
2.主函数main结束之后,计算机自动释放掉所有动态内存空间
忘记释放掉动态内存空间的话就会导致内存泄漏问题
所谓内存泄漏问题就是指有效内存空间被无效占用,如我们用完了一块动态内存空间,却不把这个空间释放,那么这一部分内存空间就会被占着,但是它又不干活,也不给别人占用,那么这一部分内存空间就会一直占着茅坑不拉屎,光吃内存不干活,这种有效空间被无效占用的情况就是内存泄漏问题。
避免这个问题的最好方法就是:
使用完动态内存空间 之后一定要马上free + 置为空指针组合拳去防止问题出现
几道题目:
一定要注意:形参变量的作用域只在函数内部,出了函数,形参变量就被销毁
stcpy (char* destination , const char*source) -- 作用是将source指针指向的数据拷贝到destination指针指向的空间中(两个指针的类型必须是char*)
空指针指向的空间为NULL ,NULL空间中只有数据零,且任何数据都不能拷贝到NULL空间中
改变临时拷贝对自身不影响
这两种写法都是一样的,因为printf输出字符串时也是通过首字符的地址输出的,上下都是在给printf传首字符的地址这一个参数,所以是一样的
数组也是函数的局部变量,开辟在栈区上,函数结束的时候,局部变量也会被销毁(指局部变量占据的空间被释放 )。
返回栈空间地址的问题 --- 在函数中创建局部变量的时候,我们会在在栈区中为局部变量开辟内存空间,当局部变量对应所在的函数结束之后,为局部变量开辟的空间也会自动被释放(归还给系统)
NULL是空指针,void是无类型指针
指针没被初始化的时候就是一个野指针。野指针不可以解引用
已经被free(释放)掉的空间是无法访问的
栈区是向下增长的,堆区是向上增长的,数据段其实就是静态区,代码段则是计算机存储可执行代码和常量(如字符串)的区域
如果想在一个语句中定义两个指针 --- 格式如下:
选第二个!!!! , 每个变量各给一个 * 才能同时创建两个指针变量 , 第一种方法则是错误的书写方式 ,当然我们也有完全不给 * 的写法:如下
通过typedef给一个指针类型起一个别名 ,并用这个别名创建指针变量时,每一个变量都不用加 *
ps : typedef是将类型名替换为一个字符串 --- 创建指针变量时不用加 *
而define则是将某一串字符替换为类型名 --- 创建指针变量时要加两颗星
最后:柔性数组
结构是指结构体,元素是指成员变量
柔性的意思就是指它的大小是可变的
结构体类型中的最后一个成员变量可以是 大未知的数组 --- 而这个数组就被称为柔性数组
上面这个是柔性数组的两种写法 :
第一种是 a[ ] 然后啥也没有
第二种是 a[ 0 ] 然后啥也没有
柔性数组的特点:
1.结构中的柔性数组成员前面必须至少有一个其它成员,然后柔性数组必须放在结构体类型的成员变量的最后一个
2.sizeof返回的(包含柔性数组成员)的结构体类型的大小不包括(计入)柔性数组的内存!!!
3.包含柔性数组成员的结构用malloc()函数进行内存的动态分配 -- 也就是说包含柔性数组的结构体类型的内存大小为动态内存大小,要用mallloc函数(统一用malloc函数来开辟)开辟
,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
然后开辟内存空间时,要专门为柔性数组开辟空间(大小为我们的预期大小)
柔性数组的大小 + 为计算柔性数组大小时的结构体类型的大小 == 新的结构体类型的大小
柔性数组一开始没有大小 ,需要我们用malloc函数来动态规划大小,分配空间
又因为柔性数组的大小是动态规划过来的,所以我们也可以随时调整这个大小,这也是柔性数组柔性的原因。
用realloc函数时,如果是重新开辟了一个动态内存空间的话,旧的动态内存空间会被自动释放掉。
柔性数组其实就相当于一个占位符,后面我们用malloc函数来给他填充内容。
柔性数组的作用
(栈区指针可以指向堆区)
在堆区中,一个动态内存空间与另一个动态内存空间之间的内存空间被称为内存碎片,内存碎片被再次利用的可能性较低,在堆区中开辟的动态内存越多,堆区中的内存碎片就越多,此时堆区的内存空间利用率就越低
内存池:
为了提高内存的申请和调用效率,编译器会直接为我们当前的程序申请一块内存,然后程序中的内存调用无需再向系统申请,直接在申请好的内存块中调用和归还即可,这样的内存块就被我们称为内存池
计算机的局部性原理 :
1.空间局部性:当我们使用了一块内存后,接下来我们会有百分之八十的可能性使用其周边的内存
柔性数组的优势: