目录
内存相关知识
我们平常开辟空间时,可能会有以下几种方式:
int a = 0;
char b = 'A';
int arr[10] = { 0 };
比如创建一个整形变量,字符变量,或是直接创建一个数组,连续开辟一大块空间
但是,这些空间都是在栈区上开辟的,如果我们在函数中这么写的话,就会导致因为空间销毁而无法找到该空间,如下:
char* test()
{
char arr[20] = "hello world!";
return arr;
}
int main()
{
char *p = test();
printf("%s\n", p);
return 0;
}
我们会发现,打印的结果并不是预期中的 hello world ,这是因为,test函数中创建的数组在出了函数之后就被销毁了,而我们返回的地址指向的是一块还给了操作系统的空间
栈区 堆区 静态区
我们需要知道,在我们日常创建的变量中,大部分都是在栈区上创建的,这部分区域上创建的变量都会因为出了函数而被销毁
另外两种不会被销毁的是放在静态区和堆区上的变量
而我们今天要讲的动态内存管理,其开辟的空间就是在堆上的
malloc
如果要使用动态内存管理相关函数,需引头文件
#include <stdlib.h>
void* malloc (size_t size);
malloc 的参数是开辟需要的字节数
该函数的作用就是在堆区上开辟一块指定大小的空间,但是我们会看到 malloc 是void*
这是因为系统并不知道使用者要开辟的是什么类型的空间,但是做为使用者的我们是知道的,因此我们在使用的时候,我们需要将其强制类型转化一下
接着还要分两种情况:
1. 空间开辟成功,返回指向该空间的指针
2. 空间开辟失败,返回NULL
所以我们在开辟完空间之后,我们需要检查一下,返回的是否为空指针
如果为空指针,那么我们就可以用 perror 进行报错,并提前结束程序。如果不为空指针,那么就正常使用
比如我们要开辟一块大小为40个字节的空间,我们可以这么写:
#include<stdlib.h>
#include<stdio.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
//如果不为空,正常使用
return 0;
}
当然,如果说你想开辟 10 个整形空间,你也可以使用 sizeof 让计算机帮你运算所需字节
int* p = (int*)malloc(10*sizeof(int));
calloc
函数介绍
C语言还提供了另一种开辟内存空间的函数——calloc
void* calloc (size_t num, size_t size);
参数 1 是需开辟的元素个数 参数 2 是每个元素的大小(如:int 的大小是4个字节)
如果我们需要用 calloc 开辟 10 个整形空间的话,就可以这么写:
int* p = (int*)calloc(10 ,sizeof(int));
if (!p)
{
perror("calloc");
return 1;
}
calloc 与 malloc 的区别
我们来看这样一段代码:
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (!p)
{
perror("malloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *p++);
}
return 0;
}
当我们打印 malloc 开辟的空间的内容时,我们会发现上面的数字是很乱的,如若不初始化则根本无法使用
我们再来看一下 calloc
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (!p)
{
perror("calloc");
return 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *p++);
}
return 0;
}
我们会发现,相同的代码,calloc 和 malloc 的效果完全不一样
这时因为相比于 malloc,calloc 会在空间开辟完之后,将每一个元素都初始化为 0
所以,如果我们开辟的空间有初始化的要求,那么我们可以使用 calloc 来轻松实现
realloc
语法介绍
realloc 的出现,使得动态内存管理变得更加灵活
当我们用 malloc 或 calloc 开辟的空间用完了时,我们就可以使用 realloc 函数对空间进行扩容
void* realloc (void* ptr, size_t size);
参数 1 是指向要扩容空间的指针
参数 2 是扩容之后新的空间的总大小
如果申请失败,那么 realloc 函数会返回一个空指针(NULL)
开辟空间的两种情况
情况一:
原空间后面没有足够的空间,realloc 函数会再找一块总大小为扩容后总大小的空间,开辟完后释放元空间,并返回指向开辟后的空间的指针
假如我们原空间的大小是 40 个字节,现在我想将其扩大到 80 个字节
但是此时原空间后面的空间位置不够
这时我们就会再找一块有 80 个字节大小的空间,并返回指向该空间的指针
情况二:
原空间后面有足够的空间,那么 realloc 函数就会直接扩容
如上,原函数后面的空间足够
有了如上两种情况,那么我们在使用 realloc 函数的时候,我们就需要注意一点了
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (!p)
{
perror("calloc");
return 1;
}
p = (int*)realloc(p, 20 * sizeof(int));
return 0;
}
如上代码,这种方法是不可取的!!!
如果空间申请失败了,那么就会返回一个空指针,但是我们的指针 p 原本指向的是 calloc 申请出来的空间,但是现在我的 p 变成了空指针,那么我这块空间就找不到了
我们无法手动将其释放,那么就造成了内存泄漏
所以我们可以先用一个其他指针来接收,if 判断完申请成功之后,再修改回来
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (!p)
{
perror("calloc");
return 1;
}
int* pr = (int*)realloc(p, 20 * sizeof(int));
if (!pr)
{
perror("realloc");
return 1;
}
else
{
p = pr;
}
return 0;
}
realloc 与 malloc
上文说:realloc 函数的第一个参数是指向需要扩容空间起始地址的指针
那如果,我们传一个空指针呢?
我们看到最后一条,上面写道:如果传了一个空指针,那么他的行为就会像一个 malloc 函数
由此我们知道,如果直接传一个 NULL,那么 realloc 函数会像 malloc 函数一样直接开辟一块空间,无初始化
int main()
{
int* p = (int*)realloc(NULL, 10 * sizeof(int));
if (!p)
{
perror("realloc");
return 1;
}
int* pr = p;
for (int i = 0; i < 10; i++)
{
printf("%d ", *pr++);
}
return 0;
}
我们会看到,这种行为是没有什么问题的,编辑器并没有报错
free
我们动态内存开辟的空间是在堆区上的,如果这块空间不需要了,那么我们就需要将其释放掉
C语言给我们提供了一个函数,就是用来释放动态开辟的空间的——free
该函数的唯一参数就是需要释放的空间的头指针
注意:如果传的是一个空指针,那么 free 函数将什么都不会做
接着我们再来看一段代码:
int main()
{
int* p = (int*)malloc(40);
if (!p)
{
perror("malloc");
return 1;
}
free(p);
return 0;
}
free 前指针 p 指向的地址
free 后指针 p 指向的地址
如上我们会看到,无论是 free 前 free 后,指针 p 指向的地址都没有变,这不就形成野指针了吗?
当我们以后操作忘记了我们指针 p 指向的空间被释放过时,我们再去使用指针 p 就会形成越界访问
综上,我们在使用 free 函数时,我们需要将 free 后的指针置为空,这样就能有效规避野指针的问题了,如下:
int main()
{
int* p = (int*)malloc(40);
if (!p)
{
perror("malloc");
return 1;
}
free(p);
p = NULL;
return 0;
}
结语
综上,就是本期关于动态内存管理的相关内容了,如果对你有帮助的话,希望可以多多支持!