- 欢迎来到我的 世界 ^ _ ^
- 希望作者的文章对你有所帮助,有不足的地方还请指正,大家一起学习交流 !
文章目录
- 前言:
- 动态内存是什么
- 一、动态内存介绍:
- 动态内存有关函数介绍
- 1.malloc和free
- 2.calloc函数
- 3.realloc函数
- 二、一些常见的动态内存错误:
- 1.对NULL指针解引用操作:
- 2.对动态开辟空间的越界访问
- 2.1. 越界访问的后果
- 3.对非动态开辟的内存使用free释放
- 4.使用free释放一块动态开辟内存的一部分
- 5.对一块动态空间的多次释放
- 6.忘记释放动态内存空间(内存泄漏)
- 完结:
前言:
前面发出一篇: C语言结构体篇,对我自己而言,还有很多地方只在了片面之上,但是收获还是很大的,比如说:为了知道结构体的嵌套又或是内存对齐,我翻阅了很多资料,这里感谢:买酒的小码农 博主满满的干货呀。
也证明了我学的知识也是不够扎实,老铁们也可以在评论区和我进行交流哦,咱们要一起进步呀。写一篇博客你会发现你许多的不足,这也是对我自己的再一次学习,对此我想告诉大家我很喜欢的一句话:
———————对过程全力以赴,对结果淡然处之
动态内存是什么
- 在C语言中我们的内存开辟是在栈上进行的,简而言之这属于静态内存:
int i=0;//这是向内存开辟了一块4个字节的空间来存放了变量 i
int arr[10]={0};//这是开辟了40个字节来存放数组 arr
// 在这里开辟的内存空间大小是不可变的;
//这里有人想问:我为什么想着改变着的内存空间呢?
// 你想啊,这里我们开辟了一块40个字节的数组,这么大的一块空间可不是想变大就大,
//想小就小的,这可能会和我们所需要的不一样,这会很不方便
//但是如果有一个你想多大就多大的空间呢?
//所以这就是我下来要讲的 动态内存空间管理 了
在C语言中我们的静态内存开辟,这样做的特点:
1.他所开辟的空间是固定的
2.数组在声明的时候,必须指定数组的长度,他所需要的内存在编译的时候分配
但对于空间的需求,我们有的时候并不知道,有可能空间开大了造成了浪费,也有可能空间开小了造成栈溢出,这样我们就需要一个动态的内存管理让我们需要多少内存的时候开辟多少。
但也不是动态内存一定静态内存开辟空间好,任何好东西都是一把双刃剑!!记住这句话!!!
一、动态内存介绍:
动态内存开辟空间是在堆区,和静态内存开辟不同,动态内存开辟需要调用函数:
动态内存有关函数介绍
1.malloc和free
void* malloc (size_t size);
这个函数可以在内存中申请一块连续的空间(堆区),并且返回这块空间的起始位置的地址,但是不会初始化这块空间。
如: int* pf= (int *) malloc(40); // 40 ---->的单位是字节
开辟了一块40个字节大小的空间,但是又不知道是什么类型的,就需要你手动强制类型转换你需要的类型(int*)。
注意:
1. 如果开辟成功,则返回一个指向开辟空间的指针。
2. 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
3. 返回值的类型是 void * ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己实现。
4. 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
这里因为我们有可能会动态内存申请失败的,所以我们应该更加加以检查,养成好习惯,在每次开辟了一块动态内存空间后面立马进行判断是否申请开辟空间成功。
- C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的:
void * free (void * ptr);
他是专门用来释放动态内存开辟的内存的;
注意:
1. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2. 如果参数 ptr 是NULL指针,则函数什么事都不做。
1.当malloc函数申请的内存空间,当程序退出时,会还给操作系统;当程序不退出时,动态内存申请的内存空间,不会主动释放。一定需要free函数来释放,所以我们要养成好习惯,每每开辟一块动态内存空间,后面一定要记得free释放这块空间。
2.free只释放动态内存空间,不能对静态内存操作哦
3.free释放该指针的地址后,会导致该指针为野指针,一定给该指针重新赋值空指针(NULL)
实例:
int main()
{
//开辟一块40个字节的动态内存空间,且强转位int*类型
int* pf =(int *) malloc(40);
//判断是否开辟成功
if (pf == NULL)
{
perror("malloc");//如果开辟失败了,打印出原因
return 1; //并且不用再继续往下了,就返回1
}
//如果开辟成功,则打印出这个地址所指的内容和往后10个的内容
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", *(pf + i));
}
//释放开辟的内存
free(pf);//这里注意,释放pf的地址后,pf就会变成一个野指针,
//这里一定需要重新给他赋值上NULL,这也是个好习惯
pf = NULL;
return 0;
}
这图也证明了,当malloc开辟的动态内存,不会给他进行初始化;
2.calloc函数
- 这个函数几乎是和 malloc 函数类似,也是用来动态内存分配:
void* calloc (size_t num,size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
实例:几乎和上述一样的代码;
int main()
{
//开辟一块40个字节的动态内存空间,且强转位int*类型
int* pf = (int*)calloc(10, sizeof(int ));
//判断是否开辟成功
if (pf == NULL)
{
perror("calloc");//如果开辟失败了,打印出原因
return 1; //并且不用再继续往下了,就返回1
}
//如果开辟成功,则打印出这个地址所指的内容和往后10个的内容
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d\n", *(pf + i));
}
//释放开辟的内存
free(pf);//这里注意,释放pf的地址后,pf就会变成一个野指针,
//这里一定需要重新给他赋值上NULL,这也是个好习惯
pf = NULL;
return 0;
}
和上述一样的代码,但是运行结果是全为0,就可以证明 calloc 开辟的动态内存是会给他初始化为0的。
3.realloc函数
- realloc函数让动态内存管理更加的灵活;
- 有时候发现申请的空间不合适,需要对内存大小进行灵活的调整,那么realloc函数就可以做到对动态开辟内存大小的调整;
void * realloc ( void* ptr , size_t size);
其参数:
1.ptr是要调整的内存地址
2.size是调整之后的新大小
3.返回值为调整之后的内存起始位置
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
5.realloc在调整内存空间有两种情况:
—————— (1).在原开辟空间后增加一块空间时:发现后面没有足够的空间,+图解:
1.开辟新空间 2.将旧的空间中的数据拷贝到新空间 3.释放旧的空间 4.返回新空间的起始位置地址
——————(2).若后面有足够的空间,则后面加上(size-原来的字节个字节)的空间大小。+图解:
直接在后面加上一块空间;
若想要使用realloc函数,首先需要一块已经开辟的动态内存空间,这样才能用realloc调整。
实现:
int main()
{
//开辟一块40个字节的整形空间大小
int* pc = (int*)malloc(40);
if (pc == NULL)
{
perror("malloc");
return 1;
}
//初始化 1 - 10 的数字
int i = 0;
for (i = 0; i < 10; i++)
{
pc[i] = i + 1;
}
//想增加一些空间
int* ptr = (int*)realloc(pc, 80);
// 这里我想特别强调一点,为什么不让 pc指针接收,
//而要创造一个新的指针接收呢?
// 因为realloc增容可能会失败,那返回的就是NUKLL,
//而如果把 NULL 给到 pc指针,那会造成内存泄露
// 当然内存泄露是一个动态内存错误,再这篇博客的后面
//会一一介绍到动态内存错误
//增加空间失败
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//增加空间成功
pc = ptr;//是将返回来的地址交给原来的指针维护
ptr = NULL;
//打印数据
for (i = 0; i < 20; i++)
{
printf("%d\n", pc[i]);
}
//释放
free(pc);
pc = NULL;
return 0;
}
前面的1到10的初始化,后面是新增加的10个字节大小空间,并没有进行初始化。
- 如果是想要减少空间,那就更简单了:
减少空间不存在上述的的情况,可以直接将realloc后参数的size改成你想要的。
如:将原来的空间大小减少至20个字节的大小。
int *ptr = (int *) realloc(pc,20);
二、一些常见的动态内存错误:
1.对NULL指针解引用操作:
这个问题很容易理解,上面有应该也是提到过:
就是在 malloc 开辟动态内存空间时,一定记得要对返回来的地址进行检查,因为malloc并不一定会开辟空间成功的,如果失败了,就会返回一个空指针(NULL),而如果后续需要对该指针进行解引用,会出现问题;
int main()
{
int *ptr;
ptr=(int*)malloc(sizeof(int));
//这里不进行判断,如果时返回NULL则这将会出现问题
*ptr=1;
//释放空间
free(ptr);
ptr=NULL;
return 0;
}
2.对动态开辟空间的越界访问
简单说就是你开辟了多大的空间就只能使用该空间,如果越过这块空间访问别的空间就是越界访问。
#include<stdio.h>
#include<stdlib.h>//malloc 和 free 都在<stdlib.h>的头文件里
int main()
{
int *ptr ;
ptr =(int*)malloc(40); //申请一个动态内存空间为40字节
if(ptr==NULL) //防止申请空间失败传入了空指针
{
perror("ptr");
return 1;
}
for(int i=0;i<20;i++)
{
*(ptr+i)=i;//申请的是四十个字节,这里产生了越界
}
free(ptr);
ptr=NULL;
return 0;
}
2.1. 越界访问的后果
- 如果有不太清楚的老铁,下面是我在网上找到比较完好的资料,可以慢慢品味:
1.未定义行为:C语言标准规定,对于越界访问的行为,编译器不会提供任何保证,其结果是未定义的。这意味着程序可能会产生不可预测的结果,包括崩溃、错误输出、数据损坏等。
2.内存损坏:越界访问可能会破坏其他变量或数据结构的内存空间,导致程序出现异常行为或崩溃。
3.安全漏洞:一些恶意用户可能会利用越界访问漏洞来修改程序的执行流程,从而执行恶意代码或获取未授权的访问权限。
4.调试困难:越界访问可能会导致程序的行为变得不可预测,增加调试程序的难度。
3.对非动态开辟的内存使用free释放
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p;
*p=10;
free(p); //这里的p并不是动态内存空间仍然进行了释放
return 0;
}
记住一点:非动态无free;
4.使用free释放一块动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p=(int*)malloc(sizeof(int)*2);
if(p==NULL)
{
perror("malloc");
return 1;
}
int i=0;
for(i=0;i<5;i++)
{ //这里只对前 5个进行赋值
*p=i;
p++;//这时p指针也会向高地址走去,
//这代表了p指针不再是指向这块空间的最初的起始位置地址
}
//而如果再对这块空间进行释放,还能只释放一部分么?
//答案是不行的,程序会崩溃
free(p);
p=NULL;
}
这种错误应该也是包含了另一个错误:内存泄漏;
如果释放的不是指向动态分配内存开始位置的指针,而是指向内存块中间位置的指针,那么会导致内存泄漏。因为从指针位置往后的内存空间仍然处于已分配状态,但是程序无法访问这些内存空间,从而造成内存泄漏。
所有在动态开辟空间时,最好不用动该空间的起始位置,最好重新创建一个指针进行操作,这也是一个好习惯哦;
5.对一块动态空间的多次释放
这种错误应该都是疏忽的原因,认为自己还没释放,结果的是已经释放了(真是哭死);
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟
int *p=(int*)malloc(sizeof(int)*2);
if(p==NULL)
{
perror("p");
}
//释放
free(p);
//不记得自己的释放
//~~~~~~
free(p); //再一次释放,会导致不可预测的后果;
// 对这最好的解决办法就是:当我们释放了一块空间后,
// 一定让他指为空指针
p=NULL;
}
- 因为如果你释放完一块空间后,就给予空指针进行维护,如果你还是忘记是不是释放了,再一次进行释放,那样有不会有太大的影响,因为我们提前的赋值空指针维护,而free空指针是什么都不会做的,所有没有太大影响。
当我们释放了一块空间后,一定让他指为空指针,这是一个好习惯哦;
6.忘记释放动态内存空间(内存泄漏)
前面提到了多次内存泄漏,终于到了
我们需要先看一段代码才好更好的理解:
void test()
{
int*p = (int*)malloc(sizeof(int) * 2);
//这是申请了这块空间返回地址给了指针 p
if (p != NULL)
{
*p = 20;
}
//但是当退出该函数时,指针 p所占的空间会还给操作系统,
// 这就代表刚刚开辟的空间无法再找到了,
// 因为只有指针p指向该空间地址
}
int main()
{
test();
return 0;
}
如果还不理解可以这样帮助理解,我也是这样理解的:
假如你是一个卧底,只有你的一个单线上司 p 警官知道你的卧底身份,但是在完成一次任务中,p 警官死了,那现在就尴尬了,谁都不认识你了,这就相当于内存泄漏了;
切记动态开辟空间一定要记得释放,并且是正确的释放;这是一个好习惯哦。
多学一点点:
内存泄漏是一种慢性问题,它不会立即导致程序崩溃,但会逐渐占据系统内存,影响系统的稳定性和性能并最终导致系统崩溃。
内存泄漏(Memory Leak)是指程序在运行过程中无法释放已经分配的内存空间,导致系统中存在大量的无用的内存空间,最终可能导致程序崩溃甚至系统崩溃。
完结:
对于动态内存还是比较重要的,因为通常情况下:堆的空间是比栈的空间的是大的,同时我们知道,动态内存是可以进行修改的,我们想开辟多少字节的空间就开辟多少字节的空间,这一点就可以证明他的优点,所有在开辟动态内存空间时,一定要注意防止出现得不偿失!!!
——————————
最后小孩码文不易,感谢支持,有你的支持就是小孩最大的动力 ~~ ^ _ ^ ~~