本文主要介绍动态内存管理的相关知识。(用心的文章往往会姗姗来迟,请大家耐心观看)
前言
本文介绍了动态内存的相关知识,讲解较全面,希望大家用心阅读
一、内存空间的分配
1.栈区:存放局部变量,函数的形式参数
2.堆区:动态的内存分配空间会在堆区上开辟
3.静态区:存放全局变量和静态变量(可以是全局也可以是局部)
4.补充:C语言是可以创建变长数组的(也就是柔性数组),c99增加了这种语法标准,但现在还是有许多的编译器不支持这种标准
二、动态内存开辟的函数介绍
2.1 开辟内存的形式
动态内存是在堆区上进行内存空间的开辟,开辟到的是一块儿连续的空间
2.2 malloc函数和free函数
2.2.1 malloc
注意malloc函数的参数是字节个数
下面通过一段代码来学习使用malloc函数
int main() { //向内存申请10个整型的空间 int* p = (int*)malloc(10*sizeof(int)); if (p == NULL) { //打印错误原因的一种方式,之前的字符串函数内容讲到过,不懂得可以去我的博客里面看 printf("%s\n", strerror(errno)); } else { //正常使用空间 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } } }
从代码中可以看出,当我们malloc函数在堆区上开辟空间成功后,使用p指针接收函数的返回值,之后我们可以通过指针p来访问堆区上的这块儿动态内存空间
而且函数的返回值类型是void*,所以使用者在使用时,要自己进行强制类型转换满足不同程序的指针类型需求
为了给大家看一下strerror函数的功能,我们再看一下修改malloc函数参数后的运行结果
注意strerror里面的INT_MAX这个是int整型的最大值,开辟这么大的空间是不可行的(这里我故意用的是x86位的环境,来给大家演示malloc函数开辟失败后的运行结果)
可以看到如果堆区空间不够的话,malloc函数是无法正常的进行开辟空间的,并且malloc函数会返回一个空指针,从而进入if语句,打印了错误信息
所以这里总结一下malloc函数的具体使用方法:
1.malloc函数会在堆区开辟一块儿空间,并且返回指向这块儿空间起始位置的指针
2.如果开辟失败则会返回一个空指针,所以我们使用malloc函数时,一定要对函数返回的指针做检查,看返回值是否为空指针,也就是malloc函数开辟空间是否成功
3.返回值是void*,指定指针类型需要使用者自己进行强制类型转换
4.如果给malloc函数传参为0,malloc的行为是C语言标准未定义的,也就是说我们也不知道malloc函数会怎么做,不同的编译器结果可能会不同
2.2.2 free(和malloc配套使用)
void free( void *memblock );//不可以传非动态内存开辟的空间的地址 • 1
这个函数会释放我们动态开辟的内存空间,将其还给操作系统。
这里有人可能会问,为什么要释放这块空间呢?道理很简单,如果我们将来有机会进入大型企业,面对的客户流量将会是巨大的,如果不停的开辟,不还给操作系统,很有可能造成不必要的空间浪费,使得程序运行性能下降,客户体验降低,所以我们再使用动态开辟的空间之后,要用free函数释放掉这块儿空间
另外一点,当我们释放掉空间之后,原来指向这块儿空间的指针就变成了野指针,如果我们不小心用了这个野指针,它还是能够找到这块已经还给了操作系统的空间的,这就造成了非法访问,所以当一个程序出现野指针时,是非常恐怖的,我们要将野指针再置为空指针,也可以将置为空指针这个步骤形象的理解为,让指针忘掉这块儿空间,就像你和前任分手之后,就完全忘掉她吧,不要再留着人家的电话号码或微信QQ了,也不要再缠着人家了,一直执迷不悟,这样的方式就像通过野指针还能找到那块儿空间一样。所以斩断情丝,开始下一段美好的篇章吧!
2.3 calloc函数
void *calloc( size_t num, size_t size );//参数分别是元素个数和元素的字节大小
void* calloc(size_t num, size_t size); //元素个数和每个元素的字节大小 void* malloc(size_t size); //总字节的大小 int main() { int* p = calloc(10, sizeof(int)); if (p == NULL) { printf("%s\n", strerror(errno)); } else { int i = 0; for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } } //释放空间 //free函数是用来释放动态开辟的空间的 free(p); p = NULL; return 0; }
从运行结果可以看到,calloc函数与malloc的唯一区别就是,calloc函数开辟空间以后,会自动将开辟的空间的每个字节初始化为0,所以如果我们想对开辟的动态内存空间进行初始化的话,我们可以使用calloc函数
2.4 realloc函数
realloc函数的出现让我们的动态内存开辟变得更加灵活。有的时候我们会觉得动态内存开辟的空间较大,有的时候又嫌开辟的空间较小,所以为了更高效的使用内存空间,我们就可以使用realloc函数对动态开辟的空间进行大小调整,以满足使用者的需求。
函数原型如下:
void *realloc( void *memblock, size_t size ); //第一个参数是需要修改的内存空间的地址,第二个参数是需要重新修改的字节大小 //返回值是调整之后的内存空间的起始位置的地址
int main() { int* p = (int*)malloc(20); if (p == NULL) { printf("%s\n", strerror(errno)); } else { int i = 0; for (i = 0; i < 5; i++) { *(p + i) = i; } } //就是在使用malloc函数开辟的20个字节空间 //假设这里,20个字节不能满足我们的使用 //我们希望有40个字节的空间 //这里就可以使用realloc函数来调整动态开辟的内存 int* p2 = realloc(p, INT_MAX); if (p2 != NULL)//如果开辟成功,那我们就重新p2赋值给p,那这样我们就很舒服的可以使用原来的指针变量了 { p = p2; int i = 0; for (i = 5; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p + i)); } } free(p); p = NULL; return 0; //realloc函数使用的注意事项 //1.如果p指向的空间之后有足够的内存空间可以追加,则直接追加,然后返回p指向的空间起始地址 //2.如果p指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域 // 开辟你想要的内存空间大小,并且把原来内存中的数据拷贝过来,释放旧的内存空间,最后返回 // 新开辟的内存空间的地址 //3.我们得需要一个新的指针变量来接收realloc函数的返回值,否则一旦由于我们开辟内存空间过大 // 而导致开辟失败的话,这时候realloc函数会返回一个空指针,这时如果我们用原来的指针接收的 // 话,那么原来指针就变成一个空指针了,这时就比较搞笑了,我想重新改变一下空间,这个函数没 // 开辟成功,反而还把我原来的指针变成空指针了,这就得不偿失了,所以我们一定要用一个新的指针 // 变量来接收realloc函数的返回值 }
我们要注意一点,就是当我们realloc函数无论开辟成功与否,我们都建议用一个新的指针变量去接收他的返回地址,这样我们可以规避掉返回指针是空指针的问题,所以当返回指针是空指针的时候,也没有关系,我们原来的指针变量还能用,当返回指针不是空指针的时候,我们可以将新的指针变量再赋值给原来的指针变量,以便我们的使用习惯
这里再将realloc函数开辟空间的情况细分一下,让大家明白realloc到底是怎样修改空间大小的
就是如果原有空间后面足够大,那我就在原有的空间后面给你再续一段空间,如果原有空间后面不够大,那我就在堆区里面重新找一块儿足够大的空间,并且把你原来空间里面的数据重新拷贝到我现在又找到的新空间当中,然后realloc函数返回新空间的地址
三、常见的动态内存错误
3.1 对空指针的解引用操作
int main() { int* p = (int*)malloc(40); //万一malloc失败,p就被赋值为NULL,那后面的解引用和指针+-就都出问题了 int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
这段代码,其实也就是想帮我们强调一个点,就是我们要对malloc函数返回的指针进行判断,如果是空指针我们应该打印个错误信息什么的(或者其他你想做的措施代码),总归,我们是要进行分支处理的,毕竟空指针是不能进行相应的±解引用等操作的
3.2 对动态开辟的内存的越界访问
int main()int main() { int* p = malloc(5 * sizeof(int)); if (p == NULL) { return 0; } else { int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i;//这里越界访问了,程序会挂掉 } } free(p); p = NULL; return 0; }
我们开辟了5个int字节大小的空间,但使用时,却超出了我们开辟的空间大小,这样就会造成越界访问,程序出现问题
3.3 对非动态内存开辟的内存使用free函数释放
int main() { int a = 10; int* p = &a; *p = 20; //对非动态开辟的内存进行free free(p); p = NULL; return 0; }
对非动态开辟的内存使用free函数释放,这样的行为是标准未定义的,也是C语言语法不支持的
3.4 使用free函数释放动态开辟内存的某一部分
int main() { int* p = (int*)malloc(40); if (p == NULL) { return 0; } int i = 0; for (i = 0; i < 10; i++) { *p++ = i; } //回收空间 free(p); p = NULL; //这时p的位置已经指向到下标为10的位置了,你释放空间的时候就把后面的空间释放掉,应该从起始位置开始释放 //如果我们用*(p+i)这样的方式就不会改变p了,那么就可以释放掉刚刚开辟的空间了 return 0; }
我们释放空间时,要给free函数传的参数是:动态开辟内存空间的首地址,然后将其后面的空间全部释放掉
但现在指针p由于进行了后置++的操作,而这样的操作是会改变指针p所指向的位置的,所以我们不能改变p指向的位置,一旦改变释放内存时就会出现问题了
3.5 对同一块动态内存多次释放
int main()int main() { int* p = (int*)malloc(40); if (p == NULL) { return 0; } //使用 //释放 free(p); /*p = NULL;*/ //…… free(p); //有的时候忘了以前已经释放的话,再次进行释放的话,就会出现问题 //所以如果我们想要避免这种问题的话,就一定要在每次释放之后。加一个赋值空指针的操作,这时候 //如果你还不小心又加了个free操作,那这样也没什么事,因为我们知道free空指针相当于什么都不干 return 0; }
一个点:释放空间后,要记得将原有指针赋值为空指针,以免多次使用free释放动态内存开辟的空间
3.6 动态开辟的内存忘记释放(内存泄漏
int main() { while (1) { malloc(1); //这里我们可以通过任务管理器中性能的内存一项看到,代码开始运行后,内存空间被疯狂的占用,一会 //内存又停止被占用(这可能是因为计算机的自我保护机制起到了作用) } //所以我们要警惕和注意,在你申请开辟内存空间之后,一定要记得给操作系统还回去 return 0; }
由动态图我们可以看出,内存由刚开始的8.多GB变到10.4GB,随后的停止可能由于计算机的自我保护机制起到了作用,所以这也提醒了我们一点,开辟内存之后,一点要记得将内存释放,还给操作系统
四、有关动态内存的几个题
4.1第一道题
void getmemory(char* p)//p就是str的临时拷贝 { p = (char*)malloc(100); //malloc在堆上开辟了一块儿字节大小是100的空间,然后把这个空间地址赋值给指针变量p //所以指针变量p现在就有能力找到字节大小为100的整个内存空间了,但是这个p仅仅只是变量str的一份 //临时拷贝而已,str依旧指向的是空指针NULL,p中存放了刚刚开辟的空间的地址 } void test() { char* str = NULL; getmemory(str);//我把指针变量str传过去,那就是传值调用,如果是传变量的地址&str过去,那才是传址调用 strcpy(str, "hello world");//空指针它并不是一块儿有效的地址,无法指向一块儿有效的空间 //所以这里程序就会崩溃,你硬要对一个空指针进行解引用操作,向其中拷贝字符串“hello world”, //那肯定是会出问题的,因为这个空指针根本就没有开辟一个有效的空间去存放你的字符串 printf(str); } int main() { test(); //1.程序出现崩溃的现象 //2.存在内存泄露的问题,str以值传递的形式给p,p是getmemory函数的形参,只在函数内部有效, // 等getmemory函数返回之后,动态开辟的内存尚未释放,并且无法找到,所以会造成内存泄露 //getmemoty函数中开辟的内存就像是一个警察的卧底,而指针变量p就是卧底的上司,只有这个单线传递的 //上司能够找到这块儿内存,一旦getmemory函数调用结束后,p就死掉了,那么没有人可以证明空间的存在了 //没有人找得到这块儿空间,相当于内存泄露了 return 0; }
p里面不就是存放的abcdef的首字符a的地址么,我们直接将它交给printf函数也可以正常打印出字符串,这三段代码的意义其实都是一样的,用心去感受!
这里代码错误的核心关键点就是,外部函数的局部变量生命周期过短,离开函数之后这些存在栈区里的变量就会被销毁,从而导致无法找到函数内部开辟的动态内存空间,这样也就无法释放这个动态开辟的内存,从而造成内存泄漏。
而且向空指针指向内容进行拷贝字符串,这也是不符合语法规定的,因为空指针根本就没有指向有效的空间啊,你硬往里面拷贝字符串,那肯定是会出问题的呀
那么如何解决这样的问题呢?我们其实有两种解决问题的策略
1.如果非要传值调用的话,我们就将动态开辟的内存首地址返回,用原来的str指针去接收这个返回的空间地址(不至于函数结束时指针变量被摧毁),随后进行使用这个空间和释放这个空间
我们来看一下具体的代码实现
char* getmemory(char * p) { p = (char*)malloc(100); return p; } void test() { char* str = NULL; str = getmemory(str); strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main() { test(); return 0; }
2.我们不妨使用传址调用,因为传址调用是可以改变函数外部的str指针变量的,这样的话,我们可以对指针的地址进行解引用操作,拿到函数外部的指针,将其指向的位置进行修改,修改为我们函数内部动态开辟的内存空间的首地址,让str指针指向这个位置,随后在main函数里面使用这块儿空间最后再释放这块儿空间即可。
也来看一下这种解决方式的代码实现:
void getmemory(char** p) { *p = (char*)malloc(100); } void test() { char* str = NULL; getmemory(&str); strcpy(str, "hello world"); printf(str); free(str); str = NULL; } int main() { test(); return 0; }
4.2第二道题
char* getmemory() { char p[] = "hello world"; //局部变量离开函数后销毁,虽然将地址返回过去,但地址所指向的内容已经销毁了,所以这时指针变成野指针了 //对野指针进行解引用操作,程序必然会崩溃,因为野指针不知道指向哪里 return p; } void test() { char* str = NULL; str = getmemory(); //其实就是返回栈空间地址的问题,地址已经不指向任何内容了,这时你对地址进行访问,那就是非法访问内存 //地址随机,那么相对应的内容其实也是随机的,程序输出结果也是不确定的 printf(str); } int main() { test(); return 0; }
所以这里要注意一个点:动态内存开辟的空间是在堆区上的,指向开辟空间的指针变量是在栈区上的,所以离开函数时,指针变量会被销毁,而内存空间是不会被销毁的,如果你的空间并未在堆区上开辟,而是在栈区的话,那必然会被销毁
我们这里再补充一个野指针的定义:以免我们以后写程序时写出野指针,从而出现大问题
1.指针未初始化
2.指针指向的空间被释放,但指针并没有被置为空指针
3.指针所指向的位置超出自己所管理的空间范围(地址范围),也就是发生越界访问
最后用一段精炼的话,来给大家总结一下野指针的本质是什么
所谓野指针,其实就是其所指向的位置是随机的,不可知的,没有明确限制的,这样的指针就被称为野指针,大家可以用上面的三种情况对照下面的这句话,你就会发现野指针其实就是这么回事**(本质总是那么令人着迷)**
相类似的代码问题
//相类似的代码问题 int* test() { int a = 10; return &a; } int main() { int* p = test(); *p = 20;//我们想要修改变量a的值为20,但其实a变量在离开函数时,已经被销毁了,所以此刻的解引用就是对空指针解引用了 printf(p); return 0; }
4.3第三道题
void getmemory(char**p,int num) { *p = (char*)malloc(num); } void test() { char* str = NULL; getmemory(&str, 100); strcpy(str, "hello"); printf(str); //这里使用完getmemory函数开辟的内存空间后,并没有释放内存,所以造成了内存泄露 //free(str); //str=NULL; } int main() { test(); return 0; }
这个题目就比较简单了嘛,不就是我们使用了动态开辟的内存之后,没有将这块儿空间还给操作系统,从而造成了内存泄漏
所以我们要好好感悟代码中的逻辑,明白逻辑后,你就能从这些题目中,发现蛛丝马迹(也就是出问题的地方)
4.4第四道题
void test() { char* str = (char*)malloc(100); strcpy(str, "hello"); free(str); //1.free释放str指向的空间之后,是不会把str置为空指针的,正因为如此,我们才需要每次将str指向的空间释放之后, //再次将str置为空指针。就好比让你失忆,让你无法再寻找到str指向的空间,也就是让你无法使用str指针 //2.所以下面的if语句会执行,strcpy将原有的hello位置用world给替换掉了,这样的使用方式其实就是非法访问内存, //因为你开辟的空间已经还给操作系统了,但你又不遵守规定,重新使用这块儿空间,这样其实就是非法访问内存了 if (str != NULL) { strcpy(str, "world");//这里代码块儿其实就是保证指针不为空指针的时候,我们再使用这个指针 printf(str); } } int main() { test(); return 0; } //所以我们每次使用完开辟的内存之后,要记得free开辟的空间和将指向开辟空间的指针置为空指针,以免我们后面 //再使用你当初承诺过还给操作系统的内存空间,这样很不礼貌也很没有道德没有操守,也很非法访问内存,这里其实 //用到了我们之前学过的知识,就是当你free之后,相应的指针变成了野指针,而野指针的使用是很危险的,所以我们 //要把野指针置为空指针
虽然我们的代码打印出来了world,并且程序没有给我们报错,但xdm千万不可掉以轻心,这个代码是绝对存在问题的,因为我们对野指针进行了操作,而野指针指向空间是未知的,所以这段代码的问题就是,我们非法访问了内存,本身已经不属于你的空间了,你还要硬生生地使用这块儿空间,这本身就存在问题。
这也告诉我们一个道理,本就不属于你的东西,就算你硬把他拿到手,他终究也是会出问题的,因为心不在你这里,你再怎么折腾,又有什么用呢?
五、补充知识
C/C++中程序内存区域的划分:
1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时,也就是他们的生命周期结束时,这些存储单元会自动被释放。栈内存的分配运算内置于CPU(处理器)的指令集中,访问效率很高,但其内存的分配容量有限,这也就是,我们平常有时候会遇到栈空间溢出的问题。栈区主要存放运行的函数所分配的局部变量,函数参数,返回数据,返回地址等。
2.堆区(heap):一般由程序猿分配释放,若程序猿不释放,程序结束时可能由OS回收。分配方式类似于链表
3.数据段(静态区)(static):存放全局变量,静态数据。程序结束后由系统释放
4.代码段:存放函数体(类成员函数和全局函数)的二进制代码
有了这些知识内容的铺垫,我们就可以很好地理解static关键字修饰局部变量的栗子了,归根结底就是,变量被存放的区域是不同的,普通的局部变量存放区域是栈区,而静态局部变量存放的区域是数据段(也就是静态区),他们被销毁的方式不同,一个是函数执行结束就被销毁,一个是程序运行结束才被销毁。
所以static修饰后的局部变量,其生命周期会变长,引起销毁时间与普通的局部变量不同
六、柔性数组
6.1 柔性数组的介绍
也许你从来没有听说过柔性数组这个概念,但是它的确是存在的。C99中,结构体中的最后一个成员允许是未知大小的数组,这个数组就被叫做柔性数组成员
代码展示:
struct S { int n; int arr[0];//未知大小的-柔性数组成员-数组的大小是可以调整的 //int arr[];也行 }; int main() { /*struct S s = ; printf("%d",sizeof(struct stu));*/ struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int)); //先求出结构体的字节大小,然后我们就可以随意改变要追加的空间字节大小了 ps->n = 100; int i = 0; for (i = 0; i < 5; i++) { ps->arr[i] = i; } struct S* ptr = realloc(ps, 44);//通过realloc函数的修改成功将空间大小增加了5个int型字节的大小 if (ptr != NULL) { ps = ptr; } for (i = 5; i < 10; i++) { ps->arr[i] = i; } for (i = 0; i < 10; i++) { printf("%d ", ps->arr[i]); } free(ps); ps = NULL; return 0; }
6.2 柔性数组的另一种替代方式(运用指针)
struct S { int n; int* arr; }; int main() { struct S* ps = (struct S*)malloc(sizeof(struct S)); //先动态开辟一块儿大小为8个字节的空间,用ps指针来指向这块儿空间 ps->arr = malloc(5 * sizeof(int)); //然后再让结构体中整形指针arr,去维护我重新开辟的一块儿5个int字节大小的空间 int i = 0; for (i = 0; i < 5; i++) { ps->arr[i] = i; } for (i = 0; i < 5; i++) { printf("%d ", ps->arr[i]); } printf("\n"); //这时我们依然可以使用realloc函数调整数组大小 int*ptr=realloc(ps->arr, 10 * sizeof(int)); if (ptr != NULL) { ps->arr = ptr; } for (i = 5; i < 10; i++) { ps->arr[i] = i; } for (i = 0; i < 10; i++) { printf("%d ", ps->arr[i]); } return 0; }
本质上其实就是,将结构体成员中的柔性数组成员用一个指针变量来代替,随后我们就可以利用指针接收地址的这个特点,用malloc函数开辟动态内存空间,然后把这个返回的动态内存空间的地址赋值给这个指针变量这个图片就很好介绍了,利用柔性数组和指针变量两种方式所开辟空间的不同,前者其实直接在原有结构体后面补上我们想要的空间大小,后者利用了指针能维护空间的特点,额外又开辟了一块儿空间,让指针来维护这个空间
6.3柔性数组的优势和特点
6.3.1 特点
1.在结构体中添加柔性数组这个成员时,必须将其放在最后一行,要不然操作系统识别不了(我之前就遇到过这种问题,代码运行不起来)
2.sizeof返回这种结构体大小时,是不包括柔性数组的内存所占字节大小的
3.包含柔性数组的结构体使用malloc函数进行内存的动态分配时,分配的内存应该大于结构体的大小,以此来适应柔性数组的预期大小
6.3.2 优势
1.第二种使用方式会产生很多内存碎片,内存利用率较低。而第一种使用方式的访问速度是比较高的,会提升程序性能
根据局部性原理,如果开辟的空间不是连续的,这样的访问效率是比较低的,CPU从下面的寄存器,高速缓存,内存,硬盘这四种访问方式,逐一访问,效率会逐渐降低,但存储的容量是会逐渐升高
2.值得注意的是,我们第二种代码方式在释放动态开辟的内存时,释放的方式是比较繁琐的,我们需要先释放结构体中指针所维护的空间,然后在释放结构体所占的内存空间
七、总结
本文主要给大家介绍了动态内存相关的知识,其中给大家也讲解了几个相关的练习题、相应的内存开辟函数、常见的使用错误、以及柔性数组的这几部分相关知识。帮助大家更深入的了解计算机内部的相关原理知识,希望大家认真阅读,学会这部分知识内容