c语言中的“柔性数组成员”(Flexible Array Member, FAM)

我最近在研究我同事写的具有特定格式的一系列命令行,例如:

指令格式:55 AA LL(包含命令字节+整个数据) MM(命令字节) DD ... 0A

55 AA:帧前缀。

LL:包含命令字节在内的整个数据长度,占一个字节,是可变的。

MM:命令字节,占一个字节,是可变的。

DD ...:具体数据,可以占多个字节,是可变的。

0A:帧后缀,占一个字节,确定的。

我以前也提到过,代码就是用c语言的各个元素(宏定义、类型定义、变量、函数)对事物细节进行描述,那么就存在一个问题,我想对这个"一系列命令行"进行描述,用c语言怎么写呢?

首先,帧的前缀和后缀都是确定不变的常量,因此可以定义多个宏定义对其进行描述。

cpp 复制代码
#define CMD_INDEX0_BYTE 0x55
#define CMD_INDEX1_BYTE 0xAA
#define CMD_INDEXLAST_BYTE 0x0A

#define CMD_INDEX0    0x00
#define CMD_INDEX1    0x01

写到这里就卡住了,因为我无法知道最后一个索引值(index)宏定义对应的具体数值是多少,因为"DD ..."这些真正数据的长度是可变的,不确定的,因此无法用宏定义方式对帧尾的索引值进行描述,也就说不能使用宏定义了,需要用变量(数据类型+变量名的方式定义,以及操纵各个变量的函数)。

cpp 复制代码
uint8_t cmd_last_index;

到这里,我们有一个小结论:c语言中的宏定义往往用来描述确定的,不变的,常量,不能用来描述不确定的,变化的事物。因为宏定义在编译的预处理阶段进行唯一的一次确定的文本替代处理,而且不会进行错误检查,替代过程那个宏名对应哪个宏值进行替代是无法发生变动的,而且整个编译阶段只执行一次替换,因此宏定义无法用来描述变量。

那么,我还要想用用c语言对命令行进行描述,是不是就没法写了呢?当然不是了,LL、MM、"DD ..."这些共同的特点就是可变的,因此用c语言中的变量进行描述,如何使用变量进行描述呢?变量是有三部分组成的变量类型、变量名和操纵变量的函数。首先变量是有数据类型和变量名组成的,LL、MM的数据类型为uint8_t,比较好描述,可以写成:

cpp 复制代码
uint8_t LL;
uint8_t MM;

比较难描述的就是"DD.."因为它的长度是不确定的,可大可小,不确定。首先它每一个成员都是uint8_t类型的,c语言中比较适合的数据结构类型就是数组,但是因为长度是不确定的,而数组定义的时候需要显式或者隐式的将数组元素个数给确定,那是不是没有办法用数组进行描述了呢?其实未必,有一个比较简单的办法就是定义一个数组个数比较大的数组,无论DD ...是几个元素都不会超限,这样就能用这个比较大的数组进行描述:

cpp 复制代码
#define ARRAY_MAX_SIZE    100
uint8_t DD[ARRAY_MAX_SIZE];    // 对DD ...进行描述

到这里起始对DD...的描述已经有所偏移了,因为使用了一个比较大的数组去近似等效DD ...的真实数组。

可能有比较聪明的想到,既然DD ...的长度是不确定的,也就是一个变量,那我定义一个变量不就可以了吗?于是对DD..的描述就变成了这样:

cpp 复制代码
uint8_t dd_length;
uint8_t DD[dd_length];

这就形成了VLA,Variable length array,翻译为可变长度数组。

我想说这样描述也是有问题的,可变长度数组无法定义为全局变量,编译器是报错的,因为全局对象的类型和大小必须在编译阶段就一次性确定下来而且不会修改,因为全局变量是在数据段,拥有整个代码运行的声明周期,是一直占用数据段内存空间,既不会释放,也不会发生改变。

这里暗含了,VLA(可变长度数组)在运行阶段,作为栈内存或者堆内存,也就是函数内部定义是允许的。

既然全局变量的VLA在编译阶段就报错,那么想更近一步的将这些全局变量打包成一个结构体类型,然后定义结构体类型的全局变量,编译阶段也是会遇到VLA编译错误。

到这里就是死局了吗?可能有人到这里就放弃了,或者有回到上面使用一个比较大数组表示DD ...,让我们回到问题根据,首先DD ...是一个数组,并且长度是变化的。以前我写过一篇文章就是函数传参的时候,是如何代码一个数组的。除了首元素的地址值之外,还要包含数组元素个数。

将DD ...这个长度变化的数组分成两部分:第一部分就是定义对应数据对首元素地址和元素个数进行描述;第二部分就是根据第一部分的变量作为前提,使用malloc,或realloc等实现堆内存申请。

因此DD ...我们使用两部分完成描述的:第一部分是对数组二元素的描述,第二部分是堆数组。

在这里也给我们一些启发,描述某个事物细节,如果无法一次就描述清楚,可以分成好部分,这样事物的描述就是有好几部分共同组成的,这也符合事物的描述特性,有点像定义结构体数据类型描述事物的属性和防范,但是我们是好几个属性共同组成某一个细节的描述

因此,描述DD...这个数组可以定义成这样:

cpp 复制代码
uint8_t dd_cnt;
uint8_t *dd;

有些聪明的想到,这样写分散了,可以定义成一个结构体类型:

cpp 复制代码
struct st_type
{
    uint8_t dd_cnt;
    uint8_t *dd;
}

案例是申请了2次堆内存,第一次申请struct st_type类型的堆内存,并对申请过的堆内存进行填充,第二次申请堆内存才是真正DD ...可变长度数组。

cpp 复制代码
#include <stdio.h>
#include <string.h>
int main()
{
	struct st_type*ps = (struct st_type*)malloc(sizeof(struct st_type));
	ps->dd_cnt = 10;
	ps->dd = (uint8_t*)malloc(ps->dd_cnt * sizeof(uint8_t));
	int j = 0;
	for (j = 0;j < 10;j++)
	{
		ps->dd[j] = j;//dd[j]=*(dd+j)
	}
	for (j = 0;j < 10;j++)
	{
		printf("%d", ps->dd[j]);
	}
	//a指向的空间不够了,希望调整大小
	int *ptr = (int*)realloc(ps->dd, 20 * sizeof(uint8_t));
	if (ptr == NULL)
	{
		printf("扩容失败");
		return -1;
	}
	else
	{
		ps->dd = ptr;
	}
	//使用...
	//释放
	free(ps->dd);
	ps->dd = NULL;
	free(ps);
	ps = NULL;
}

到这里完成了一个VLA可变长度数组的描述,有两部分组成(第一部分是对数组二元素的描述,第二部分是堆数组),那么到现在是不是完美无缺了呢,答案当然是否定的。首先使用指针变量指向使用malloc分配堆内存的首地址,如果直接使用结构体类型定义变量存储在全局变量区域(实在编译阶段确定占用确切内存大小),或者定义在函数内部,在运行阶段占用的是栈变量,就算是第一次使用malloc申请堆内存并将堆内存起始地址复制给栈指针变量,也和第二次使用malloc申请的变量所在内存是堆内存是不相邻的。因此上述方式虽然能够描述DD ...这种可变长度数组VLA,但是存在可变长度数组和管理结构体内存不相邻问题,内存相邻就无法高效间接的实现访问,而且容易产生内存碎片,以及稍微不注意容易产生内存泄露问题。

还有没有更好的描述方法呢?答案是肯定的,这就引入了今天的主题"柔性数组成员(Flexible Array Member,FAM)"。

首先有个前提,柔性数组成员出现的比较晚,实在C99以及以后版本的编译器才支持的,柔性数组成员的目的就是解决VLA可变长度数组使用堆内存带来的不相邻的弊端。柔性数组成员是可变长度数组的进阶版。

首先看一下看一下什么是柔性数组成员。

所谓柔性数组成员就是在定义结构体类型时,最有一个是数组个数为0或者直接不写的数组,这个数组成员前面至少有一个普通成员变量,并且这个柔性数组成员是不占内存的,只是一个地址标识,因此使用sizeof(struct xxx)的数值只包含柔性数组成员前面的变量,如下所示:

cpp 复制代码
struct st_type
{
	int i;//4字节,可能没有任何意思,但是必须要有一个普通成员
	int a[0];//柔性数组成员,也可以写int a[];
};

sizeof(struct st_type)的数值为4。

因为柔性数组成员发明的原因就是解决VLA申请内存不相邻问题(描述堆内存的结构体变量的内存以及申请的堆数组内存),因此具体使用方法如下:

cpp 复制代码
int main()
{
    //假设我现在需要a里有10个元素
	struct st_type*ps=(struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (ps == NULL)//由于空间可能不够开辟导致malloc开辟失败,开辟失败会返回空指针
	{
		printf("%s\n", strerror(errno));
		return -1;//程序出问题后,跳出程序
	}
	//开辟成功
	int j = 0;
	for (j = 0;j < 10;j++)
	{
		ps->a[j] = j;
	}
	for (j = 0;j < 10;j++)
	{
		printf("%d ", ps->a[j]);//打印0-9
	}
	printf("\n");
	//如果想继续用柔性数组a进行打印
	//比如现在a里只有10个元素,我用完10个了,我还要继续来10个,用realloc追加
	struct st_type*ptr=realloc(ps, sizeof(struct st_type) + 20 * sizeof(int));//ps:realloc第二个参数是调整后的整体大小
	if (ptr == NULL)
	{
		printf("扩容失败\n");
		return -1;
	}
	ps = ptr;
	//扩容成功
	int k = 0;
	for (k = 10;k < 20;k++)
	{
		ps->a[k] = k;
	}
	for (j = 0;j < 20;j++)
	{
		printf("%d ", ps->a[j]);//打印0-19
	}
	//释放空间
	free(ps);
	ps = NULL;
	return 0;
}

因为柔性数组成员是不占内存的,所以在编译阶段编译到结构体类型定义的时候,是允许的。函数内部的运行阶段,使用malloc申请了一个相邻的比较大的堆内存,然后分别对对内存使用ps->axx的方式进行数据填写,以及读取,后面还进行库容堆内存操作,以及相同的ps->axx进行读写操作,最后释放内存只需要free(ps)一次就完成了。

注意:柔性数组成员所在结构体类型定义之后,不能使用该类型直接定义全部变量或者在函数内部定义局部变量,原因很简单,因为柔性数组的元素个数是未知的,如果定义成了全局变量,在编译阶段就报错了,因为编译阶段全局变量的大小,数据类型大小都要确定下来,否则编译报错。如果定义成了函数内部栈临时变量,虽然运行阶段动态分配栈变量,但是到底分配多少个栈变量是未知的,因此无法也不能使用包含柔性数组成员的数据类型直接定义变量。

最后,参考文章:

C/C++柔性数组 - 教程 - slgkaifa - 博客园

柔性数组详解:定义、优势与动态内存管理-CSDN博客

相关推荐
zh路西法1 天前
【玩转VLA具身智能机械臂】(二):MoveIt2 运动规划——架构、配置与 C++ 上手
c++·机械臂·路径规划·vla·planner·moveit2
thesky1234563 天前
智能体面试准备(四十三):具身智能体与机器人实操——从 VLA 到 Sim2Real
机器人·导航·操作·具身智能·智能体·vla·视觉语言动作
猫先生Mr.Mao4 天前
具身智能之UniJEPA详解:让机器人同时“听懂、预见、动起来”的统一表征学习
论文解读·具身智能·vla·机器人学习·世界模型
chen_zn957 天前
《VLA 系列》π0.7 | 多模态提示可控机器人基座模型 | 组合泛化与跨本体迁移 | 论文与复现思路
具身智能·vla·世界模型·记忆机制·子任务
_张一凡7 天前
【论文解读】视觉语言导航 VLN:用 Matterport3D 真实全景和 R2R 众包指令,把“听懂人话再走路“做成首个可打分的具身基准
具身智能·vla
还不秃顶的计科生7 天前
具身智能论文学习10:π0: A Vision-Language-Action Flow Model for General Robot Control
人工智能·深度学习·算法·机器学习·语言模型·vla·vlm
_张一凡7 天前
【论文解读】VLN-R1:让大视觉语言模型直接吃第一人称视频走出连续动作,用 GRPO 强化微调和时间衰减奖励把“导航“做成 LVLM 的可验证后训练任务
机器人·具身智能·vla·世界模型
chen_zn958 天前
《VLA 系列》Human-to-Robot Transfer | 人类视频共训练 | 跨本体涌现迁移 | 论文解析
人工智能·深度学习·transformer·具身智能·vla
还不秃顶的计科生8 天前
具身智能论文学习8:Octo: An Open-Source Generalist Robot Policy
人工智能·深度学习·学习·机器学习·语言模型·vla·vlm