目录
🤩前言🤩:
一、动态内存概述⚔️:
1.什么是动态内存:
2.动态内存分配的意义:
二、常用的动态内存函数🏹:
1. malloc 和 free函数:
①. malloc 函数:
②. free 函数:
③. malloc 函数与 free 函数的使用:
2. calloc 函数:
3. realloc 函数:
三、常见动态内存错误🛡️:
1.对 NULL 指针的解引用操作:
2.对动态内存空间的越界访问;
3.对非动态内存空间使用 free 函数:
4.使用 free 函数释放动态内存空间的一部分:
5.对同一块动态内存空间多次释放:
6.不释放动态内存空间(内存泄漏):
🥳总结🥳:
🛰️博客主页:✈️銮同学的干货分享基地
🛰️欢迎关注:👍点赞🙌收藏✍️留言
🛰️系列专栏:💐【进阶】C语言学习
🛰️代码仓库:🎉VS2022_C语言仓库
家人们更新不易,你们的👍点赞👍和👉关注👈真的对我真重要,各位路过的友友麻烦多多点赞关注,欢迎你们的私信提问,感谢你们的转发!
关注我,关注我,关注我,你们将会看到更多的优质内容!!
🏡🏡 本文重点 🏡🏡:
🚅动态内存🚃动态内存函数🚃常见动态内存错误🚏🚏
🤩前言🤩:
C 语言是一门神奇、实用而又基础的语言,我们几乎可以说所有的编程语言,都是在 C 语言的基础上发展而来的。许多小伙伴们学习 C 语言的目的,都是为了继续学习其它的高级语言,而我们学习各种语言的最终目的,一定是为了写出更好的代码和程序,寻找一份满意的工作。
而今天这篇博客的内容,就是关于如何通过开辟动态内存,从而写出更加优秀的程序的。同时今天的内容对于以后想要继续学习 C++ 的同学们来说尤为重要,希望各位小伙伴们能够认真学习仔细思考,多多练习牢固掌握。
一、动态内存概述⚔️:
接下来我们就正式开始关于动态内存的学习,首先我们先来大致了解一下动态内存。
1.什么是动态内存:
在c/c++语言中,所谓动态内存分配,就是指在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分配的大小就是程序要求的大小。
2.动态内存分配的意义:
我们在之前的学习过程中,在使用各种变量与数组等等功能时,都需要从内存中开辟出一片空间用于存放我们的数据,而在之前我们掌握的内存开辟方式有:
int value = 20;
//在内存栈空间上开辟4个字节的空间
char arr[10] = { 0 };
//在栈空间上开辟10个字节的连续空间
但是我们最经常时使用的这两种内存空间的开辟方式有一些共同的特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候必须指定数组的长度,它所需要的内存将会在编译时分配。
换句话说,这两种内存开辟方式都是静态内存分配。
但是我们在日常的代码编写和程序使用过程中,对于空间的需求往往不仅限于上述情况。更多的时候我们需要的空间大小只有在在程序运行的时候才能知道。如此,数组等在编译时开辟空间的方式无法满足我们的实际运行需求。
例如:
1. 通讯录创建联系人数据 data[1000],实际有联系人13个,造成大量的空间浪费。
2. 通讯录创建联系人数据 data[20],后期共有联系人113人,初期开辟的空间不够用。
于是就需要使用一种更好的内存分配方式进行处理,动态内存分配应运而生。
二、常用的动态内存函数🏹:
1. malloc 和 free:
malloc 函数(memory allocate,即内存分配)的作用为向内存的堆区申请空间用于存储数据,free 函数的作用为释放使用 malloc 函数向堆区申请的空间,并将空间归还给内存的堆区空间,通过配合使用这两个函数可以从堆区申请(释放)动态内存空间。
①. malloc 函数:
我们首先来看 malloc 函数:
我们可以看到,malloc 函数的使用格式为:
void* malloc (size_t size);
从它的使用格式中我们可以看出,该函数向堆区申请了一块连续的空间,同时返回的是这块空间的指针。
★ 如果开辟成功,则返回一个指向开辟好空间的指针。
★ 如果开辟失败,则返回一个 NULL 指针,故我们在使用时一定要仔细检查 malloc 函数的返回值。
★ 返回值的类型是 void* ,即 malloc 函数并不了解开辟空间的类型,至于空间的具体类型将在使用时由使用者自己决定。
★ 如果参数 size 为 0,则 malloc 函数的行为是标准未定义的,将会取决于编译器。
②. free 函数:
我们同样先来看看我们的 free 函数:
我们可以看到,free 函数的使用格式为:
void free (void* ptr);
不同的是,与 malloc 函数恰好相反,free函数的作用为释放动态开辟的内存,同时没有返回值:
★ 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
★ 如果参数 ptr 是NULL指针,则 free 函数将什么都不会做。
③. malloc 函数与 free 函数的使用:
关于两个函数的实际使用,我们直接来看下面这段 malloc 函数与 free 函数的使用实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
//初始化指针
ptr = (int*)malloc(40);
//使用malloc函数动态申请40字节空间
//同时因为指针ptr类型为int*,而 malloc 函数的返回类型为void*,故使用强制类型转换
int* p = ptr;
//在此定义指针p的原因是,在之后的使用中,指针p的指向会发生改变
//若不对初始指针指向进行保存,将无法释放改变前与改变后之间的空间
if (p == NULL)
//mallocc函数在动态空间开辟失败时返回空指针
//即若此处为空指针,说明动态内存空间申请失败
{
perror("malloc");
//打印malloc函数动态空间申请错误原因,并结束程序
return 1;
}
//没有结束说明指针不为空,动态空间申请成功
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
//循环向动态内存空间中存入数据
p++;
//在此处的操作中,指针p的指向发生了改变
}
//使用完成后释放动态内存空间:
free(ptr);
//指针p指向发生改变,但指针ptr仍指向初始指针
ptr = NULL;
//动态内存空间归还后,重新将指针ptr置空
//动态内存空间已经归还,若不将指针ptr进行置空,指针ptr将变为野指针,指向将不可控,这在程序运行中非常危险
return 0;
}
具体的使用方法在注释中已经注明了,在这里我们要在对其中几个地方再次进行强调:
1. 在对指针进行操作前一定要保存指向初始位置的原始指针。
2. 在对指向动态内存空间的指针进行使用前,一定要进行非空判断。
3. 在动态内存空间使用完毕并释放后,一定要将指针进行置空操作。
4. 当程序结束时若动态内存空间没有被释放,将会被操作系统自动回收。
5. 但若程序不结束且申请的动态内存空间持续不归还,动态内存将不会被回收,就会导致内存泄漏问题。
2. calloc 函数:
我们来看这个函数:
可以看到,calloc 函数的使用格式为:
void* calloc (size_t num, size_t size);
更通俗的说,calloc 函数的功能就是,为 num 个大小为 size 的元素开辟一块动态内存空间,并将空间内每个字节都初始化为 0。
其使用方式与 malloc 函数无异:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)calloc(10, sizeof(int));
//使用calloc函数动态申请10个int类型大小的空间
//同时因为指针ptr类型为int*,而calloc函数的返回类型为void*,故使用强制类型转换
int* p = ptr;
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
free(ptr);
ptr = NULL;
return 0;
}
并且实际作用相比较来说,与函数 malloc 的区别仅在于 calloc 函数在返回地址前会把申请的空间内每个字节都初始化为 0 ,其它方面完全相同。
也就是说,如果我们需要将申请来的动态内存空间进行初始化,那么我们就可以通过使用 calloc 函数来达到我们的目的。
3. realloc 函数:
我们来看 realloc 函数:
realloc 函数(re - allocate,即重新分配)的作用为重新分配从堆区申请来的动态内存空间的大小。其使用格式为:
void* realloc (void* ptr, size_t size);
前面 malloc 、free 与 calloc 三个函数的存在,都是为了向堆区申请或释放动态内存空间,但是倘若只使用前三个函数,我们可以发现,申请来的内存空间的看起来与静态内存空间似乎没有什么太大的区别。
于是,recalloc 函数出现了。
realloc 函数的出现,使得动态内存管理更加的灵活。例如有些时侯我们觉得前面申请的空间太小了不够用,或者我们会觉得申请的空间过大了太浪费,这个时候我们就可以通过使用 realloc 函数对之前开辟的动态内存空间的大小再次进行合理的调整。
换句话说,正是 realloc 函数才使得动态内存空间真正变得“ 动态 ”起来。
例如我们可以直接修改上面的例子,当我们在使用过程中发现我们申请来的动态内存空间不够用时,我们就可以通过使用 realloc 函数来对我们申请来的动态内存空间进行扩容:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(40);
int* p = ptr;
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
realloc(ptr, 80);
//空间不够用,重新分配更大的空间
//将指针ptr指向的空间扩容至80字节
free(ptr);
PTR = NULL;
return 0;
}
但哪怕是在成功扩容时,也仍会出现两种情况:当前空间与后相邻空间之间的空间是否足够 realloc 函数进行扩容操作。
★ 若空间足够,则直接执行扩容操作,并在扩容完成后返回指向起始位置的指针。
★ 若空间不够,则将会在堆区中重新寻找合适的空间(足以容纳下扩容后的全部空间),并将原空间内的数据全部拷贝过来,接着释放原空间,并在扩容完成后返回指向新空间起始位置的指针。
但是还有最特殊的情况,即内存堆空间中没有能够容纳整个扩容后的动态内存空间的空间时,将会返回空指针。所以,我们在想要使用扩容后的动态内存空间时,为了避免使用指针内容为空指针而造成的意外错误,在使用之前首先应当对 realloc 函数返回的指针进行非空判断,之后再拿来使用:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* ptr = NULL;
ptr = (int*)malloc(40);
int* PTR = NULL;
PTR = (int*)realloc(ptr, 80);
int* p = NULL;
//空间不够用,重新分配更大的空间
//将指针ptr指向的空间扩容至80字节
if (PTR != NULL)
//使用前进行非空判断,避免出现无法预料的错误
{
p = PTR;
}
//判断非空,即扩容成功,开始使用:
int i = 0;
for (i = 0; i < 20; i++)
{
*p = i;
p++;
}
free(PTR);
PTR = NULL;
return 0;
}
三、常见动态内存错误🛡️:
1.对 NULL 指针的解引用操作:
int main()
{
int* p = (int*)malloc(INT_MAX);
//当尝试开辟的空间过大时,将会导致动态内存开辟失败
//此时malloc函数将会返回空指针,即此时指针p也是空指针
*p = 20;
//没有判断非空就对指针p进行解引用操作,导致产生对空指针进行了解引用操作
//将会造成内存非法访问的错误
free(p);
p = NULL;
return 0;
}
避免出现此类问题的方法是,在指针使用前对 malloc 等函数的返回值进行非空判断。
2.对动态内存空间的越界访问;
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i <= 10; i++)
{
*(p + i) = i;
//当i=0时,将会出现越界访问的错误
}
free(p);
p = NULL;
return 0;
}
避免此类错误的方式是,在使用时仔细认真的进行内存边界的检查,并选择合适的空间访问范围。
3.对非动态内存空间使用 free 函数:
int main()
{
int a = 10;
//变量a所使用的内存空间不是动态内存空间
int* p = &a;
free(p);
//释放非动态内存空间
p = NULL;
return 0;
}
避免此类错误的方式是,在进行空间释放时注意区分静态内存空间与动态内存空间。
4.使用 free 函数释放动态内存空间的一部分:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
//此时指针p的指向已经发生变化,不再指向起始位置
}
free(p);
//释放部分动态内存空间
p = NULL;
return 0;
}
避免此类错误的方法是,在使用指针前保存好初始指向,并在进行动态内存释放时释放完整的动态内存空间。
5.对同一块动态内存空间多次释放:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
free(p);
//...(在中间又进行了很多其它操作之后,忘记了已经释放过动态内存空间,并进行了重复释放)
free(p);
//重复释放动态内存空间
p = NULL;
return 0;
}
避免此类问题的方法是,在已经释放过动态内存空间之后,一定要加以注释以提醒自己避免重复释放同一块动态内存空间。
6.不释放动态内存空间(内存泄漏):
void test()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p != NULL)
{
*p = 20;
//判断非空后进行使用
}
//使用后没有释放动态内存空间,在程序终止前该动态内存空间都将无法被释放,将会逐渐蚕食计算机系统的内存
}
int main()
{
test();
while (1);
//为了演示内存泄漏,使程序永不终止
return 0;
}
避免此类问题的方法是,永远记住,使用一个释放一个,使用结束立刻释放。
🥳总结🥳:
到这里我们今天关于动态内存管理的介绍与学习就结束啦,通过今天内容的学习,相信各位小伙伴们已经掌握了动态内存的开辟、释放与动态修改,并且已经对动态内存空间的各项使用注意事项有了一定的认知和了解。希望小伙伴们在下去以后,在对动态内存空间的使用和管理中一定要慎之又慎,尽最大可能避免出现类似的相关问题。
🔥🔥只有承担起旅途风雨,才能最终守得住彩虹满天!!!🔥🔥
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!