结构体可以把多种数据类型整合在一起,这样在操作数据的时候只需要定义一个结构体变量就可以了,而不必另外再定义很多的变量。
开发嵌入式软件的时候,在两个设备的通信,通常会自定义一个内部的通信协议,按照这个通信通信协议来收发数据,解析数据。
例如:
可以看到这个数据帧有9个字段,这样的一个数据帧如果能用一个结构体来描述,那么对于应用程序编程来说,会是非常的便利和清晰。但是用结构体来描述这一个数据帧,有很多需要注意的问题。
一、一个帧的数据在内存上需要保持连续,不能间断。
在两个设备之间的通信是严格按照约定好的协议来工作的,如果设备A和设备B通信,设备A包装了一个数据帧,但是在数据帧的某一处空了一个字符,然后发给了设备B,那么设备B按照这个协议来解析这个数据帧,在空了一个字符的前面数据,应该都能解析成功,但是在这后面,应该就会解析错误了。
所以在利用结构体在描述一个数据帧的时候,需要确保结构体内的每一个字段在内存上都是连续排放的。
二、在结构体如何保证每个字段内存的连续性
如果有以下结构体
struct command_frame
{
int head;
int device_id;
char type;
char word;
char read;
char len;
char *data; // 长度不固定,需要按照实际情况进行分配
char crc;
char tail;
}
怎么能保证内存地址连续性呢?
1、首先需要知道,结构体的字节对齐
需要字节对齐的根本原因在于CPU访问数据的效率问题。例如,假设一个处理器总是从存储器中取出8个字节,则地址必须为8的倍数。如果我们能保证将所有的double类型数据的地址对齐成8的倍数,那么就可以用一个存储器操作来读或者写值了。否则,我们可能需要执行两次存储器访问,因为对象可能被分放在两个8字节存储块中。因此在linux c中,结构体是自动开启结构体对齐的。
一旦有了结构对齐,假如4字节对齐,也许一个char 型数据,会占用四个字节的内存(其实还是占用一个字节的内存,只不过后面的三个字节是空出来了),当然这个也要和结构体内的字段排放顺序有关。这样,一个结构体的每一个字段在内存上就不是连续了,因此需要取消字节对齐,才能用作数据帧来发送。
取消字节对齐的方法
#pragma pack(1) //其实是按照一个字节对齐,即保证了每一个字节的连续性
2、其次需要注意结构体内的指针类型
如上面的结构体,有一个成员 char *data,因为在通信过程中,也许需要携带的数据长度是不定的,所以只能通过动态分配来精准分配内存。在c语言中,指针是必须需要初始化后才使用,因此我们在使用这个结构体当做数据帧的时候,需要通过mallco函数来精准的分配内存,但是通过malloc来分配的内存,是在堆上面,如果你的结构体变量是分配在栈上面的话,那么内存的连续性就会中断。就算把结构体变量也分配到堆上面,那又如何能把成员变量中的内存和结构体变量中的内存保持连续性呢?结论是不能的,因为一个malloc函数分配内存的地址是不定的。
如果结构体内不用指针改用如下
struct command_frame
{
int head;
int device_id;
char type;
char word;
char read;
char len;
char data[255]; // 数据长度不固定,但是数据固定最大长度为255
char crc;
char tail;
}
在取消字节对齐后,上面的结构体可以保证地址的连续性。
但是这样做,是需要在双方设备都指定数据的长度为255,即不管真实数据有多长1个字节,还是10个字节,数据字段都是占用255字节。这样双方通信,通过这个结构体可以通信。