C语言笔记(鹏哥)上课板书+课件汇总(动态内存管理)--数据结构常用
动态内存管理
引言:将内存升起一段空间存放数据有几种手段
- 创建变量:存放一个值
- 创建数组:存放多个连续的一组值
以上开辟的内存空间是固定的,创建大了,空间浪费,创建小了,空间不够。并且一旦创建好了就不能调整,不够方便灵活
- 动态内存管理(分配):程序员可以自己申请和释放空间,灵活多用
一、malloc函数
memory allocate 内存,申请(开辟)
查看函数声明请见:c/c++官网
头文件:stdlib.h
作用:在内存堆区中申请一段连续的内存空间
实参:size_t类型(unsigned int,long,long long)类型,单位是字节的空间
返回类型:void* 返回开辟内存空间的起始首地址(首指针)
- 编译器并不知道你要用这段内存空间存储什么数据,所以设置了void*类型的返回指针类型。—>其实就是泛型编程,使用更加广泛,可以存储各种类型数据,使用时只需要强转就好
- 当你使用的时候,确定自己要存储什么类型的数据,就用什么类型的指针接收,然后将malloc返回类型强转成与当前指针类型一致的即可,一致才能赋值
创建空间成功:返回开辟内存空间的首地址
创建空间失败:返回空指针
因此需要对返回值进行检验,使用perror函数print+error:perror(“malloc”);编译器自动在malloc后面追加错误
参数 size 为0:malloc的⾏为是标准是未定义的,取决于编译器。
malloc综合使用:
#include<stdlib.h>
int main()
{
//申请10个字节整形空间,使用sizeof编译器自己计算使用空间
int* p = malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1; //错误程序退出为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;
}
如果开辟空间失败了:将参数设置为INT_MAX整形最大值,编译器环境设置为x86,x64太大了
int main()
{
//申请10个字节整形空间,使用sizeof编译器自己计算使用空间
int* p = malloc(INT_MAX);
if (p == NULL)
{
perror("malloc");
return 1; //错误程序退出为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;
}
malloc函数申请的空间和数组的空间有什么区别呢?
1、数组申请的空间不可调整大小,malloc动态内存申请的空间可以调整大小
2、申请空间的位置不同
二、free函数的使用
释放和回收动态内存申请的空间,经常与malloc,calloc,realloc搭配使用,申请+释放,然后用NULL将指针置空,malloc和free都声明在 stdlib.h 头⽂件中。
void free (void* ptr);
参数是任意类型的指针,也就是存放任意类型数据的空间都可以被释放掉,只要是动态申请来的空间。
- 释放空间之后,指针变量里面还是会存储之前的地址,所以是很危险的,这块空间已经不属于你了,但是你还记得地址,相当于是野指针,所以需要拿NULL把野指针拴住,也就是将指针置空,后续在使用的时候就找不到不属于你的空间了。
- free叫做手动释放动态申请的内存,如果使用free在程序结束后这块内存也会自动被操作系统回收,最好是使用free释放你申请的空间,既然申请了就对他负责嘛,就要释放掉哦,
- 如果程序运行的时间很长,然后你申请的空间用不上了而且还有剩余,但是你不去释放,这些空间就会一直被占用着,就无法被其他地方所使用,造成空间的浪费,到最后才将这些空间一并还给操作系统(好比说我借你一本书,你看完了就还给我嘛,还给我我还可以借给别人,使得值本书被高效利用)
- 还有一种情况如果你没有free也可能造成空间被占用着,但是你找不到地址的情况,也释放不了,这样的代码多了,就造成内存泄漏了。
综合使用规则:
//使用之后应该释放动态内存空间
free(p);
p = NULL;
注意事项:
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
回收内存空间:
如果是数组创建的空间,是在栈区,那么在进入大括号是被创建,出了大括号便把这块空间归还,但是动态内存申请实在堆区,需要你手动释放或者是程序结束后被操作系统自动回收。
三、calloc函数
参数及作用:在堆区中申请num个字节为size的空间,并且将申请到的空间进行初始化全0操作
返回值:和malloc一样
malloc与calloc的区别:
malloc:不会初始化操作,申请到的空间中存放的都是cd,cd,cd…这样的16进制随机值,步骤更少,更快捷
calloc:会初始化,申请空间并且将空间中的值都初始化为0,步骤多一些
请观看他们在内存中申请的空间样子:
首先按f10或者f11进入调试,点击窗口,内存,接下来继续按f11逐步调试,直到调试完malloc或calloc这个语句之后,输入p回车,编译器会自动计算p的地址,然后找到地址内存处
malloc:
calloc:
四、realloc函数
repeat+allocate,再次开辟(调整动态申请的内存空间) 使得动态空间缩放灵活
参数:任意类型的指针ptr,是原来使用malloc,realloc,calloc函数开辟的动态内存空间的起始地址,size是你预期的空间大小,就是你希望将空间调整为多大
返回值:请先看下面realloc开辟空间成功的两种情况,如果情况1则返回原来的地址,如果是情况2,则返回新的地址,如果开辟失败则返回空指针
realloc开辟空间成功的两种情况:
1、最开始要调整的那块空间后面还有足够的容量能够调整到你需要的大小,那么直接调整大小,并且返回和之前一样的地址(内存空间的起始地址)
2、最开始要调整的空间后面并没有足够的容量能够调整到你需要的大小,需要的内存空间已经被占用了,realloc函数会直接在内存中找一块符合你要求大小的内存块,并且将新的地址返回给你
在情况2中realloc函数会进行的操作:
1、在内存中找一块符合你要求大小的内存空间
2、将原始内存中的数据拷贝过来
3、释放掉旧的空间
4、返回新空间的起始地址
注意事项:
1、realloc返回值的接收要使用其他的指针(以ptr为例),指针的类型还是和你要存储的数据保持一致,不可使用原来用malloc/calloc/realloc函数开辟空间的起始地址,因为如果使用原来指针接收,一旦开辟失败,就会返回一个空指针,那么原来的指针变成了一个空指针,不但没有扩容或缩容成功,原来开辟的空间也找不到了,就得不偿失了。
2、传入的指针只能是你需要调整空间的首地址,不可以是你想从哪扩大就直接传入那的地址
3、在使用之前要判断ptr是否为空,如果不为空才可以把ptr的值传给p,否则就直接是扩容或缩容失败,直接上perror函数捕捉错误,终止程序return 1;
综合使用方法:
int main()
{
int* p = calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//开辟成功,想要扩大空间,缩小直接将size改小一点就可以了
int* ptr = realloc(p, 12 * sizeof(int));
if (ptr != NULL)
{
p = ptr;
}
else
{
perror("realloc");
return 1;
}
//使用过程......
//释放空间
free(p);
p = NULL;
return 0;
}
使用realloc函数开辟空间:
如果将ptr置为NULL,那么使用方法就和malloc一样了
int main()
{
int* p=(int*)realloc(NULL, 10 * sizeof(int));
//数组一样去使用...
//释放
free(p);
p = NULL;
return 0;
}
前面说的空间开辟成功会有两种情况,我在监视中给大家调试看一看:
- 监视和上面我教给大家的步骤一样,只是把内存换成了监视窗口
1、调整的大小比较小,属于第一种情况,地址不变
2、调整的大小较大,属于第二种情况,地址改变:
3、空间开辟失败呢:有可能的,内存也是资源,资源就不是无节制的
4、当你不想使用ptr之后也可以将ptr置空,因为不使用某指针的时候就需要将其置空,防止其成为野指针,也方便后续再次使用这个名字作为指针名,也就是我们之前讲过的,使用指针前都要进行assert断言判断一下指针是否为空的原因,为空才可以继续使用,不为空要先置空在使用。
五、动态内存分配常见的错误
1、对空指针的解引用操作:对于malloc的返回值一定要进行判断,是否为空,实际上这样写编译器也会给你警告
int main()
{
int* p = malloc(10 * sizeof(int));
//直接使用
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = 0;//形成对p的解引用,如果i是0的话,*(p+i)就是空指针的解引用
}
//....
//释放
free(p);
p = NULL;
return 0;
}
1、对动态开辟的空间越界访问
对malloc开辟空间的单位理解不清:申请了10个整形的空间,结果访问40个
int main()
{
int* p = malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//*(p+i),p这个指针走一步跳过一个整形4字节,开辟了40个字节的空间,
// 并不是每次解引用之后都跳过一个字节走40次
int i = 0;
for (i = 0; i < 40; i++)
{
p[i] = 0;
}
//....
//释放
free(p);
p = NULL;
return 0;
}
2、对非动态开辟的内存使用free释放
与动态内存相关的错误,关闭窗口要关闭好几次才行,都是这个特点
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
//。。。。使用
//糊里糊涂的释放了
free(p);
p = NULL;
return 0;
}
3、free释放一块动态开辟内存的一部分
起始位置处的指针不要乱动,起始位置的指针记录着开辟空间的起始位置,想找到这块空间就必须依靠这个指针,所以尽量不要动这个指针,如果还想使用指针记录这个位置往后移动的话(例如遍历等等操作)可以创建一个新的指针来移动,循环中p[i]其实p还是在初始位置,其他的元素的地址是p+i所以不用担心指针p位置变了。
事例代码:造成释放内存的一部分
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
for (int i = 0; i < 10; i++)
{
*p = i;
p++;
}
//将指针移动了,指针没有指向空间的起始位置,所以释放的时候释放不掉整个动态内存申请的空间
free(p);
p = NULL;
return 0;
}
4、对同一块内存空间多次释放
每次free之后,都将指针置空是很有必要的
- 如果后续忘记了,又将其置空一次也没关系,因为已经置空指针了,再次释放就相当于空指针释放,不起任何作用
- 如果不将其置为空指针,使用两次free就会出现问题。
请看代码:
1、前期置空了,代码不会出现问题:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//...
//释放
free(p);
p = NULL;
//...
//....
//忘记了又去释放了
free(p);
p = NULL;
return 0;
}
2、如果没有置空,又去释放:
5、内存泄漏
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
事例代码:
void test()
{
int flag = 1;
int* p = malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
//此处定义一个条件,这个条件发生了提前返回主函数了
if (flag)
{
return;
}
//释放
free(p);
p = NULL;
}
int main()
{
test();
//....
//...程序一直运行下去,造成内存蚕食(内存泄露)
return 0;
}
- 在代码中我malloc申请了空间,也对其进行了非空判断,也进行了释放
- 但是在释放之前如果发生某条件,直接返回到主函数,就没有起到释放空间的作用。出了这个函数,p是局部变量出了test会自动被回收,那么在程序后面你就再也找不到p这个地址了,想释放都释放不了这个空间了
- 这块空间存在,占用着,但是仿佛消失了,你找不到它,这样的代码多了就会造成你的内存在一点一点被蚕食,最终导致程序崩溃------>这叫做内存泄漏
- 如果在服务器这种系统中,代码7*24小时一直运作下去,程序不结束,发生内存泄露之后一直不能将内存释放还给操作系统,就会崩溃
切记:一定要对申请的动态内存进行释放,并且要正确释放,动态内存管理很方便灵活,但是风险也大,所以有些编程语言不允许进行动态内存管理的,给你提供各种垃圾回收的系统,c/c++提供,可见其追求性能到极致哈哈哈
六、一些常见的笔试题:
1、对NULL指针进行解引用程序崩溃 + 内存泄漏
输出结果是:程序崩溃
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
在内存中开辟的空间是这样的:
- 首先进入主函数,调用test函数,栈区创建指针str,存储NULL,调用函数GetMemory,将str进行值传递传递给形式参数p
- 形式参数指针p是str的一份临时拷贝,所以也在栈区空间上开辟一块空间给p拷贝str的值,malloc在内存的堆区开辟一块100字节空间,地址赋给指针p,p就能指向动态开辟空间了
- 但是出了GetMemory函数之后这个p就被销毁了,局部变量存储在栈区,在进入大括号时被创建,出大括号时销毁,事实上这个地址就不见了,找不到这块内存空间后续也释放不了,也不能给其他人使用,直到程序运行结束之后才会被回收到操作系统,就造成了内存泄露。
- 然后回到Test函数,strcpy—>string copy函数,将前面的指针解引用后找到那块空间,并将后面的字符串 + \0拷贝到这块空间里面,此时相当于对str进行解引用,空指针解引用,程序会崩溃
实际上这种操作是想干什么呢?
想使用p指针在动态内存上开辟空间并且传给栈区上创建的指针str,然后str上便具有这个地址也就能找到这块空间进行strcpy操作了,出函数之后p被销毁了就不会造成野指针p了,所以不用对p置空,然后我们不使用空间之后进行free(str);str = NULL;就可以了
正确操作:最终输出结果都是hello world
1、使用传址调用,形参直接修改实参值,在函数里面释放内存空间
void GetMemory(char** p)//传进来一级指针的地址,用二级指针接收
{
*p = (char*)malloc(100);//*p就是str指针
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);//将str中存储了动态空间的地址
strcpy(str, "hello world");//内存中拷贝了hello world
printf(str);
//释放
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
2、使用传址调用,形参直接修改实参值,在主函数里面释放内存空间
void GetMemory(char** p)//传进来一级指针的地址,用二级指针接收
{
*p = (char*)malloc(100);//*p就是str指针
}
char* Test(void)
{
char* str = NULL;
GetMemory(&str);//将str中存储了动态空间的地址
strcpy(str, "hello world");//内存中拷贝了hello world
printf(str);
return str;//想让主函数给你释放动态内存的话,你要给他地址啊
}
int main()
{
char * str = Test();
free(str);
str = NULL;
return 0;
}
3、直接将p地址返回,用str接收,不用传递str过去了
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;//返回动态内存存储的地址
}
char* Test(void)
{
char* str = NULL;
str = GetMemory();//使用同类型空指针接收
strcpy(str, "hello world");//内存中拷贝了hello world
printf(str);
return str;//想让主函数给你释放动态内存的话,你要给他地址啊
}
int main()
{
char* str = Test();
free(str);
str = NULL;
return 0;
}
2、注意:printf打印字符串
1、打印字符串的时候可以直接printf(“字符串内容”);
实际上就是将字符串的首地址直接给printf了,就不用使用printf(“%s”,“字符串内容”)了
3、返回栈空间地址----常见的错误(返回变量是可以的,但是返回地址是不可的)
返回变量:是先将其出处在寄存器中,出括号销毁变量就销毁吧,值还是给了接收变量
返回地址:
1、输出结果是:随机值
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0 ;
}
在内存中的过程:
- 进入Test(),栈区创建指针变量str,赋值为空指针
- 进入GetMemory函数,创建字符数组p存储hello world\0,p数组名存储了数组首元素地址,返回p,然后出函数
- 本意是想让str接收到这个地址,并且将地址内容打印出来,printf(“字符串数组的首地址”)也是打印出字符串哦。
- 出了函数之后这段空间就还给操作系统了,即使你记住了地址传给了str也没有用
- 这个指针str现在就是一个野指针,去访问它没有访问权限的地方,如果这块空间已经被其他值占用了那么输出其他值 + 剩余的位数就输出随机值,如果这块空间还没有被利用,那么就可能会非法访问到这个值,但也是非法的
- 其实就相当于我今天在如家酒店开一个房间,然后我把房间号发给你了,让你第二天过来住,但是第二天早上我把房间退了,你找到这个房间想住进去,结果被告知没有住的权力,或者这个房间已经租给别人了
改正:如果使用动态内存开辟空间就不会这样,只要程序没有结束并且没有手动释放空间就一直可以访问空间
验证一下我刚刚说的:
1、这段代码中虽然这块空间已经不能由a的地址解引用去访问了,但是非法访问之后仍然是10,这是为什么呢,因为这块空间使用权限还给操作系统之后还没有被利用,也就是值还没有被覆盖,空间依然是存在的,只是访问权限会发生变化
2、如果是这样呢:我将打印hello world放在访问前使用,为什么就会输出hello world11呢,其实11是随机值请看下面栈中分配空间解释的很详细
-
栈区中的解释:首先开辟主函数空间,在主函数空间开辟a指针变量记录test地址,在上面开辟test空间,在test里开辟a的空间,返回a的地址,出了函数之后,这块test空间还给操作系统了,然而如果我不去调用printf申请这块空间存放值,这块空间的值就还是10,没有被改变,这就是为什么将地址a解引用之后还会10
-
然而我调用了printf这样的函数使用了原来开辟的这块空间去存放hello world了,(原来打印值也会被放在栈区),可能这块被申请的空间还有剩余就会输出一些其他值,
鹏哥给找的一点笔试题
这道题就是典型的返回栈空间地址,一旦有接收他的指针就是一个野指针
- 这道题ptr并没有初始化是一个野指针,所以对它进行解引用是非法的,压根没有任何东西给你解引用,由于局部变量未初始化是随机值,所以不知道会有什么样的地址给ptr,但这块空间绝对是非法访问
4、没有对动态内存空间释放
使用一气呵成,最后却没释放造成内存泄漏
改正:只需要加上我注释掉的两行代码即可
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
//free(str);
//str = NULL;
}
int main()
{
Test();
return 0;
}
内存中的过程:由于上面已经讲的够详细这种思路了,这里就不细讲了大家自己看图理解
5、free之后不置空的危害:造成野指针还能继续使用
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
- 操作步骤是:malloc申请100个字节空间,创建字符指针接收空间首地址(说明未来这些空间是用来存放字符类型的),将str指向的空间拷贝hello\0,释放str,此时str还不是空指针,还记得位置,所以自然不会为空,继续在非法访问空间并拷贝word\0了
- 运行结果是:world -----说明续写操作非法将原来的空间写入了world\0并且将hello\0覆盖掉了
总结几点比较好的习惯:荧光笔画的
- malloc之后判断指针是否为空
- 所以这里就体现了free之后将指针置为空的好处,防止其变成野指针,而且之前也讲过,如果忘记了已经释放过了的话,释放两次,如果是空指针在释放就没有任何影响,否则会报错
- 在指针第二讲也讲过,使用指针之前可以先使用断言assert来判断指针是否为空,或者使用if判断,为空说明不再使用指针将其置空了,不再使用,报错程序终止。如果不为空,则说明还可以继续使用。所以不使用指针了一定要记住及时置空哦
这道题目实际上想考察的点是:free之后置空,然后空指针想拷贝也拷贝不了了,非法访问也就不可进行了,这就体现了代码的严谨性