将二进制数据(从文件)读入结构体中

12

我正在从文件中读取二进制数据,具体来说是从zip文件中读取。(要了解有关zip格式结构的更多信息,请参见http://en.wikipedia.org/wiki/ZIP_%28file_format%29

我创建了一个存储数据的结构体:

typedef struct {
                                            /*Start Size            Description                                 */
    int signatute;                          /*   0  4   Local file header signature = 0x04034b50                */
    short int version;                      /*   4  2   Version needed to extract (minimum)                     */
    short int bit_flag;                     /*   6  2   General purpose bit flag                                */
    short int compression_method;           /*   8  2   Compression method                                      */
    short int time;                         /*  10  2   File last modification time                             */
    short int date;                         /*  12  2   File last modification date                             */
    int crc;                                /*  14  4   CRC-32                                                  */
    int compressed_size;                    /*  18  4   Compressed size                                         */
    int uncompressed_size;                  /*  22  4   Uncompressed size                                       */
    short int name_length;                  /*  26  2   File name length (n)                                    */
    short int extra_field_length;           /*  28  2   Extra field length (m)                                  */
    char *name;                             /*  30  n   File name                                               */
    char *extra_field;                      /*30+n  m   Extra field                                             */

} ZIP_local_file_header;

sizeof(ZIP_local_file_header)返回的大小为40,但如果使用sizeof操作符计算每个字段的总和,则总大小为38。

如果我们有以下结构体:

typedef struct {
    short int x;
    int y;
} FOO;

sizeof(FOO)返回8,因为每次分配的内存是4个字节。因此,为了分配x,会保留4个字节(但实际大小为2个字节)。如果我们需要另一个short int,它将填充先前分配的剩余2个字节。但由于我们有一个int,它将多分配4个字节,空闲的2个字节将被浪费。

要从文件中读取数据,我们可以使用fread函数:

ZIP_local_file_header p;
fread(&p,sizeof(ZIP_local_file_header),1,file);

但是由于中间有空字节,它并没有被正确读取。

我应该怎么做才能顺序地并且高效地存储数据,并且不浪费任何字节使用 ZIP_local_file_header


可能重复:https://dev59.com/J1HTa4cB1Zd3GeqPORTd#3913152 - Prof. Falken
3
问题写得很好。 - Amardeep AC9MF
5个回答

12
为了满足底层平台的对齐需求,结构体可能在成员之间有“填充”字节,以使每个成员从适当对齐的地址开始。
有几种方法可以解决这个问题:一种是使用适当大小的成员单独读取头文件中的每个元素:
fread(&p.signature, sizeof p.signature, 1, file);
fread(&p.version, sizeof p.version, 1, file);
...

另一种方法是在结构体定义中使用位域;这些不受填充限制。缺点是位域必须是unsigned intint或者在C99中是_Bool,你可能需要将原始数据强制转换为目标类型以正确解释它:

typedef struct {                 
    unsigned int signature          : 32;
    unsigned int version            : 16;                
    unsigned int bit_flag;          : 16;                
    unsigned int compression_method : 16;              
    unsigned int time               : 16;
    unsigned int date               : 16;
    unsigned int crc                : 32;
    unsigned int compressed_size    : 32;                 
    unsigned int uncompressed_size  : 32;
    unsigned int name_length        : 16;    
    unsigned int extra_field_length : 16;
} ZIP_local_file_header;

如果文件是大端字节序但你的系统是小端字节序,你可能还需要在每个成员中进行一些字节交换。

请注意,name extra field 不是结构体定义的一部分;当你从文件中读取时,你将不会读取名称和扩展字段的指针值,而是会读取名称和扩展字段的实际内容。由于在读取头部的其余部分之前你不知道这些字段的大小,因此你应该在读取上面的结构体之后再读取它们。类似以下代码:

ZIP_local_file_header p;
char *name = NULL;
char *extra = NULL;
...
fread(&p, sizeof p, 1, file);
if (name = malloc(p.name_length + 1))
{
    fread(name, p.name_length, 1, file);
    name[p.name_length] = 0;
}
if (extra = malloc(p.extra_field_length + 1))
{
    fread(extra, p.extra_field_length, 1, file);
    extra[p.extra_field_length] = 0;
}

非常好的解释。但是,如果我把一个结构体的指针传递到函数中,并使用字段的地址,我会得到一个错误:zip.c:42:2:错误:无法获取位域“signature”的地址 zip.c:42:2:错误:对位域应用了“sizeof” - rigon
2
@Ricardo - 你应该传递指向原始结构类型中定义的结构成员的指针 或者 使用位域并传递整个结构的地址。你不能获取位域的地址。 - John Bode

9

C语言中的struct仅仅是将相关数据组合在一起,并不指定内存中的特定布局。(就像int的宽度也没有定义一样。)小端/大端也没有定义,取决于处理器。

不同的编译器、不同体系结构或操作系统上相同的编译器等都会以不同的方式布局结构体。

由于您要读取的文件格式是根据字节的位置定义的,因此结构体虽然看起来非常方便和诱人,但并不是正确的解决方案。您需要将文件视为char[],提取所需的字节并进行移位,以便生成由多个字节组成的数字等。


这是我拥有的解决方案。但它使阅读更加复杂,且依赖于结构。 - rigon
7
结构体成员将按照它们被声明的顺序排列。在《C语言标准》6.7.2.1第13段中指出:“在结构体对象中,非位域成员和位域所占用的单元的地址按照它们被声明的顺序递增。经过适当转换的指向结构体对象的指针指向其初始成员(如果该成员是位域,则指向它所在的单元),反之亦然。结构体对象内可能会存在未命名的填充区域,但不会出现在其开头。”(强调是我添加的) - John Bode

5

我自己实现了zip格式,这解决了我的问题。 - Peterdk

2

我有一段时间没有处理过压缩文件了,但我还记得添加自己的填充以符合PowerPC架构的4字节对齐规则。

最好的方法是将结构体中的每个元素定义为您要读取的数据块的大小。不要只使用“int”,因为它可能在不同的平台/编译器上定义为不同的大小。

在头文件中可以这样做:

typedef unsigned long   unsigned32;
typedef unsigned short  unsigned16;
typedef unsigned char   unsigned8;
typedef unsigned char   byte;

在你已知一个4字节值的情况下,使用unsigned32替代int。对于任何已知的2字节值,请使用unsigned16。

这将帮助你看到哪里可以添加填充字节以达到4字节对齐,或者哪些2个2字节元素可以组合成4字节对齐。

理想情况下,你可以使用最少的填充字节(这些填充字节可以用于在扩展程序时添加附加数据),或者如果你可以将所有内容与变长数据对齐到4字节边界,则不需要使用填充字节。


0

此外,名称和 extra_field 不包含任何有意义的数据,很可能至少在程序运行期间是这样的,因为它们是指针。


我知道这个问题,但我的问题是因为我有5个“short int”,而分配的内存是8个字节,但只使用了6个。 - rigon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接