动态内存管理
在开始这个知识点的总结前,我要先强调两个非常重要的点。
- 所有的内存申请都要进行返回值判断。
- 申请完,如果这段内存不再使用后,要调用free()函数进行释放。
为什么要进行动态内存分配?
我们现在已经掌握的内存开辟方式有:
int val = 7; //在栈上开辟4个字节
char arr[10] = {0}; //在栈空间上开辟10个字节连续的空间
但是上面两种开辟空间的方法有两个特点:
- 空间开辟的大小是固定的。
- 数组在申明的时候,必须制定数组的长度,它所需要的内存在编译时分配。
但是有的时候,我们的需求并不满足于上面的情况:
1. 有时候我们需要的空间大小需要在程序运行的时候才能知道。
2. 需要大块内存的时候,就要使用动态内存开辟。
动态内存函数的介绍
malloc和free
动态开辟内存都是在堆上开辟,由程序员亲自开辟,亲自释放,如果不释放,可能会造成内存泄漏问题,严重可能会造成宕机。
内存泄漏会随着进程的退出而结束,会归还内存。
但是这只是一种机制,在使用完开辟的内存后,释放内存还是非常非常非常重要滴!
在C语言里,malloc 的原型是:
void *malloc(size_t size);
由这里我们就可以知道,malloc是函数,既然是函数就有可能存在调用失败,所以必须检查返回值,如果调用失败,则返回值为NULL。
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定一定要做检查。
- 返回值的类型是
void *
,所以malloc函数并不知带开辟空间的类型,具体在使用的时候使用者自己来决定。 - 如果参数
size
为0,malloc的行为是标准未定义的,取决于编译器。
虽然malloc开辟的空间是在堆上开辟的,但是指向它的指针是在栈上开辟的。
既然有开辟内存的函数,那么就会有释放内存的函数。
在C语言里free函数的原型为:
void free(void *ptr);
free函数的参数是malloc函数开辟内存后的返回值。
free函数用来释放动态开辟的内存。
- 如果参数
ptr
指向的空间不是动态开辟的,那么free函数的行为是未定义的。 - 如果参数
ptr
是NULL指针,那么函数什么事都不做。
malloc和free都声明在stdlib.h
头文件中。举个例子
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <Windows.h>
#include <stdlib.h>
/*
** 本程序为动态内存管理专题课件代码练习
** 郭文峰
** 2018/11/26
*/
int main(void)
{
int n = 0;
scanf("%d", &n);
int *ptr = (int *)malloc(n * sizeof(int));
if (!ptr)
{
printf("Can't get memory.");
exit(EXIT_FAILURE);
}
int i = 0;
for (; i < n; i++)
{
ptr[i] = 0;
}
free(ptr);
ptr = NULL;//这一步是否重要???
system("pause");
return 0;
}
那么最后的那一步ptr = NULL
是否重要呢??
答案是:非常重要,在free之后,指针内容是不变的,但是此时指针已无法访问堆空间。这样的后果就是会造成野指针。
在这里可以看到,在对指针进行free
之后,指针的值依然是一个地址,只是这个地址已经无法访问刚刚创建的堆空间。但是若不对其进行ptr = NULL
操作的话,就会造成野指针。
calloc
c语言里还提供了一个函数叫calloc
,calloc
函数也用来动态内存分配,原型如下:
void *calloc(size_t num, size_t size);
- 函数的功能是为
num
个大小为size
的元素开辟一块空间,并且把空间的每个字节初始化为0 。 - 与函数
malloc
的区别只是在于calloc
函数会在返回地址之前把申请的空间的每个字节初始化为全0 。
举个例子:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <Windows.h>
/*
** 本程序为动态内存管理专题课件代码练习
** 郭文峰
** 2018/11/26
*/
int main(void)
{
int *p = calloc(10, sizeof(int));
if (!p)
{
printf("Can't get memory!\n");
exit(EXIT_FAILURE);
}
free(p);
p = NULL;
system("pause");
return 0;
}
因此,如果我们需要对开辟的内存进行初始化,可以使用calloc
函数。
realloc
realloc
函数的出现让动态内存管理更加灵活。- 有时我们会发现申请的内存有点小或者有点大了,我们需要重新调整内存的大小,这时
realloc
函数就完美的可以解决这个问题。
realloc
函数的原型为:
void *realloc(void *ptr, size_t size);
ptr
是要调整的内存地址。size
是调整之后的大小。- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
- 情况1:原有空间之后有足够大的空间,那么此时扩大空间或缩小空间之后,返回的地址都不会改变。
- 情况2: 原有空间之后没有足够大的空间用来调整新的需求,所以该函数会将这个所有的数据复制到一个新的足够大能满足新的需求的空间,并返回新的地址。
但是在使用realloc
的时候需要注意一点,那就是realloc
到底是可以直接在原有的内存地址直接开辟,还是需要新定义一个指针,如果返回值不为空,在将新的指针赋值给老指针??
int *ptr = (int *)malloc(100);
if (!ptr)
{
printf("ERROR!\n");
exit(EXIT_FAILURE);
}
//情况一
ptr = realloc(ptr, 1000);
if (!ptr)
{
printf("ERROR!\n");
exit(EXIT_FAILURE);
}
free(ptr);
ptr = NULL;
//情况二
int *p = realloc(ptr, 1000);
if (!p)
{
printf("ERROR!\n");
exit(EXIT_FAILURE);
}
ptr = p;
free(ptr);
ptr = NULL;
结论:情况二是一种安全的行为,因为,如果realloc
申请内存失败,则情况一会将原内存的内容都丢失。
柔性数组
柔性数组(flexible array)这个概念并不常见,但是它确实存在。C99中,结构中最后一个元素允许是位置大小的数组,这就叫做柔性数组成员。
例如:
struct FleArr
{
int i;
char a[0];//柔性数组成员
//也可以写成
//char a[];
};
柔性数组是为了解决结构体内需要变长数组的需求。
结构体内最后一个元素为数组且数组的有且仅有0个成员的数组,为柔性数组。
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
sizeof
返回的这种结构体大小不包括柔性数组的内存。- 包含柔性数组成员的结构体用
malloc
函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
不管柔性数组有无元素,柔性数组的大小都不算进结构体大小。
例如:
struct FleArr
{
int i;
char a[0];//柔性数组成员
};
printf("%d\n", sizeof(struct FleArr));
柔性数组的使用
int i = 0;
struct FleArr *p = (struct FleArr*)malloc(sizeof(struct FleArr), 100 \
* sizeof(char));
if (!p)
{
exit(EXIT_FAILURE);
}
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = 'a';
}
free(p);
p = NULL;
在开辟空间的时候,就相当于先创建一个结构体大小的空间,在给柔性数组成员a创建了一个100个字符型元素的连续空间。
柔性数组的优势
typedef struct st_type
{
int i;
int *p_a;
}type_a;
type_a *p = malloc(sizeof(type_a));
p->i = 100;
p->p_a = (int *)malloc(p->i*sizeof(int));
for(i=0; i<100; i++)
{
p->p_a[i] = i;
}
//释放空间
free(p->p_a);
p->p_a = NULL;
free(p);
p = NULL;
如果不采用柔性数组的形式,我们需要在一个结构体内创建一个变长的数组,就需要再次动态分配内存,这样就需要动态分配两次内存,再释放的时候也需要释放两次。
这样的复杂度,在简单的代码里体现不出来,如果再一个多文件的代码里,自己就会被扰乱,不知道自己到底有没有创建,或者有没有释放。
综上:柔性数组的优势有两点:
-
方便内存的释放。
-
有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。