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

8

我遇到了一个问题,无法将二进制文件正确地读入我的结构体中。这个结构体如下:

struct Student
{
    char name[25];
    int quiz1;
    int quiz2;
    int quiz3;
};

这段代码占用了37个字节(来自字符数组的25个字节和每个整数的4个字节)。我的.dat文件大小为185个字节。这是5个学生的3个整数成绩。因此,每个学生占用37个字节(37*5=185)。

在纯文本格式下,它看起来像这样:

Bart Simpson          75   65   70
Ralph Wiggum          35   60   44
Lisa Simpson          100  98   91
Martin Prince         99   98   99
Milhouse Van Houten   80   87   79

通过使用这段代码,我可以逐个读取每个记录:

Student stud;

fstream file;
file.open("quizzes.dat", ios::in | ios::out | ios::binary);

if (file.fail())
{
    cout << "ERROR: Cannot open the file..." << endl;
    exit(0);
}

file.read(stud.name, sizeof(stud.name));
file.read(reinterpret_cast<char *>(&stud.quiz1), sizeof(stud.quiz1));
file.read(reinterpret_cast<char *>(&stud.quiz2), sizeof(stud.quiz2));
file.read(reinterpret_cast<char *>(&stud.quiz3), sizeof(stud.quiz3));

while(!file.eof())
{
    cout << left 
         << setw(25) << stud.name
         << setw(5)  << stud.quiz1
         << setw(5)  << stud.quiz2
         << setw(5)  << stud.quiz3
         << endl;

    // Reading the next record
    file.read(stud.name, sizeof(stud.name));
    file.read(reinterpret_cast<char *>(&stud.quiz1), sizeof(stud.quiz1));
    file.read(reinterpret_cast<char *>(&stud.quiz2), sizeof(stud.quiz2));
    file.read(reinterpret_cast<char *>(&stud.quiz3), sizeof(stud.quiz3));
}

我得到了一个漂亮的输出,但我希望能够一次读取整个结构,而不是一次只读取每个结构的成员。我认为这段代码需要完成这项任务,但它并不起作用(我将在此之后展示输出):
* 不包括打开文件和结构声明等相似部分。
file.read(reinterpret_cast<char *>(&stud), sizeof(stud));

while(!file.eof())
{
    cout << left 
         << setw(25) << stud.name
         << setw(5)  << stud.quiz1
         << setw(5)  << stud.quiz2
         << setw(5)  << stud.quiz3
         << endl;

    file.read(reinterpret_cast<char *>(&stud), sizeof(stud));
}

输出:

Bart Simpson             16640179201818317312
ph Wiggum                288358417665884161394631027
impson                   129184563217692391371917853806
ince                     175193530917020655191851872800

它唯一不出错的部分是名字,然后就全错了。我尝试了所有方法,也不知道哪里出了问题。我甚至查阅了我的书籍,但是找不到任何有用信息。那些书中的内容看起来和我的一样,而且它们可以运行,但由于某种奇怪的原因,我的却不能。在第25个字节处,我使用了file.get(ch)(ch为char),返回了K,这是75的ASCII码,即第一个测试成绩,所以一切都应该在正确的位置上。只是我的结构体没有被正确读入。如果有帮助,将不胜感激,我现在束手无策。

编辑:在收到大量意外和精彩的反馈后,我决定采纳你们的建议,一次只读取一个成员。通过使用函数,使代码更简洁、更小。再次感谢你们提供如此快速和有启发性的反馈。

如果你感兴趣 一种不被大多数人推荐的解决方法,请向下滚动到user1654209的第三个回答。这种解决方法可以完美地工作,但请阅读所有评论以了解为什么它不受欢迎。


2
如果你打印 sizeof(Student),你会发现它不是37个字节。它可能是40或56。 - Some programmer dude
3
这是因为结构体进行了填充,以便整数从一个漂亮的32位边界开始。编译器在字符串后添加了三个字节。 - Some programmer dude
3
你需要使用相同的方式写和读代码。如果你写了某部分,那么你就需要读取它。你可以强制使结构体紧凑排列而没有填充,但通常这不是处理问题的最佳方式。最好的建议是逐个编写和读取每个字段。 - Retired Ninja
1
如果它是未使用打包方式编写的,则需要以相同方式读取它,可以逐个字段地读取(建议),或通过修改打包来强制内存表示与文件匹配。 - Retired Ninja
3
@Noobacode #pragma pack(1) 使用 MSVC 和 attribute ((packed)) 使用 GCC - user18428
显示剩余13条评论
5个回答

10
你的结构体几乎肯定已经被填充以保持其内容的对齐。这意味着它不会是37个字节,这种不匹配会导致读取失步。从每个字符串失去3个字符的方式来看,它似乎已经被填充到40个字节。
由于填充很可能在字符串和整数之间,即使第一条记录也无法正确读取。
在这种情况下,我建议不要尝试将数据作为二进制块读取,而是坚持读取单个字段。这样更加健壮,特别是如果你想改变你的结构。

是的,它有40个字节...不幸的是。有什么方法可以解决这个问题吗?.dat文件有185个字节。如果可能的话,我确实想尝试将其作为一个整体读取。除非没有办法。 - B.K.
1
这不是问题,而是一种特性。你可能可以绕过它进行黑客攻击,但结果会非常脆弱。我建议不要这样做。 - JasonD
“fragile”是什么意思? - B.K.
@Noobacode 假设你已经让它工作了,然后你决定需要向结构体添加另一个字段,或者改变字符串的长度。或者你换了编译器,int 变成了 64 位。一旦你的结构体不再匹配二进制数据,你就没办法了。单独处理每个字段,你就可以应对变化。 - JasonD
2
@JasonD 我不知道你怎么想,但我宁愿把结构定义放在一个地方:结构的定义中,而不是逐个字段阅读。为什么?因为如果数据布局有任何更改(正如你所担心的),唯一需要更新的地方就是结构的定义。但是,如果逐个字段进行阅读,那么代码中每个读取这些数据的位置都与其格式紧密耦合,并且必须进行更新,这可能会成为一个维护噩梦。 - SasQ

4
一个简单的解决方法是将你的结构体打包为1个字节,使用gcc。
struct __attribute__((packed)) Student
{
    char name[25];
    int quiz1;
    int quiz2;
    int quiz3;
};

使用MSVC

#pragma pack(push, 1) //set padding to 1 byte, saves previous value
struct  Student
{
    char name[25];
    int quiz1;
    int quiz2;
    int quiz3;
};
#pragma pack(pop) //restore previous pack value

编辑:根据用户ahans的说法,自gcc 2.7.2.3版本(1997年发布)起,支持pragma pack。因此,如果你的目标是msvc和gcc,则似乎可以放心使用pragma pack作为唯一的打包符号。


非常感谢! :) 它实际上完美地加载了整个结构。不过,看了所有的评论后,我还是有些担心使用它。 - B.K.
2
实际上没有必要使用特殊的GCC版本,它也能像MSVC一样理解#pragma pack - ahans
@ahans 当然可以,不过我不记得是哪个gcc版本引入了pragma pack支持。如果有人知道,请评论一下,我会编辑答案。 - user18428
是的,我测试了 #pragma pack 版本,并且它也能正常工作。要使用哪个更好,#pragma pack 还是 attribute((packed))? - B.K.
如果你不针对某些“奇特”的CPU架构或gcc版本,我认为使用#pragma足够安全了。 - user18428

4
没有看到写入数据的代码,我猜测你像在第一个例子中读取数据一样写入数据,即逐个写入每个元素。那么文件中的每个记录确实将会是37字节。

然而,由于编译器为优化目的将结构填充到漂亮的边界上,所以你的结构体是40字节。因此,当你一次性读取完整个结构体时,实际上每次读取40字节,这意味着你的读取将与文件中的实际记录失去同步。

你要么重新实现写入,一次写入完整的结构体,或者使用第一种方法进行读取,即逐个成员字段读取。


是的,听起来大家都对这个问题达成了共识...原始文件可能是逐个成员编写的,因此没有产生填充(?)。 - B.K.
写入数据的方法与此有什么关系?唯一重要的是这些数据在二进制文件中的样子,它们到达那里的方式并不重要。它们甚至可以完全不由任何代码编写,而是手工制作的或其他方式。重要的是数据的外观(文件中的布局),读取代码必须匹配它(即使用相同的内存结构布局或相同的寻址偏移量)。 - SasQ

3

正如您已经发现的那样,这里的问题是填充(padding)。另外,正如其他人建议的那样,解决这个问题的正确方法是像你在例子中所做的那样逐个读取每个成员。从性能上来说,我不认为这会比一次性读取整个内容花费更多。但是,如果您仍然想一次性读取它,您可以告诉编译器以不同的方式进行填充:

#pragma pack(push, 1)
struct Student
{
    char name[25];
    int quiz1;
    int quiz2;
    int quiz3;
};
#pragma pack(pop)

使用 #pragma pack(push, 1),您告诉编译器将当前的pack值保存在内部栈中,并从此时起使用pack值为1。这意味着您获得了1字节的对齐方式,在这种情况下完全没有填充。使用 #pragma pack(pop),您告诉编译器从堆栈中获取上一个值并在此后使用它,从而恢复编译器在定义struct之前使用的行为。

虽然#pragma通常表示不具可移植性和依赖于编译器的特性,但此指令至少与GCC和Microsoft VC++兼容。


啊,谢谢你进一步解释那个实现的作用。我不是完全确定每个意思,你的解释比我在其他网站上读到的更容易理解。 - B.K.

1

解决本线程问题的方法不止一种。下面是一种基于使用结构体和字符缓冲区并集的解决方案:

#include <fstream>
#include <sstream>
#include <iomanip>
#include <string>

/*
This is the main idea of the technique: Put the struct
inside a union. And then put a char array that is the
number of chars needed for the array.

union causes sStudent and buf to be at the exact same
place in memory. They overlap each other!
*/
union uStudent
{
    struct sStudent
    {
        char name[25];
        int quiz1;
        int quiz2;
        int quiz3;
    } field;

    char buf[ sizeof(sStudent) ];    // sizeof calcs the number of chars needed
};

void create_data_file(fstream& file, uStudent* oStudent, int idx)
{
    if (idx < 0)
    {
        // index passed beginning of oStudent array. Return to start processing.
        return;
    }

    // have not yet reached idx = -1. Tail recurse
    create_data_file(file, oStudent, idx - 1);

    // write a record
    file.write(oStudent[idx].buf, sizeof(uStudent));

    // return to write another record or to finish
    return;
}


std::string read_in_data_file(std::fstream& file, std::stringstream& strm_buf)
{
    // allocate a buffer of the correct size
    uStudent temp_student;

    // read in to buffer
    file.read( temp_student.buf, sizeof(uStudent) );

    // at end of file?
    if (file.eof())
    {
        // finished
        return strm_buf.str();
    }

    // not at end of file. Stuff buf for display
    strm_buf << std::setw(25) << std::left << temp_student.field.name;
    strm_buf << std::setw(5) << std::right << temp_student.field.quiz1;
    strm_buf << std::setw(5) << std::right << temp_student.field.quiz2;
    strm_buf << std::setw(5) << std::right << temp_student.field.quiz3;
    strm_buf << std::endl;

    // head recurse and see whether at end of file
    return read_in_data_file(file, strm_buf);
}



std::string quiz(void)
{

    /*
    declare and initialize array of uStudent to facilitate
    writing out the data file and then demonstrating
    reading it back in.
    */
    uStudent oStudent[] =
    {
        {"Bart Simpson",          75,   65,   70},
        {"Ralph Wiggum",          35,   60,   44},
        {"Lisa Simpson",         100,   98,   91},
        {"Martin Prince",         99,   98,   99},
        {"Milhouse Van Houten",   80,   87,   79}

    };




    fstream file;

    // ios::trunc causes the file to be created if it does not already exist.
    // ios::trunc also causes the file to be empty if it does already exist.
    file.open("quizzes.dat", ios::in | ios::out | ios::binary | ios::trunc);

    if ( ! file.is_open() )
    {
        ShowMessage( "File did not open" );
        exit(1);
    }


    // create the data file
    int num_elements = sizeof(oStudent) / sizeof(uStudent);
    create_data_file(file, oStudent, num_elements - 1);

    // Don't forget
    file.flush();

    /*
    We wrote actual integers. So, you cannot check the file so
    easily by just using a common text editor such as Windows Notepad.

    You would need an editor that shows hex values or something similar.
    And integrated development invironment (IDE) is likely to have such
    an editor.   Of course, not always so.
    */


    /*
    Now, read the file back in for display. Reading into a string buffer
    for display all at once. Can modify code to display the string buffer
    wherever you want.
    */

    // make sure at beginning of file
    file.seekg(0, ios::beg);

    std::stringstream strm_buf;
    strm_buf.str( read_in_data_file(file, strm_buf) );

    file.close();

    return strm_buf.str();
}

调用quiz()并接收一个字符串,格式化后显示到std::cout、写入文件或其他操作。

其主要思想是:联合体内的所有项在内存中的地址都相同。因此,你可以有一个大小与要写入或从文件读取的结构体相同的char或wchar_t缓冲区。请注意,代码中不需要进行零次转换。

我也不必担心填充。

对于不喜欢递归的人来说,抱歉。对我而言,使用递归更容易、更少出错。也许对其他人来说并不容易?这些递归可以转换为循环,并且对于非常大的文件,它们需要被转换为循环。

对于喜欢递归的人,这又是另一个使用递归的例子。

我并不声称使用联合体是最好的解决方案还是不是。似乎它是一种解决方法。也许你会喜欢它?


你建议的是未定义行为。cppreference.com:「联合体的大小仅足以容纳其最大数据成员。其他数据成员分配在与该最大成员的一部分相同的字节中。该分配的细节是实现定义的,而从最近没有写入的联合体成员读取是未定义行为。许多编译器作为非标准语言扩展实现了读取联合体非活动成员的能力。」 - Evg

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