当前位置: 首页 > news >正文

【C语言进阶】动态内存管理及柔性数组

目录

  • 1.为什么存在内存动态分配
  • 2.malloc和free
    • 2.1 malloc函数功能介绍
    • 2.2 free函数的功能介绍
    • 2.3 代码演示
  • 3.calloc
    • 3.1calloc函数的功能介绍
    • 3.2代码演示
  • 4.realloc
    • 4.1realloc函数功能介绍
    • 4.2realloc在调整内存空间的是存在两种情况
    • 4.3代码演示
  • 5.常见的动态内存错误
    • 5.1对NULL指针的解引用
    • 5.2对动态开辟空间的越界访问
    • 5.3对非动态开辟内存使用free释放
    • 5.4使用free释放一块动态开辟内存的一部分
    • 5.5对同一块内存动态的多次释放
    • 5.6动态开辟内存忘记释放(内存泄漏)
  • 6.C/C++程序的内存开辟
  • 7.柔性数组
    • 7.1柔性数组的特点
    • 7.2柔性数组的使用
    • 7.3柔性数组的优势
    • 7.4图解演示

1.为什么存在内存动态分配

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

开辟空间的方式有两个特点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就只能试试动态内存开辟了。

下面我们来讲述三种我们常见的动态内存开辟函数

2.malloc和free

2.1 malloc函数功能介绍

void* malloc (size_t size);

函数功能
这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

参数
size是内存块的大小,以字节为单位。
是无符号整数类型-size_t

返回值
void*
成功时,指向函数分配的内存块的指针。
这个指针的类型总是 ,它可以被强制转换为所需的数据指针类型,以便被解引用。
如果函数未能分配请求的内存块,则返回空指针。

2.2 free函数的功能介绍

函数free,专门是用来做动态内存的释放和回收的

void free (void* ptr);

函数功能
free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。

参数
指向以前使用 或 分配的内存块的指针

返回值
没有

2.3 代码演示

malloc开辟空间是在堆区开辟空间

#include<stdio.h>
#include<errno.h>
int main()
{
	int* p= (int*)malloc(40);//我们要开辟的空间是整形的,所以我们把它强制转换成int型,这个可以根据自己的需求来
	int* ptr = p;//保留指向malloc开辟空间的地址

	if (ptr == NULL)//判断ptr是否是空指针
	{
		printf("%s\n", strerror(errno));
		//把错误码记录到错误码的变量中,然后再编译,打印错误信息
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*ptr = i;
		 ptr++;
	}
	free(p);//释放开辟的空间
	p = NULL;//p置为空指针,让p不再是野指针
	return 0;
}

看完这段代码可能会有许多疑问,为啥要保留指向malloc开辟的空间?
当我们用malloc申请空间后,我们要对申请的空间进行释放,如果不释放内存,内存可能就会越占越少以至于不够,所以我们需要free来释放空间,free的参数是刚开始指向空间的起始地址,也就是p指针,如果是参数是ptr,就不能释放开辟的空间,因为ptr++已经改变了ptr原来的地址,所以我们要保留原来的指针对它进行释放。为啥要把p置为NULL呢?,如果我们不把p置为NULL,p里面还是存放着原来的地址,但是p所指向的空间已经被释放,这样p就会变成一个野指针,如果继续访问,就会非法访问,为了防止这样我们就会把它置为NULL.

3.calloc

3.1calloc函数的功能介绍

void* calloc (size_t num, size_t size);

函数功能
函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0

参数
num:要分配的元素数。
size:每个元素的大小,单位字节。

返回值
成功时,指向函数分配的内存块的指针。
这个指针的类型总是 ,它可以被强制转换为所需的数据指针类型,以便被解引用。
如果函数未能分配请求的内存块,则返回空指针。

3.2代码演示

#include<stdio.h>
#include<errno.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	//开辟10个元素的空间,每个元素的大小为int型
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

开辟空间初始化为0
在这里插入图片描述

4.realloc

4.1realloc函数功能介绍

void* realloc (void* ptr, size_t size);

函数功能
更改 所指向的内存块的大小
该函数可能会将内存块移动到新位置(该位置的地址由函数返回)。

参数
ptr :是要调整的内存地址
如果ptr为空指针(NULL),这是类似于malloc的功能
size:内存块的新大小,以字节为单位。
是无符号整数类型

返回值

指向重新分配的内存块的指针,该块可能与原来位置相同,也可以是新位置。
这个指针的类型是,它可以被强制转换为所需的数据指针类型,以便被解引用

4.2realloc在调整内存空间的是存在两种情况

情况1:原有空间之后有足够大的空间,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化,函数返回原来内存的地址。
情况2:原有空间之后没有足够大的空间
原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用,把旧空间的数据拷贝到新空间,释放旧空间的地址。这样函数返回的是一个新的内存地址。

图解
在这里插入图片描述

4.3代码演示

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 1;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//增加空间
	int* ptr = (int*)realloc(p, 80);
	//如果开辟失败,返回的是NULL,要重新设置一个指针
	//如果满足条件,把ptr赋给p指针
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;
	}
	for (i = 10; i < 20; i++)
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 20; i++)//打印元素
	{
		printf("%d ", *(p + i));
	}
	//释放空间
	free(p);
	p = NULL;
	return 0;
}

在这里插入图片描述

5.常见的动态内存错误

5.1对NULL指针的解引用

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果开辟空间失败,p的值是NULL,对p进行解引用就会有问题
 free(p);
}

所以我门要对p进行判断是否为空指针

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

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

这里我们原本是开辟了20个字节,也就是5个整形的大小,但是在循环时,往后打印了20个整数,超出了我们的访问权限,也就是越界访问,原本只能访问5个整形的大小,你直接访问了20个整形的大小,会造成程序报错,这个是跟数组在内存中连续存放一致

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

#include<stdio.h>
int main()
{
	int num = 10;//非动态内存开辟空间
	int* p = &num;
	free(p);
	p = NULL;
	return 0;
}

num是非动态内存开辟空间,不能使用free对它进行空间释放。free的使用是与malloc calloc realloc配合进行使用

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

#include<stdio.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 0;
	for (int i = 0; i < 5; i++)
	{
		*p = i;
		p++;
	}
	free(p);
	p = NULL;
	return 0;
}

在free释放的时候,p指向的不再是动态内存空间的起始位置。如果要释放malloc开辟的空间,我们要从开辟空间的起始位置开始进行释放,如果p不是空间的起始位置,我们就不能释放空间或是释放空间的一部分。

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

#include<stdio.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
		return 0;
	for (int i = 0; i < 5; i++)
    {
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	free(p);//重复释放空间
	return 0;
}

对一次开辟空间只需要free一次,一次动态空间开辟对应一次free释放空间。

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

#include<stdio.h>
int* get_memory()
{
	int* p = (int*)malloc(40);
	return p;
}
int main()
{
	int* ptr = get_memory();
	//使用

	return 0;
}

函数会返回动态开辟空间的地址,我们要对空间进行释放,否则会导致内存泄漏,上述代码中,malloc开辟空间,free没有释放空间。

6.C/C++程序的内存开辟

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

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

7.柔性数组

也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

代码演示

//表示1
#include<stdio.h>
struct s
{
	int a;
	char ch;
	int arr[0];//柔性数组成员
};

//表示2
struct s
{
	int a;
	char ch;
	int arr[];//柔性数组成员
};

上述有两种柔性数组的表示方法,有时候其中一种表示,编译器无法编译,就需要表示另一种方法

7.1柔性数组的特点

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

7.2柔性数组的使用

代码1

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int n;
	float s;
	int arr[];//柔性数组成员
};
int main()
{
	//为结构体成员开辟空间,同时为柔性数组预留空间
	struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
	if (ps == NULL)
		return 1;

	ps->n = 100;
	ps->s = 6.5;
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d",&(ps->arr[i]));
	}

	printf("%d %f\n", ps->n, ps->s);

	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//增加柔性数组的空间 ,从16个字节增加到40个字节
	struct S*ptr=(struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 10);
	if (ptr == NULL)
	{
		return 1;
	}
	else 
	{
		ps = ptr;
	}
	//然后在对新开辟的空间进行使用
	
	//释放空间
	free(ps);
	ps = NULL;
	return 0;
}

在对结构体开辟空间时,.sizeof 返回的这种结构大小不包括柔性数组的内存,也就是没有对柔性数组开辟空间,所以我们要在堆中为结构体开辟空间时,要另外给柔型数组预留一块空间,当我们想增加柔性数组的空间,我们就可以通过realloc来为柔性数组增加空间,这样我们就可以控制它的内存空间,使它可大可小,这也体现出它的柔性。

同样我们也可以设计一下结构体,一样可以实现上面的功能。
代码2

#include<stdio.h>
#inlucde<stdlib.h>
struct S
{
	int n;
	float s;
	int *arr;//最后一个成员是指针
};

int main()
{
	//结构体开辟空间
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;
	ps->n = 100;
	ps->s = 5.5f;

	//为结构体中的arr开辟空间
	int* ptr = (int*)malloc(4 * sizeof(int));
	if (ptr == NULL)
	{
		return 1;
	}
	else
	{
		ps->arr = ptr;
	}
	int i = 0;
	for (i = 0; i < 4; i++)
	{
		scanf("%d", &(ps->arr[i]));
	}

	printf("%d %f\n", ps->n, ps->s);
	for (i = 0; i < 4; i++)
	{
		printf("%d ", ps->arr[i]);
	}
     //释放空间
	free(ps);
	ps = NULL;
	free(ps->arr);
	ps->arr = NULL;
	return 0;
}

这里第一个malloc为结构体中的三个成员都开辟了空间,所以这里我们不用另外开辟出一块空间预留出来给第三个成员,第二个malloc是对arr进行增加内存空间,所以他们内存空间不是连续的,而柔性数组的所在的结构体空间是连续的。

7.3柔性数组的优势

从上面的两个代码,可以看出代码1的好处
1.方便内存释放

从上述的代码1中,结构体的内存及其成员的内存是一次性分配好了,所以只需要返回一个结构体指针,只用一次free就可以把所有的内存给释放了。
而代码2中,第一个malloc对结构体做了第一次内存分配,第二次malloc又对结构体中的arr做了一次内存分配,所以我们到最后要free两次,才能把所有的内存给释放了

2.有利于访问速度

代码1中结构体的内存是连续存放的,而代码2中的结构体不是连续存放,这样就会导致内存之间的间隙,从而降低访问速度。

7.4图解演示

下面两个分析,可以让你更加理解两个代码中的结构体是如何分配空间的。
在这里插入图片描述

相关文章:

  • 闲暇之际敲敲代码,记录Leetcode刷题Day-01
  • ISO27001认证需要准备什么资料?
  • 腾讯研究成果登 Nature 子刊:scBERT 攻克单细胞测序数据分析痛点
  • Docker部署go项目
  • 【面试题】面试官: Vue如何实现权限管理?
  • Linux常用操作汇总:内容有点杂,但很实用
  • pytorch深度学习训练模板(分类、回归)
  • CSS进阶篇——伪类 (pseudo classes)
  • 基于affine+sift特征提取的图像配准算法matlab仿真
  • 【重温Linux】一、Ubuntu系统一些常识性的东西(这节持续更新)
  • Redis安装,性能优化
  • mysql group_concat 与 union 联合查询漏洞,数据列最大长度为341
  • 机器学习基础:统计量与抽样分布
  • 工作中整理的常用的Linux命令
  • Vue2和Vue3的区别——实例创建、响应式数据代理、v-for和v-if优先级、生命周期
  • MyBatis-Plus中的更新操作(通过id更新和条件更新)
  • easy-rules规则引擎最佳落地实践
  • 看我如何连夜自建网站背刺我的求职对手们
  • 027.将有序数组转换为二叉搜索树
  • 第五届传智杯-初赛【B组-题解】
  • 最全面的SpringMVC教程(一)——SpringMVC简介
  • OpenCV-Python小应用(六):车道线检测
  • 微信小程序介绍
  • matlab实现MCMC的马尔可夫转换MS- ARMA - GARCH模型估计
  • 华为云桌面Workspace,让你的办公更加舒适惬意