动态内存管理详细介绍
Posted 捕获一只小肚皮
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态内存管理详细介绍相关的知识,希望对你有一定的参考价值。
文章目录
1.为什么存在动态内存分配?
目前为止,我们已经掌握的内存开辟方式有两个,且知道在内存中,我们主要使用的是 栈区,堆区,静态区
.其中三者的作用分别如下:
(1)静态存储区:主要存放static静态变量、全局变量、常量。这些数据内存在编译的时候就已经为他们分配好了内存,生命周期是整个程序从运行到结束。
(2)栈区:存放局部变量。在执行函数的时候(包括main这样的函数),函数内的局部变量的存储单元会在栈上创建,函数执行完自动释放,生命周期是从该函数的开始执行到结束。
(3)堆区:程序员自己申请一块任意大小的内存—也叫动态内存分配。这块内存会一直存在知道程序员释放掉。C语言中,用
malloc or new
动态地申请内存,用free or delete
释放内存。良好习惯:若申请的动态内存不再使用,要及时释放掉,否则会造成内存泄露
示例:
第一:创建局部变量,函数调用等
void print()
{
char str[] = "I'm your father";
printf("%s",str);
}
int main()
{
int a = 0;
print();
return 0;
}
例如上面,就是创建了局部变量
str和a
,以及进行了函数调用
.而他们是在 栈区使用内存
第二: 全局变量,常量等
int b = 20;
int main()
{
const int c = 10;
return 0;
}
例如上面,就是创建了 全局变量
b
,常量c
, 而他们是在 静态区使用
但是上述的内存开辟方式却有两个特点:
-
空间开辟大小是固定的.
-
数组在声明的时候,必须指定数组长度,且此后长度固定,无法修改.它所需要的内存在编译时进行分配
而最后剩下的一种----- 堆区,便是我们此篇文章的核心与主角.
**引言:**有时候我们需要的空间大小在程序运行的时候才能知道,那数组在编译时才开辟空间的方式就不能满足,这时候就只能试试动态存开辟,即使用 堆区内存
比如下面的例子:
struct info
{
char name[20];
int age;
};
int main()
{
int n = 0;
scanf("%d",&n);
struct info arr[50];
return 0;
}
上面的结构体 struct info
是一个人(学生)的信息(姓名,年龄).下面创建了一个大小为50的数组arr
用来存各个学生的信息.
但是我们难免会碰到一些情形,比如一些学生因为特殊原因不来上学,就不会记录信息.或者因为生源太好,名额超出50.那么最开始设置的50个名额,就不会够用,或者空间浪费.因此我们便需要对数组长度进行动态调整.
2.各种动态内存函数介绍
2.1 malloc
官方声明:
void* malloc (size_t size)
- size 需要开辟空间的字节大小
- void* 开辟的空间的地址
2.1.1声明与解释
作用: 开辟一块数组空间,返回地址.
使用:
-
如果开辟成功,则返回一个指向开辟成功的空间的指针
-
如果开辟失败,则返回一个
NULL
指针,因此malloc
的返回值必须检查 -
返回值类型是
void*
,所以malloc
函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。 -
如果参数
size
为0,malloc
的行为是标准是未定义的,取决于编译器。
2.1.2 使用
例子:
- 向内存申请10个整形空间
int* p = (int *)malloc(sizeof(int) * 10);
可以看见
size
的实参是sizeof(int) * 10
最后返回时,指针
void*
被转化为整型指针.
- 在申请的时候可能也会申请失败,比如:
#include<stdio.h>
#include <stdlib.h>
int main()
{
double* p = (double*)malloc(sizeof(double) * 1000000000000);
if (p == NULL)
perror("申请失败原因:");
return 0;
}
结果:
- 使用空间
#include<stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
perror("申请失败原因:");
else
{
for (int i = 0; i < 10; i++)
{
p[i] = i;
printf("%d ",p[i]);
}
}
return 0;
}
上面已经介绍了如何在 堆区申请空间,判断申请是否成功,如何使用
malloc
.但是我们最开始就说了,堆区申请的空间是 程序员自己手动释放,而放眼看前面,我们释放了吗??
并没有,因此,引出了下面的函数
free
2.2 free
上述已经学会了使用
malloc
,现在就应该还回去我们所申请的空间了.那怎么还回去呢? 答案是 使用free
函数
官方文档:
void free(void *memblock);
memblock
需要释放的空间的地址
使用:
#include<stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
perror("申请失败原因:");
else
{
for (int i = 0; i < 10; i++)
{
p[i] = i;
printf("%d ",p[i]);
}
}
free(p); // 成功释放
return 0;
}
上面已经成功释放了,但是还有个小问题,比如我们在free
后面和最开始申请空间语句后面加一条语句 printf("%p\\n",p);
会发现,申请的空间虽然被释放,但是p的值并没有改变,仍然记住了,此时p成了野指针,及其危险,所以我们需要在free(p)
后面加上 p = NULL;
比如:
#include<stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
perror("申请失败原因:");
else
{
for (int i = 0; i < 10; i++)
{
p[i] = i;
printf("%d ",p[i]);
}
}
free(p); // 成功释放
p = NULL; //置为空
return 0;
}
助记:
此时的p就相当于男女朋友中的男生,当他们分手了以后,男生还记得女生的所有信息(p仍然记住了原地址) ,女生为了防止自己以后不被骚扰,就给了男的当头一棒,让男的失忆(p = NULL)
总结:
malloc,free,NULL 需要成对使用.
free函数用来释放的是动态开辟的内存,如果参数是指向非动态开辟的内存,那么free函数未定义.
如果free的参数是空指针NULL,则free函数什么也不做
2.3 calloc
calloc
函数的用法与malloc
很像,但是calloc
比malloc
函数多了一个功能---------可以在开辟空间时候,给数组每个位置初始化为0官方文档:
void* calloc (size_t num , size_t size);
num
------>>> 数组长度
size
------>>> 数组元素类型的字节大小开辟成功返回数组地址,不够返回
NULL
示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int* )calloc(10,sizeof(int)); //申请空间
if(p == NULL) //检测是否开辟失败
perror("错误原因:");
else
for(int i = 0;i<10;i++) //打印数组每个值
printf("%d ",p[i]);
free(p); //释放空间
p = NULL; //置为空指针
return 0;
}
2.4 realloc (王者出场,真正的调整数组长度)
前面我们介绍了
malloc
与calloc
,但是好像都是在申请一块空间,并没有与我们开头介绍的数组长度可变有半毛钱关系,于是顺应我们的要求,王者老大哥---------realloc
出场.
realloc
的出现使内存管理更加灵活,可以调整动态内存开辟的大小.
官方文档: void* realloc (void* memblock,size_t size)
-
memblock
已经开辟的内存块的地址 -
size
新的空间大小 -
开辟成功就会返回开辟的空间地址
-
失败就会返回
NULL
-
realloc
的返回值需要用一个新的变量接收
示例:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int* )malloc(sizeof(int)*10); //申请空间
if(p == NULL) //检测是否开辟失败
perror("错误原因:");
else
for(int i = 0;i<10;i++) //打印数组每个值
printf("%2d ",p[i] = i);
printf("\\n");
//调整空间
int* p1 = (int*) realloc(p, sizeof(int) * 20);
if(p1 == NULL) //检测开辟是否成
perror("错误原因:");
else
{
p = p1; //重新交给p管理申请的空间
for(int i = 10;i<20;i++)
printf("%d ",p[i] = i);
}
free(p); //释放空间
p = NULL; //置为空指针
return 0;
}
可以看见,之前malloc
只开辟了10个长度,但是现在却可以有20个空间.
2.4.1 realloc的空间开辟方式
但是看了上面的使用示例,大家是否有什么疑问呢? 比如:
-
为什么一定要用新的指针变量来接收
realloc
的 -
后面为什么又重新交给原来的指针变量p
-
释放空间时候,为什么只需要释放p,而不用管p1了.
在回答这几个问题之前我们需要知道realloc
开辟空间的方式.以下面的图为例
在堆区,空间占有比较混乱,如图中所示malloc
已经成功开辟了一块空间,但是可以清晰的看到,在malloc
开辟的地方和右上角其他占用地区中间还有一块小缝隙.在下方的两块其他占用区中间也有一块空白的地方.
而realloc
对malloc
申请的空间进行调整时候,会首先自动检测新的需要开辟的空间,如果直接跟在mallo后是否够用,比如下图:
-
比如此时
realloc
还需要三个整型长度----------realloc (p, sizeof(int)*13);
检测到malloc
后面还剩下4可用,于是便直接在其后追加三个空间,然后依然返回p的地址. -
而倘若
realloc
还需要8个整型长度----------------realloc(p,sizeof(int)*18);
检测到malloc
后面只有4个空间,并不够用,于是便再另起山头,在下面看到了一个可以开辟20整型长度的空地,于是便在这里重新开辟 18个整型空间,开辟好后,便把原malloc
开辟空间内容拷贝一份放到新开辟的18个空间.然后返回新开辟的空间地址,返回时候,便同时自动释放掉原来malloc
开辟的空间 -
倘若上面两种都开辟失败,就会返回NULL
因此,这就是我们为何说,一定要用新的指针接收realloc
的返回值,因为如果开辟失败,非但没有得到新的空间,还返回了一个NULL
给原来管理malloc
开辟的空间的变量p,导致p无法再管理malloc
. -------------这种危险写法p = (int*)realloc(p,sizeof(int)*18);
而如果开辟成功了,再重新把新的地址交给p管理,p就有两种可能.
- 第一,仍然接管p原来空间(现在增大了)
- 第二,接管了新的空间.
会发现,无论上面哪种情况,最后释放p时,都会完全释放两块空间(因为新开辟时候,会自动释放原malloc
开辟空间).
3.常见的动态内存错误
示例1
int main()
{
int *p = (int *)malloc(INT_MAX/4);
for(int i = 0;i<10;i++)
*(p+i) = i;
free(p);
return 0;
}
上面的例子有什么错误??
答案:
- 没有判断是否成功开辟空间,如果开辟失败p将会是
NULL
,那么后面的*(p+i) = i;
就是在对NULL
指针进行解引用操作- p最后未置为
NULL
,是不安全的,正如博主一开始写的男女朋友关系那个例子
示例2
int main()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
perror("开辟失败原因:");
return -1;
}
for(i=0; i<=10; i++)
{
*(p+i) = i;
}
free(p);
p = NULL;
return 0;
}
上面的例子有什么错误??
答案:
数组越界,10个空间的数组,索引最大只能9.
示例3
int main()
{
int a = 10;
int *p = &a;
free(p);
p = NULL;
return 0;
}
上面的例子有什么错误??
答案:
free
前面已经讲到只能对管理堆区的内存进行释放,而这里的p管理的是栈区
小插曲:
| 之前忘记讲了,我们知道realloc
是对动态开辟的内存进行管理,但是我们如果把realloc
的第一个参数给一个NULL
|
| 那么,此时的realloc
就完全是个malloc
, 比如 realloc(NULL, sizeof(int)*10)
等价于 malloc(sizeof(int) * 10)
|
示例4
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int)*20);
if (p != NULL)
{
for (int i = 0; i < 10; i++)
*p++ = i;
}
free(p);
p = NULL;
return 0;
}
上面的例子有什么错误??
答案:
并没有完全释放堆区内存,下图进行解释
在执行循环后,free释放的只是后面橙色的空间,黄色空间并没有释放.
示例5
int main()
{
int *p = (int *)malloc(100);
if(p == NULL)
perror("错误原因");
else
{
for(int i= 0;i<25;i++)
printf("%d ",p[i] = i);
}
free(p);
...... //省略几万行代码
...... //省略几万行代码
free(p);
return 0;
}
上面的例子有什么错误??
答案:
多次释放同一块内存,原因是当代码功能过多时候,我们会忘记前面是否释放过,而导致重复释放.
第一次释放后,空间已经没有了,但是p还记忆着在堆区的原地址,再次释放,如果此时该地址其他东西被占用,将会导致内存管理出错,或者程序挂掉.
防止重复释放的解决办法: 每次释放后记得加 p = NULL
,free
对空指针NULL
不做处理
示例6
int main()
{
while(1)
{
malloc(1);
}
return 0;
}
上面的例子有什么错误??
答案:
在疯狂的开辟空间,并没有还回去,导致自己不用了,别人也不能用(内存泄露).
这会导致整个计算机最后崩溃.
4.几个经典笔试题
题目一:
void GetMemory(char* p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
会出现什么结果???
答案:
程序会崩溃.原因:
GetMemory
属于传值调用,不是传址调用.
即p虽然获取了堆区申请地址,可以进行管理,但由于GetMemory
函数结束后,p的生命周期也就结束了.管理不了该空间,且下面用的还是str,不是p
最后,我们字符串拷贝的第一个参数是str,传值调用时,str的值并没有改变,仍然是NULL.
那么最后指针str
还是一个空指针,便不能进行拷贝.程序出错.其次,在函数
GetMemory
内部并没有释放内存.
怎么修改??
第一种改法:
void GetMemory(char* * p) //修改形参
{
*p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);//释放内存
str = NULL;
}
第二种改法:
char * GetMemory(char* p)
{
p = (char *)malloc(100);
return p; //直接返回值.
}
void Test(void)
{
char* str = NULL;
str = GetMemory(str); //用str接收
strcpy(str, "hello world");
printf(str);
}
题目二:
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
答案:
烫烫烫烫烫烫烫烫烫…
解析:
GetMemory
函数内部的局部变量的地址是在栈区
,当GetMemory
函数调用结束,字符数组p的生命周期也就结束,里面的内容也不复存在.而其却返回了p的地址,因为不复存在,系统便给未初始化的栈空间一个中文GB2312编码中
的0xccdd
,正好对应烫
怎么修改?
第一种改法:
char *GetMemory(void)
{
static char p[] = "hello world"; //变为静态变量,延长生命周期
return p;
}
void Test(void)
{以上是关于动态内存管理详细介绍的主要内容,如果未能解决你的问题,请参考以下文章