目录
今天,博主给大家带来的是动态内存分配的学习和讲解。在之前,我们学习了通讯录,文章中利用到一些动态内存分配的一些知识,有些可能大家会看不懂,那么相信通过今天的这篇文章,大家的问题就会迎刃而解。本篇,我们将从“为什么存在内存分配”,“动态内存函数介绍”,以及“常见的动态内存错误”三个板块来为大家一 一解答。
一、为什么存在动态内存分配
在之前,我们学过的内存开辟有哪些呢?比如,创建一个变量,或者创建一个数组。
int a = 10;//在栈空间开辟四个字节
char arr[10] = { 0 };//在栈空间开辟十个字节的连续空间
上面两种开辟方式是我们常用的开辟内存的方式,但是这两种开辟内存空间的方式有两个特点:
① 空间开辟大小是固定的
② 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是由于空间的需求,有时候我们需要空间的大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足我们的需求了,这时就要试试动态内存开辟的方式了。
二、动态内存函数介绍
在学习动态内存函数之前,我们需要知道动态内存开辟的空间是放在堆区的,如上图所示,局部变量和形式参数占用的空间是在栈区的,全局变量以及静态变量开辟的空间是在静态区的。
2.1 malloc
C语言提供了一个动态内存开辟函数
void* malloc ( size_t size ) ;
这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。① 如果开辟成功,则返回一个指向开辟好空间的指针。② 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查。③ 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。④ 如果参数 size 为 0 , malloc 的行为是标准是未定义的,取决于编译器。
int main()
{
int* p = (int*)malloc(40);//开辟40个字节的空间
if (p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功
for (int i = 0; i < 10; i++)
{
printf("%d\n", *(p + i));
}
return 0;
}
因为返回的类型是void*,所以我们要根据自己的需求来进行强制类型转换,其次,我们需要判断是否开辟成功,如果返回值为NULL指针,那么就结束了,反之则是开辟成功。然后我们打印一下看看开辟成功的空间里面是什么。
此时我们发现开辟的空间里面存的是一堆没见过的随机数数,其实是malloc函数申请的空间,在申请成功后,直接返回这片空间的起始地址,不会初始化空间的内容。
2.2 free
void free ( void* ptr ) ;
上面我们学习了malloc函数,我们发现,malloc只负责申请空间,那么申请的这个空间当我们使用完之后会怎么样呢?其实这块空间并不会主动的还给操作系统,除非程序结束,否则这块空间将会一直存在堆区。这个时候就需要另一个内存函数了。总的来说: malloc申请的内存空间,当程序退出时,还给操作系统,当程序不退出时,动态申请的内存不会主动释放。需要free函数来释放空间。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//开辟成功
free(p); //释放开辟的空间
p = NULL; //置空
return 0;
}
注意:p本来指向的空间被释放后,p就变成野指针了,比较危险,这时候我们需要主动将p置为空指针。
①free只能释放动态开辟的空间,不能释放静态区或者栈区开辟的空间(标准未定义) 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
② 如果参数 ptr 是 NULL 指针,则函数什么事都不做。malloc和free都声明在 stdlib.h 头文件中
2.3 calloc
void* calloc ( size_t num , size_t size ) ;
① 函数的功能是为 num 个大小为 size 的元素开辟一块空间 ,并且把空间的每个字节初始化为 0 。② 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0 。
int main()
{
int* p = (int*)calloc(10, sizeof(int));//开辟十个连续的sizeof(int)大小的空间
if (p == NULL)
{
perror("calloc");
return 1;
}
//打印数据
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
当我们打印完之后,发现calloc会把每个字节初始化为0。
总的来说:calloc函数和malloc函数很相似,功能也是一样,唯一不同的就是,会把开辟的每个字节都初始化为0。
所以如何我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc 函数来完成任务。
2.4 realloc
void* realloc ( void* ptr , size_t size ) ;
① ptr 是要调整的内存地址(也就是被调整空间的起始地址,这块空间之前已经开辟好了)如果ptr为空指针,那么它的功能和malloc就是一样的,开辟一个新的空间。② size 调整之后新大小 (需要调整的新的空间的大小)③ 返回值为调整之后的内存起始位置。这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。④ realloc 在调整内存空间的是存在两种情况:情况1 :原有空间之后有足够大的空间情况2: 原有空间之后没有足够大的空间
情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2
当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,同时把原来空间的内容拷贝过来,然后自动释放原来的内存空间,这样函数返回的是一个新的内存地址。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//初始化为1~10
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = i + 1;
}
//增加一些空间
int* ptr = (int*)realloc(p, 80);
if (ptr != NULL) //开辟成功
{
p = ptr;
ptr = NULL;
}
else //开辟失败
{
perror("realloc");
return 1;
}
//开辟成功,打印数据
for (i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
free(p); //释放空间
p = NULL; //置空
return 0;
}
注意:考虑到可能开辟失败,所以我们需要先进行判断一下,如果开辟成功,则将返回的指针赋值给p来维护。
当我们开辟完之后,打印一下看看效果。
当我们使用完动态开辟的内存之后,仍然需要手动去释放内存空间。
当然,上面情况只是减少增加空间,如果要减少空间就比较简单了,直接在原来的基础上减少,地址返回的也是原来的地址。
好了,到这里动态内存管理的基本知识就介绍清楚了,实际上把这四个内存函数了解清楚,基本上对动态内存的知识点也就基本掌握了。接下来,我们来了解一下动态内存管理常见的内存错误,通过解释这些错误,来更清楚更深入的了解动态内存管理。
三、常见的动态内存错误
3.1 对NULL指针的解引用操作
当我们动态内存开辟的时候,会存在开辟失败的情况,此时返回的就是空指针。
如下代码就是一个典型的例子:
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
此时动态内存开辟可能失败了,就导致返回的指针为空指针,也就是p为空指针,如果再对p这个空指针进行解引用操作,那么就会报错 。为了解决这种问题,我们在平时写代码的时候,为了避免空指针异常,要对动态开辟的空间返回的指针进行空指针判断检查。(好习惯)
3.2 对动态开辟空间的越界访问
这里直接拿代码来解释吧!
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
这段代码中,我们使用malloc开辟40个字节大小的空间,然后进行空指针检查判断,紧接着,在我们赋值的时候,我们最多只能访问到p[9]这块空间,代码中我们i的最大值为10,此时很明显,就出现了越界访问。
总的来说:开辟多少空间,就只能使用多少空间,没有开辟的,属于操作系统的空间,我们不可以随便进行操作访问。
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
在前面讲free的时候说过,free释放的空间,只能是动态内存开辟的空间,不能是静态区或者栈区开辟的空间,上面代码的例子中,a是局部变量,不是动态开辟的空间,所以不能被free释放。
3.4 使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i + 1;
p++; //此时p不再指向malloc开辟的空间的起始地址
}
//释放
free(p);
p = NULL;
return 0;
}
上面代码中,我们使用malloc开辟一块空间,然后将起始地址返回给p,也就是说p指向molloc动态开辟的空间的起始地址,紧接着进行空指针检查判断,然后给p指向的空间进行赋值,但是,在赋值的过程中,p的指向发生了变化(如下图),不再指向malloc开辟的空间的起始地址,此时在对p指向的空间进行释放,这种做法是不被允许的,会报错。
总结:free中的参数只能是动态内存开辟空间的起始地址,对于动态开辟的空间的起始地址不要随便的更改指向。
3.5 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
上面这种情况也是一直很低级的错误,也是不被允许的,会报错,最好的解决办法就是,在释放完之后,将p指针置为空,这样在后面多次释放也不影响,因为p已经是空指针了。
总结:不能对同一块动态内存多次释放,解决办法:在第一次释放完之后,将指针置为空指针(好习惯)
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
对于上面这个代码,在test这个函数种,我们开辟了100个字节的动态空间,但是在最后,我们并没有对这块空间进行free释放,这时候当我们跳出函数之后,指针变量p也销毁了,这个时候,p原来指向的空间我们根本找不到了,但是这块空间仍然存在,仍然被占用,只是我们丢失了它的起始地址,不能再对这块空间进行操作或者访问了,这就造成了这块空间仍然存在占用内存,但是我们却访问不到,并无法释放,这就是所谓的内存泄露。
针对这个问题的解决:在我们使用完动态空间之后,一定要进行free释放。
总结:动态开辟的空间一定要释放,并且正确释放(切记)
好了,今天的动态内存分配和管理讲到这里就结束了,听到这里,相信大家的一些关于动态内存分配管理的问题就会迎刃而解了吧。如果哪里有问题,欢迎在评论区留言。如果觉得小编写的还不错的,那么可以一键三连哦,您的关注点赞和收藏是对小编最大的鼓励。谢谢大家!!!