动态内存管理目录:
为什么会有动态内存管理呢
我们在日常使用中,创建一个数组,一个变量时都会开辟空间
如:
int a; //在栈上开辟一个四字节的空间
char str[5]; //在栈上开辟一个五字节的连续的空间
但是上面这种开辟空间的方法都具有一个特点
1. 空间开辟的大小是固定的,无法修改
2. 声明数组的时候必须指定长度
但是当我们在实际使用中,因为适用不同的情况,我们的空间应该是可以随时修改来应对所有需求的,但是上面的那种方法是不能实现的,所以我们就需要动态内存管理了。
动态内存函数的介绍
malloc和free
void* malloc (size_t size);
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则放回一个NULL指针,因此malloc的返回值一定要做检查
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定
- 如果参数size为0,malloc的行为是否是未定义的,取决于编译器
在我们开辟一段空间后,我们也必须要将它释放掉,而C语言同样有这个函数
void free(void* ptr);
free函数用来释放动态开辟的内存
- 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数ptr是NULL指针,则函数什么都不做
但是,free函数并不是完全销毁这段空间,而是将这段空间的使用权收回,它可能会分配给其他东西,也可能一直放在那里不用,如果打个比方的话:我们申请一个空间,就相当于是拿到了一间房子的钥匙,等到我们使用权结束后,我们并没有把房子给销毁,而是归还房子的钥匙,但是在归还钥匙之前,我们还是能够进入这个房间,所以如果仅仅使用free,可能还不够安全,所以应该采用下面的形式
int* arr = (int*)malloc(sizeof(int) * 5);
free(arr);
arr = NULL;
calloc
void* calloc (size_t num, size_t size);
- 函数的功能是为num个大小为size的元素开辟一段空间,并把空间中的所有字节初始化为0
- 与malloc基本一样,只是calloc会将空间的所有字节初始化为0
在上面有提到过,我们之所以选择动态内存管理,就是因为它可以让我们随心所欲的更改我们需要的空间的大小,那这个时候,我们就要用到realloc这个函数
realloc
void* realloc (void* ptr, size_t size);
- ptr是要调整的内存地址
- size是调整后的大小
- 返回值为调整之后的内存起始地址
- 这个函数在调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
但是这个函数在使用的时候会有两种情况
-
原有空间之后有足够大的空间
当情况1时,我们拓展空间时会直接在原有空间后面追加一段空间 -
原有空间之后没有足够大的空间
当情况2时,因为后面的空间无法存放,我们就需要在堆上面找到一个足够的,连续的空间,将我们之前的数据拷贝过去,并且返回新的空间的地址
常见的动态内存错误
- 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX / 4); //申请一段巨大的空间,申请失败所以返回NULL
*p = 20; //对NULL指针进行解引用操作
free(p);
}
因为当我们内存申请失败的时候,malloc函数会返回一个NULL指针,如果我们对NULL指针进行解引用操作,就会产生错误,所以我们应该对malloc的返回值进行检查,确保内存申请成功。
2 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int* p = &a;
free(p);
}
对于这段函数,我们在编译运行时可能不会报错,但是我们在进行调试的时候就可以看到错误。
因为free函数不能对非动态开辟的内存进行操作
3.使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
p++;
free(p);
}
这段代码也是,在编译的时候没有任何报错,但是在运行的时候就产生了错误。因为我们让指针往前移动,指向的不再是动态内存的起始位置,但是free函数必须要对动态内存的起始地址进行操作,而我们的指针p已经不再指向动态内存的起始地址了,所以产生了错误
4.对同一块动态内存多次释放
void test()
{
int* p = (int*)malloc(100);
free(p);
free(p);
}
我们重复free的时候,在编译阶段只会给出一个警告,但是在运行的时候就会报错。警告的提示是使用未初始化的内存p,因为我们第一次free的时候已经将p给释放了,所以重复free就会产生这样的结果
拓展
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码,我们乍一看是没有错误的,编译也没有报错没有警告,但是在我们运行的时候就完全没有作用。
要了解这一点,我们就首先得知道栈帧的概念。
当我们在调用一个函数的时候,我们会在栈上开辟一个栈帧,然后再函数结束的时候,释放这个栈帧,而这个GetMemory调用时我们传入的p就是在这个栈帧中作用,如果我们想让它真的操作这个p,就应该传这个p的地址,即一个二级指针
修改后:
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void test()
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
}
这样修改后就可以成功了
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void test()
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "hello");
printf(str);
}
}
这里是可以运行的,这里就是我刚刚讲解的将str置为空指针的问题,因为我们free了之后这个指针就变成了一个野指针,它可能可以访问原来的空间,也有可能不行。
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
这里就是考察我们对于指针和数组的理解,在之前的指针那一章节中我就提到过
char* str1 = "hello world"; //指向常量区的这段字符串
char strl2[11] = "hello world"; //在栈上开辟一段内存空间,拷贝常量区的这段字符串
当我们在一个栈帧中,如果是str1的形式,我们返回的其实是在常量区中这段字符串的地址,而str2的方式就是其在这个栈帧中开辟的这段内存的地址,但随着栈帧的销毁,所以我们返回的其实是一段乱码
char* GetMemory()
{
char *p = "hello world";
return p;
}
void test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
这样才能得到我们想要的答案
柔性数组
C语言中还存在这样一个鲜为人知的东西,在一个结构中,最后一个允许是位置大小的数组,这就叫做柔性数组成员,它的使用如下
typedef struct st_type
{
int i;
int a[0]; //柔性数组成员 有些编译器不能在下标中写入0
}type_a;
- 结构中的柔性数组成员前面必须有至少一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
柔性数组的使用
与结构同时分配内存
int main()
{
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
p->i = 100;
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p);
}
先给结构体分配空间,再给柔性数组分配空间
typedef struct st_type
{
int i;
int *a; //柔性数组成员 有些编译器不能在下标中写入0
}type_a;
int main()
{
int i = 0;
type_a* p = (type_a*)malloc(sizeof(type_a));
p->i = 100;
p->a = (int*)malloc(p->i * sizeof(int));
for (i = 0; i < 100; i++)
{
p->a[i] = i;
}
free(p->a);
p->a = NULL;
free(p);
p = NULL;
}
两段代码作用相同,但是第一个只需要释放一次空间,而第二个要释放两次。但是第二种方法也是存在好处的,就是它的的访问速度会比第一种要快。