为什么存在动态内存分配
要想知道为什么会存在动态内存分配,就要看我们已知的内存开辟方式
int num = 20;
//在栈空间上开辟四个字节
char arr[20] = {0};
//在栈空间上开辟20个字节的连续空间
观察我们已经知道的方式,可以发现以下特点
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配
但是根据以上两点我们发现有时候我们需要的空间的大小是运行时才可以知道的,以上的空间开辟方式就无法满足我们日常的需求.
动态内存函数的介绍
在将动态内存函数之前,先带大家认识一个和动态内存函数共同生存的函数:free
free函数
free函数用来释放动态开辟的内存。在动态内存申请函数中必须要和free函数配套使用.
忘记释放不再使用的动态开辟的空间会造成内存泄漏
如果长期使用内存申请函数后,对内存操作结束忘记使用free函数释放内存.可能会引起程序崩溃,这在项目中是一个不容易察觉,但是却很危险的操作.所以我们在写代码的时候一定要仔细仔细再仔细
void free (void* ptr);
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr是NULL指针,则函数什么事都不做
1.malloc
先看函数的声明
void* malloc (size_t size);
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。
- 如果参数 size为0,malloc的行为是标准是未定义的,取决于编译器。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//动态内存分配
//malloc 申请的空间在堆区,结束时需要释放 开辟空间不初始化
int main()
{
//申请的空间在堆区
int* p = (int*)malloc(40);
int* ptr = p;
//判断是否申请成功
if (p == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*ptr = i;
ptr++;
}
//结束时释放
free(p);
//并将指针指向空,防止后续会发生根据指针操作内存的情况
p = NULL;
return 0;
}
2.calloc
这是C语言提供的另一个动态内存申请的函数,它和malloc的区别就是,他申请内存时是会进行初始化的.
void* calloc (size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//动态内存分配
//calloc 申请的空间在堆区,结束时需要释放 开辟空间会初始化
int main()
{
int* p = (int*)calloc(10,sizeof(int));
int* ptr = p;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr+i) = i;
}
free(p);
p = NULL;
return 0;
}
3.realloc
这个函数的存在,可以让我们对已经申请动态内存做灵活的调整
因为realloc函数可以重新申请内存空间,还会将内存之前的数据拷贝到新内存中.
void* realloc (void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到"新"的空间。
为什么在新字上带引号呢,这是因为realloc在调整内存空间的是存在两种情况的:
- 情况1:当原有空间之后有足够大的空间的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
- 情况2:当原有空间之后没有足够大的空间的时候,新的扩展方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
由于可能发生情况2,我们使用realloc也要谨慎注意:我们来看以下代码
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//动态内存分配
//realloc 重新申请的空间
//会将旧的数据拷贝过去
int main()
{
int* p = (int*)malloc(40);
int* ptr = p;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(ptr + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d\n", *(ptr + i));
}
//这里使用realloc重新申请内存后,并没有直接使用原有的p来接收内存地址.
//这是因为如果使用p来接收的话申请的内存,内存失败的话会返回NULL,p就指向了NULL,就丢失了原来指向的地址.
int* pp = (int*)realloc(p,80);
if (pp != NULL) {
p = pp;
pp = NULL;
}
free(p);
p = NULL;
return 0;
}
我们可以发现代码中,使用realloc重新申请内存后,并没有直接使用原有的p来接收内存地址.
这是因为如果使用p来接收的话申请的内存,内存失败的话会返回NULL,p就指向了NULL,同时也就丢失了原来指向的地址.这会造成很大的bug.所以该函数要谨慎使用.
常见的动态内存错误
了解了内存申请函数,我们也要知道动态内存的常见的错误:以警示自己避免出错.
- 对NULL指针的解引用操作
- 对动态空间越界访问
- 对动态内存开辟的空间释放
- 释放使用free释放一块动态开辟内存的一部分
- 对同一块动态内存多次释放
- 动态开辟内存忘记释放
注!!!:以下代码是错误代码的示范,千万千万千万不可拿去测试运行(只展示main函数中的部分)
1.对NULL指针的解引用操作
int main()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL) {
perror("malloc");
return 1;
}
else {
printf("%d\n",*p);
}
return 0;
}
2.对动态空间越界访问
int main()
{
int* p = (int*)malloc(20);
if (p == NULL) {
return 1;
}
int i = 0;
for (i = 0; i < 20; i++) {
*(p + i) = i;
}
free(p);
p = NULL;
return 0;
}
3.对动态内存开辟的空间释放
int main()
{
int i = 0;
free(i);
p = NULL;
return 0;
}
4.释放使用free释放一块动态开辟内存的一部分
int main()
{
int* p = (int*)malloc(20);
if (p == NULL) {
return 1;
}
int i = 0;
for (i = 0; i < 5; i++) {
*(p) = i;
p++;
}
//释放的已经不是动态空间的起始位置
free(p);
p = NULL;
return 0;
}
5.对同一块动态内存多次释放
int main()
{
int* p = (int*)malloc(20);
if (p == NULL) {
return 1;
}
int i = 0;
for (i = 0; i < 5; i++) {
*(p + i) = i;
}
free(p);
//已经释放过了,释放后直接置为空,可避免发生二次释放// p = NULL;
free(p);
p = NULL;
return 0;
}
到此我们的动态内存分配函数也就介绍完了,看完的小伙伴,我相信你们一定领悟到了一点,那就是我们写代码的时候一定要仔细仔细再仔细