C++结构体对齐和STL向量

7
我有一个遗留的数据结构,长度为672字节。这些结构以顺序方式存储在文件中,我需要读取它们。
虽然我可以逐个读取它们,但最好是这样做:
// I know in advance how many structs to read in
vector<MyStruct> bunchOfStructs;
bunchOfStructs.resize(numberOfStructs);

ifstream ifs;
ifs.open("file.dat");
if (ifs) {
    ifs.read(&bunchOfStructs[0], sizeof(MyStruct) * numberOfStructs);
}

这种方法虽然可行,但我认为它只能正常工作是因为数据结构的大小恰好可以被编译器的结构体对齐填充整除。我怀疑它会在另一种编译器或平台上出现问题。

另一个选择是使用 for 循环逐个读取每个结构体。

问题是 -> 我什么时候需要关注数据对齐?在向量中动态分配的内存是否使用填充?STL 是否保证元素是连续的?


这些结构体是由遗留代码写入文件的,还是您也控制了它们? - Georg Fritzsche
它们是由遗留代码编写的。即使我可以更改它,我可能不得不读取由应用程序旧版本编写的文件。 - Nate
5个回答

4
标准要求您能够创建一个结构类型的数组。当您这样做时,所创建的数组需要是连续的。也就是说,无论为结构体分配了多大的空间,都必须能够创建一个数组。为了确保这一点,编译器可以在结构体内部分配额外的空间,但不能要求在结构体之间分配任何额外的空间。 vector 中数据的空间(通常)是使用 ::operator new(通过 Allocator 类)进行分配的,并且 ::operator new 要求分配的空间能够适当地对齐以存储任何类型。
您可以提供自己的 Allocator 和/或重载 ::operator new——但如果您这样做,您的版本仍然需要满足相同的要求,因此在这方面它不会改变任何内容。
换句话说,只要文件中的数据是以与您尝试读取它们的方式基本相同的方式创建的,那么您想要的正是需要工作的。如果它是在另一台机器上或使用不同的编译器(甚至是使用不同的标志的相同编译器)创建的,则可能会出现许多潜在问题——您可能会得到字节序的差异、结构体中填充的差异等等。
编辑:鉴于您不知道结构体是否以编译器所期望的格式写出,因此您不仅需要逐个读取结构体——您确实需要逐个读取结构体中的项,然后将每个项放入一个临时的 struct 中,最后将填充好的 struct 添加到您的集合中。
幸运的是,您可以重载 operator>> 来自动化大部分操作。这不会提高速度(例如),但它可以使您的代码更加简洁:
struct whatever { 
    int x, y, z;
    char stuff[672-3*sizeof(int)];

    friend std::istream &operator>>(std::istream &is, whatever &w) { 
       is >> w.x >> w.y >> w.z;
       return is.read(w.stuff, sizeof(w.stuff);
    } 
};

int main(int argc, char **argv) { 
    std::vector<whatever> data;

    assert(argc>1);

    std::ifstream infile(argv[1]);

    std::copy(std::istream_iterator<whatever>(infile),
              std::istream_iterator<whatever>(),
              std::back_inserter(data));  
    return 0;
}

完美。我知道磁盘上结构体之间没有填充,结构体内部也没有填充。但是我想我无法确定编译器是否会在内存中结构体内部添加填充,因此看起来我需要逐个读取数据以确保安全。 - Nate

2
在您的情况下,当对齐方式可能更改结构的布局时,需要关注对齐。有两种选项可以使您的代码更具可移植性。
首先,大多数编译器都有扩展属性或预处理器指令,允许您将结构打包到最小空间中。此选项可能会使结构中的某些字段错位,从而降低性能,但可以保证在任何构建它的机器上都以相同的方式布局。请查看编译器的文档,了解有关#pragma pack()的信息。在GCC中,您可以使用__attribute__((__packed__))。
其次,您可以向结构添加显式填充。此选项允许您保持原始结构的性能特性,但会使其布局变得明确无误。例如:
struct s {
    u_int8_t  field1;
    u_int8_t  pad0[3];
    u_int16_t field2;
    u_int8_t  pad1[2];
    u_int32_t field3;
};

2

对于您现有的文件,最好先确定其文件格式,然后逐个读取每种类型,在读入和丢弃任何对齐字节。

最好不要做任何与结构对齐有关的假设。

要将新数据保存到文件中,您可以使用像boost serialization这样的工具。


那听起来像是安全的方式。虽然缓慢而繁琐,但是安全。 :-) 我知道在磁盘格式中没有填充。 - Nate

1
与其担心对齐问题,你应该更关心endianness。STL保证了vector中的存储方式和数组相同,但是结构体本身的整数字段在x86和RISC之间会以不同的格式存储。
至于对齐问题,可以搜索一下#pragma pack(1)

0
如果你编写需要了解类内部工作原理的面向对象代码,那么你的做法是错误的。你应该对类的内部工作原理一无所知;你只能假设方法和属性在任何平台/编译器上都能正常运行。
你最好实现一个模拟向量功能的类(例如通过子类化向量)。充当"代理模式"实现,它仅加载被调用者访问过的结构。这将允许您同时处理任何字节序问题。这种方式应该适用于任何平台或编译器。

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