为什么存在动态内存分配?:
但是这两种方式在某些情况下是不适用的,假设现在我们要在结构体中存放学生的信息。
struct S
{
char name[20];
int age;
};
int main()
{
struct S arr[50];
return 0;
}
但很明显,学生的数量是不确定的,如果我们分配内存为50个学生的大小,当学生人数超过这个数量就会出现存放不下的情况,那么有的人会说,我们可以开辟足够大的空间,这种方式看似解决了这个问题,但是会出现内存浪费的情况,假如说你申请了1000个内存空间,结果你只用了10个或者100个,这就浪费了大量的内存空间,那么我们是否可以将arr[]中的元素值先设置为n,再通过scanf函数输入n的值?
答案是:C语言不支持,但C99支持,要想创建变长数组,gcc是支持这种操作的,而我们使用的VS编译器并不支持这种操作。
什么叫变长数组?
数据的长度可以任意变化。
举例:
int main()
{
int n;
scanf("%d", n);
struct S arr[n];//数组的长度并不是不变的
return 0;
}
由此,C语言给出了一种动态内存分配。
动态内存分配的介绍:
malloc和free:头文件:#include<stdlib.h>
C语言提供了一个动态内存开辟的函数:
void* malloc(size_t size);
这个函数向内存申请一块连续可用的函数,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一个要做检查。
返回值的类型是void ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自行决定。
如果参数size为0,malloc的行为是标准未定义的,取决于编译器。
申请空间:
*void*malloc(sizeof(ELemType)num)
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
//向内存申请10个整形空间
int *p=(int*)malloc(10 * sizeof(int));
if (p == NULL)
{
//打印错误原因
printf("%s\n", strerror(errno));
}
else
//正常使用空间
{
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
return 0;
}
输出如下所示:
0 1 2 3 4 5 6 7 8 9
释放空间:
free函数用来释放动态开辟的内存:
如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数ptr是NULL指针,则函数什么事都不做。
当动态申请的空间不在使用的时候,就应该还给操作系统:
方法:
free(p);
将该语句加入到上面的代码中,再运行!
输出结果为:
0 1 2 3 4 5 6 7 8 9
输出这个结果难免会让我们产生疑惑,好像释放空间和不释放没有任何区别,其实事实并非如此。
上述代码即使没有加free,其内存空间也会被释放,其原因是当程序的生命周期结束时,动态申请的内存空间会自动还给操作系统,但我们并不提倡这种做法,原因是:上述代码,恰好在free语句下面就执行完了所有的程序,因此加不加free的效果是相同的,但是,如果free后面还有其他的逻辑语句,就会导致上面不用的代码一直占着内存,因此在以后的代码中,我们一定不能忘记加free.
但是这种释放方式还不够彻底,p的内存空间只是被释放了,但是它的值并没有改变,要想彻底使p释放,我们应在释放内存空间的语句后面再将p指向NULL,这才会彻底释放。
calloc函数:
calloc函数也可以用来动态内存分配;
void*calloc(size_t num,size_t size);
函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为0。
举例:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
//malloc(10*sizeof(int));
int* p=(int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n",strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
free(p);//释放动态开辟的空间
p = NULL;
return 0;
}
输出如下:
0 0 0 0 0 0 0 0 0 0
因此如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。
realloc函数:
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间太大了,那为了合理的使用内存,我们必须要对内存的大小做灵活的调整,那么realloc函数就可以做到对动态开辟内存大小的调整,函数原型如下:
void*realloc(void*ptr,size_t size);
ptr:要调整的内存地址
size:调整之后新的大小
返回值为调整之后内存起始位置
realloc函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc在调整内存空间的是存在两种情况:
情况一:原有空间之后有足够大的空间,不需要开辟新的空间,直接进行追加
原有空间的数据不发生变化。
情况二:原有空间之后没有足够大的空间,需要开辟新的空间
举例:
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<string.h>
int main()
{
int* p = (int*)malloc(20);
if (p == NULL)
{
printf("开辟失败");
}
else
{
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
}
//上述所使用的空间为我们在程序前面通过malloc函数申请的20个空间
//假设此时该空间不够用了,我们还需要20个字节的空间,此时我们可以使用realloc来调整动态内存空间的大小
int*ptr=(int*)realloc(p, 40);
if (ptr != NULL)//追加成功
{
p = ptr;
int i = 0;
for (i = 5; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
}
free(p);//如果返回的是新地址,新地址需要用free函数释放,而旧地址会被realloc函数释放
p = NULL;
return 0;
}
常见的动态内存错误:
对NULL指针进行解引用操作:
int main()
{
int* p = (int*)malloc(40);
int i = 0;
//如果malloc没能正确进行,p就被赋值为NULL,此后进行++操作为非法操作
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
对动态开辟空间的越界访问:
int main()
{
int* p = (int*)malloc(5*(sizeof(int)));
if (p == NULL)
{
return 0;
}
else
{
int i = 0;
for (i = 0; i < 10; i++)//上面只开辟了5个整形元素的空间,这里i<10;为越界访问
{
*(p + i) = i;
}
}
free(p);
p = NULL;
return 0;
}
对非动态开辟内存的free:
int main()
{
//p并不是存在堆区的,free不能对齐进行访问
int a = 10;
int* p = &a;
*p = 20;
free(p);
p = NULL;
return 0;
}
使用free释放一块动态开辟内存的一部分:
#include<string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i;//p的指向位置已经发生改变,已经不是动态开辟的内存的起始位置
}
free(p);
p = NULL;
return 0;
}
对同一块动态内存多次释放:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p++ = i;//p的指向位置已经发生改变
}
free(p);
//...
free(p);//多次释放p
p = NULL;
return 0;
}
编译程序崩溃,原因是我们对p进行多次释放。
那么如何避免这种错误呢?
方法:释放完内存,立即将其指向NULL,上面我们提到过,如果指针指向的是NULL,再对其进行释放,编译器将不会做任何事情。
free(p);
p = NULL;
//...
free(p);
动态开辟内存忘记释放(内存泄漏):
int main()
{
while (1)
{
malloc(1);//不断的开辟空间,但不进行释放
}
return 0;
}
将程序运行后,打开资源管理器,我们会发现,内存的占用比一直在增加。
忘记释放不再使用的开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放。