目录
动态内存管理产生的原因
我们在使用数组的时候经常会产生浪费,因为数组是一次性开辟的空间,可能一次开辟了100个字节大小的空间,但是最终只使用了10个,从而造成了浪费。
为了减少这种不必要的浪费,就有了动态内存管理这一概念,我们可以开辟一块动态的、不固定大小的空间,方便我们调整,满了就扩容。
动态内存函数的介绍
一、malloc
malloc函数只有一个参数,size_t 类型的size,代表的是字节数,意思是malloc申请了多少个字节。返回类型是void* ,因为malloc函数事先不知道为谁开辟的空间,所以交给程序员自己强转,开辟空间。
示例:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; ++i) {
*(p + i) = i;
}
return 0;
}
给变量p开辟 40个字节大小的空间,p是int*类型的,所以malloc强转成int*类型。
开辟完空间后,要记得判断p是否为空,是的话用strerror(errno)报出错误信息并结束程序。
如果不为空就往p里赋值。
二、free
free是和malloc、calloc配套使用的,向内存申请了空间,结束时就要返回空间,如果程序一直运行,申请的空间一直占着但也不用,又不返回,就会造成内存泄漏。
什么是内存泄漏?比如一台服务器一直连续运行着一个程序,但程序向内存申请的空间一直没有释放,内存就会一点一点减少,直到不够了出现死机现象。重启恢复正常但运行程序一段时间又重复上述过程,这就是内存泄漏,因为申请空间没有释放。
比如这段问题代码:
while (1)
{
malloc(1);
}
早期编译器不像现在这么智能,系统会一直给它分配空间,电脑容易死机。
现在有些电脑也一样,我写博客的时候亲测过了,运行6秒电脑直接挂了。
上面示例一中的代码中没有free,但不会产生内存泄漏,因为在这个程序结束时系统会自动回收内存,或者说这个程序不是一直在运行,而是只执行很短的一段时间,结束时系统就自动回收内存了。
当然我们也可以free一下:
我们可以看到虽然这里free了p,但是p里的内容没有变,这是很危险的,必须将其置空,防止野指针的问题。
三、calloc
calloc函数其实就是在malloc的基础上自动将所有元素初始化为0,calloc== malloc+memset
四、realloc
realloc函数就可以实现扩容,使得动态开辟内存更加灵活,它有2个参数,ptr是指向要扩容空间的地址,size则是扩容后新的空间的大小。 返回值为调整成功返回空间的地址,开辟失败返回空。
示例:
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
if (p == NULL)
return 1;
int* ptr = (int*)realloc(p, sizeof(int) * 20);
if (ptr != NULL)
p = ptr;
free(p);
p = NULL;
return 0;
}
注意这里realloc的返回值不能直接交给p,如果扩容太大导致失败返回空指针的话,p接收,本来指向的是40个字节的内容,现在变成了空,就会出现问题,所以先用ptr接收一下,不为空再给p.
realloc在扩容的时候,有两种情形:
一、原空间后面有足够的空间支持扩容,这种直接在后面开辟空间就可以,返回原空间的地址。
二、原空间后面空间不足,不支持直接扩容。
这时realloc会在内存其他地方找一块连续的空间实现扩容,将原空间数据拷贝到新空间,最终返回新空间的地址,原空间realloc会自动释放。
这样原空间和其他程序占用之间会有一块空间存在浪费,所以我们知道动态开辟内存太多的话不仅会造成空间碎片化(很多小块的空间浪费),还会导致效率下降,malloc、realloc都有消耗。
建立内存池来维护创建的空间是一种常用的手段,这是后话了。
常见的动态内存错误
一、对空指针解引用操作
int main()
{
int* p = (int*)malloc(10);
*p = 20;
free(p);
p = NULL;
return 0;
}
开辟空间有可能失败,会返回空指针,直接对其解引用就会出现错误。
解决方法:
int main()
{
int* p = (int*)malloc(10);
if (p == NULL)
return 1;
*p = 20;
free(p);
p = NULL;
return 0;
}
在使用之前一定要先判断。
二、对动态开辟的空间越界访问
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
return 1;
for (int i = 0; i <= 10; ++i)
{
*(p + i) = i;
printf("%d ", i);
}
free(p);
p = NULL;
return 0;
}
当i == 10的时候越界访问,出现错误。
三、对非动态开辟内存使用free
int main()
{
int a = 10;
int* p = &a;
free(p);
p = NULL;
return 0;
}
四、使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(40);
for (int i = 0; i < 5; ++i)
{
*p = i;
p++;
}
free(p);
p = NULL;
return 0;
}
切记不能p++改变p的指向,因为最后释放的是p的地址开始的位置,p移动到p+5的位置,释放动态内存的一部分是不可以的。
五、对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(40);
for (int i = 0; i < 10; ++i)
{
*(p + i) = i;
}
free(p);
//代码...
//...
//...
free(p);
return 0;
}
不能多次释放,所以free完后一定要置空,这样就算再free也没什么影响,free完后p里保存的任然是p原来的地址,是野指针,所以一定要置空。
六、动态内存开辟忘记释放(内存泄漏)
上面已经详细讲过内存泄漏问题了,就不再赘述了,这是最可怕的一种错误,一定要注意。
或者假设p申请了空间,又把空间给了ptr,ptr忘记释放了,也会造成内存泄漏。
经典笔试题讲解
题目一:
问:能否打印str?
首先进入Test函数,给str赋空指针,传参给GetMemory函数,注意,这里传参传的是str的值,而不是地址,是传值调用,所以GetMemory函数中为p开辟了100个字节的空间后,出了函数p就销毁了,str没有改变,还是空指针。strcpy函数使用时要对两个参数解引用,空指针不能解引用,就出现错误了。并且上面GetMemory函数中没有free释放内存,p虽然销毁了,但开辟的空间还在,会造成内存泄漏。
改法1:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
改法2:
char* GetMemory()
{
char* p = (char*)malloc(100);
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
题目二:
问:能不能打印str的内容?
和上一题类似,GetMemory函数中的p数组也是局部变量,出了函数就销毁了,返回的str其实是个野指针,虽然还是指向p的位置,但其内容随着销毁可能已经被覆盖了,所以打印结果应该不是hello world,而是其他随机内容了,大概率是被覆盖了。
题目三:
问:能不能打印str的内容?
和题目一我们改的版本基本一样,就是少了free,所以能打印hello,但存在内存泄漏。
题目四:
问:能不能打印str的内容?
还是野指针的问题,不能打印。
free之前都没问题,但是free后没置空,此时的str就是野指针,将world放入str形成非法访问。
柔性数组
柔性数组只能出现在结构体中,它的长度是不确定的,一般形式写成:
struct S
{
int n;
int arr[0];
};
但有的编译器编不过去,最好写成:
struct S
{
int n;
int arr[];
};
柔性数组必须是结构体里面最后一个成员,且大小未知。
柔性数组的特点:
第三点,柔性数组成员用malloc函数动态分配内存。
示例1 :
#include<stdlib.h>
struct S
{
int n;
int arr[];
}a;
int main()
{
struct S* a = (struct S*)malloc(sizeof(struct S)+40);
if (a == NULL)
return 1;
struct S* b = (struct S*)realloc(a,sizeof(struct S) + 80);
if (b != NULL)
{
a = b;
b = NULL;
}
free(a);
a = NULL;
return 0;
}
示例2:
struct S
{
int n;
int *arr;
}a;
int main()
{
struct S* a = (struct S*)malloc(sizeof(struct S));
if (a == NULL)
return 1;
a->arr = (int*)malloc(40);
int* b = (int*)realloc(a->arr, 80);
if (b == NULL)
return 1;
free(a->arr);
free(a);
a = NULL;
return 0;
}
示例1的方法比示例2的好,1只需要malloc和free一次,而2中需要两次,malloc次数越多,内存碎片就可能越多,性能也会越低。