在正式介绍动态内存管理前,我们先简单了解为什么会存在动态内存分配。
我们常常使用的空间开辟方式是这样的:
int a = 20;//在栈空间上开辟四个字节
char arr[10] = { 0 };//在栈空间上开辟10个字节的连续空间
这样的开辟空间方式有两个特点:
1.开辟空间的大小是固定的
2.数组在声明的时候必须注意长度,所需内存在编译时分配
但是我们在某些情况下,程序运行所需的空间大小在程序运行的时候才能明确知道,比如我需要一个结构体数组来存放每个人的信息,随着我添加的信息增加,数组所需空间也会相应增加,这是,以上的空间开辟方式就不能满足需求了。于是,就有了动态内存开辟。
1.动态内存函数介绍
我们回顾一下内存分配方式, 局部变量、函数形参存放在栈上,而堆区是用来动态内存开辟的,
C语言提供的动态内存函数开辟函数有:malloc ,free ,realloc,calloc,头文件是<stdlib.h>。
1.malloc 和 free
malloc函数向堆区申请一块连续可用空间,如果成功并且返回一个void*指针,如果失败返回空指针。
malloc函数的声明是这样的:
void* malloc ( size_t size );
其中size_t size是要开辟空间的字节数。我们以在堆区开辟一个存放10个整形的空间(其实就是一个存放10个元素的int数组,只不过位置不同)为例,看一下malloc使用方式:
#include <stdio.h>
#include <stdlib.h>
int main()
{
//在堆区开辟10个int空间
int* p = malloc(10 * sizeof(int));
//以指针p来接收开辟的空间,转化为int*以便后续的使用
if (p == NULL)//如果开辟失败返回NULL
return;
for(int i = 0;i < 10 ;i++)
{
*(p + i) = i;
printf("%d ",p[i]);//可以数组使用方式来使用p
}
//释放空间并置为空指针
free(p);
p=NULL;
return 0;
}
当这块空间使用结束后,我们要使用free函数释放这块空间,还给操作系统,并且为了提高程序的安全性,我们在free后,我们最好要将p置为空指针。
在使用malloc函数的时候要注意:
1.如果开辟失败,会返回一个空指针,所以一定要检查。
2.返回的类型是void*,所以malloc函数并不知道开辟空间的类型,要由使用者自己确定。
3.如果参数size_t 是0,malloc的行为是标准未定义的,取决于编译器。
4.使用完空间后要用free函数释放。
在使用free函数时要注意:
1.如果free函数调用的参数指向的空间不是动态开辟的,那么free函数的行为是标准未定义的。
2.如果free函数调用的参数是一个NULL指针,那么函数什么都不做。
2.calloc
calloc和malloc的功能类似,都是开辟一块连续的空间,calloc的声明如下:
void* calloc ( size_t num , size_t size );
calloc与malloc的不同是calloc有两个参数,一个是元素的个数,另一个是每个元素的字节大小。
calloc的具体功能是为num个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0,而malloc并没有初始化。我们还是以开辟10个整型的动态内存为例看看calloc的使用:
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
return;
for (int i = 0; i < 10; i++)
printf("%d ", p[i]);
free(p);
p = NULL;
return 0;
}
跟malloc一样,在使用完calloc开辟的空间后,要用free释放,并且置为NULL指针。
3.realloc
realloc用于重新开辟空间,对原开辟的动态内存进行调整。realloc声明如下:
void* realloc ( void* ptr , size_t size );
其中,ptr指向之前开辟的内存空间(malloc,calloc开辟空间的首地址),size_t为调整之后新的大小(以字节为单位),返回值为调整之后内存的起始位置。
在之前的两个函数介绍中,我们以开辟10个int大小空间为例,现在我们要增加之前开辟的空间,来存放20个整型。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = calloc(10,sizeof(int));
if (p == NULL)
return 0;
for (int i = 0; i < 10; i++)
*(p + i)= i;
//对之前的空间进行调整,将空间大小增加10个int
int* ptr = realloc(p, 20 * sizeof(int));
//为什么要用ptr接收,而不以p直接接收?
if (ptr != NULL)
p = ptr;
free(p);
p = NULL;
return 0;
}
那么为什么要先用ptr接收呢?其实,realloc在调整空间时有两种情况:
情况1:原有空间之后有足够大的空间
在这种情况下,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,ptr和p指向相同的空间。
2.动态内存使用误区
1. 对NULL指针进行解引用操作
就像之前所说的,我们在开辟完一块内存后,一般要进行检查,以防开辟失败返回了空指针。如果我们没有意识到内存开辟失败,直接对p所指向的空间进行操作,那么程序就会产生一系列问题。
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
2.对非动态开辟内存使用free释放
之前提过,free函数只能释放动态开辟的空间,如果我们传入一个指向非动态开辟空间的指针,就会导致程序报错。
void test()
{
int a = 1;
int* p = &a;
free(p);
}
3.使用free释放一块动态开辟内存的一部分
在使用free函数释放开辟的空间时,一定要将所有空间释放,即我们传入的指针必须指向开辟空间的其实位置,否则程序会报错。
void test()
{
int* p = (int*)malloc(100);
p++;
//p不再指向动态内存的起始位置
free(p);
}