今天我们来聊聊动态内存管理:
要掌握动态内存管理,必须要掌握以下几个函数:malloc, calloc ,realloc 和 free
① malloc
函数原型:
void* malloc(size_t size)
这个函数的功能是向内存申请size个字节的连续空间,并且返回指向这个空间的指针,如果开辟失败,则返回NULL,来看下面一段代码:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
return 0;
}
在这里,我使用malloc向内存申请开辟 5 个整形的空间,如果开辟失败, 则打印错误信息,然后返回1(与正确返回0做一个区分), 接下来就处理开辟成功的情况.
for (int i = 0; i < 5; i++)
{
p[i] = i + 1;
}
这种使用方法,可能初看起来有些疑惑,怎么还可以用数组的表达方式? 实际上,"[ ]" 这个操作赋就相当于先相加后解引用,所以 *(p + i) == p[ i ]
所以,从 p 往后 4 个整形依次放的是 1 2 3 4 5 打印出来看一下:
for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
到这里这块空间我使用完了,不想再用了,那怎么办? 有借有还,再借不难,我把这块空间还给操作系统.怎么还? 这就涉及到 free 函数
② free
free 函数原型:
void free(void* ptr)
其功能是把ptr所指向的并且是动态开辟的这块空间(也就是本节内容所介绍的几个函数所开辟的空间) 释放 ,下面来一个错误案例:
// free 的错误用法
int main()
{
int a = 10;
int* pa = &a;
free(pa);
return 0;
}
再强调一遍: 这是一个错误案例,你不要完美记住了一个错误案例,到时候逢人就说哎呀那个谁谁谁就这样写的...我不背锅
第二点需要注意的是,free的参数必须是要释放的空间的 头指针 ,比如我写一个free(p), 这里的p一定指向了你要释放的这块空间的第一个元素,你不要传一个中间的指针过来, 不然的话它就从中间开始释放了,那就不好玩了
言归正传:接下来我不想再用这块空间了,就得把这块空间释放掉,把它还给操作系统,并把 p 置为空指针:
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
// 使用
for (int i = 0; i < 5; i++)
{
p[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
free(p);
p = NULL;
return 0;
}
有人就问了,为什么要把 p 置为 NULL 呢,原因是:我 free 之后, p所指向的空间已经还给操作系统了,已经不属于你了,如果这个时候 p 里面放的还是这块空间的地址,那 p 就变成野指针了. 举个例子: 你和你对象分手了,但是你还记得她家的地址,天天上门求复合,合适吗? 就不合适了,所以 p 置为 NULL 之后,相当于你忘记了她家的地址,这样是不是才断的干净,也符合逻辑对吧,换言之如果 p != NULL ,那操作系统回收后你将不知道人家这块空间里面放的什么玩意,这是一件很危险的事情
到这里,我的 malloc 和 free 的使用就完了
③ calloc
calloc 函数原型
void* calloc(size_t num, size_t size)
这个函数的功能与malloc相似,都是开辟空间的,有两点不同:
① malloc 开辟空间,只需在其参数后面写多少个字节就行了,而这个函数的两个参数则是表示要开辟 num 个 大小为 size 个字节的空间
③ calloc 在开辟完之后会把空间中的每个字节都初始化为0 而malloc 不会
比如,我要开辟 10 个整形,两个函数的写法分别是:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
int* p1 = (int*)calloc(10, sizeof(int));
return 0;
}
与 malloc 一样, calloc 也有可能开辟失败:
int main()
{
int* pa = (int*)calloc(5, sizeof(int));
if (pa == NULL)
{
perror("calloc");
return 1;
}
return 0;
}
如果开辟失败返回NULL,我就把错误信息打印在屏幕上然后停止
如果开辟成功我就使用:
for (int i = 0; i < 5; i++)
{
pa[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", pa[i]);
}
使用完了之后释放,并让 pa = NULL
free(pa);
pa = NULL;
总的:
int main()
{
int* pa = (int*)calloc(5, sizeof(int));
if (pa == NULL)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
pa[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", pa[i]);
}
free(pa);
pa = NULL;
return 0;
}
④ realloc
realloc 函数原型
void* realloc(void* ptr, size_t size);
其功能是:调整ptr所指向的空间,size 为调整之后空间的大小,返回值是指向调整之后的空间的指针
有人就问了,你不是调整空间大小吗,头指针位置的位置应该不变呀,其实不然:
realloc 扩容的三种情况:
情况一:同一块连续区域中有足够的空间来扩容
来看一下,我这个代码,表示这一块连续空间后面有足够的,尚未被分配的空间来扩容,在这种情况之下,只需在后面补上 20 byte 就好,所以这种情况返回值仍然为 pa 不变
情况2:后面空间不足,重新找一块尚未分配的,足够的连续空间,把源数据拷到新空间之后,释放原空间,再在新空间扩容.
所以到这里,我想你应该能理解为什么返回值不一定是原来的pa,因为也有可能是图中的ptr
到这里,有的同学就说了,那反正不论是情况1 还是 情况2 我直接做一个处理:
int* ptr = (int*)realloc(pa, 10 * sizeof(int));
pa = ptr;
ptr = NULL;
这样不就能让 pa 再此管理我扩容后的空间嘛? 确实,我们的目的也是让pa管理扩容后的空间,这样做也能达到效果
但是还有 情况3 : 扩容失败:在情况2的前提下,realloc 函数没有重新找到一块足够扩容的空间,因而扩容失败,返回NULL
那这种情况下,就不能让 pa = ptr 了 因为p 指向了扩容前的空间,一旦扩容失败, pa = ptr 会让 pa = NULL , 这时候 pa 就喊冤了,本来我之前指向的源空间还有数据, 你这样搞,一下把我整废了呀,所以正确做法应该这样:
int* ptr = (int*)realloc(pa, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
pa = ptr;
先判断一下ptr是否为空指针,如果不为空指针再赋值过去
总代码:
int main()
{
int* pa = (int*)malloc(5 * sizeof(int));
if (pa == NULL)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 5; i++)
{
pa[i] = i + 1;
}
int* ptr = (int*)realloc(pa, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
pa = ptr;
ptr = NULL;
for (int i = 5; i < 10; i++)
{
pa[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", pa[i]);
}
free(pa);
pa = NULL;
return 0;
}
⑤ 柔性数组
⑴ 柔性数组的使用
所谓柔性数组,就是长度可变的动态数组,在C中,结构体的最后一个元素允许是柔性数组: 其写法是(以整形数组为例):
struct S
{
int i;
char c;
int arr[];//或者是 int arr[0]
};
这样我就在 S 这个结构体的最后面放了一个柔性数组,由于没有指定其大小,所以在算结构体大小时,没有把柔性数组算进去,所以后面动态开辟柔性数组时,整个数组不存在内存对齐,也就是整个数组作为一块连续的空间镶嵌在结构体的后面,并成为了结构体中的一员,接下来为柔性数组开辟空间
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
return 0;
}
由于整个柔性数组是镶嵌在结构体的最后面,所以后面所开辟的5个整形的空间就相当于是柔性数组的空间,当然,还要处理开辟失败:
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
//判空
if (ps == NULL)
{
perror("malloc");
return 1;
}
//开辟成功则使用
for (int i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
for (int i = 0; i < 5; i++)
{
printf("%d ", ps->arr[i]);
}
free(ps);
ps = NULL;
return 0;
}
来看一下运行结果
那不是说柔性数组吗,柔性体现在哪里呀,接下来我就对arr数组进行扩容:
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for ( i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
//扩容
struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 10 * sizeof(int));
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps = tmp;
tmp = NULL;
for (i = 5; i < 10; i++)
{
ps->arr[i] = i + 1;
}
for (int j = 0; j < 10; j++)
{
printf("%d ", ps->arr[j]);
}
free(ps);
ps = NULL;
return 0;
}
来看一下运行结果:
这便是柔性数组的使用.
⑵ 柔性数组的模拟实现
其实你想,我搞一个柔性数组的目的不就是为了让arr数组的空间可大可小嘛.那我还有另外一种方式:
struct S
{
int n;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc-1");
return 1;
}
ps->n = 10;
ps->arr = (int*)malloc(5 * sizeof(int));
if (ps->arr == NULL)
{
perror("malloc-2");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i + 1;
}
int* tmp = (int*)realloc(ps->arr, 10 * sizeof(int));
if (tmp == NULL)
{
perror("realloc");
return 1;
}
ps->arr = tmp;
tmp = NULL;
for (i = 5; i < 10; i++)
{
ps->arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n%d", ps->n);
free(ps->arr);
ps->arr = NULL;
free(ps);
ps = NULL;
return 0;
}
检验一下结果: