【C语言】动态内存管理
一、为什么有动态内存分配
前面我们学习函数的时候有将过内存大概分为下面三个部分:
栈区:存放局部变量和函数参数
堆区:动态内存管理
静态区:全局变量和静态变量
所以我们在创建一个局部变量和创建函数参数的时候是在内存的栈区开辟空间。
当我们创建的是全局变量和静态变量的时候就是开辟的静态区的空间了:
上面的空间开辟方法有下面两个特点:
1、空间开辟的大小是固定的。
2、数组在定义的时候,必须指定其大小,数组的大小一旦定了就无法调整了。
但是我们有时候对于这个空间的大小是不确定的,有时候在 程序运行后才可以确定这个空间的需求是多大,那么数组的开辟方式就没办法满足我们的这个需求了。
那么在C语言中就引入了动态内存开辟,可以让程序员按照自己的需要来进行申请和释放空间,提高了程序的灵活性和空间的利用率。
上面我们提到了,动态内存管理是存放在内存的堆区的。
二、malloc和free
C语言提供了库函数来实现动态内存分配,下面我们来学习malloc函数和free函数。
使用这两个函数需要包含头文件<stdlid.h>
1、malloc函数的使用。
malloc可以实现向内存的堆区申请一块连续的空间,其返回的是这块空间的地址。
原型:
void * malloc(size_t size);
它的参数是一个size_t类型,单位:字节,其表示的是在堆区中开辟size个字节的空间。
然后其返回值的类型是一个无类型的指针,这是因为我们申请的空间存放的数据类型是不定的,可能是字符型,也可能是整型等,所以设置为void类型就都可以使用,在使用的时候我们通过强制类型转换即可。
如果说空间开辟不成功,那么此时就会返回一个空指针NULL。
下面我们总结一下malloc函数:
1、如果空间开辟成功,那么就返回这个空间的地址。
2、如果空间开辟不成功,那么就返回一个空指针NULL。
3、返回值的类型是void*,这是为了方便存放多种类型数据的空间的开辟。
4、如果参数size为0,那么malloc的行为是标准为定义的,取决于此时的编译器。
下面我们通过一个例子学习这个函数的使用和其效果:
我们分析上面的代码,首先我们使用malloc函数开辟了20个字节的空间,也就是5个整型数据的空间,然后将其强制类型转换为整型指针赋给arr指针,由于mallco函数开辟的空间是连续的,那么我们可以将其当成数组来使用。
然后我们需要判断一下这个空间有没有开辟成功,如果返回的是一个空指针NULL那么就说明这个空间开辟失败了,那么此时我们可以使用perror函数将开辟失败的原因打印出来,然后给个错误的返回,然后终止程序。
然后我们使用了一个循环,对这个开辟的空间进行赋值,此时我们可以将arr当成一个数组来使用,和数组一样,arr就是首地址,然后在其后面使用[]就是第几个元素的空间了。
下面我们看看代码的运行结果:
可以看到其完好的将这个数组的元素赋值且打印出来了,说明这个函数开辟的空间就是连续的,其可以实现数组的功能。
那么当我们对这个数组不需要的时候,是不是就不用理了呢?
并不是,我们开辟一个空间后,如果说不需要了,那么可以将其释放,那么就需要使用到下面学习的函数了。
2、free函数的使用
当上面我们开辟的空间不需要再使用的时候,那么此时我们就可以释放掉这个空间,让其他程序进行使用。
那么此时就需要使用free函数了。
原型:void free (void * ptr);
其是一个没有返回值的函数,其参数是一个未知类型的指针,其作用就是从这个指针的地址开始进行释放我们开辟的动态空间。
其使用要注意:
1、如果参数prt指向的空间不是动态开辟的,那么free函数的行为就是未定义的。
2、如果参数prt是一个空指针NULL,那么函数就啥也不干。
下面我们使用这个函数将上面我们使用malloc函数开辟的空间释放看看。
上面我们将arr占用的空间释放后,arr中实际上还存放着一开始的时候的地址,free只是将这部分的空间释放了,并没有改变arr指针指向的地址。
所以这就导致了arr指针指向的地址是已经还给操作系统的了,导致此时的arr为野指针,所以我们后面再将arr置为空。
三、calloc和realloc函数
1、calloc函数的使用
calloc函数和上面的malloc函数一样也是用来开辟空间的,就是在使用方式上和上面的函数有点区别。
其效果是一样的。
其原型如下:
void * calloc (size_t num,size_t size);
可以看到其和malloc函数除了参数的个数是不一样的,其他都是一样的。
其中第一个参数是要开辟的元素的个数,然后第二个参数是开辟的空间的一个元素的字节数。
那么实际上这两个函数是一样的。
那么上面我们开辟的数组空间一样可以使用calloc函数实现。
那么这两个函数有啥区别呢?
calloc函数在开辟空间后,会将开辟好的空间的每个字节初始化为0,而malloc函数就不会。
下面我们试试看:
运行结果:
可以看到我们上面的代码,只是对其开辟了20个字节的空间,然后就直接将这个空间每个元素直接打印出来了。
但是其值直接是0了。
下面我们看看calloc函数的特点:
1、函数的功能是为num个大小为size字节的连续空间,并且为这个空间的每个字节的内容置为0。
2、和函数malloc的区别就是参数不一样,然后在返回这个开辟的空间的地址之前,对这个地址的 从内容初始化0了。
2、realloc函数的使用
前面的两个开辟空间的函数,增加了程序的灵活性,但是可以发现当我们需要扩大这个连续的空间的时候,上面的两个开辟空间的函数还是没办法实现的,所以我们再学习realloc函数。
这个函数可以对calloc和malloc函数开辟的空间进行扩容,这个函数可以使得程序更加灵活。
其原型如下:
void * realloc(void * prt,size_t size);
第一个参数是一个地址,那么其就是需要调整的空间的地址,第二个参数是一个size_t类型数据,其就表示要再开辟的空间的大小。单位是字节。
realloc函数的特点如下:
1、当后面的空间足够其扩容的时候,那么其就返回的是原来的地址。
2、当后面的空间不足够扩容的时候,那么其就会寻找一个可以把原空间和要增容的空间都放下的 地方。然后将这个新的空间的首地址prt2返回。原空间的内容会被拷贝到新的空间。
注意:
我们在增容的时候,如果增容失败,那么其会返回一个空指针,那么我们在使用这个函数进行增容的时候,不要直接使用原空间来接收它。
这是因为我们扩容的原空间是还有数据的,如果扩容失败,那么就会导致原来空间就找不到了,导致数据的丢失。
那么我们在增容的时候,可以创建一个指针变量来接收,然后先判断其是不是空指针,即判断其增容是否成功。如果成功那么再将这个空间给prt。
下面我们对上面我们开辟的数组空间进行增容:
四、常见的动态内存的错误
1、对NULL指针进行解引用操作:
上面的代码,我们应该先判断这个空间有没有开辟成功,然后才可以对这个指针进行解引用操作。
2、对动态开辟空间的越界访问:
上面当i变成10的时候,此时就越界访问了。
3、对⾮动态开辟内存使⽤free释放:
4、使⽤free释放⼀块动态开辟内存的⼀部分
上面的,指针p进行了++操作,那么此时其指向的不是开辟的空间的首地址了,此时对其进行释放导致前面的空间没有释放掉。
5、 动态开辟内存忘记释放(内存泄漏)
五、动态内存的经典笔试题
题1:
我们分析以一下上面的代码,可以看到其创建了一个字符指针变量,然后将其为参数传入函数GetMemory中,然后函数体中是开辟了100个字节的空间,然后传给了这个参数。
然后再将hello world字符串拷贝给了这个空间。
那么我们看看这个程序有没有将这个字符串打印出来呢?
运行结果:
可以看到其没有任何的打印,而且还报错了。
那么是那里出现了问题呢?
很明显就是传参的部分有问题,因为我们的参数的地址,然后是要从地址的层面上进行修改,那么我们使用一个一重指针来接收,实质上就是值传递,那么是没办法修改实参的。那么我们应该使用二重指针来作为参数。
如下:
运行结果:
可以看到我们将函数的参数改成一个二级指针,然后表达式中使用一个一级指针变量来接收 ,就可以实现我们的功能了。
题2:
这里的问题应该就很明显了,当函数执行完后,那么这个字符数组p应该就会随着函数空间的释放一起释放了,那么其返回的p这个空间的内容也就没了。
运行结果:
题3:
首先我们应该先对malloc函数的返回值进行判断,判断这个空间是否开辟成功,然后就是我们在释放str这个空间后,应该将这个指针置空,这就导致了str指针变成了野指针。
六、总结C/C++中程序内存区域划分
C/C++程序内存分配的⼏个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时 这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内 存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。《函数栈帧的创建和销毁》
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅ 式类似于链表。
3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。