如何将二进制文件读入一个无符号字符向量中

104

最近我被要求写一个函数,将二进制文件读入到std::vector<BYTE>中,其中BYTEunsigned char。很快我就想出了这样的代码:

#include <fstream>
#include <vector>
typedef unsigned char BYTE;

std::vector<BYTE> readFile(const char* filename)
{
    // open the file:
    std::streampos fileSize;
    std::ifstream file(filename, std::ios::binary);

    // get its size:
    file.seekg(0, std::ios::end);
    fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    // read the data:
    std::vector<BYTE> fileData(fileSize);
    file.read((char*) &fileData[0], fileSize);
    return fileData;
}

这似乎过于复杂,而且在调用file.read时被迫使用显式转换为char*,使我感到更加不满意。

另一个选择是使用 std::istreambuf_iterator

std::vector<BYTE> readFile(const char* filename)
{
    // open the file:
    std::ifstream file(filename, std::ios::binary);

    // read the data:
    return std::vector<BYTE>((std::istreambuf_iterator<char>(file)),
                              std::istreambuf_iterator<char>());
}

这很简单,但是当我读取到std::vector<unsigned char>时,仍然需要使用std::istreambuf_iterator<char>


最后一个看起来非常简单的选项是使用std::basic_ifstream<BYTE>,它明确地表达了“我想要一个输入文件流,并且我想要使用它来读取BYTE”:
std::vector<BYTE> readFile(const char* filename)
{
    // open the file:
    std::basic_ifstream<BYTE> file(filename, std::ios::binary);

    // read the data:
    return std::vector<BYTE>((std::istreambuf_iterator<BYTE>(file)),
                              std::istreambuf_iterator<BYTE>());
}

但我不确定在这种情况下是否适合使用basic_ifstream

将二进制文件读入vector的最佳方法是什么?我还想知道"幕后发生了什么"以及可能遇到的问题(除了流没有正确打开,这可以通过简单的is_open检查避免)。

是否有任何好的理由在此处使用std::istreambuf_iterator
(我唯一能看到的优点是简洁性)


1
@R.MartinhoFernandes:我的意思是第三个选项似乎并不比第二个选项更好。 - LihO
有人在2011年进行了测量,至少可以将其加载到字符串中。http://insanecoding.blogspot.hk/2011/11/how-to-read-in-file-in-c.html - jiggunjer
一种更安全的查找文件大小的方法是使用特殊函数ignore()计数:file.ignore(std::numeric_limits<std::streamsize>::max());,然后使用auto size =file.gcount();返回提取的std::streamsize - Brett Hale
5个回答

62

在测试性能时,我会包括一个测试用例:

std::vector<BYTE> readFile(const char* filename)
{
    // open the file:
    std::ifstream file(filename, std::ios::binary);

    // Stop eating new lines in binary mode!!!
    file.unsetf(std::ios::skipws);

    // get its size:
    std::streampos fileSize;

    file.seekg(0, std::ios::end);
    fileSize = file.tellg();
    file.seekg(0, std::ios::beg);

    // reserve capacity
    std::vector<BYTE> vec;
    vec.reserve(fileSize);

    // read the data:
    vec.insert(vec.begin(),
               std::istream_iterator<BYTE>(file),
               std::istream_iterator<BYTE>());

    return vec;
}

我认为方法1的构造函数会访问vector中的元素,然后read再次访问每个元素。

方法2和方法3看起来最有希望,但可能会遇到一个或多个resize。因此,在读取或插入之前需要使用reserve

我还会使用std::copy进行测试:

...
std::vector<byte> vec;
vec.reserve(fileSize);

std::copy(std::istream_iterator<BYTE>(file),
          std::istream_iterator<BYTE>(),
          std::back_inserter(vec));

最终,我认为最好的解决方案将避免使用istream_iterator中的operator >>(以及尝试解释二进制数据的operator >>所带来的所有开销和好处)。但我不知道该使用什么来直接将数据复制到向量中。

最后,我的二进制数据测试显示ios::binary没有被执行。这就是需要在<iomanip>中使用noskipws的原因。


1
有没有一种方法可以将特定大小读入数组,而不是像这里描述的那样读取整个文件? - superhero
1
我认为只有在使用operator>>时才需要 file.unsetf(std::ios::skipws); - jiggunjer
1
@jiggunjer std::istream_iterator 内部使用 >> 运算符从流中提取数据。 - tomi.lee.jones
尝试了8个以上的代码片段,但都没有起作用,只有这个可以,非常感谢!+1 - user11043757
3
使用vector::insert()和迭代器非常慢。可能是因为调用了很多读取每个字节的虚拟函数。在Release模式下,我甚至不能等到它完成读取一个巨大的文件(在我的情况下是3 GB)。通过将最后一部分更改为以下内容,我获得了多倍的加速。std::vector<uint8_t> vec; vec.resize(fileSize); file.read(reinterpret_cast<std::ifstream::char_type*>(&vec.front()), fileSize); - Anton Breusov
显示剩余3条评论

30
std::ifstream stream("mona-lisa.raw", std::ios::in | std::ios::binary);
std::vector<uint8_t> contents((std::istreambuf_iterator<char>(stream)), std::istreambuf_iterator<char>());

for(auto i: contents) {
    int value = i;
    std::cout << "data: " << value << std::endl;
}

std::cout << "file size: " << contents.size() << std::endl;

1
请注意,这不会产生任何错误(没有异常或其他)。您只会得到空的“内容”。您可以使用“if(!stream)”检查文件错误,但我不知道是否有任何方法可以检查读取错误。 - Timmmm

8
由于您将整个文件加载到内存中,最优的版本是将文件映射到内存中。这是因为内核会将文件加载到内核页缓存中,通过映射文件,您只需将缓存中的页暴露给您的进程即可。也被称为零复制。
当您使用std :: vector<>时,它会将数据从内核页缓存复制到std :: vector<>中,而在只想读取文件时,这是不必要的。
此外,当向std :: vector<>传递两个输入迭代器时,它会在读取时增加其缓冲区大小,因为它不知道文件大小。将std :: vector<>调整大小为文件大小后,它会无谓地归零其内容,因为它将被文件数据覆盖。这两种方法在空间和时间上都不够优化。

1
与“resize”不同,“reserve”不会初始化。 - jiggunjer
你可以将迭代器传递给保留向量,以避免冗余的调整大小。这是指你上一段所提到的内容。 - jiggunjer
1
@jiggunjer嗯,这样是不行的,因为在没有先调整向量大小的情况下,您无法访问保留的容量。 - Maxim Egorushkin
@jiggunjer 啊,我现在明白你的意思了,那就是已经在被接受的答案中提到了... - Maxim Egorushkin
1
对于没有参考标准的读者来说,这是不清楚的。它没有解释如何映射到内存 - 我假设 streambufbasic 这样做?此外,术语假定 Linux/UNIX 是使用的操作系统,这感觉可能不适用于所有可由 C++ 目标化的平台 - 相同的概念和最佳实践是否存在于所有操作系统中? - underscore_d
显示剩余4条评论

3
我原本认为,使用大小和使用 stream::read() 的第一种方法是最有效的。将类型转换为 char * 的“代价”很可能为零 - 这种类型的转换只是告诉编译器:“嘿,我知道你认为这是不同的类型,但我真的想要在这里用这种类型...”,并没有添加任何额外的指令 - 如果您希望确认此点,请尝试将文件读入字符数组,并比较实际的汇编代码。除了需要一点额外的工作来确定向量内缓冲区的地址之外,不应该有任何区别。
像往常一样,唯一确定在您的情况下最有效的方法是进行测量。在互联网上“询问”不能证明什么。

0

下面的类扩展了向量,具有二进制文件的加载和保存功能。我已经多次回答过这个问题,所以这是我的下一个回答代码 - 也是所有其他需要查找二进制文件保存方法的人的代码。 :)

#include <cinttypes>
#include <fstream>
#include <vector>

// The class offers entire file content read/write in single operation
class BinaryFileVector : public std::vector<uint8_t>
{
    public:

        using std::vector<uint8_t>::vector;

        bool loadFromFile(const char *fileName) noexcept
        {
            // Try to open a file specified by its name
            std::ifstream file(fileName, std::ios::in | std::ios::binary);
            if (!file.is_open() || file.bad())
                return false;

            // Clear whitespace removal flag
            file.unsetf(std::ios::skipws);

            // Determine size of the file
            file.seekg(0, std::ios_base::end);
            size_t fileSize = file.tellg();
            file.seekg(0, std::ios_base::beg);

            // Discard previous vector content
            resize(0);
            reserve(0);
            shrink_to_fit();

            // Order to prealocate memory to avoid unnecessary reallocations due to vector growth
            reserve(fileSize);

            // Read entire file content into prealocated vector memory
            insert(begin(),
                std::istream_iterator<uint8_t>(file),
                std::istream_iterator<uint8_t>());

            // Make sure entire content is loaded
            return size() == fileSize;
        }

        bool saveToFile(const char *fileName) const noexcept
        {
            // Write entire vector content into a file specified by its name
            std::ofstream file(fileName, std::ios::out | std::ios::binary);
            try {
                file.write((const char *) data(), size());
            }
            catch (...) {
                return false;
            }

            // Determine number of bytes successfully stored in file
            size_t fileSize = file.tellp();
            return size() == fileSize;
        }
};

使用示例

#include <iostream>

int main()
{
    BinaryFileVector binaryFileVector;

    if (!binaryFileVector.loadFromFile("data.bin")) {
        std::cout << "Failed to read a file." << std::endl;
        return 0;
    }

    if (!binaryFileVector.saveToFile("copy.bin")) {
        std::cout << "Failed to write a file." << std::endl;
        return 0;
    }

    std::cout << "Success." << std::endl;
    return 0;
}

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