引言:
假设我们要写一个简单的学生成绩管理系统。在这个系统中,我们需要存储学生的成绩信息,但是我们并不知道会有多少个学生的数据需要存储。如果我们一开始就定义一个很大的数组来存储成绩,可能会浪费大量的内存空间;而如果定义的数组过小,又无法满足实际需求。那我们有什么办法来解决这种问题呢?”
1、为什么要有动态内存分配
到目前为止,我们已经有两种开辟内存的方式:
int i = 20;//在线空间上开辟四个字节char [20] = {0};//在线空间上开辟20个字节的连续空间;
不过呢!就我们目前学习的两种开辟内存的方式,都有一个共同特点:空间开辟的的大小是固定的。且对于数组来说,必须指定数组的长度,数组空间一旦确定了大小就不能调整了。
但是对于空间的需求,不仅仅会出现上诉的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组开始就开辟的空间就有可能出现多或者少的情况。
比如引言中所提到的学生成绩管理系统,假如说我们要记录一个班级的学生,我们开辟了一个数组arr[30]的空间,但是这个班级只有十五个人,那么就会有15个空间被浪费;如果该班级有31个人,那么我们开辟的空间又会不够,无法存放该班级所以学生的信息,这时候就会发现已知的两种开辟空间方式的局限性。
所以针对这种情况C语言引入了动态内存开辟,让程序员自己可以申请和释放空间。增加了灵活性。
动态内存分配的特点:
主动申请大小调整释放空间2、malloc和free
2.1 malloc函数
C语言中提供了一个动态内存开辟的函数:
void* malloc (size_t size);//函数原型
这个函数向内存中申请了一块连续可用的空间,并且返回指向这个空间的指针。
如果开辟成功,返回一个指向该空间的指针。如果开辟失败,则返回一个空指针NULL。返回值的类型是void*是因为malloc函数不知道开辟空间的类型,具体在使用的时候由使用者自己来决定如果参数size为0时,malloc的行为时标准还是未定义的,取决于编译器。参数介绍:size_t size:
size是待开辟的内存块大小,注意是以字节为单位的。
返回值介绍:void*:
示例:
#include <stdio.h>int main(){//比方说要存放十个整型int* p = (int*)malloc(40);//10*sizeof(int)//使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i + 1;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}
如果你需要存放整型,就使用整型指针来接收,然后将malloc类型从void*强制转换为int*。
当我们开辟完空间使用后,我们不打算继续使用它,便需要将空间释放,这时候就需要用到另一个函数了。这就相当于你去图书馆借书,当你看完后,你需要将书在还回去,这样别人才能够借到这本书。空间亦是如此。
2.2 free函数
在C语言中,提供了另外一个函数free,专门用来做动态内存的释放和回收的,函数原型如下:
void free (void* ptr);
注意:free函数只能用来释放动态开辟的内存。
如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。如果参数ptr是NULL指针,则函数什么都不用做。malloc和free都声明在stdlib.h头文件中。
函数原型解释:
这里的void* ptr一定指的是你申请到的那块动态内存的起始地址。不能是你要释放的那块空间的中间地址,只能是起始地址。
示例:
#include <stdio.h>int main(){ //开辟空间int* p = (int*)malloc(40);//使用空间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;}
注意这里free接收的指针p,它的地址是没有变化的,从始至终都是我们开辟的动态内存的起始地址。因此,我们将p传给free函数用来释放内存。
free的作用呢,便是断开上面的箭头,我们不能在使用上面的那块空间了。
但是呢,free断开后,p里面还是会记录着开辟空间的起始地址。
这样的话,你通过p里面的地址,还能找到已经还给操作系统的空间了。因此为了防止这种情况发生,我们可以给p赋值一个空指针。p = NULL;
这里有一个比较生动的解释,可能有些扎心。
比如说你交了一个女朋友,后来你们两分手了,但是你还记着人家的电话号码,你依旧可以通过这个电话号码找到你前女友,并天天给人家打电话,这样合适吗?肯定不合适,这时候需要给你重重一击,让你彻底忘记你前女友的电话号码,彻底断开你们之间的联系。
这里给p赋上空指针还是很有必要的,毕竟,如果不赋上空指针,p里面存的地址就变成了野指针了。
明明我指向那块空间,但是那块空间却不属于我,明明喜欢那个女孩,但是那个女孩却不喜欢我。这就是如果不给free赋上空指针的结果。
关于野指针的危害前面也有介绍,这里就不细说了。
关于free,free只是将这块空间还给操作系统,但是p里面的地址还是原来的地址。所以切记,在使用完free后,要记得给p赋上NULL。
如果在开辟完动态内存后不free的话会怎么样,这样的话你就会一直占用着这块空间,就像你去图书馆借书,你既不还书,也不毕业,这样的话这本书就会一直在你手上,别人想借这本书就借不到。
但是如果你一直不换书的话,最后等你毕业后,学校也会强制收回你借的那本书。也就是说,你一直不free的话,等到程序结束后,操作系统也会主动的将你开辟的动态内存空间回收。
3、calloc和realloc
3.1 calloc函数
在C语言中还提供了一个函数calloc,calloc函数也用来动态内存分配,函数原型如下
void* calloc(size_t num,size_t size);
该函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0.
与函数malloc的区别只在于calloc会在返回地址之前把申请到的空间的每个字节初始化为全0.
#include <stdio.h>int main(){ //开辟 int* p = (int*)calloc(10,sizeof(int)); //使用 //释放 free(p); p = NULL; return 0;}
如果我们要对申请的内存空间的内容要求初始化,那么可以使用calloc函数来完成。
3.2 realloc函数
realloc函数的出现使得动态内存管理更加灵活。
有时候我们会发现申请的空间太小了,有时候会觉得申请的空间过大了,所以为了能够合理的使用内存,我们需要进行对内存大小的灵活调整。因此realloc函数就横空出世了!
函数原型如下:
void* realloc(void* ptr,size_t size);
解读:
ptr是要调整的内存地址size是调整之后新的空间大小返回值为调整之后的内存起始位置。这个函数调整原内容空间大小的基础上,还会将原来内存中的数据移动到新的空间。realloc在调整内存空间的时候存在两种情况 1.原有空间之后有足够大的空间 2.原有空间之后没有足够大的空间示例:
#include <stdio.h>#include <stdlib.h>int main(){int*p = (int*)malloc(20);//5个整型//使用int i = 0;for (i = 0; i < 5; i++){*(p + i) = i + 1;}//1 2 3 4 5//在扩大p的20个字节空间relloc(p, 40);//在原基础上扩大20个字节,因此新空间占40个字节return 0;}
realloc的两种开辟空间的情况。
因此,关于realloc的返回值,我们是否还能拿p来接收?在上面的示例中,答案肯定是不行的
如果开辟失败的话,会给p赋上空指针,导致原来开辟的空间也找不到了。
因此这里我们需要单独创建一个指针变量ptr
int* ptr = (int*)relloc(p, 40);//在原基础上扩大20个字节,因此新空间占40个字节if (ptr != NULL){p = ptr;}
如果ptr这个指针不是空指针,那么我们就将ptr中的值赋给p。
如果是情况1的话,ptr的值等于p,所以赋值等于没赋;如果时情况2的话,p指向的那块旧的地址释放掉了,因此我们将ptr的地址(也就是新开辟的空间的起始地址)给到p。
结语:
本篇文章介绍了动态内存所需要的一些函数,下篇文章将会介绍常见的动态内存错误问题