1、为什么存在动态内存分配?
❔首先,让我们思考一下,为什么需要动态内存管理,它有什么优势吗?
我们当前使用的内存开辟方式
int var;//在栈区开辟四个字节
char str[10];//在栈区开辟10个字节的连续内存单元
- 通过观察可以发现,上面开辟空间的方式有以下特点:
- 开辟空间大小固定
- 必须在声明时给定大小,在编译时分配内存
❔那么这样的特点会造成什么样的缺陷呢?
- 例如我需要统计班上n名同学的期末考试成绩,对于这种情况,我们不知道需要开辟多少空间以供使用,如果开辟空间少了会造成数组越界,开辟多了会造成空间浪费。
#include<stdio.h>
#include<stdlib.h>
int stu[30];//空间大小固定,当数据量超过开辟空间大小,会造成数组越界
int main()
{
int n;
scanf("%d", &n);
for (int i = 0; i < n; i++)
{
int score; scanf("%d", &score);
stu[i] = score;
}
return 0;
}
☑️对于这种情况,就需要动态内存管理出手了。
2、动态内存管理函数介绍
在这里,我们将介绍
malloc
,calloc
、realloc
三个函数,以及内存释放函数free
。
(1)malloc
-
malloc
为memory allocation
的简写,意为内存分配。 -
让我们来看看
malloc
函数的函数原型和文档信息
【函数原型】
void* malloc (size_t size);
🔸返回类型:void*
🔸参数类型:size_t
(无符号整形)
【文档信息】
- 通过阅读函数原型和文档信息,我们可以得出以下特性。
malloc
函数返回类型为void*
,不能识别开辟空间的类型,因此需要用户自主决定类型。
✔️这里我们通常会使用强制类型转换来决定开辟空间的类型
- 例如这里我想开辟10个整形的连续内存空间单元,用
int*
对malloc
开辟的内存空间进行强制类型转换。
int *a = (int*)malloc(10);
这10个内存空间都存储了什么呢,malloc
有对其进行初始化操作吗?
👇🏼那么就引出了第二条特性。
malloc
函数只能分配内存空间,并不能对内存空间的元素初始化。
- 可以看到
malloc
成功开辟出10个int类型的内存空间单元,但开辟的空间元素均为随机值,
✔️因此对于malloc申请的空间,需要我们手动初始化。
#include<stdio.h>
#include<stdlib.h>
int main()
{
//开辟十个整形大小的内存空间
int* a = (int*)malloc(sizeof(int)*10);
//手动初始化
for (int i = 0; i < 10; i++)
{
*(a + i) = i + 1;
}
return 0;
}
- 调试可以看到我们开辟的10个整形内存空间存放了10个整形数据。
❔那么问题来了,malloc
是否会申请空间失败呢,失败了又会作何处理呢?
- 可以看到,当我们申请
-1
内存空间大小单元,a指针接收的地址为0x0000000000000000
,是一个空地址,表示malloc
申请空间失败。
👇🏼那么就引出了第三条特性。
- 如果内存空间开辟成功,则返回该内存空间的起始位置;否则返回空指针
NULL
,注意该空指针不应该被解引用。
✔️正因为会有开辟失败的情况,因此需要对malloc
的返回值进行检查,避免解引用空指针。
- 这里使用
perror
函数,用于将错误信息输出到标准设备(stderr),方便开发者定位错误。
int *a = (int*)malloc(10);
if (a == NULL)
{
perror("malloc fail!");
exit(-1);
}
- 例如开辟-1内存空间大小的整形空间,可以看到输出窗口打印了错误信息。
❓有同学会问,什么情况malloc
开辟内存空间会失败呢?
⚫️开辟内存空间过大。
开辟的内存空间超过了当前环境(编译器版本,程序的内存寻址64位、32位)下堆的最大可分配内存。
⚫️内存碎片导致无法开辟大空间
长时间使用动态内存分配和释放可能导致内存碎片,使得虽然总体上有足够的内存,但是无法找到足够大的连续内存块。
⚫️开辟内存空间大小不合法
例如开辟空间大小为0的内存空间。
- 如果参数
size
为0
,malloc
的行为是标准是未定义的,取决于编译器。
- 对于这种情况,我们来测试一下。
✔️可以看到,虽然没有申请到任何空间,但是指针a
不为NULL
,即malloc
仍然返回了一块地址。(不同的编译器会有不同的结果,博主使用的是vs2022)
malloc
开辟的内存空间会在程序结束时自动归还给操作系统。
那么问题来了,如果我在程序中使用后不再需要这块内存空间,没有立即释放会导致什么问题呢?
- 内存泄漏(Memory Leak):内存泄漏是指程序在申请内存后,未能在不再需要时正确释放,导致这部分内存无法被再次使用。随着程序的运行,内存泄漏会逐渐累积,可能导致可用内存减少。
- 程序性能下降:如果内存泄漏严重,程序可能会因为可用内存不足而频繁触发内存分配和回收(这里主要是realloc的分配回收),这会降低程序的性能。
❔有没有在使用完毕后立即释放该空间的方法呢?
👇🏼那么就引出了free
函数来解决这个问题。
(2)free
【函数原型】
void free (void* ptr);
🔸返回类型:void
🔸参数类型:void*
【文档信息】
- 通过阅读函数原型和文档信息,我们可以得出以下特性。
- 如果参数
ptr
指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 观察下面代码,我们尝试用
free
去释放一个在栈上分配的内存。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 1;
int* b = &a;
if (a == NULL)
{
perror("malloc fail!");
exit(-1);
}
free(b);
return 0;
}
- 可以看到,这里出现了断言错误,错误信息指出表达式
_CrtIsValidHeapPointer(block)
失败,其中block
是传递给free
函数的指针。
☑️因为局部变量a是在栈上分配的,不是通过malloc
、calloc
、realloc
在堆上分配的。因此a在程序结束时会自动被编译器回收,不应该让开发者手动释放。
- 如果传入的指针是空指针,
free
函数将不做任何操作。 free
函数无法修改指针本身指向内存块的地址。
我们来测试一下
- 可以看到,当指针
a
被free
函数释放后,初始化的1~10变为了随机值,但指针a
指向的仍然是被分配内存块的地址。
✔️使用free函数释放指针a指向的内存块,则这块空间归还给了操作系统,因此被初始化的1~10变为了随机值,但是指针a仍然指向原来内存块的地址,这意味着a指针变为了”野指针“。这是很不安全的。
☑️为了解决问题,开发者应当手动给a指针置空,这样就能避免野指针的产生。
a=NULL;
- 置空后就不会指向原内存块的地址了
🔹需要注意的是,malloc
和free
函数都在头文件stdlib.h
中,记得要包含头文件哦。
- 到这里,我们就可以解决上面的给n个学生统计成绩的问题了。
#include<stdio.h>
#include<stdlib.h>
//int stu[30];//空间大小固定,当数据量超过开辟空间大小,会造成数组越界
int main()
{
int n;
scanf("%d", &n);
//分配n个整形大小的内存空间
int* stu = (int*)malloc(sizeof(int) * n);
if (stu == NULL)
{
perror("malloc fail!");
exit(-1);
}
for (int i = 0; i < n; i++)
{
int score; scanf("%d", &score);
stu[i] = score;
}
//释放内存块
free(stu);
//指针置空,防止出现野指针
stu = NULL;
return 0;
}
🔴我们来小结一下,使用malloc
分配内存要记得判空,预防malloc
申请失败,使用完这块空间后要记得将其归还给操作系统,使用free
函数释放内存空间,最后给指针置空,防止出现野指针。
(3)calloc
【函数原型】
void* calloc (size_t num, size_t size);
返回类型:void*
参数类型:size_t
(无符号整形)
(num
为要开辟内存块中的元素个数,size
为每个元素分配的字节数)
【文档信息】
特性
- 为
num
个字节数为size
的元素开辟一块内存,所有元素初始化为0。 - 与
malloc
功能不同的地方在于calloc
可以将内存块初始化为0。
我们来测试一下
(4)realloc
【函数原型】:
void* realloc (void* ptr, size_t size);
返回类型:void*
参数类型:void*
和size_t
(无符号整形)
(ptr
是要调整的内存地址、size
是调整之后的新大小)
【文档信息】
特性
- 可用于重新分配原内存块的大小(有可能把内存移动到一个新位置)
- 如果传入的指针为空指针,
realloc()
的作用和malloc()
一致 - 如果需要缩小内存块大小,则在原内存块释放一部分空间
🟤扩容机制
-
本地扩容
当前内存块的后面就有足够的空间可以扩容,此时直接在后面续上新的空间即可 -
异地扩容
当后边没有足够的空间可以扩容,realloc
函数会找一个满足空间大小的新的连续空间。把旧的空间的数据,拷贝到新空间的前面的位置,并且把旧的空间释放掉(无需手动释放),同时返回新的空间的地址
注意:如果扩容失败了,relloc会返回一个NULL指针,因此当扩容失败我们不能在其上面赋值。这里要加个判断,别忘了把tmp指针置空。
int* tmp=(int*)relloc(p,sizeof(int)*20);
if(tmp == NULL)
{
perror("relloc fail!");
exit(-1);
}
p = tmp;
tmp = NULL;
3、常见动态内存错误
(1)使用free释放动态内存开辟的一部分空间
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 100);
for (int i = 0; i < 100; i++)
{
*p = i;
}
int* origin = p;
for (int i = 0; i < 10; i++)
{
p++;
}
free(p);
return 0;
}
- 为什么当p指针偏移后再使用free释放会出现报错呢?
✔️这是因为free
函数需要做到申请多少空间就释放多少空间,因此释放了一部分空间后,就会超出申请的空间外,造成内存访问错误。
(2)对同一块动态开辟内存多次释放
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 100);
free(p);
free(p);
return 0;
}
- 可以看到,当对同一块内存块释放多次时出现了报错。这是为什么呢?
✔️这是因为当调用第一次free()
时,指针p指向的内存块的数据已经还给操作系统了,此时p指针就成为了一个野指针,当我们再次使用free()释放p时就会造成释放【野指针】的错误
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int) * 100);
free(p);
p = NULL;
free(p);
return 0;
}
☑️这里我们只需要给p指针置空即可,这样就不会出现报错了
(3)向堆区申请空间后忘记释放
#include<stdio.h>
void test()
{
int* p = (int*)malloc(sizeof(int) * 100);
}
int main()
{
test();
return 0;
}
- 观察上面代码,可以发现主函数调用了test函数,test函数中向堆区申请了100个整形大小的空间,但是并没有释放,那么这就会造成【内存泄漏】的问题。
#include<stdio.h>
void test()
{
int* p = (int*)malloc(sizeof(int) * 100);
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
☑️这里我们只需要在test函数中释放p指向的内存块,再给p指针置空即可
4、笔试题分析
(1)题目一
#include<stdio.h>
#include<stdlib.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
//为什么会销毁?
//因为 p指针 出了函数栈帧后会销毁,此时p指针指向的内存块中的内容呗操作系统回收,因此打印出错
printf(str);
}
int main()
{
Test();
return 0;
}
- 观察以上代码,其中有一个错误,分析错误的地方和原因。
✔️分析可知这是栈空间地址问题,Test()
函数中调用了GetMemory()
函数并p字符数组的首元素地址,此时str指针接收了p数组的首元素地址,但是GetMemory()
函数调用结束后,局部变量p字符数组出了函数作用域,被操作系统回收,p数组的内容被清除,因此打印str时出现错误。
(2)题目二
#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
- 观察以上代码,其中有一个错误,分析错误的地方和原因。
✔️分析可知,这段代码是可以编译运行打印出world
的,但是这是非法的,因为str指向一块动态开辟内存的空间,使用free()
函数释放该空间后,str指向内存块的空间被销毁了,但str指针仍然指向被原来的内存块。接下来的操作就是非法的了,我们向一个已经被释放的空间插入字符串,这就造成了【非法访问内存】问题。
☑️正确做法是给str指针置空即可
#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
//问题:没有置空
//虽然运行没有出错但是 其实是非法访问
//正确做法:加上置空
str = NULL;
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
5、柔性数组
(1)概念和声明
柔性数组,也称为伸缩性数组成员,是C99标准中引入的一种结构体特性。它允许在结构体定义的最后一个元素声明一个未知大小(可动态分配)的数组,使得结构体能够处理可变长度的数据。
🟤柔性数组的声明有两种形式
- 结构体最后一个成员为
数组名[0]
的形式
typedef struct st_type
{
int x;
int a[0];
}type_a;
- 结构体最后一个成员为
数组名[]
的形式
✔️两种柔性数组的声明方式不同,在某些编译器支持int a[0]
的写法,而有些则支持int a[]
的写法,具体取决于编译器的实现。
(2)特点
-
柔性数组必须是结构体的最后一个成员
-
柔性数组前面必须至少一个其他成员
- 即结构体本身必须要有空间,否则将无法通过结构体找到柔性数组,那么就无法为柔性数组分配空间。
typedef struct st_type
{
int a[];
}type_a;
- 柔性数组不占内存空间
- 前面我们学过了内存对齐,知道了计算结构体内存空间时每个成员都是计算在内的。
- 但观察下图,可以发现柔性数组不参与结构体大小的计算。
(3)使用场景
- 使用柔性数组首先要开辟结构体空间,随后再去给柔性数组开辟空间。
type_a* s = (type_a*)malloc(sizeof(type_a)+sizeof(int) * 10);
其中sizeof(type_a)
是结构体空间,sizeof(int)*10
是为柔性数组开辟的十个整形空间。
注意:分配的内存应该大于结构体的大小,以适应柔性数组的预期大小
- 下面我们来测试一下柔性数组是否被正确声明,并给其初始化。
#include<stdio.h>
#include<stdlib.h>
typedef struct st_type
{
int x;
int a[];
}type_a;
int main()
{
//开辟结构体空间,并给柔性数组10个整形空间
type_a* s = (type_a*)malloc(sizeof(type_a)+sizeof(int) * 10);
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
s->x = 99;
for (int i = 0; i < 10; i++) {
s->a[i] = i + 1;
}
for (int i = 0; i < 10; i++) {
printf("%d ", s->a[i]);
}
printf("\n");
free(s);
return 0;
}
✔️通过观察运行结果可以发现,柔性数组被正确声明和初始化。(和普通数组的使用区别不大)
🟠此外,柔性数组可以动态改变大小,那么我们就可以使用所学的realloc()
去实现动态扩容操作。
type_a* tmp = (type_a*)realloc(s,sizeof(type_a) + sizeof(int) * 20);
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
- 给开辟的空间初始化并打印
❕需要注意的是,柔性数组的大小不计入结构体的空间总大小。
❔那么,要如何正确释放柔性数组开辟的空间呢?
☑️使用free(s)
即可。这是因为柔性数组的内存是与结构体的其他部分连续分配的,因此无法单独释放柔性数组的空间(即free(s->a)
是错误的)。
柔性数组的内存和结构体的内存是不可分割的,它们共享同一块内存区域。
🟤让我们分析一下这块内存的布局
- 前面是结构体的非柔性数组部分(
int x
) - 后面是柔性数组部分(
int a[]
)
这块内存是一个整体,free()
只能释放整个分配的内存块,而不能单独释放其中的某一个部分。
☑️因此,如果需要单独管理其中的空间,我们可以使用指针数组替换柔性数组,方便更灵活的管理内存
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int x;
int* a; // 使用指针代替柔性数组
} type_a;
int main()
{
// 分配结构体空间
type_a* s = (type_a*)malloc(sizeof(type_a));
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
// 分配柔性数组的空间
s->a = (int*)malloc(sizeof(int) * 10);
if (s->a == NULL) {
perror("malloc fail");
free(s); // 释放结构体空间
exit(-1);
}
s->x = 99;
// 给柔性数组赋值
for (int i = 0; i < 10; i++) {
s->a[i] = i + 1;
}
// 输出柔性数组的值
for (int i = 0; i < 10; i++) {
printf("%d ", s->a[i]);
}
printf("\n");
// 释放柔性数组的空间
free(s->a);
// 释放结构体的空间
free(s);
return 0;
}
🔴如果想在某些情况共享内存,在另一些情况分开管理内存,这个时候可以使用联合体union
来实现
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int x;
//使用联合体将指针和柔性数组联合
union {
int a[]; // 柔性数组
int* ptr; // 指针
} u;
} type_a;
int main()
{
// 分配结构体空间,并给柔性数组10个整形空间
type_a* s = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
s->x = 99;
// 使用柔性数组
for (int i = 0; i < 10; i++) {
s->u.a[i] = i + 1;
}
// 输出柔性数组的值
for (int i = 0; i < 10; i++) {
printf("%d ", s->u.a[i]);
}
printf("\n");
// 释放整个结构体空间
free(s);
// 如果需要单独管理内存,可以这样
type_a* s2 = (type_a*)malloc(sizeof(type_a));
if (s2 == NULL) {
perror("malloc fail");
exit(-1);
}
s2->u.ptr = (int*)malloc(sizeof(int) * 10);
if (s2->u.ptr == NULL) {
perror("malloc fail");
free(s2);
exit(-1);
}
// 使用指针
for (int i = 0; i < 10; i++) {
s2->u.ptr[i] = i + 1;
}
// 输出指针数组的值
for (int i = 0; i < 10; i++) {
printf("%d ", s2->u.ptr[i]);
}
printf("\n");
// 释放指针数组的空间
free(s2->u.ptr);
// 释放结构体的空间
free(s2);
return 0;
}
(4)优缺点分析
🟤柔性数组对比我们常用的指针数组有什么不同呢?
- 柔性数组
//开辟结构体空间和柔性数组空间
type_a* s = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
//同时释放结构体空间和柔性数组空间
free(s);
✔️可以看到,柔性数组使用了一次malloc
,一次free
- 指针数组
//开辟结构体空间
type_a* s = (type_a*)malloc(sizeof(type_a));
if (s == NULL) {
perror("malloc fail");
exit(-1);
}
//开辟结构体指针数组空间
int* tmp = (int*)malloc(sizeof(int) * 10);
//把这块空间给指针数组
s->a = tmp;
//初始化
for (int i = 0; i < 10; i++) {
s->a[i] = i + 1;
}
//打印
for (int i = 0; i < 10; i++) {
printf("%d ", s->a[i]);
}
printf("\n");
//由于内存地址不连续,因此需要分开释放
free(s->a);
free(s);
✔️可以看到,指针数组使用了两次malloc
,两次free
- 内存分布对比
柔性数组
指针数组
🟢因此使用柔性数组的主要优势有以下两点
☑️ 方便内存申请和释放
当我们不需要单独管理结构体中数组的内存大小时,使用柔性数组可以使代码更清晰简洁,无需多次对内存进行操作,实现内存管理的简化。
☑️ 提高代码运行速度
结构体和数组的内存是连续分配的,释放内存时只需要一次 free 操作,避免了多次分配和释放内存导致的内存碎片问题,访问柔性数组的元素时,内存访问更加高效,减少了缓存不命中的可能性。
6、总结和回顾
最后我们来回顾一下本文所学
- 首先,我们讲解了三个动态分配内存函数,分别是只负责开辟空间的
malloc()
,可以开辟空间又可以分配字节的calloc()
,可以重新分配内存块的realloc()
以及用于释放内存的free()
函数。 - 了解这几个函数还不够,如何正确的使用它们很关键。于是总结了三个常见动态内存分配错误情况,方便我们使用时规避它们。
- 学会了使用它们,就要来几道题目练练手!我们可以通过笔试题加深对它们的理解,了解使用细节。
- 最后我们谈到了【柔性数组】,了解它的声明,释放等使用细节,以及配合
realloc()
实现无限扩容的特点。最后通过【内存分布】分析柔性数组和指针数组的不同点,得出了其优势所在。
好,这就是本文的全部内容,感谢阅读🌹