前言:
在之前我们学习的内存空间的大小开辟是固定的,但是在我们日常生活中,如一个班级的人数确有可能变化,那我们该怎么开辟空间呢?那就一起来学习动态内存的管理吧。
一、为什么存在动态内存分配
在之前我们学习开辟内存空间的大小是固定的(不能被修改),如:
int a ;//在栈区开辟4个字节的空间
int arr[5];//在栈区开辟20个字节的连续空间
我们发现,上述开辟内存空间的方式有两个特点:
1、空间开辟大小是固定的。
2、数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。(注:变长数组仅仅是在开辟之前可以人为的指定数组的大小,但是在数组开辟之后数组的大下就不能改变了!)
但是现实生活中对空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时开辟空间的方式就不能满足了,这个时候就需要动态内存开辟来解决问题了。
动态内存分配:
百度百科:
在C/C++语言中,编写程序有时不能确定数组应该定义为多大,因此这时在程序运行时要根据需要从系统中动态多地获得内存空间。所谓动态内存分配,就是指在程序执行的过程中动态分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
简单的说,动态内存分配就是动态地分配或回收存储空间的方法。
特点:
由系统根据程序的需要即时分配。
C语言:
在C语言中,动态内存分配是由系统提供的库函数malloc、calloc、realloc和free实现。
C++语言:
在C++语言中,动态内存分配是由运算符new、delete实现。
简单内存图:
二、动态内存函数的介绍
1、malloc函数
malloc是C语言提供的一个动态内存开辟的函数:
函数原型:
void* malloc(size_t size);
函数作用:
申请一个内存块,该函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
具体讲解:
1、如果开辟成功,则返回一个指向开辟好空间的指针。
2、如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
3、返回值的类型是void*,因为malloc函数并不知道开辟空间的类型,具体类型在使用的时候使用者自己来决定。
4、如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
5、参数size是开辟空间的大小(单位:字节)
2、free函数
free是C语言提供来专门用来做动态内存的释放和回收的函数:
函数原型:
void free(void* ptr);
函数作用:
free函数专门用来释放动态开辟的内存。
具体讲解:
1、如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
2、如果参数ptr是NULL指针,则函数什么事都不做。
3、参数ptr是指向释放的动态开辟的内存的指针,void*是因为不知道是什么类型的指针
为什么要有free函数呢?
答案是:在堆区申请的空间,在程序结束后才会被操作系统回收,如果程序一直不结束,也不使用该空间就造成了浪费,所以我们使用free库函数用来释放动态开辟的内存。
编程好习惯:
1、当申请的动态内存使用完之后一定要记得使用free函数释放所开辟的空间。
2、free释放完之后的指向动态内存开辟的空间的指针变量不会改变,仍能找到那块空间,有危险(可能非法访问),所以使用完free之后一定记得将其赋值为NULL。
代码演示1:动态内存开辟10个整形
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { //向内存堆区申请40个字节的空间,用来存放10个整形 int* p = (int*)malloc(40); //检查malloc的返回值是否为空 if (NULL == p) { //打印错误信息 printf("%s\n", strerror(errno)); return 1; } //开辟成功,则存放1~10的整形 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i + 1; } //打印 for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //当动态申请的空间不再使用的时候,我们使用free释放在堆区申请的内存 free(p); //空间释放之后p没有改变,所以仍然能找到那块空间,有危险(非法访问),所以将p赋NULL p = NULL; return 0; }
运行结果:开辟成功
为什么malloc函数调用前面要强制类型转换?
答案是:malloc的函数返回类型是void*
知识巩固:空类型
void表示空类型(无类型),通常应用于函数的返回类型,函数的参数,指针类型。
1、函数的返回值类型:函数调用完后,什么都不需要返回。
2、函数的参数:这个函数无参,在调用的时候不能传参。
3、指针类型:当你不知道指针变量存放什么类型的地址时,设计成void*,void*是五具体类型的指针。
①void*的好处:可以存储任意类型的地址;
②void*的坏处:不能直接使用,因为无具体类型,不知道是什么类型的地址。
③使用方式:强制类型转换。
代码演示2:动态内存开辟失败
#include<stdio.h> #include<stdlib.h> #include<string.h> int main() { //向内存堆区申请INT_MAX个字节的空间,用来存放10个整形 //#define INT_MAX 2147483647 int* p = (int*)malloc(INT_MAX); //检查malloc的返回值是否为空 if (NULL == p) { //打印错误信息 printf("%s\n", strerror(errno)); return 1; } //开辟成功,则存放1~10的整形 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i + 1; } //打印 for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //当动态申请的空间不再使用的时候,我们使用free释放在堆区申请的内存 free(p); //空间释放之后p没有改变,所以仍然能找到那块空间,有危险(非法访问),所以将p赋NULL p = NULL; return 0; }
运行结果:
3、calloc函数
calloc函数也是C语言提供用来动态内存分配的。
函数原型:
void* calloc(size_t num,size_t size);
函数作用:
calloc函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0,然后返回它的起始地址。
calloc与malloc的区别:
1、malloc只有一个参数(开辟空间的大小),calloc有两个参数,分别为num元素的个数和size每个元素的大小。
2、malloc申请到空间,没有初始化,直接返回起始地址;calloc申请好空间后,会把空间初始化为0(申请空间的每个字节初始化为全0),然后返回起始地址。
3、malloc的效率高些但不初始化,calloc的效率低些但初始化。
代码演示:申请10个整形空间
#include<stdio.h> #include<stdlib.h> int main() { //在堆区申请10个整形空间 int* p = (int*)calloc(10, sizeof(int)); //检查calloc的返回值是否为空指针 if (NULL == p) { //打印错误信息 perror("calloc"); return 1; } //使用 int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //动态内存使用完后用free释放所开辟的空间 free(p); //free释放之后p不改变,防止非法访问,将其置为空指针 p = NULL; return 0; }
运行结果:
4、 realloc函数
realloc是调整动态开辟内存空间的大小的函数。
函数原型:
void* realloc(void* ptr,size_t size);
函数作用:
realloc函数让动态内存管理更加灵活,可以做到对动态开辟内存大小的调整。
具体讲解:
1、ptr是要调整的内存地址(如果ptr为一个空指针,则功能与malloc函数类似)。
2、size是调整之后的新大小(有时我们发现过去申请的空间太小了,有时我们又会觉得申请的空间太大了,那为了合理的内存,我们一定会对内存的大小做灵活的调整)。
3、开辟成功,返回值为调整之后的内存起始地址;开辟失败,返回值为一个空指针。
4、realloc函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到(拷贝)新的空间。
5、realloc在调整内存空间时存在三种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间
情况3:找不到合适的空间来调整大小
图示:
情况1:后边有足够大的空间可以扩容,直接在原有空间之后追加空间,原有空间数据不变,新空间数据为随机值,返回旧的空间起始地址。
情况2:后边没有足够的空间可以扩容,realloc会另找一块合适大小的新的的连续空间,把旧的数据拷贝到新空间的前面位置,并且把旧的空间释放掉,同时返回新的空间的起始地址。
情况3:找不到合适的空间,realloc开辟空间失败返回空指针。
编程好习惯:
使用一个新的指针变量来接收realloc函数的返回值(防止realloc开辟空间失败后,原来的空间也不能使用。)
代码演示:
#include<stdio.h> #include<stdlib.h> int main() { //开辟5个整形的空间 int* p = (int*)malloc(5 * sizeof(int)); //判断空间是否开辟成功 if (NULL == p) { //打印错误信息 perror("malloc"); return 1; } //使用 int i = 0; for (i = 0; i < 5; i++) { *(p + i) = 1; } //malloc开辟的空间不够用了,希望再加5个整形的空间 int* ptr = (int*)realloc(p,10 * sizeof(int)); //判断是否开辟成功,开辟成功仍用p来指向这块空间 if (ptr != NULL) { p = ptr; ptr = NULL; } else { //打印错误信息 perror("realloc"); return 1; } //继续使用 for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } //释放 free(p); p = NULL; return 0; }
运行结果:
三、常见的动态内存错误
1、对NULL指针的解引用操作
malloc、calloc、realloc函数开辟空间失败,返回一个空指针。
好的编程习惯:
对他们的返回值进行判断,是否为空指针。
坏代码的演示:不对他们的返回值进行判断,直接使用
//1、对空指针进行解引用操作 //malloc、calloc、realloc函数开辟空间失败返回一个空指针 #include<stdlib.h> int main() { int* p = (int*)malloc(5 * sizeof(int)); //不判断malloc是否开辟成功,p是否为空指针直接使用p int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i;//直接使用p } //释放 free(p); p = NULL; return 0; }
编译器都会警告:
好代码的演示:对他们的返回值进行判断,再使用
#include<stdlib.h> #include<stdio.h> int main() { int* p = (int*)malloc(5 * sizeof(int)); //判断malloc是否开辟成功,p是否为空指针直接使用p if (NULL == p) { //打印错误码 perror("malloc"); return 1; } int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i;//直接使用p } free(p); p = NULL; return 0; }
2、对动态开辟空间的越界访问
为了防止越界访问,使用空间时要自己注意开辟空间的大小。
错误代码演示:
#include<stdio.h> #include<stdlib.h> int main() { //开辟的是25个整形空间 int* p = (int*)malloc(100); if (NULL == p) { perror("malloc"); return 1; } int i = 0; //指针p只管理25个整形的空间,当i >= 25时发生越界访问 for (i = 0; i < 100; i++) { *(p + i) = i; } //释放 free(p); p = NULL; return 0; }
3、对非动态开辟内存使用free释放
free函数是专门用来释放动态开辟的内存空间的。
错误代码演示:使用free释放堆区的内存
#include<stdlib.h> #include<stdio.h> int main() { int a = 10;//栈区 int* p = &a; free(p);//使用free释放栈区的空间 p = NULL; return 0; }
运行结果:
4、使用free释放一块动态开辟内存的一部分
1、free释放动态内存空间的时候,要从它的起始地址开始释放。
2、注意自增自减运算符的副作用,他们不仅仅是值发生了改变,自身也发生了改变。
3、动态开辟的内存由指针变量p维护时,p不要改变。
错误代码演示:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(20); //判断是否开辟成功 if (NULL == p) { perror("malloc"); return 1; } //使用 int i = 0; for (i = 0; i < 5; i++) { *p = 1; p++;//注意自增带来的副作用,它不仅是把地址向后跳了4个字节,自己的值也跳了4个字节 } //因为p变了,所以释放的p不再指向动态内存的起始地址 free(p); p = NULL; return 0; }
运行结果:
5、对同一块动态内存多次释放
free函数:
如果参数ptr是NULL指针,则函数什么事都不做。
编程好习惯:
free释放完之后的指向动态内存开辟的空间的指针变量不会改变,仍能找到那块空间,有危险(可能非法访问),所以使用完free之后一定记得将其赋值为NULL。
错误代码演示:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(4 * sizeof(int)); if (NULL == p) { perror("malloc"); return 1; } //使用 //…… //释放 free(p); //不将p置为空指针 // …… //忘记已经释放了p,重复释放 free(p); return 0; }
运行结果:
6、动态开辟内存忘记释放(内存泄漏)
动态开辟的内存空间有两种回收方式:
1、主动释放(free)
2、程序结束(操作系统回收)
但是如果程序在服务器上一直运行,如果你不主动释放或者你找不到这块空间了,就会导致内存泄漏问题。
内存泄漏(memory leak):
内存泄漏是指程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
坏代码演示:malloc开辟的内存没有free释放
#include<stdlib.h> void test() { int* p = (int*)malloc(20); if (NULL == p) { return 1; } //使用 //…… //此时使用完之后忘记释放这块空间 } int main() { test(); //…… //想起来test中动态开辟内存未释放, //此时在main释放,释放不了,因为维护那块内存的指针p是局部变量,在主函数找不到p free(p); return 0; }
好代码演示:malloc开辟的内存使用完之后free释放
#include<stdlib.h> #include<stdio.h> //test1内部进行了malloc操作,仅在test函数内部使用这块空间, // 所以在test内部记得释放 void test() { int* p = (int*)malloc(20); if (NULL == p) { return 1; } //使用 //…… //此时使用完之后释放这块空间 free(p); p = NULL; } //test1内部进行了malloc操作,在主函数还要使用这块空间, // 所以返回了malloc开辟空间的起始地址,在主函数记得释放 int* test1() { int* p = (int*)malloc(20); if (NULL == p) { return 1; } //使用 //…… //此时使用完之后在主函数还要使用,返回malloc开辟空间的起始地址 return p; } int main() { test(); int* ptr = test1(); //使用 //…… //释放 free(ptr); ptr = NULL; return 0; }
作者水平有限,希望对大家有帮助,如有错误恳望读者指正!