学到这里了,大家应该对C语言的了解跟深一层了吧。我们C语言写代码不能只局限于直接写代码。我们要了解C语言的内存分布,我们都知道C语言的内存是有堆区,栈区,静态区的。然后栈区是我们平常创建临时变量存储的地方,静态区是我们全局变量,和静态变量。堆区就是动态内存了,并且我们今天讲的就是关于堆区的相关知识大家看一张图片可能会更加了解一些。
动态内存
当然我们学习动态内存首先要知道什么是动态内存,为什么会存在,怎么使用。
什么是动态内存,大家可以先看,名字中有一动而且后面跟着一个内存这说明什么,说明它可以改变吧,改变内存,那怎么改变嘞,这就是我们的第三个问题,如何使用了。我们下来处理第二个问题,为什么会存在动态内存。其实这个我认为给大家举一个例子,大家可以感受的更加深刻一些
我们可以看到我首先创建看了一个变量y是int类型的,有4个字节,那么至少在现在我们看到y是被确定为4个字节的无法改变的吧,那么我们想改变y的直接大小或者直接改为数组的话,是不是只能在这里重新写。然后我为什么需要改,那时因为y只有4个字节不能满足我的需要了,那么我肯定是写了一些代码才发现这个不能满足我的需求了才改的呀。那这些代码只要涉及到了我的y是不是就要跟着改变要,很麻烦吧,而且要是一个不小心有一个没改掉是不是就出现BUG了。然后就是下面的,大家可能会说那我先不写有多少字节,我先创建最后写完了,看需要多少,我在写多少不就行了,诶,刚开始看好像还可以,但是大家想想,我们现在才写多少行代码呀,我们可以最后计算,那要是我们工作了做项目,写几百行代码几千行代码,我们最好一个一个数,那岂不是又做了一遍呀,而且一个不小心又要找BUG。所以这个时候就体现出来动态内存的重要性了。我先申请一部分内存,要是不够我就在申请一点,但是以前的是不会消失的,我们只需要接着写就行了。用完了以后返回去,像借东西一样,我要用先借一点不够再借一点,用完了以后还回去。动态内存差不多就是这样。
malloc
好,那么我们就像来看看我们动态内存的第一个相关的库函数malloc,如果依据前面的那个例子的话,这个就是我们借东西的借条,好比我们向图书馆借书,我们肯定要先写借记卡吧,这是我们借东西的前提。那么malloc就是我们向堆区借内存的借据,并且堆区很大方,只要我们借并且成功的话内存都是连贯的。好了我们大概知道了malloc的作用了,那么我们就来学习如何使用吧。
void* malloc (size_t size);
首次运用这个库函数我们需要的头文件是<stdlib.h>,接着如果开辟成功,则返回⼀个指向开辟好空间的指针。 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。其次是,我们开辟内存在运行的时候其实是无法看出是否开辟成功的,所以我们需要检验我们是否开辟成功。然后就是void*,为什么我们要用void*那时因为我们开辟内存,我们知道开辟的内存是类型的,但是系统不知道啊,在没运行前,那么系统只能用一个都可以接受的来接收了啊。那只有void了。接着就是size,这个是字节的意思。我们需要开辟多少个直接那么我们直接写就可以。好,那么我们来看一段代码:
int main()
{
int arr[5] = { 0 };
int* ptr = NULL;
ptr = (int*)malloc(5*sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{//不为空的话就工作
int i = 0;
for (i = 0; i<5; i++)
{
*(ptr + i) = 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
当然这里的数组arr其实可以不用创建的,但重点是大家看到旁边的内存窗口,我们看的是ptr内存变化,我们开始创建了一个ptr的指针,并且是NULL。但是后来我们用malloc把它变成了一个20个字节的数组(这个大家应该很好理解吧,因为一个常量或者变量是无法存放几个元素的,只有数组可以),并且我们将数组全部变为0。那么这就是我们最开始学习的先申请开辟动态内存。
也许大家会突发奇想如果我接0个字节会怎么样嘞。这个事情其实并没有明确的答案,这个只能看编译器了,因为这里本来是接书的,然后你在借记卡上写个我借0本书,干什么,搞事情啊,人家不打你就算好的了。所以这个无意义的代码,大家知道就是可以了,不要在日后的代码中写出这种无用反增烦恼的事情了。
注:这个函数向内存申请⼀块连续可⽤的空间,并返回指向这块空间的指针。那么我们就要用指针来接收。
free
大家看了上面的代码,不知道是否看见了在最最好几行看见free的身影啊。那这个是什么东西嘞,代码肯定是写有关系的代码嘛,如果无关的话,写它干嘛岂不是徒增烦恼。那么它是干什么的嘞。还是那个例子,我们向图书馆借了书,是不是要还啊,不换也许当时没什么问题,但要是过一段时间,被拉如黑名单了,是不是就有麻烦了。就算到时候还回去了,麻烦已经产生了,双方还是不开心啊。那么free就是我们还东西的动作。每当我们开辟了动态内存后,要还回去,怎么还,用free。但是free只有一个作用,那就是只能还动态内存,其他的不归它管,要是你强行叫它还的话,如果是同行,也是空间开辟的话,那么它会直接告诉你,这不是动态内存开辟的空间,我不认是错误的。但如果参数 ptr 是NULL指针,则函数什么事都不做。我们先看正常释放动态内存开辟后会有什么结果:
我们可以看到虽然内存没有任何明显改变,但是它内部已经改变了,因为我们前面已经说过free是释放空间,但是我们ptr没有被销毁啊,还可以用它的地址啊。但是它内部已经被处理如果不小心用了ptr的话,岂不是就gg了。所以我们需要写ptr=NULL来证明ptr确确实实已经无法使用了。好,我们看过正常使用free了,我们来看看错误示范:
我们改为释放一个数组,这里就报错了,是不是。那么我们看看如果释放的是NULL看看:
这里我们创建了一个NULL,然后释放,因为它原本就是NULL,释放了的话,还是一样的啊,相当于没用啊,是吧。
注:大家不要忘了,借了东西一定要还,开辟了空间的话,一定要free,不然会有不可估量的后果。并且是申请了几个,就释放几个,free一次只能释放一个开辟空间指令。
calloc
讲了这么多,我们知道了借东西要用malloc,还东西用free。是不是只能用malloc借嘞,其他人没用吗,我要是不想找它借岂不是没有其他途径借了。这个时候眼睛一撇看到了一个calloc,calloc的功能是为 num 个大小为 size 的元素开辟⼀块空间。但是calloc有一个问题,它喜欢把自己处理的事情变为0,只要是自己经手的都变为0。这是它的一个不好习惯。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为0。
void* calloc (size_t num, size_t size);
那我们学习过malloc了,calloc直接上代码吧,更方便:
int main()
{
int *ptr = (int*)calloc(5, sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
for (int a = 0; a <5; a++)
{
printf("%d ", *(ptr+a));
}
free(ptr);
ptr = NULL;
return 0;
}
那么我们看过了这个运行结果后就应该了解了如果我们需要对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。
realloc
当我们学习过了calloc后,我们知道calloc申请空间要将空间的内容全部变为0,那很麻烦啊,每一次都搞为0,那么realloc就站出来说,找我吧,我行为习惯良好,你要什么我给你什么,不会改变内容的。并且可以在你用了其他的库函数后,就是不够想扩大内存的话找我。但是世界上看到没有十全十美的东西,realloc肯定也是,那么大家看看下面我们来看看realloc有哪些问题。
void* realloc (void* ptr, size_t size);
那么我们首先看看realloc怎么用:ptr 是要调整的内存地址,size 调整之后新大小返回值为调整之后的内存起始位置。 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到新的空间。但大家不知道是否有想过我们借东西,对方也没有了,不够了,怎么办是吧。那么这里就有两种情况了,可以借出足够的空间,无法借出足够的。
状况一:足够。要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
状况二:空间不够。都说世界上还是好人多啊,当空间不够的话,它去帮你找,谁有那么大的空间,叫它借给你。在堆空间上另找⼀个合适大小的连续空间来使⽤。这样函数返回的是⼀个新的内存地址
正常使用:上面我们写了realloc可能发生的两种特殊情况,那么接下来就正常搞了吧
int main()
{
int *y = NULL;
int arr[5] = { 0 };
int* ptr = NULL;
ptr = (int*)malloc(5*sizeof(int));
if (NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for (i = 0; i<5; i++)
{
*(ptr + i) = 0;
}
}
ptr = (int *)realloc(ptr,8 * sizeof(int));
if (NULL != ptr)
{
int i = 0;
for (i = 0; i<8; i++)
{
*(ptr + i) = 1;
}
}
free(y);//释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
我们看到开始我们是创建的5个int,全部赋为0,后来我又重新为8个int并且全部赋为1。当然出于习惯我还是给realloc一个判断是否为NULL,这是习惯,虽然看起来麻烦,但是习惯了使用起来还是没什么的。
动态内存常见问题
当我们学习了上面的4个库函数后,我们再来了解一些除了库函数引起的问题。
动态开辟空间的越界访问
我们知道有多少钱就花多少钱嘛,当然也可以省钱,但是你不能超前消费啊。毕竟要还的呀。但是C语言是没有这种的吧,你超过了这个范围,那么就实打实的有问题呀,那我们看看越界访问会发生什么:
我们可以看到,我申请了5个int类型的空间,但是我却打印了6个,是不是最后一个打印出来值是没想到的。所以我们在动态内存开辟的时候需要注意有多少空间就有多少空间,不要超过这个范围。
释放一部分
这个大家看标题一个很容易理解,就是free释放的时候只释放了一部分,好比,我们借书,我们借了10本书,但是我只还了9本,但是我还有一本没还,是不是还是有问题呀。
虽然哈,我看不懂这个英文但是大家可以明显看到我们是报错了,这就代表我们这个代码是有问题的,那我们与上一个代码相比,我们将ptr+a改为了ptr++就报错了。所以这可以侧面体现出前者优于后者吧。
多次释放
看了标题大家可能会想,怎么会多次释放嘞,我开始释放了就是释放了嘛,谁显得没事干多搞一次是。虽然大家都可以想到这件事,但是我们也不能排除我们下代买太多,自己都忘了前面是否释放过了啊,是吧。这也是一个问题,当然我觉得要是大家实在爬忘记,可以在每次释放了,就设置一个断点,表明我直接在这里释放了。这样也算一个方法嘛。
动态内存忘记释放
好了前面我们写的都是释放错误,但我觉得还是忘记释放是最多的,因为在刚开始的时候我们还没习惯每次用完动态内存后要写一个free。
注:动态内存常见的错误除了库函数使用错误就是忘记释放或者释放错误,所以希望大家每次使用动态内存后都不要忘记释放内存!!!
小结
1:malloc()和calloc()函数用法一样, 区别是calloc()会对所申请内存的每个字节初始化为0
2:malloc(), calloc(), realloc()申请的内存不再使用时 ,一定要用free()释放 ,否则会造成内存泄漏,并且不要释放错误。
好了这就是鄙人想与大家分享的关于动态内存的拙见了。肯定还有很多不足之处,希望大家可以在评论区指出。