目录
1.动态内存分配的原因
我们目前知道的申请内存的两种方法:
int val = 20;//在栈空间上开辟四个字节 char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是,上述申请内存的办法,一旦开辟完成,内存空间的大小就固定了,就像我们的数组超出内存范围就会产生越界的问题,没有办法改变内存的大小,非常死板,固定化,不够灵活。我们想要更灵活的使用内存空间,当我们觉得少了就补充空间。
总的来说:
上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道(比如主页的通讯录项目,只能存储1000个人信息,当我们的联系人信息到101个就无法存储了), 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
注意:
固定的内存开辟如我们熟知的两种方式虽然内存使用不自由但是内存管理更为简便。
自由的动态内存分配可能会导致更为复杂的内存管理问题。
2.动态内存函数的介绍
2.1malloc和free函数
C语言提供了动态内存开辟,也就是可以申请内存的函数:
函数头文件:<stdlib.h>
函数功能:
① 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针(空间的起始地址)。
②如果开辟成功,则返回一个指向开辟好空间的指针。
比如:我们以前用数组申请40个字节的空间我们会这样来做:
int arr[10];
现在使用malloc可以这样来用:
malloc(40)
那么现在我们对这个malloc申请的空间进行访问,最好的方式是不是可以当做一个整型来访问,那我们将函数的返回值放在一个整型指针中,访问一次跳过4个字节,但是malloc的返回值是void*需要强制类型转换:
int *p = malloc(40);
③如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
int* p = (int*)malloc(40);//开辟成功返回指针,自己要知道申请的这段空间用于做什么,才好确定类型 //可能开辟失败,做一下判断 if (p == NULL) { peror("malloc");//打印错误原因 return 1; } //开辟成功,就可以利用p来访问这块空间 int i = 0; for (i = 0; i < 10; i++) { printf("%d\n", *(p + i)); }
看一下打印结果:
这里补充图解:
③返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。
④如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器
malloc(0)
建议不这么写。
⑤malloc只负责申请或者开辟空间,当空间用完(不是吧空间用尽的意思,是申请内存来做事这个动作结束)不会主动还给操作系统,除非程序结束。当程序不退出,动态申请的内存不会主动释放。所以当我们用完申请的空间后,就要使用free空间来释放我们申请的空间将他还给操作系统,不然就会出现很大的内存问题。
举例来说:
我们看这段代码:
while(1) { int* p = malloc(40); }
这就是一个病毒程序,只要我们一启动,程序就一直向我们的内存申请空间原先申请的又不释放,最终会导致内存卡死,除非关机,,如果写入开机程序,就会循环往复。所以申请空间后需要释放我们介绍free内存释放函数。
C语言提供了free函数,用于做动态内存的释放和回收:
函数头文件:<stdlib.h>
函数功能:
①如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
就是说我们的free只能释放我们的动态申请的内存空间,如果:
int a = 0; int * ptr = &a; free(ptr);//错误写法
这种用法就是C语言标准未定义的是错误的。
②如果参数 ptr 是NULL指针,则函数什么事都不做。
③当空间被释放后,原先指向这片空间的指针需要我们手动将其置空,变为空指针,因为原先的空间被释放了,但是指针里面还保存着我们原先的地址,当不慎使用这个指针的时候,这个时候我们的指针就会变成野指针,会造成程序出现问题所以需要手动置空。
int main() { //代码1 int num = 0; scanf("%d", &num); int arr[num] = {0}; //代码2 int* ptr = NULL; ptr = (int*)malloc(num*sizeof(int)); if(NULL != ptr)//判断ptr指针是否为空 { int i = 0; for(i=0; i<num; i++) { *(ptr+i) = 0; } } free(ptr);//释放ptr所指向的动态内存 ptr = NULL;//手动置空 return 0; }
补充:free函数可能运作的机制:
当我们的malloc去开辟空间的时候,比如我们需要开辟40个字节的空间,但是当我们的函数在执行的时候可能会稍微开辟大一点的空间:
![]()
2.2calloc函数
C语言还提供了一个函数叫 calloc , calloc 函数也用来动态内存分配。原型如下:
函数头文件:stdlib.h
函数功能:
①函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
②与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
③也有可能开辟失败,所以需要和malloc一样做判断
例子使用:
int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } //开辟成功打印 int i = 0; for (i = 0; i < 10; i++) { printf("%d", p[i]); } //释放 free(p); p = NULL; return 0; }
将每个字节初始化为0.
我们看一下开辟失败的情况:
int main() { int* p = (int*)calloc(INT_MAX*2, sizeof(int)); if (p == NULL) { perror("calloc"); return 1; } //开辟成功打印 int i = 0; for (i = 0; i < 10; i++) { printf("%d", p[i]); } //释放 free(p); p = NULL; return 0; }
2.3realloc函数
realloc函数的出现让动态内存管理更加灵活。 有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时 候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小 的调整。
函数头文件:stdlib.h
- ptr 是要调整的内存地址(被调整空间的起始地址,如果这里传入一个空指针和malloc函数功能一样)
- size 调整之后新大小,这里不是在后面补充,是包括原先已经申请好的大小,比如原先申请了10个空间,不够,不能说size传10变成20个空间,而是传20,变成一个20大小的空间。
- 返回值为调整之后的内存起始位置。(所以当我们用一个指针来接收返回值的时候,这个指针可能指向原先malloc开辟的空间的起始位置也有可能指向一块新空间,这和realloc函数功能相关,下面两种情况:)
- 增容失败会返回空指针。
补充:
int * p = (int*)malloc(40); if(p ==NULL) { perror("malloc"); return 1; } //增加空间 p = realloc(p,80);
注意:这种写法不严谨,因为我们的realloc函数有可能增容失败,比如后续实在没有空间了,就会增容失败,失败返回空指针赋值给P,就导致P原先维护的40个字节的空间也找不到了,内存空间既用不上也找不到,就会造成内存泄漏。正确的做法应该是重新使用一个指针来接收。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。 realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间:直接在后面开辟空间后返回原始空间的地址。
情况2:当申请空间的后续空间的内存被别的占用无法扩展,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。
四步:
1.首先会开辟新的空间2.会将旧空间中的数据看拷贝到新空间
3.释放旧的空间(realloc函数会做)
4.返回新的空间的起始地址
我们来看一下这个函数的正确使用:
int main() { int* p = (int*)malloc(40); if (p == NULL) { perror("malloc"); return 1; } //开辟成功打印 int i = 0; for (i = 0; i < 10; i++) { p[i] = i + 1; } //增加一些空间 int * ptr = (int*)realloc(p, 80); //判断一下 if (ptr != NULL) { p = ptr; ptr = NULL; } else { perror("realoc"); return 1; } for (i = 0; i < 20; i++) { printf("%d\n", p[i]); } //释放 free(p); p = NULL; return 0; }
3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
如果malloc开辟空间失败,返回空指针就会出问题,应该是开辟伴随着判断。
3.2 对动态开辟空间的越界访问
我们申请的空间也是有大小的,不可以申请40字节空间,做需要80空间的事。
3.3 对非动态开辟内存使用free释放
我们的free只能释放我们的动态申请的内存空间
3.4 使用free释放一块动态开辟内存的一部分
注意对比:
p[i] = i+1;
p[i] = *(p+i)//p指向的位置没变
3.5 对同一块动态内存多次释放
3.6 动态开辟内存忘记释放(内存泄漏)
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
这是一段极端代码,我们的程序运行起来,,由于p是局部变量,出了函数就销毁了,那开辟的空间虽然在,我却没有地址找到了,想释放都释放不了用不上也找不到就内存泄漏了。
每天跑一跑,内存少一少,内存耗干重启又开始耗,这就是一个服务器BUG代码。
最好的理解例子:
梁朝伟的无间道:
当卧底的一直和唯一的警察单线联系,当这个警察死掉了,就没人知道你的真实身份了。
4.动态内存经典笔试题
4.1题目1
void GetMemory(char *p) { p = (char *)malloc(100); } void Test(void) { char *str = NULL; GetMemory(str); strcpy(str, "hello world"); printf(str); }
问运行会有什么样的后果
分析:
代码问题:
①对空指针进行解引用,导致程序崩溃
②动态申请的空间没有释放,且无法释放导致内存泄漏。
4.2 题目2:
char *GetMemory(void) { char p[] = "hello world"; return p; } void Test(void) { char *str = NULL; str = GetMemory(); printf(str); }
问,运行以上代码会有什么样的结果:
分析:
结果是非法访问
这是典型的返回栈空间地址的问题
当我们使用static修饰,延长这个空间的生命周期就可以了:
4.3 题目3:
请问运行Test 函数会有什么样的结果?
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;
5. C/C++程序的内存开辟
C/C++程序内存分配的几个区域:
1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。
2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。 实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。
6.结语
以上就是本期的所有内容,知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,感谢大家的关注与喜欢。