解析二进制文件.什么是现代方式?

nik*_*ack 46 c++ binary casting

我有一个二进制文件,我知道一些布局.例如,让格式如下:

  • 2个字节(无符号短) - 字符串的长度
  • 5个字节(5个字符) - 字符串 - 一些id名称
  • 4个字节(unsigned int) - 一个步幅
  • 24个字节(6个浮点数 - 每个3个浮点数的2个步长) - 浮点数据

该文件应该看起来像(为了便于阅读,我添加了空格):

5 hello 3 0.0 0.1 0.2 -0.3 -0.4 -0.5
Run Code Online (Sandbox Code Playgroud)

这里5 - 是2个字节:0x05 0x00."你好" - 5个字节,依此类推.

现在我想读这个文件.目前我这样做:

  • 将文件加载到ifstream
  • 读这个流到 char buffer[2]
  • 把它变成无符号的短语:unsigned short len{ *((unsigned short*)buffer) };.现在我有一个字符串的长度.
  • 读取流vector<char>std::string从此向量创建一个.现在我有字符串ID.
  • 以相同的方式读取接下来的4个字节并将它们转换为unsigned int.现在我有一个进步.
  • 虽然不是文件结尾读取浮动的方式相同 - 为每个浮点数创建一个char bufferFloat[4]和转换*((float*)bufferFloat).

这有效,但对我来说它看起来很难看.我可以直接读取unsigned shortfloatstring等没有char [x]创造?如果不是,那么正确投射的方法是什么(我读过我正在使用的那种风格 - 是旧风格)?

PS:当我写一个问题时,我头脑中提出的解释越清晰 - 如何从任意位置投出任意数量的字节char [x]

更新:我忘了明确提到字符串和浮点数据长度在编译时是未知的并且是可变的.

fja*_*don 13

如果它不是出于学习目的,并且如果您可以自由选择二进制格式,那么最好考虑使用protobuf之类的东西,它将为您处理序列化并允许与其他平台和语言进行互操作.

如果您不能使用第三方API,您可以寻找QDataStream灵感

  • @ Mr.kbok在我看来,每种二进制格式都是一种糟糕的格式.我认为这就是为什么3个替代品中有2个包括**text**文件格式.使用二进制格式的最佳原因是紧凑性和读/写速度.protobuf perfetcly满足这两个目标.它还增加了可移植性和版本控制. (9认同)
  • 一点也不.我使用文本格式作为示例,因为它们易于用于新程序员,但是有很多优秀的二进制格式(想想OLAP,媒体文件等).Protobuf很难正确使用,并且,作为一种流格式,需要您浏览整个文件以查找某些特定信息.在这方面,这是一个糟糕的文件格式. (2认同)

sla*_*ppy 11

在C++中可以正常工作的C方式是声明一个结构:

#pragma pack(1)

struct contents {
   // data members;
};
Run Code Online (Sandbox Code Playgroud)

注意

  • 你需要使用一个编译使编译器对齐数据作为-IT-看起来在结构;
  • 此技术仅适用于POD类型

然后将读缓冲区直接转换为struct类型:

std::vector<char> buf(sizeof(contents));
file.read(buf.data(), buf.size());
contents *stuff = reinterpret_cast<contents *>(buf.data());
Run Code Online (Sandbox Code Playgroud)

现在,如果您的数据大小是可变的,您可以分成几个块.要从缓冲区读取单个二进制对象,读取器功能非常方便:

template<typename T>
const char *read_object(const char *buffer, T& target) {
    target = *reinterpret_cast<const T*>(buffer);
    return buffer + sizeof(T);
}
Run Code Online (Sandbox Code Playgroud)

主要优点是这样的阅读器可以专门用于更高级的c ++对象:

template<typename CT>
const char *read_object(const char *buffer, std::vector<CT>& target) {
    size_t size = target.size();
    CT const *buf_start = reinterpret_cast<const CT*>(buffer);
    std::copy(buf_start, buf_start + size, target.begin());
    return buffer + size * sizeof(CT);
}
Run Code Online (Sandbox Code Playgroud)

现在在你的主解析器中:

int n_floats;
iter = read_object(iter, n_floats);
std::vector<float> my_floats(n_floats);
iter = read_object(iter, my_floats);
Run Code Online (Sandbox Code Playgroud)

注意:正如Tony D观察到的那样,即使您可以通过#pragma指令和手动填充(如果需要)获得对齐,您仍可能遇到与处理器对齐不兼容的问题,以(最佳情况)性能问题或(最坏情况)形式陷阱信号.只有在您控制文件格式的情况下,此方法才有意义.

  • 任何人的恩典? (31认同)
  • -1,这是一个非常糟糕的主意.结构可以(并且经常会)为对齐添加不可见的填充字节,文件将不具有该字节. (22认同)
  • 这无法正确对齐数据. (16认同)
  • 通过编译指示纠正对齐.这不会改变技术的本质. (11认同)
  • 您可以使用编译指示校正结构的正常对齐和填充,但是1)您当前的代码不知道`buf.data()`的数据对齐,所以在某些CPU上你仍会得到SIGBUS或类似的或性能降低当尝试通过"stuff"提取数据时,未对齐读取,并且二进制文件本身可能没有偏移量的数据,只需通过读取特定对齐的数据即可对齐.例如,如果有两个32位浮点数之间有一个字符,那么使用`struct`和批量二进制读取的任何方法都有潜在的问题. (10认同)
  • 语用库不可移植. (4认同)
  • @ RedAlert的不同之处在于编译器不需要为别名违规生成工作代码.事情出错的最直接的例子是,当访问未对齐的对象时,某些硬件可能会产生总线错误.优化器还可以生成假定从不发生别名违规的代码,因此如果出现这种情况会产生奇怪的结果. (4认同)
  • 如果数据结构包含非pod类型,则无效. (3认同)
  • 由于违反别名规则,当您访问数据时,`reinterpret_cast`将导致未定义的行为.如果你想这样做,那么最好创建一个struct类型的变量并将其用作缓冲区,或者将缓冲区中的`memcpy`放入struct中.在所有情况下,您都应验证数据,以避免输入格式错误导致的安全漏洞. (3认同)
  • @RedAlert但是通过指向其他类型的指针访问char数组仍然不合法. (3认同)
  • 读取二进制文件也不是。但是,杂用命令现在已足够普遍,以假定它们可在OP的编译器上使用。 (2认同)
  • @nikitablack抱歉,忘记指定(仅由Obvlious船长说),它仅适用于POD类型:浮点数,整数,字符等。 (2认同)
  • 哦,nvm,它的字符串长度后跟N个字节,因此它不是静态类型.但是,这个答案值得比-3更好...... sheesh. (2认同)

Ton*_*roy 9

目前我这样做:

  • 将文件加载到ifstream

  • 将此流读取到char缓冲区[2]

  • 把它投到unsigned short:unsigned short len{ *((unsigned short*)buffer) };.现在我有一个字符串的长度.

最后冒险SIGBUS(如果您的字符阵列恰好从奇数地址开始,而您的CPU只能读取在偶数地址对齐的16位值),性能(某些CPU会读取未对齐的值但速度较慢;其他类似于现代x86是好的和快的)和/或字节序问题.我建议阅读这两个字符,然后你可以说(x[0] << 8) | x[1],反之亦然,htons如果需要纠正字节顺序.

  • 读取流vector<char>std::string从中创建一个vector.现在我有字符串ID.

不需要......只需直接读入字符串:

std::string s(the_size, ' ');

if (input_fstream.read(&s[0], s.size()) &&
    input_stream.gcount() == s.size())
    ...use s...
Run Code Online (Sandbox Code Playgroud)
  • read接下来的4个字节以相同的方式投射它们unsigned int.现在我有一个进步. while不是read float以相同的方式结束文件- 为每个人创建一个char bufferFloat[4]和强制转换.*((float*)bufferFloat)float

最好直接通过unsigned ints 读取数据floats,因此编译器将确保正确对齐.

这有效,但对我来说它看起来很难看.我可以直接读取unsigned shortfloatstring等没有char [x]创造?如果不是,那么正确投射的方法是什么(我读过我正在使用的那种风格 - 是旧风格)?

struct Data
{
    uint32_t x;
    float y[6];
};
Data data;
if (input_stream.read((char*)&data, sizeof data) &&
    input_stream.gcount() == sizeof data)
    ...use x and y...
Run Code Online (Sandbox Code Playgroud)

请注意,上面的代码避免将数据读入可能未对齐的字符数组,其中由于对齐问题,它对reinterpret_cast可能未对齐的char数组(包括a内部std::string)中的数据不安全.同样,htonl如果文件内容有可能以字节顺序显示,则可能需要进行一些事后读取转换.如果有一个未知数量的floats,你需要计算和分配足够的存储,并且至少有4个字节的对齐,然后瞄准Data*它... y只要内存内容索引超过声明的数组大小是合法的在访问的地址是分配的一部分,并保存float从流中读入的有效表示.更简单 - 但附加读取可能更慢 - 先阅读第uint32_t一个然后再new float[n]进一步read...

实际上,这种类型的方法可以工作,并且许多低级别和C代码正是这样做的.可能帮助您阅读文件的"清洁"高级库最终必须在内部执行类似操作....

  • 你将无法像这样读入`std :: string`,因为`.data()`返回`const char*`,而`.read()`需要`char*`.它也可能是'UB`. (3认同)

Mat*_* M. 7

我实际上.zip在上个月实现了一个快速而又脏的二进制格式解析器来读取文件(遵循维基百科的格式描述),并且现代化我决定使用C++模板.

在某些特定平台上,打包struct可以工作,但有些事情处理不好......例如可变长度的字段.但是,对于模板,没有这样的问题:您可以获得任意复杂的结构(和返回类型).

一个.zip归档是比较简单的,幸运的,所以我来实现简单的东西.脱离我的头顶:

using Buffer = std::pair<unsigned char const*, size_t>;

template <typename OffsetReader>
class UInt16LEReader: private OffsetReader {
public:
    UInt16LEReader() {}
    explicit UInt16LEReader(OffsetReader const or): OffsetReader(or) {}

    uint16_t read(Buffer const& buffer) const {
        OffsetReader const& or = *this;

        size_t const offset = or.read(buffer);
        assert(offset <= buffer.second && "Incorrect offset");
        assert(offset + 2 <= buffer.second && "Too short buffer");

        unsigned char const* begin = buffer.first + offset;

        // http://commandcenter.blogspot.fr/2012/04/byte-order-fallacy.html
        return (uint16_t(begin[0]) << 0)
             + (uint16_t(begin[1]) << 8);
    }
}; // class UInt16LEReader

// Declined for UInt[8|16|32][LE|BE]...
Run Code Online (Sandbox Code Playgroud)

当然,基本OffsetReader实际上有一个恒定的结果:

template <size_t O>
class FixedOffsetReader {
public:
    size_t read(Buffer const&) const { return O; }
}; // class FixedOffsetReader
Run Code Online (Sandbox Code Playgroud)

由于我们正在讨论模板,您可以在闲暇时切换类型(您可以实现代理读取器,将所有读取委托给shared_ptr记忆它们的内容).

然而,有趣的是最终结果:

// http://en.wikipedia.org/wiki/Zip_%28file_format%29#File_headers
class LocalFileHeader {
public:
    template <size_t O>
    using UInt32 = UInt32LEReader<FixedOffsetReader<O>>;
    template <size_t O>
    using UInt16 = UInt16LEReader<FixedOffsetReader<O>>;

    UInt32< 0> signature;
    UInt16< 4> versionNeededToExtract;
    UInt16< 6> generalPurposeBitFlag;
    UInt16< 8> compressionMethod;
    UInt16<10> fileLastModificationTime;
    UInt16<12> fileLastModificationDate;
    UInt32<14> crc32;
    UInt32<18> compressedSize;
    UInt32<22> uncompressedSize;

    using FileNameLength = UInt16<26>;
    using ExtraFieldLength = UInt16<28>;

    using FileName = StringReader<FixedOffsetReader<30>, FileNameLength>;

    using ExtraField = StringReader<
        CombinedAdd<FixedOffsetReader<30>, FileNameLength>,
        ExtraFieldLength
    >;

    FileName filename;
    ExtraField extraField;
}; // class LocalFileHeader
Run Code Online (Sandbox Code Playgroud)

显然,这是相当简单的,但同时又极其灵活.

一个明显的改进轴线是改进链接,因为这里存在意外重叠的风险.我的存档阅读代码在我第一次尝试时工作,这足以证明这个代码足以完成手头的任务.