一、为什么要有动态内存管理
话说为什么要动态内存管理?
**我们已经的内存开辟方式有:**
int val=20;
char arr[10]={
0}
上述的开辟空间的方式有两个特点:
●空间开辟的大小是固定的。
●数组在申请的时候,必须指定数组的长度,它所需要的内存在编译时分配。
因为我们在实际应用的时候,在栈上开辟的空间时候,
必须明确所开辟的大小(即在程序运行前),
而用户本身有所需要的空间,导致先前在栈上开辟的空间或大或小,
不能完全匹配用户所需,导致我们写的程序能解决的问题面比较窄,
不能解决通用的问题。而且在栈上开辟的空间不会很大,
而在堆上可以开辟很大的空间,在栈上申请的空间一般定义的都
是临时的变量,而在堆上开辟的空间必须要由程序员管理(程序员申请,程序员释放)
二、什么是动态内存函数
C语言提供了一个动态开辟的函数
void* malloc(size_t size)
这个函数向内存申请了一块连续可用的空间,并返回指向
这块空间的指针
●如果开辟成功,则返回一个指向开辟好空间的指针。
●如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
●返回值得类型void* ,所以malloc函数并不知道开辟空间的类型,
具体在使用的时候使用者自己来决定。
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void* ptr)
free函数用来释放动态开辟的内存。
●如果参数ptr指向的空间不是动态开辟的,那free
函数的行为是未定义的。
●如果参数ptr是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h头文件中。
C语言还提供了一个函数叫 calloc,calloc函数
也用来动态内存分配。
原型如下:
void* calloc(size_t num,size_t size);
●函数的功能是为num个大小为size的元素开辟一块空间,
并且把空间的每一个字节初始化为0;
●与函数malloc的区别只在于calloc会在返回地址之前
把申请的空间的每一个字节初始化全为0.
**realloc**
● realloc函数的出现让动态内存函数管理更加灵活。
●有时候我们发现过去申请的空间太小了,有时候我们
又会觉得申请的空间过大了,那为了合理的时候内存,
我们一定会对内存的大小做灵活的调整,那realloc
函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc(void* ptr,size_t size);
●ptr 是要调整的内存地址
● size 调整之后的新大小
● 返回值为调整之后的内存起始位置。
●这个函数调整原内存空间大小的基础上,还会将原来内存
中的数据移动到新的空间。
●realloc 在调整内存空间的是存在两种情况:
情况1:原有空间之后又足够大的空间
情况2:原有空间之后没有足够大的空间。
三、错误案例分析
1、对NULL指针的解引用操作
void text()
{
int *p = (int *)malloc(INT_MAX / 4);
*p = 20;
free(p);
}
2、对非动态开辟内存使用free释放
void text()
{
int a = 10;
int *p = &a;
free(p);
}
3、使用free释放一块动态开辟内存的一部分
堆空间是整体申请,整体释放的,不能整体申请,局部释放
void text()
{
int *p = (int *)malloc(100);
p++;
free(p);
}
4、对堆空间重复释放
void text()
{
int *p = (int *)malloc(100);
free(p);
free(p);
}
5、动态开辟内存忘记释放(内存泄漏)
void text()
{
int *p = (int *)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
text();
while (1);
}
由于text()是一个函数,调用函数就会形成栈帧结构
函数中在堆上申请空间,申请空间的地址就由指针p保存(即p指向堆空间的起始地址)
p是函数内部的指针变量(即是在栈上开辟的变量)
函数调用完之后,函数内部的所有临时变量就会被释放(即p就会被释放,p的指向就不存在了)
而申请的堆空间仍然存在(因为必须要有程序员申请,程序员释放)
6、对开辟的空间的越界访问
void text()
{
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;
}
free(p);
}
四、几道经典笔试题
**第一题:**
调用GetMemory函数,形成栈帧
void GetMemory(char *p)
{
p = (char*)malloc(100);
}
int main()
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
return 0;
}
```
总结:
● 返回值没有判空
● 使用完毕没有free
● 在使用期间一定会发生内存泄漏
● p空间与str空间不是同一份空间,对p进行修改,
并不修改str,GetMemory函数无法把堆空间通过str带出来,此时拷贝就会出错,导致程序崩溃
上述代码不能将堆空间带出来,若想实现该功能,则要用到二级指针
void GetMemory(char **p)
{
*p = (char*)malloc(100);
}
int main()
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
return 0;
}
或直接用带返回值的函数返回
char *GetMemory(int num)
{
char *temp = (char*)malloc(num);
return temp;
}
int main()
{
char *str = GetMemory(num);
return 0;
}
**第二题:**
问题:程序打印出来的乱码是在GetMemory函数返回的
时候就已经乱了呢?还是怎么着?
大部分人都会认为:
首先p是个数组,且是一个临时数组
一旦函数结束,p数组就会被释放
所以str指向的空间就是废弃空间,打印str就是乱码
而实际上:
我们应该知道一点就是,电脑上对数据的拷贝是真的进行
数据拷贝,而删除,只要设置数据无效即可
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
调用GetMemory函数形成栈帧结构,产生了p数组,
函数返回,栈帧结构被释放,但栈帧结构里面的数据仍然在,并不会被清空,只是被设置为了无效数据,
str仍然指向p数组的内容,因为你要打印str,
而printf是一个函数,在执行打印逻辑之前(即在调用打印之前,要先调用printf函数),要给printf函数形成栈帧(由于GetMemory函数释放,所以是基于Test()函数向下形成新的栈帧结构,且printf函数中也有临时变量)
栈帧结构一旦形成就会覆盖GetMemory函数的数据
所以再进行打印,str指向的内容已经是被修改过的内容,所以打印的是乱码。
*所以乱码不是由于GetMemory函数调用返回导致的
而是调用printf后数据被改写所导致的*
**注意**
printf也是函数,调用则也会形成栈帧结构,里面的临时变量会覆盖
旧的数据。
**第三题:**
void Text(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}