动态内存管理

前言

今天我们来聊聊动态内存管理,这可是数据结构绕不开的。


1.为什么会有动态内存分配

其实,看过数据结构就知道,有两种方式实现顺序表——静态分配和动态分配。而静态分配一旦确认数组大小就无法再更改,存不下数据时只能放弃治疗。

反之,动态分配就灵活很多,可以随时进行修改。

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = { 0 };//在栈空间上开辟10个字节的连续空间

但是上述开辟空间的方式有两个特点:

1.空间开辟大小是固定的
2.数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整

但是对于空间的需求,不仅仅是上述的情况,我们有时候需要的空间大小在程序运行的时候才能知道,那数组编译时开辟空间的方式就不能满足需求了。
C语言引入了动态内存开辟,程序员可以自行申请和释放空间,比较灵活。


2.常用函数

C语言提供了多个动态内存开辟的函数。

2.1 malloc和free

malloc与free都声明在stdlib.h头文件中

2.1.1 malloc函数说明

在这里插入图片描述
void* malloc (size_t size);
malloc函数用于分配内存块,向内存申请一块连续的大小为size字节的空间。
如果开辟成功,返回指向这块空间起始地址的指针,返回类型为泛型指针。
如果开辟失败,则返回NULL指针,在使用malloc函数时要对其返回值做检查。

注意:

1.新分配的内存块的内容未初始化,仍为不确定值。
2.如果传入的size为零,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

对malloc进行测试,可以看到分配的内存未初始化。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(10*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
		printf("%d ",p[i]);
	free(p);
	p = NULL;
	return 0;
}

VS2022 X64环境的输出结果如下
在这里插入图片描述

2.1.2 free函数说明

C语言提供另外一个函数free,用来释放和回收动态内存。
在这里插入图片描述
函数原型为void free (void* ptr);
free函数用于取消由malloc, calloc或realloc函数动态分配的内存块,参数是需要进行释放内存块的起始地址,函数没有返回值。

其使用如下所示:

1.之前通过调用malloc、calloc或realloc分配的内存块被释放,使其再次可用于进一步分配。
2. 如果ptr没有指向上述函数分配的内存块,则会导致未定义的行为。
3.如果ptr是一个空指针,则函数什么也不做。
4.请注意,此函数不会改变ptr本身的值,因此它仍然指向相同的(现在无效的)位置。

2.1.3 malloc和free使用举例

#include <stdlib.h>
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)//分配失败
	{
		perror("malloc");//对原因进行打印
		return 1;//提前结束程序
	}
	//分配成功,进行赋值
	for (int i = 0; i < 10; i++)
		*(p + i) = i+1;
	free(p);//释放p所指向的动态内存
	p = NULL;//置为空指针
	return 0;
}

执行free(p);之后,这块内存返还给操作系统,可是p中依然存储着空间的地址;因此p指向的空间不属于当前程序,但还是可以通过p找到已经释放的空间,此时p是野指针,因此要置为空指针。

2.2 calloc函数

在这里插入图片描述
void* calloc (size_t num, size_t size);calloc函数用于分配并清零初始化数组,与malloc不同的是,其参数不同,且对分配的内存置零。

其使用如下所示:

1.为num个元素分配一块内存,每个元素的大小为size字节长,并将其所有位初始化为零
2.有效的结果是分配了一个(num*size)字节的零初始化内存块。如果size为零,则返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。

对calloc进行测试,可以看到分配的内存未初始化。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10,sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
		printf("%d ",p[i]);
	free(p);
	p = NULL;
	return 0;
}

VS2022 X64环境的输出结果如下

0 0 0 0 0 0 0 0 0 0

2.3 realloc函数

2.3.1 realloc说明

在这里插入图片描述
void* realloc (void* ptr, size_t size);realloc函数用于重新分配内存块

官网的解释如下

1.更改ptr所指向的内存块的大小。 该函数可以将内存块移动到新位置(其地址由函数返回)。
2.即使内存块被移动到新位置,其内容也会保留到新旧大小中的较小值。如果新大小较大,则新分配部分的值是不确定的。
3.如果ptr是一个空指针,则函数的行为类似于malloc,分配一个大小为bytes的新块,并返回指向其开头的指针。

如果剖析的细的话,

1.函数会将传入的指针ptr指向的动态分配的内存块的大小修改为size字节,如果ptr是空指针,那么此时realloc会分配一块大小为size字节的空间,与malloc类似;
2.如果size大于原来的内存,那么存在两种情况,第一种是ptr原本指向的内存块及其后面的区域有size字节大小的空间,那么系统自动分配size字节;第二种是ptr原本指向的内存块及其后面的区域没有size字节的小的内存,系统就会重新开辟一块空间,并将原来内存的值拷贝到新分配的内存中。
3.如果size小于原来的内存,那么系统会舍弃一部分数据。

2.3.2 realloc使用举例

#include <stdlib.h>
int main()
{
	int* ptr = (int*)malloc(5 * sizeof(int));
	if (ptr == NULL)
	{
		perror("malloc");
		return 1;
	}
	int* tmp = (int*)realloc(ptr, 10 * sizeof(int));
	//注意此处引入临时变量tmp,因为如果直接用ptr接受,如果未开辟成功,ptr直接接收返回的空指针,那么原本的地址也找不到了
	if (tmp != NULL)
		ptr = tmp;
	free(ptr);
	ptr = NULL;
	//不需要对tmp进行任何操作,因为tmp被赋值给ptr后,二者指向同一片空间,不能重复释放
	return 0;
}

realloc使用的三种情况,
第一种,扩容;
扩容时,如果原本的20byte后续20byte也未分配,那么系统将其分配,原本的20byte变为40byte,并且返回这40字节的起始地址,如下图;
在这里插入图片描述
扩容时,如果原本的20byte后续没有20byte用于分配,那么系统就会另外开辟一块40byte大小的内存,并且将原来20byte的内容进行拷贝,并且返回这40byte的起始地址,如下图。
在这里插入图片描述
扩容时,系统没有空间用于开辟,内存开辟失败,返回空指针。

第二种,缩小空间,系统会舍弃一部分数据。


3.常见的动态内存错误

3.1 对NULL指针的解引用操作

未考虑可能开辟失败返回的是空指针,不能进行解引用

void test()
{
	int* p = (int*)malloc(INT_MAX / 4);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
}
int main()
{
	test();
	return 0;
}

我们的满分选手VS也进行了提示
在这里插入图片描述

3.2 对动态开辟空间的越界访问

void test()
 {
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问
 }
 free(p);
 }

系统会报错,如下图所示。
在这里插入图片描述

3.3 对非动态开辟内存使用free释放

#include <stdlib.h>
void test()
{
	int a = 10;
	int* p = &a;
	free(p);//ok?
}
int main()
{
	test();
	return 0;
}

如果free释放的不是动态分配的内存,会出现错误,如下图所示。
在这里插入图片描述
这是因为a是局部变量,存储在栈区,而free只能释放堆区的内存,内存中的分布如下图所示。

在这里插入图片描述

3.4 使用free释放一块动态开辟内存的一部分

#include <stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}
int main()
{
	test();
	return 0;
}

喜提报错!

3.5 对同一块动态内存多次释放

#include <stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	free(p);
	free(p);//重复释放
}
int main()
{
	test();
	return 0;
}

喜提报错!
VS给出的是,free后的p未初始化
在这里插入图片描述

3.6 动态开辟内存忘记释放(内存泄漏)

#include <stdlib.h>
void test()
{
	int* p = (int*)malloc(100);
	if (NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while (1);
}

忘记释放不再使用的动态开辟空间会造成内存泄漏。如果程序一直在运行,可能会出现电脑内存不断被泄露,知道死机,开机后重复出现之前的状况,像中了病毒。

如果不释放的话,程序结束的时候会被操作系统回收。
但服务器的程序一直在运行,比如阿里,百度等。


4.经典笔试题分析

4.1 题目一

请问运⾏Test 函数会有什么样的结果?

#include <stdlib.h>
#include <string.h>
void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}
int main()
{
	test();
	return 0;
}

在这里插入图片描述
在这里插入图片描述
没有确认动态分配返回指针非空就使用

4.2 题目二

malloc动态分配的内存不再使用后没有进行释放

#include <stdio.h>
char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

p是局部变量,作用域是GetMemory函数,出了这个函数p的生命周期就终止了,因此将p返回并且赋值给str,可是这个时候p已经被销毁,p数组所占的内存空间可能会被其他数据覆盖,不可预测。

VS2022 X64环境的输出结果如下
在这里插入图片描述
但如果是返回值的话,系统会将值先存在寄存器中,然后赋值,如下所示。

#include <stdio.h>
int test()
{
	int a = 10;
	return a;
}
int main()
{
	int n = test();
	printf("%d\n", n);
	return 0;
}

4.3 题目三

请问运⾏Test 函数会有什么样的结果?

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

VS2022 X64环境的输出结果如下

hello

存在的问题是,malloc动态分配的内存不再使用后没有进行释放。

下面这个代码也存在没有内存释放的问题

#include <stdlib.h>
void test()
{
	int flag = 1;
	int* p = (int*)malloc(100);
	if (p == NULL) 
		return;
	if (flag)
		return;//程序提前结束
	free(p);
	p = NULL;
}
int main()
{
	test();
	return 0;
}

4.4 题目四

请问运⾏Test 函数会有什么样的结果?

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");//没有对str判断是否为NULL就直接使用
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");//在释放内存后继续使用,非法访问
		printf(str);//直接传地址进行打印
	}
}
int main()
{
	Test();
    return 0;
}

在这里插入图片描述
在这里插入图片描述
VS2022 X64环境的输出结果如下

world

在这里插入图片描述
题目a)返回的是野指针,因为出了函数,x被销毁,&x指向的内容很可能被其他数据覆盖。
题目b)声明指针ptr的时候没有赋值,野指针,因此后续不能解引用。


5.柔性数组

柔性数组(flexible array),C99中,结构体中的最后一个元素允许是未知大小的数组,叫做柔性数组成员。
例如:

struct st_type
{
	int i;
	int a[0];
};

有些编译器会报错无法编译可以改为

struct st_type
{
	int i;
	int a[];
};

5.1 柔性数组的特点

1.结构体中的柔性数组成员前面必须至少有一个其他成员。
2.sizeof返回的这种结构体大小不包括柔性数组的内存。
3.包含柔性数组成员的结构体用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。

举例进行柔性数组的计算,代码如下

#include <stdio.h>
struct S
{
	int n;
	int arr[];
};
int main()
{
	printf("%zd\n", sizeof(struct S));
	return 0;
}

VS2022 X64环境的输出结果如下

4

5.2 柔性数组的使用

柔性数组使用举例,代码如下

//代码1
#include <stdlib.h>
#include <stdio.h>
struct S
{
	int n;
	int arr[];
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	//业务处理
	ps->n = 100;
	for (int i = 0; i < 20; i++)
		ps->arr[i] = i + 1;
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40 * sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
		ptr = NULL;
	}
	else
		return 1;
	for (int i = 0; i < 40; i++)
		printf("%d ", ps->arr[i]);
	free(ps);
	ps = NULL;
	return 0;
}

5.3 柔性数组的优势

也可以用指针实现柔性数组的功能,代码如下,和上述代码实现功能一致,但比较麻烦。

//代码2
#include <stdlib.h>
#include <stdio.h>
struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int* tmp = (int*)malloc(20*sizeof(int));
	if (tmp != NULL)
		ps->arr = tmp;
	else
		return 1;
	ps->n = 100;
	for (int i = 0; i < 20; i++)
		ps->arr[i] = i + 1;
	tmp = (int*)realloc(ps->arr, 40 * sizeof(int));
	if (tmp != NULL)
		ps->arr = tmp;
	else
		return 1;
	for (int i = 0; i < 40; i++)
		printf("%d ", ps->arr[i]);
	free(ps);
	ps = NULL;
	return 0;
}

代码1的内存分布
在这里插入图片描述
代码2的内存分布
在这里插入图片描述

上述代码1和代码2可以完成同样的功能,但是方法1的实现有两个好处:

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,在里面做了二次内存分配,并把整个结构体返回给用户,。用户调用free可以释放结构体,但是用户不知道这个结构体的成员也需要free,所以不能指望用户来发现这个事。因此,如果我们把结构体的内存及其成员所需内存一次性分配好,并返回给用户一个结构体指针,用户一次free就可以将内存进行释放。
第二个好处是:有利于访问速度。
连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,或许也没能提高多少,因为避免不了做偏移量的加法来寻址)。

扩展阅读:C语⾔结构体⾥的数组和指针


6.C/C++种程序内存区域划分

在这里插入图片描述
C/C++程序内存分配的几个区域:

1.栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些内存单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放函数运行而分配的局部变量、函数参数、返回数据、返回地址等。
2.堆区(heap):一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(operate system 操作系统)回收。分配方式类似于链表。
3.数据段(静态区):(static)存放全局变量、静态数据,程序结束后由系统释放。
4.代码段:存放函数体(类成员函数和全局函数)的二进制代码


总结

动态内存分配,很重要,一定要对动态内存分配函数的返回值进行判空;动态分配的内存不用了,一定要释放,将指针置为NULL;因为指针还是会找到那片地址,但是释放后那篇地址不属于我们,就是野指针了。柔性数组,如其名,很灵活。同时了解C/C++内存分布。

心理学中有一个有趣的三分之二定理:一件事情的三分之二处是最难的,也是最容易放弃的,却也是最有价值的。高二,壮年之时伴着它的三分之二跌跌撞撞走来。你借用莎士比亚的话说“时间的大钟上只有两个字:现在”,你说,人生百年无几何。你拨正那颗浮躁的心,你浇灭那颗渐生放弃的心。你说,三分之二后,将是我们愿意为之拼搏的人生,江阔但云不会低,所谓功崇惟志,业广惟勤也是如此。你拉着我走向现在,此刻。

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。 经导师精心指导并认可、获 98 分的毕业设计项目!【项目资源】:微信小程序。【项目说明】:聚焦计算机相关专业毕设及实战操练,可作课程设计与期末大作业,含全部源码,能直用于毕设,经严格调试,运行有保障!【项目服务】:有任何使用上的问题,欢迎随时与博主沟通,博主会及时解答。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值