TIPS
1.
2. malloc, free, calloc, realloc 这些的基本前提都是在内存堆区
内存堆区不能与内存栈区两者混淆乱套
动态内存管理存在的原因
1. 为什么要有动态内存管理?其实我们之前学过比如说对内存的管理,比方说我申请一块内存空间:
1.1 有一种普通的方式int a = 10; 这一种是固定的向内存申请四个字节/申请一个变量的空间;
1.2 还有一种方式就是申请连续的一块空间,如:int arr [10];
2. 这两种申请开辟内存空间的方法都是可以的,但是都有一个缺点,一旦把空间申请好,这个空间的大小是不能变与修改了的。因此这些内存的申请与使用的方式有局限性。 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
3. C语言里面给我们提供一种方式,让我们向内存申请空间,然后申请到空间之后我就去用。当我不够的时候,内存空间可以变长;当我觉得多了的时候,这个内存空间可以变小。这个就是动态内存管理。
(变长数组不是说你能把数组给它随意的变长变短,只是在一开始你可以用一个变量指定数组元素的个数,一旦开辟好内存空间之后,也就不能发生变化了)
4. 动态内存管理指的是向内存申请空间之后,根据我实际用的情况,我可以让这个空间可以变大,也可以变小。非常灵活,适配度高。
内存简单介绍
1.
2. 内存分为几个区域,有栈区,堆区与静态区(这是我们学语言的时候划分的区域,实际上更为复杂)。栈区里面放的是局部变量/数组,函数的形式参数,反正都是临时的变量.... 这里面的东西都是进入作用域创建,一旦出了作用域就会销毁。
3. 还有一个区域是堆区,这个堆区是malloc, calloc, realloc所操作的内存空间,都是在堆区上的。堆区上我们来干什么?就是来进行动态内存分配/管理的。我们进行动态内存管理申请与释放的空间都是在堆区里面的。
4. 而在静态区里面,放的是静态变量,全局变量。今天我们学的函数都是针对堆区而言,操作空间都是在堆区,申请与释放的内存空间都是在堆区里面。
malloc()函数
1. malloc是用来向堆区申请一块内存空间。
2. malloc就是向内存堆区(是在堆上)申请一个size字节的内存空间,然后返回一个指向这个内存空间的起始位置的指针(void*)。
3. malloc只是负责向堆区申请size字节的空间,至于里面放什么我也不知道。所以返回指针的时候,这个时候的类型无法确定,所以是瞎子垃圾桶void*(没有具体类型的指针)
4. malloc堆区内存申请完之后,会返回一个void*的指向起始位置的指针。按道理来说,我接受的时候也应该用void*接受。但是这样子的话,在后面我用的时候我都要进行强制类型转换不太方便。事实上,我们在申请内存的时候不会糊里糊涂,在事先肯定要想清楚里面放什么的,因此比如说不如直接拿整型指针接受,把malloc前面强制类型转换。
5. malloc申请失败的话,返回NULL,因此要对返回值进行判断。这时候如果库函数malloc调用失败,就会把错误码放入errno,然后我用strerror去接受errno解读错误信息并用printf打印即可。
6.如果malloc申请成功后,那该怎么使用这个空间呢?首先得知道: malloc向堆区申请的是一块连续的空间并且有了起始地址,因此可以把它当成数组来访问( 利用偏移量....*(p+i)在赋值即可)
7. 在堆区里面,你要的时候用malloc申请空间了,那你到底要不要还一下?如果不还的话会有隐患,如果不还给操作系统,程序在运行结束的时候也会被操作系统回收。那如果这个程序运行一直不结束,你不用了之后又不主动去还,那你就是占着茅坑不拉屎了,这块内存空间相当于是限制与浪费了。
8. 这个时候有一个函数叫做free()。free是释放申请的内存
9. 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
注意点:
1. 参数里面是向堆区申请开辟的字节个数,勤用sizeof(),如10*sizeof(int)
2. 判断malloc返回结果是否为空指针, 可以搭配errno与strerror()
3. 将malloc返回指针强制类型转换(除非你糊里糊涂的)
4. free()归还与指针置空
free()函数
1. free(p),将p指向的那块堆区内存空间释放/还给操作系统,但不受影响,还是指向原先的位置
2. 但是虽然把p指向的堆区空间还给操作系统了,但是这个指针p还存着原先那个空间的起始地址,这就不合适了。万一有人*p(解引用),这时候就形成了非法访问。
3. 因此需要主动把p = NULL置空,因为对空指针而言,无法进行解引用操作了
4. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的,如果参数 ptr 是NULL指针,则函数什么事都不做。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
//
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//
free(p);
p = NULL;
return 0;
}
calloc()函数
1. calloc也是可以用来开辟空间,但是函数原型与malloc不同。
2. calloc是向堆区为num个元素开辟的内存空间,每个元素占size个字节
3. calloc开辟空间后的返回指针与void*、合理指针接受形式、失败后NULL的预先判断都是与malloc雷同的,失败后可以用strerror(errno)+printf或者直接perror()就可以返回错误信息
4. calloc向堆区申请空间之后,用完了也需要free(p)并且将原先的指针p置空。
注意点:
1. 参数里面为num与size,勤用sizeof(),如10, sizeof(int)
2. 判断calloc返回结果是否为空指针, 可以搭配errno与strerror()
3. 将calloc返回指针强制类型转换(除非你糊里糊涂的)
4. free()归还与指针置空
malloc()与calloc()的区别
1. 在参数上面有区别。malloc是直接我先计算好然后传给malloc一个总大小(单位是字节)就可以,而calloc是需要开辟多少个元素每个元素多少字节的这么一个内存空间
2. 其次就是malloc申请到堆区空间后,不会初始化里面是随机值,直接返回起始地址;而calloc申请到空间之后,会把里面全部初始化为0。
3. 也正是因为如此,malloc效率更高。
所以malloc申请到空间之后不会初始化,而calloc申请到空间之后会初始化成0
接下来也可以侧面证明free释放了空间还给操作系统了
realloc()函数
1. 我们之前讲过:动态内存分配可以让空间变大变小,可以根据实际情况来看. 而前面的malloc与calloc还有free都还没有讲到调整,调整的大任应该交到realloc头上。
2. 参数为指针ptr和size。realloc调整ptr所指向的堆区空间,并且要输入你希望调整后新的空间大小是多少
3. realloc在帮你把空间调整好之后,也会返回一个void*指针(指向起始地址)
6. 但是呢,realloc函数的使用比我们的想象还要更加复杂一些,当realloc使用之后返回的指针不能用原先的指针去接受,需要用一个新的指针去接受。接受的形式与malloc一样,先强制类型转换(除非你是糊里糊涂的)。
realloc()工作原理
1. (情形一)realloc在调整空间的时候,比如说把原先的空间加长,这时候首先要看原先空间后面。如果后面还有一大片没有被使用的空间(支持你去扩容),那是相当于是允许加长操作的。那么我就在原先开辟的内存空间屁股后面去追加另需要的空间,同时我追加完之后还是返回旧的指针。这个时候只是后面的空间多了。我的起始地址并没有发生变化,返回的还确实是旧的指针。
2. (情形二)比如说我要用realloc去加长原先开辟的空间,可是在原先空间的屁股后面,已经被别人占用了,这时候如果你继续在原先空间的屁股后面扩容的话,就把别人的空间给覆盖与破坏了。这时候realloc就会在内存当中重新找一块满足需求大小的空间。把旧的空间里面的数据在搬过来/拷贝到新空间前面的位置,搬过来之后把旧的空间释放掉还给操作系统。然后返回新的空间起始地址处的指针。
3. (情形三)realloc函数在申请空间扩容失败的时候。失败的话直接返回空指针。这也就解释了为什么realloc的返回类型不能用旧的那个指针去接受(上面两种情形还真不能给你解释,这种情形可以解释了)因为一旦扩容失败,realloc返回NULL,然后把NULL赋给旧的那个指针,好了旧的那个指针现在不指向原先的起始地址处了指向0地址去了,你realloc扩容失败后还把人家原先的数据搞丢
了,p再也想不起来它曾经维护的那个空间了。
小注意点:
1. 如果realloc的第一个参数是NULL,那么它相当于就是malloc()
2. 先把realloc的返回值用新指针接受,然后去判断一下这个新指针是不是空指针,如果不是的话,再把这个新指针的地址赋给旧指针,这时候旧指针就在维护扩容之后的空间了。
调试一下,总结一下
开判用调回
常见的动态内存错误
1. 对空指针NULL进行解引用操作
2. 对动态开辟空间的越界访问
3. 对动态开辟空间的越界访问
4. 对非动态开辟内存使用free释放
放在栈区的内存空间跟free一点关系都没有,free是去释放堆区上的空间的
5. 使用free释放一块动态开辟内存的一部分
使用free释放一块动态开辟内存的一部分
就是说你这个free释放的指针必须是指向这块动态开辟内存的起始地址的
6. 对同一块动态内存多次释放
7. 动态开辟内存忘记释放(内存泄漏)
涉及到函数里面(记性差)
涉及到函数内外部联系的问题
(一个函数里面,可能有些东西是在内存栈区的,有些东西是在内存堆区的。比如说:函数形参,临时变量,局部变量,指针等这些都是在栈区的,跟堆区半毛钱关系没有,而比如说你用malloc开辟的空间是在内存堆区的)
1. 内存栈区里面进入作用域时开辟的空间出了作用域之后就自动会销毁还给操作系统。
2. 而堆区里面你函数里面动态开辟的内存出了函数之后你还是在的,不会还给操作系统,这时候如果你函数里面没有释放的话,一定要返回一个指向空间起始地址的指针,谁申请了空间谁释放,你如果不释放,你也要告诉别人我这边申请过了,你回头去释放
3. 我在函数出来后外边进行释放,如果不返回指针,函数出来后临时变量全部销毁,没有人记得这块空间了,但它还没有还给操作系统了呢。从这个函数里面走出来之后,由于指向这块空间的指针已经被销毁了。所以没人记得这块空间了,这时候想释放都来不及了
这就是内存泄露,操作系统把控的内存被少了去了,malloc申请空间从来不释放,一直在吃内存
内存泄露(动态内存的空间忘记释放了,这被称为内存泄露)
1. 你不用了
2. 你又不释放
3. 别人也用不上
4. 这块空间回头又可能找不上了
最后,有几个调试技巧我还不知道,这边记录一下:
1. 按一下这个,调试的时候就直接会来到此处
2. 要在监视窗口观察指针指向的内存堆区的空间时,可以用逗号