【问题标题】:Reading/Writing Hex Bytes from/to a Binary File从/向二进制文件读取/写入十六进制字节
【发布时间】:2015-05-15 08:40:19
【问题描述】:

我需要以二进制模式读取文件并将字节作为十六进制值存储在任何 STL 容器(最好是 std::list)中。稍后我需要将它们写回文件,也是二进制模式。所以,我声明,

typedef unsigned char BYTE;

std::ifstream File("File_Name", std::ios::binary);

std::list<BYTE> File_Bytes;

通过所有的搜索,我明白了一些事情。可以使用 std::istream::read() 或 std::istreambuf_iterator 进行读取(我可能非常错误。请纠正我。)并且 read() 函数仅将 char* 作为内存中存储字节的参数,并且输入流的大小。

如果我必须将文件中的字节读取到 BYTE 列表中,然后再次使用 istream 和 ostream 从 BYTE 列表中写入文件,我该怎么做?请为我澄清这一点。谢谢。

注意:这实际上适用于 Huffman 编码器/解码器,我需要在程序内部进行压缩和解压缩,并将解压缩的位写入输出文件。这是为了验证压缩的无损和程序的正确性。此外,谁能告诉我如何将编码的二进制位写入文件以及编码的 Huffman 文件具有什么文件扩展名?非常感谢。

【问题讨论】:

  • 为什么更喜欢 std::list?
  • 看看这个:[使用迭代器将文件读入向量[(stackoverflow.com/questions/3795326/…)
  • @laser_wizard std::list 具有随机访问权限,这对我解决其他问题有点帮助。我知道文件是按顺序写入的,但 std::list 有点成为我的主要偏好。我知道我们必须根据最适合该程序的方式进行更改,但这在大多数情况下对我有用,所以我使用它。你为什么要问?是不是不适合这种情况?
  • 我认为您希望将 whole 文件读入容器并写入包含容器内容的整个文件的想法是否正确?
  • list 可能是顺序容器中最糟糕的选择;因为它是一个双向链表,您可以为文件中的每 1 个字节数据使用 17 个字节的 ram(可能更多取决于分配器)。它也不是随机访问;只有前进和后退。 vectordeque 更适合随机访问。

标签: c++ stl fstream ifstream ofstream


【解决方案1】:

正如 cmets 所阐明的,您希望将二进制文件的字节加载到 char 的一些 STL 容器 - 或更准确地说,uint8_t - 和 将这样的容器保存回二进制文件。

有很多方法可以做到这一点,包括如您所发现的,使用 std::basic_istream::readstd::basic_ostream::write, 或std::istream_iteratorstd::ostream_iterator

后一种方法产生最简单的代码。 fread/fwrite 方法 产生最快的代码,但更简单的是显然是更好的 将只是您程序的序幕和尾声操作。

这是一对匹配的模板函数,它们将分别:

返回参数类型 Container 的 STL 容器,填充为 输入文件的字节序列。

将参数类型Container的STL容器的元素复制到一个 输出文件中的字节序列。

#include <fstream>
#include <iterator>
#include <algorithm>
#include <stdexcept>
#include <cstdint>

template<class Container>
Container binary_load(std::string const & bin_file_name)
{
    std::ifstream in(bin_file_name,std::ios::binary);
    if (!in) {
        throw std::runtime_error("Could not open \"" + bin_file_name + 
            "\" for reading");
    }
    std::noskipws(in); // PON 1
    return Container(std::istream_iterator<std::uint8_t>(in),
                        std::istream_iterator<std::uint8_t>()); //PON 2

}

template<class Container>
void binary_save(Container && data, std::string const & bin_file_name)
{
    std::ofstream out(bin_file_name,std::ios::binary);
    if (!out) {
        throw std::runtime_error("Could not open \"" + bin_file_name + 
            "\" for writing");
    }
    std::copy(data.begin(),data.end(),
        std::ostream_iterator<std::uint8_t>(out,"")); // PON 3  
}

要编译一个基本用例,请附加以下内容:

#include <vector>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
    string infile = argv[1];
    string outfile = infile + ".saved";
    auto data(binary_load<vector<std::uint8_t>>(infile));
    binary_save(data,outfile);
    return 0;
}

这编译为 C++11 或更好。生成的程序加载您指定为其第一个文件 命令行参数到std::vector&lt;std::uint8_t&gt; 然后只是 将该向量保存到同名的文件中 扩展.saved。当然,您的程序将加载一个向量 并保存一个不同的。

注意事项 (PON):

  1. 需要此语句来通知流in 它应该提取所有个字节,而不是跳过空白字节。

  2. 此语句直接从[begin,end) 构造填充的Container 迭代器范围,以构造每个 STL 容器的方式。 begin 迭代器 std::istream_iterator&lt;char&gt;(in)start-of-stream in 的迭代器和 end 迭代器 std::istream_iterator&lt;char&gt;() 是每个流的 end-of-stream 迭代器。

  3. 此语句将字节序列复制到一个连续位置 std::ostream_iterator&lt;char&gt; 最初位于 out 的开头。 迭代器构造函数的 "" 参数通知它 空字符串(即无)应分隔连续的输出字节。

这些函数模板比​​你严格的更通用 要求:

  • 调用 binary_load 时使用的 Container 类型不必是 uint8_t 的容器,甚至是相同大小的容器。它需要 只能是可以从迭代器范围构造的容器类型 uint8_t 的序列。

  • 同样,您调用 binary_saveContainer 类型需要 仅是其元素的类型为 E 的类型,即隐式 可转换为uint8_t,但需要注意的是会发生截断 如果您随意选择保存在uint8_t 中无法表示的任何Es。

因此,将这些放在一起不会造成任何伤害,例如,如果您 在示例程序中将vector&lt;uint8_t&gt; 替换为vector&lt;long&gt;

当然,如果您错误地使用 不满足模板要求的容器类型 Container,代码无法编译。

OP 的 cmets 继续

我可以使用 unsigned char 代替 [uint8_t] 吗?

是的,uint8_t 几乎不可避免地被您定义为 unsigned char 编译器,任何 8 位类型的整数类型都可以。 uint8_t 只是 最清楚地说“字节”。如果你愿意 进一步参数化关于“字节”的模板函数 输入,你可以这样做:

...
#include <type_traits>

template<class Container, typename Byte = std::uint8_t>
Container binary_load(std::string const & bin_file_name) {

    static_assert(sizeof(Byte) == 1,"Size of `Byte` must be 1");

    // `std::uint8_t` becomes `Byte` 
    ...
}

template<class Container, typename Byte = std::uint8_t>
void binary_save(Container && data, std::string const & bin_file_name) {

    static_assert(sizeof(Byte) == 1,"Size of `Byte` must be 1");
    // `std::uint8_t` becomes `Byte` 
    ...
}

关于霍夫曼编码文件的正确文件扩展名,没有 事实标准。选择你喜欢的。

除非你需要使用 MS VC10(有不完整的 C++11 支持) 对于您的控制台版本,无需。 Bang up-to-date GCC toolchains are freely available for Windows,和 支持IDE:CodeLite,Code::Blocks

【讨论】:

  • 天哪,迈克·金汉,非常感谢!当你问我的问题时,我没想到你会给我一个如此清晰和如此详细的解释。我非常清楚地理解了你解释的一切。我编译了代码,它就像一个魅力。再次非常感谢您。向你致敬! :-)
  • @WDKKS 已为 cmets 更新答案,因为已删除。如果这个答案解决了您的问题,您可以accept it。然后它似乎不再没有答案;你会得到一些 SO 声望点,我也一样。
  • 是的,我会接受这个答案。非常感谢您的帮助和额外的信息。我今天真的学到了很多东西。不过,我还有最后一个疑问。我使用 for 循环从头到尾打印出 vector 中的所有“字节”,并将其与在十六进制编辑器中打开的文件进行比较。我注意到输出窗口中的字节顺序与十六进制编辑器中的顺序不同。为什么呢?你能告诉我吗。我确实使用了正确的方法以正确的格式(十六进制、二进制、整数等)打印它。
  • 编辑:我更仔细地比较了它,它检查出来了。再次非常感谢您,金汉先生。我接受了你的回答。向你竖起大拇指。 :-)
  • @WDKKS 不客气。测试示例程序的最简单和决定性的方法是使用二进制差异工具,例如KDiff3 来比较输入和输出文件,使用一些zip 文件或图像/音乐文件作为测试数据。当然,我已经做到了。顺便说一句,std::enable_if 脚注是不明智的。答案已更正。
【解决方案2】:

我建议使用uint8_t的固定大小缓冲区:

const unsigned int BUFFER_SIZE = 1024*1024;  
uint8_t buffer[BUFFER_SIZE];
// ...
my_file.read((char *)buffer, BUFFER_SIZE);

在您的程序中,您将读取一个缓冲区,对其进行处理,然后从输入中读取另一个缓冲区。

对于您的目的,数组是比std::vectorstd::list 更有效的容器。

另外,请使用uint8_t,因为它是一种标准化类型。

【讨论】:

  • 如果文件大于 1024^2 会怎样?正确使用容器可以使代码更容易编写和阅读。 std::vector::resize() 可以避免与大多数容器性能问题相关的代价高昂的增长/复制。
【解决方案3】:

您的问题涉及两个截然不同的主题。

  1. 如何读写二进制文件
  2. 如何操作二进制数据。 (充气/放气)

文件 IO

两种流行的文件读取方法是read()getline()。我在处理二进制文件时使用read(),在每行读取文本文件时使用getline()。由于您正在处理二进制数据,我建议您使用read()

// Open Binary file at the end
std::ifstream input(filePath, std::ios::ate | std::ios::binary);
assert(input.is_open());

// Calculate size
size_t end = input.tellg();
input.seekg(0,std::ios::beg);
size_t beg = input.tellg();
size_t len = end - beg;
assert(len > 0);

// Read in Binary data
std::vector<char> binaryData(len);
input.read(&(binaryData[0]),len);

// Close
input.close();

在游戏的这个阶段,您将所有二进制数据存储在一个向量中。我知道在您的示例中您已经使用 list 来表达,但鉴于您想要处理连续的字节流,vector 似乎更符合您的操作。

二进制

有几种方法可以处理二进制数据。您可以使用可靠的移位运算符&lt;&lt;&gt;&gt; 加上一些好的和&amp; 和或| 逻辑。但是,如果您想在代码中更直观地表示,我建议您查看std::bitset

使用 bitset,您可以轻松地将 vector 的内容加载到 8 位二进制表示中。

std::bitset<8>  deflatedBinary(binaryData[0]);
std::bitset<12> inflatedBinary;

第一个 bitset 保存第一个 char 的 8 位二进制表示,第二组 inflatedBinary 有 12 位全部归零。从这里您可以通过索引[] 访问它们的元素。你可以阅读更多关于std::bitsethere的信息。

【讨论】:

  • 感谢弗雷迪提供的信息。虽然,我有几个问题。首先,您制作了一个 char 类型的向量。这甚至可以用于声明 unsigned char (BYTE) 类型,对吧?愚蠢的问题,但要确定。其次,我程序中的二进制数据存储在 std::vector 中。并且程序说明页面坚持使用您还建议的位运算符。我可以将这个向量与这些运算符一起使用吗?请为我澄清这一点。谢谢。
  • 我使用了char,但也可以轻松使用unsigned char。老实说,Thomas Matthews 在使用uint_8 方面有更好的方法,因为它的大小保证为 1 字节。我的代码做了一个危险的假设,即 char/unsigned char 始终为 1byte,但对于某些机器而言并非如此。对于第二个问题,std::vector 只是一个容器,您不想对其执行位运算符。听起来您的教授希望您在容器中使用真/假bool,就好像它是二进制一样。
  • 谢谢你告诉我。我阅读了std::bitset 并注意到您使用 bitset 将从文件中读取的相同字节作为二进制数据放置。我应该更清楚我的问题。二进制数据是指与每个字节有关的霍夫曼编码值,其中每个值的长度是可变的,因为它基于字节频率。我看到std::bitset 长度是恒定的,空位用零填充。这将更改编码值。有解决方法吗?如果不是,std::vector&lt;bool&gt; 非常适合std::strings。它也可能适用于字节。
猜你喜欢
  • 2022-01-17
  • 1970-01-01
  • 1970-01-01
  • 2020-10-25
  • 2021-10-27
  • 1970-01-01
  • 1970-01-01
  • 2011-12-22
  • 1970-01-01
相关资源
最近更新 更多