目录
一、为什么会有动态内存分配
在前面的学习中我们学到了数据类型+变量名
来申请一个空间。
int a = 100;
int arr[10] = { 0 };
这样开辟出来的空间内存大小是固定的。
那么怎么样才能使申请空间的内存大小是能够变化呢??
那么这里就要引入动态内存管理了。
二、动态内存函数的介绍
首先下面介绍函数的头文件都是 #include <stdlib.h>
下面关于perror函数和strerror函数在 strerror函数介绍 中提及过。
1. malloc函数
void* malloc (size_t size);
malloc函数能够申请一个空间,并将申请空间的首地址作为返回值传回去。
注意:
1.1 malloc函数的返回值:malloc函数若申请成功则返回该空间的首地址,若没有申请成功则返回 NULL
。
1.2 malloc函数的使用:由于设计malloc函数的人不知道我们申请空间用来存储哪种类型的变量,在我们使用的时候自己会知道存储哪种变量,所以在使用的时候要将返回值强制类型转换成我们需要的类型。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
//假设这里申请十个整形内存大小的空间
int* p = (int*)malloc(10 * sizeof(int));
//判断是否申请成功
if (p == NULL)
{
printf("malloc:%s\n",strerror(errno));
perror("malloc");//若申请失败则打印malloc失败的原因
//这里strerror函数与perror函数的作业相同
return 1;
}
//使用
for (int i = 0; i < 10; i++)
{
p[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
//...
return 0;
}
2. calloc函数
void* calloc (size_t num, size_t size);
calloc函数与malloc函数一样能申请空间,但是calloc函数能够将申请出来的空间全部初始化为 0
。
注意 :
2.1 calloc函数的参数:需要两个参数,第一个参数是需要元素个数的数量,第二个参数是每个元素的内存大小。
2.2 calloc函数的返回值:与malloc函数一样,申请成功返回该空间的首地址,否则则返回 NULL
。
2.3 calloc函数的使用:与malloc函数一样,需要将返回值强制类型转换成自己需要的类型。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p =(int*) calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
//使用
for (int i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
//....
return 0;
}
3. realloc函数
void* realloc (void* ptr, size_t size);
realloc 函数能够将 malloc函数 和 calloc函数 申请出来的空间进行调整。
注意:
3.1 realloc函数的参数:需要两个参数,第一个参数是需要调整内存空间的地址,第二个参数是调整后的内存大小。
3.2 realloc函数的返回值:与 malloc函数 和 calloc函数 一样,调整成功返回该空间的首地址,否则则返回 NULL
。
3.3 realloc函数的使用:与 malloc函数 和 calloc函数 一样,需要将返回值强制类型转换成自己需要的类型。
3.4 realloc函数不仅仅可以将申请出来的空间进行调整,当realloc函数的第一个参数为 NULL
的时候,就和malloc函数一样能够直接申请一个空间。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int i = 0;
//这里开辟5个int类型内存的大小的空间
int* p = malloc(5 * sizeof(int));
if (p == NULL)
{
perror("malloc");
return 0;
}
//使用
for (i = 0; i < 5; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 5; i++)
{
printf("%d ", p[i]);
}
printf("\n");
//这里将malloc函数开辟的5个int内存大小调整为10个
int* ptr = realloc(p, 10 * sizeof(int));
if (ptr == NULL)
{
perror("realloc");
return 1;
}
//这里若realloc函数返回的值不为NULL
p = ptr;
for (i = 5; i < 10; i++)
{
p[i] = i + 1;
}
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
//释放
//...
return 0;
}
3.5 realloc函数的原理
(1)当参数地址后的连续空间比需要调整的空间大小大的时候,那么realloc函数会直接在传入地址的后面直接调整空间。
(2)当参数地址后的连续空间比需要调整的空间大小小的时候,realloc函数会另外申请一个连续的空间,并将这个连续空间的首地址作为返回值返回,原来的空间释放。
4. free函数
void free (void* ptr);
free函数能够将动态内存开辟的空间释放 。
注意:
4.1 若传入函数的参数不是动态类型开辟出来的,会导致未定义行为的发生,这可能会导致程序崩溃或产生其他意外结果。
4.2 若传入函数的参数是 NULL
,那么函数什么都不做。
三、常见的动态内存的错误
1.对 NULL 进行解引用操作
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(4 * sizeof(int));
//如果这里p不进行判断,如果p为NULL
//那么这里就是对NULL进行解引用操作
*p = 4;
free(p);
p = NULL;
return 0;
}
2. 对动态开辟的空间进行越界访问
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
//判断
if (p == NULL)
{
perror("malloc");
return 0;
}
int i = 0;
//使用
for (i = 0; i <= 5; i++)
{
p[i] = i; //当i = 5的时候越界访问
}
//释放
free(p);
p = NULL;
return 0;
}
3.对非动态开辟的空间进行释放
int main()
{
int a = 10;
int* p = &a;
free(p);
return 0;
}
4.使用free函数释放部分动态开辟的空间
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
//判断
if (p == NULL)
{
perror("malloc");
return 0;
}
p++;
//释放
free(p);
p = NULL;
return 0;
}
5.对一个动态开辟的空间进行多次释放
注意:若每次释放后将指针置为 NULL
,那么这里就不会出问题,后面的 free函数 就相对于释放 NULL
,不进行任何操作。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(5 * sizeof(int));
//判断
if (p == NULL)
{
perror("malloc");
return 0;
}
free(p);
//释放
free(p);
p = NULL;
return 0;
}
6.动态内存开辟但忘记释放(导致内存泄漏)
void test()
{
int* p = (int*)malloc(sizeof(int));
if (p == NULL)
{
perror("malloc");
return 0;
}
*p = 100;
}
int main()
{
while (1)
{
test();
}
return 0;
}
四、内存开辟
1. 栈
栈区的主要作用是用于存储函数调用时的临时变量、函数参数以及函数返回地址等信息。栈区从高地址开始使用。
2. 堆
堆区主要是存放动态内存开辟申请的空间。一般在使用完后需要释放,若忘记释放,动态内存开辟申请的空间会被操作系统释放。堆区从低地址开始使用。
3. 数据段
数据段也被称为数据区和静态数据区,用于存储全局变量和静态数据(static修饰的变量),程序结束后自动回收。
4. 代码段
代码段用于存储执行指令的二进制代码。它通常包含程序的指令、常量值和全局变量。
五、柔性数组
1. 柔性数组的特点
1.1 柔性数组在结构体中,前面必须至少有一个其他类型的结构体成员。
1.2 当结构体中含有柔性数组时,对其 sizeof()
求结构体大小时,不算上柔性数组。
1.3 包含柔性数组成员的结构体用 malloc ()
函数进行内存的动态分配 ,当我们开辟一个空间时,要预期一下柔性数组的大小 ,方便使用柔性数组。
2. 柔性数组的使用(代码一)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct node
{
int a;
char c;
int arr[];
}Node; //这里重命名了,但未使用
int main()
{
//申请空间 结构体大小 + 10个整形大小
struct node* p = (struct node*)malloc(sizeof(struct node) + 10 * sizeof(int));
//这里相当于柔性数组申请了10 int类型大小的空间
if (p == NULL)
{
perror("malloc");
return 1;
}
//使用
p->a = 10;
p->c = 'v';
for (int i = 0; i < 10; i++)
{
p->arr[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p->arr[i]);
}
//释放
free(p);
p = NULL;
return 0;
}
3. 不使用柔性数组完成柔性数组的功能(代码二)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
typedef struct node
{
int a;
char c;
int* pa;
}Node;
int main()
{
//申请空间 结构体大小 + 10个整形大小
struct node* p =(struct node*) malloc(sizeof(struct node));
//判断第一次malloc是否成功
if (p == NULL)
{
perror("malloc1");
return 1;
}
//这里申请十个int类型大小的空间,并将申请的空间赋给p->pa
p->pa = (int*)malloc(sizeof(int) * 10);
//判断第二次malloc是否成功
if (p->pa == NULL)
{
perror("malloc2");
return 1;
}
for (int i = 0; i < 10; i++)
{
p->pa[i] = i + 1;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", p->pa[i]);
}
//释放
free(p->pa);
p->pa = NULL;
free(p);
p = NULL;
return 0;
}
4.通过代码一和代码二比较得到使用柔性数组的好处
(1)使用柔性数组能够方便释放内存
在代码一中我们只申请一次空间,释放一次空间。而代码二中我们申请两次空间,释放两次空间,在申请的时候需要先给结构体申请空间,再给结构体成员中的指针申请空间,释放的时候要先释放结构体成员中的指针申请空间,再释放给结构体申请的空间。若用户不知道程序先释放结构体申请的空间,那么结构体成员中的指针申请的空间就再也没有谁知道他的位置,不能再释放。
(2)能够减少内存碎片,能够加快访问速度(但不多)。
结尾
如果有什么建议和疑问,或是有什么错误,希望大家能够提一下。
希望大家以后也能和我一起进步!!
如果这篇文章对你有用的话,希望能给我一个小小的赞!