将文件读入结构体(C++)

5
我将尝试从二进制文件中读取数据并将其放入一个结构体中。 data.bin 文件的前几个字节为:
03 56 04 FF FF FF ...

我的实现是:
#include <iostream>
#include <fstream>

int main()
{
    struct header {
        unsigned char type;
        unsigned short size;
    } fileHeader;

    std::ifstream file ("data.bin", std::ios::binary);
    file.read ((char*) &fileHeader, sizeof header);

    std::cout << "type: " << (int)fileHeader.type;
    std::cout << ", size: " << fileHeader.size << std::endl;

}

我期望的输出是 type: 3, size: 1110,但出现了 type: 3, size: 65284 的结果,因此文件中的第二个字节被跳过了。这是怎么回事呢?

1
sizeof(header)是多少?我敢打赌它是4... - Cameron
嘿,是的。我应该检查一下的。 - vind
4个回答

7
实际上,这种行为是由实现定义的。在您的情况下,实际发生的可能是,在结构体的type成员之后有一个1字节的填充,然后紧随其后的是第二个成员size。我基于输出来做出了这个论断。
以下是您的输入字节:
03 56 04 FF FF FF

第一个字节 03 被写入结构体的第一个字节,即 type,你会看到输出为 3。接下来的字节 56 写入第二个字节,也就是填充字节,因此被忽略。然后,下两个字节 04 FF 写入结构体的下两个字节,即 size(长度为 2 个字节)。在小端机器上,04 FF 被解释为 0xFF04,也就是 66284,这就是输出结果。

如果你需要一个紧凑的结构体来压缩填充,可以使用 #pragma pack。但这样的结构体与普通结构体相比速度较慢。更好的选择是手动填充结构体,如下所示:

char bytes[3];
std::ifstream file ("data.bin", std::ios::binary);
file.read (bytes, sizeof bytes); //read first 3 bytes

//then manually fill the header
fileHeader.type = bytes[0];
fileHeader.size = ((unsigned short) bytes[2] << 8) | bytes[1]; 

最后一行的另一种写法如下:

fileHeader.size = *reinterpret_cast<unsigned short*>(bytes+1); 

但这是实现定义的,因为它取决于机器的字节序。在小端机器上,它很可能会起作用。

一个友好的方法是这样的(实现定义):

std::ifstream file ("data.bin", std::ios::binary);
file.read (&fileHeader.type, sizeof fileHeader.type);
file.read (reinterpret_cast<char*>(&fileHeader.size), sizeof fileHeader.size);

但是,最后一行取决于机器的字节序(endian-ness)。


填充应该在结构体中而不是文件中。尝试使用#pragma pack:http://msdn.microsoft.com/en-us/library/2e70t5y1%28v=VS.100%29.aspx - Steve Wellens
1
非常感谢您提供如此详细的解释。 我现在按照您的建议逐个填充结构体的每个成员。这似乎是最好的方法。 - vind
@Castilho:什么是“lol”?另外,什么方法有问题?我提到了几种方法,请具体说明。至于你的方法,在大端机器上能行吗? - Nawaz
请看这里:http://solidsmoke.blogspot.com.br/2010/07/woes-of-structure-packing-pragma-pack.html。至少Krister Andersson把默认值恢复了。 - Castilho
@Castilho:你说“pragma packing”好像我说它是“最佳方法”一样。我在哪里说它是“最佳方法”了?另外,你所说的“交换字节”和“分配临时变量”是什么意思?顺便说一下,你的“lol”很可爱,但有点幼稚。;-) - Nawaz
显示剩余5条评论

1

这可能是结构体填充。为了使结构体在现代架构上快速工作,一些编译器会在其中放置填充以使它们保持在4或8字节边界上对齐。

您可以使用#pragma或编译器设置来覆盖此设置。例如,Visual Studio使用/Zp

如果发生这种情况,则您将在第一个字符中看到值56,然后它将读取下一个n个字节到填充中,然后读取下一个2个字节到short中。如果第二个字节丢失为填充,则下一个2个字节正在读入short中。由于short现在包含数据'04 FF',因此(在小端)相当于0xff04,即65284。


0
编译器将结构体填充到 2 或 4 的倍数字节,以便于在机器码中实现对它们的访问。除非确有必要(通常仅在低级别(如固件级别)工作时才需要),否则我不会使用 #pragma pack。 这是维基百科上相关文章。 这是因为微处理器具有访问内存地址多个 4 或 2 倍数的特定操作,这使得源代码更易于制作,可以更有效地使用内存,有时代码运行速度也更快。当然有方法可以阻止这种行为,比如使用 pragma pack 指令,但这取决于编译器。但是覆盖编译器默认设置通常并不明智,编译器开发者有很好的理由让它这样工作。
对我而言,更好的解决方案是使用纯 C 来解决这个问题,这非常简单,并且遵循一个良好的编程实践,即:永远不要依赖编译器在低层次上对数据的处理方式。

我知道仅仅使用 #pragma pack(1) 很性感、简单,让我们感觉直接处理和理解计算机内部的东西,这会让每个真正的程序员都兴奋不已。但最好的解决方案总是使用你正在使用的语言实现的那一个。它更容易理解,因此更容易维护;它是默认行为,所以应该在任何地方都能工作,在这种特定情况下,C语言的解决方案非常简单和直接:只需逐个读取结构体属性,就像这样:

void readStruct(header &h, std::ifstream file)
{
    file.read((char*) &h.type, sizeof(char));
    file.read((char *) &h.size, sizeof(short));
}

(当然,如果你在全局定义结构体,这将起作用)

更好的方法是,在使用C++时,定义一个成员方法来为您执行读取操作,稍后只需调用myObject.readData(file)。你看到它的美和简单了吗?

易于阅读、维护、编译,导致更快速和优化的代码,这是默认设置。

通常我不喜欢搞乱#pragma指令,除非我对自己在做什么相当确定。影响可能是出乎意料的。


如果生成该文件的程序在大端机器上运行,则是。如果您必须编写代码以处理大端机器上的小端文件,则必须显式处理它。 - Castilho
这意味着它不会在所有地方都起作用,与你的声明“所以它应该在任何地方都能工作”相反,并且肯定不是最好的方法。我提供了几种方法,并提到了每种方法的问题(如果有的话)。 - Nawaz
嘿,伙计,你应该控制一下自己的愤怒。在阅读任何内容时都要有一些常识。没有适用于所有情况的代码。你必须意识到你所做的事情的局限性。正如指出的那样,如果文件写入者和读取者来自同一台机器,它将起作用。如果不是,你就必须单独处理这些问题。但是,足够了,这些只是评论,我试图帮助你,而你却把它当成个人攻击。如果我让你生气了,我很抱歉,现在好好享受你的周六吧。 - Castilho
没错。正如你所说的那样,阅读时必须有一些常识。没有适用于所有情况的代码。我并不生气。你只是告诉我去阅读你的答案,我也确实阅读了,但发现它有问题。 - Nawaz

0
你可以使用 #pragma pack 编译器指令来覆盖填充问题:
#pragma pack(push)
#pragma pack(1)
struct header {
    unsigned char type;
    unsigned short size;
} fileHeader;
#pragma pack(pop)

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