我如何读一个文件到一个std::字符串,即,读取整个文件一次?
文本或二进制模式应该由调用者指定。解决方案应该是符合标准的、可移植的和高效的。它不应该不必要地复制字符串的数据,并且应该避免在读取字符串时重新分配内存。
一种方法是统计文件大小,调整std::string和fread()到std::string的const_cast<char*>()'ed data()。这要求std::string的数据是连续的,这不是标准所要求的,但它似乎是所有已知实现的情况。更糟糕的是,如果以文本模式读取文件,std::string的大小可能不等于文件的大小。
一个完全正确的、符合标准的、可移植的解决方案可以使用std::ifstream的rdbuf()构造成std::ostringstream,再从那里构造成std::string。但是,这可能会复制字符串数据和/或不必要地重新分配内存。
是否所有相关的标准库实现都足够智能以避免所有不必要的开销?
还有别的办法吗?
我是否错过了一些已经提供所需功能的隐藏Boost函数?
void slurp(std::string& data, bool is_binary)
这样的事情应该不会太糟糕:
void slurp(std::string& data, const std::string& filename, bool is_binary)
{
std::ios_base::openmode openmode = ios::ate | ios::in;
if (is_binary)
openmode |= ios::binary;
ifstream file(filename.c_str(), openmode);
data.clear();
data.reserve(file.tellg());
file.seekg(0, ios::beg);
data.append(istreambuf_iterator<char>(file.rdbuf()),
istreambuf_iterator<char>());
}
这样做的好处是,我们先做了预留,这样我们就不必在读入时增加字符串。缺点是我们一个字符一个字符地做。更聪明的版本可以抓取整个read buf,然后调用下流。
一种方法是将流缓冲区刷新到一个单独的内存流中,然后将其转换为std::string(错误处理省略):
std::string slurp(std::ifstream& in) {
std::ostringstream sstr;
sstr << in.rdbuf();
return sstr.str();
}
这是非常简洁的。然而,正如问题中所指出的那样,这执行了冗余拷贝,不幸的是,基本上没有办法省略这个拷贝。
不幸的是,避免冗余拷贝的唯一真正解决方案是在循环中手动读取。由于c++现在保证了连续的字符串,可以编写以下代码(≥c++ 17,包含错误处理):
auto read_file(std::string_view path) -> std::string {
constexpr auto read_size = std::size_t(4096);
auto stream = std::ifstream(path.data());
stream.exceptions(std::ios_base::badbit);
auto out = std::string();
auto buf = std::string(read_size, '\0');
while (stream.read(& buf[0], read_size)) {
out.append(buf, 0, stream.gcount());
}
out.append(buf, 0, stream.gcount());
return out;
}
请看类似问题的答案。
为了方便大家,我转发了CTT的解决方案:
string readFile2(const string &fileName)
{
ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);
ifstream::pos_type fileSize = ifs.tellg();
ifs.seekg(0, ios::beg);
vector<char> bytes(fileSize);
ifs.read(bytes.data(), fileSize);
return string(bytes.data(), fileSize);
}
当对《白鲸记》(Moby Dick, 1.3M)的文本进行平均100次运行时,该解决方案比本文给出的其他答案的执行时间快了约20%。对于一个可移植的c++解决方案来说还不错,我想看看mmap'ing文件的结果;)
如果你有c++ 17 (std::filesystem),也有这种方法(通过std::filesystem::file_size而不是seekg和tellg来获取文件大小):
#include <filesystem>
#include <fstream>
#include <string>
namespace fs = std::filesystem;
std::string readFile(fs::path path)
{
// Open the stream to 'lock' the file.
std::ifstream f(path, std::ios::in | std::ios::binary);
// Obtain the size of the file.
const auto sz = fs::file_size(path);
// Create a buffer.
std::string result(sz, '\0');
// Read the whole file into the buffer.
f.read(result.data(), sz);
return result;
}
注意:如果你的标准库还不完全支持c++ 17,你可能需要使用<experimental/filesystem>和std::experimental::filesystem。你可能还需要用&result[0]替换result.data(),如果它不支持非const std::basic_string数据。
我没有足够的声誉来直接评论使用tellg()的响应。
请注意,tellg()会在出错时返回-1。如果将tellg()的结果作为分配参数传递,则应该首先对结果进行完整性检查。
这个问题的一个例子:
...
std::streamsize size = file.tellg();
std::vector<char> buffer(size);
...
在上面的例子中,如果tellg()遇到错误,它将返回-1。在signed(即tellg()的结果)和unsigned(即vector<char>构造函数的arg)之间隐式强制转换将导致your vector错误地分配大量字节。(可能是4294967295字节,或4GB。)
修改paxos1977的答案以解释上述问题:
string readFile2(const string &fileName)
{
ifstream ifs(fileName.c_str(), ios::in | ios::binary | ios::ate);
ifstream::pos_type fileSize = ifs.tellg();
if (fileSize < 0) <--- ADDED
return std::string(); <--- ADDED
ifs.seekg(0, ios::beg);
vector<char> bytes(fileSize);
ifs.read(&bytes[0], fileSize);
return string(&bytes[0], fileSize);
}
该解决方案将错误检查添加到基于rdbuf()的方法中。
std::string file_to_string(const std::string& file_name)
{
std::ifstream file_stream{file_name};
if (file_stream.fail())
{
// Error opening file.
}
std::ostringstream str_stream{};
file_stream >> str_stream.rdbuf(); // NOT str_stream << file_stream.rdbuf()
if (file_stream.fail() && !file_stream.eof())
{
// Error reading file.
}
return str_stream.str();
}
I'm adding this answer because adding error-checking to the original method is not as trivial as you'd expect. The original method uses stringstream's insertion operator (str_stream << file_stream.rdbuf()). The problem is that this sets the stringstream's failbit when no characters are inserted. That can be due to an error or it can be due to the file being empty. If you check for failures by inspecting the failbit, you'll encounter a false positive when you read an empty file. How do you disambiguate legitimate failure to insert any characters and "failure" to insert any characters because the file is empty?
您可能会认为显式地检查空文件,但这是更多的代码和相关的错误检查。
检查失败条件str_stream.fail() && !str_stream.eof()不工作,因为插入操作没有设置eofbit(在ostringstream或ifstream上)。
所以,解决办法就是改变操作。不要使用ostringstream的插入操作符(<<),而是使用ifstream的提取操作符(>>),它确实设置了eofbit。然后检查失败条件file_stream.fail() && !file_stream.eof()。
重要的是,当file_stream >> str_stream.rdbuf()遇到合法的失败时,它不应该设置eofbit(根据我对规范的理解)。这意味着上述检查足以检测出合法的故障。
基于CTT解决方案的更新函数:
#include <string>
#include <fstream>
#include <limits>
#include <string_view>
std::string readfile(const std::string_view path, bool binaryMode = true)
{
std::ios::openmode openmode = std::ios::in;
if(binaryMode)
{
openmode |= std::ios::binary;
}
std::ifstream ifs(path.data(), openmode);
ifs.ignore(std::numeric_limits<std::streamsize>::max());
std::string data(ifs.gcount(), 0);
ifs.seekg(0);
ifs.read(data.data(), data.size());
return data;
}
有两个重要的区别:
Tellg()不保证返回自文件开始以来的字节偏移量。相反,正如Puzomor Croatia所指出的,它更像是一个可以在fstream调用中使用的令牌。但是Gcount()会返回上次提取的未格式化字节数。因此,我们打开文件,使用ignore()提取并丢弃其所有内容,以获得文件的大小,并基于此构造输出字符串。
其次,我们通过直接写入字符串来避免必须将文件的数据从std::vector<char>复制到std::string。
就性能而言,这应该是绝对最快的,提前分配适当大小的字符串并调用read()一次。有趣的是,在gcc上使用ignore()和countg()而不是ate和tellg()会一点一点地编译成几乎相同的东西。
由于这似乎是一个广泛使用的实用程序,我的方法是搜索并选择已经可用的库,而不是手工制作的解决方案,特别是如果boost库已经在您的项目中链接(链接器标志-lboost_system -lboost_filesystem)。在这里(以及旧的boost版本),boost提供了一个load_string_file实用程序:
#include <iostream>
#include <string>
#include <boost/filesystem/string_file.hpp>
int main() {
std::string result;
boost::filesystem::load_string_file("aFileName.xyz", result);
std::cout << result.size() << std::endl;
}
作为一个优点,这个函数不寻求整个文件来确定大小,而是在内部使用stat()。然而,一个可能可以忽略不计的缺点是,在检查源代码时可以很容易地推断出:字符串不必要地用'\0'字符来调整大小,而'\0'字符是由文件内容重写的。
这是我使用的函数,当处理大文件(1GB+)时,由于某种原因std::ifstream::read()比std::ifstream::rdbuf()快得多,当你知道文件大小时,所以整个“先检查文件大小”的事情实际上是一个速度优化
#include <string>
#include <fstream>
#include <sstream>
std::string file_get_contents(const std::string &$filename)
{
std::ifstream file($filename, std::ifstream::binary);
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
file.seekg(0, std::istream::end);
const std::streampos ssize = file.tellg();
if (ssize < 0)
{
// can't get size for some reason, fallback to slower "just read everything"
// because i dont trust that we could seek back/fourth in the original stream,
// im creating a new stream.
std::ifstream file($filename, std::ifstream::binary);
file.exceptions(std::ifstream::failbit | std::ifstream::badbit);
std::ostringstream ss;
ss << file.rdbuf();
return ss.str();
}
file.seekg(0, std::istream::beg);
std::string result(size_t(ssize), 0);
file.read(&result[0], std::streamsize(ssize));
return result;
}
我知道这是一个非常古老的问题,有很多答案,但没有一个人提到我认为最明显的方法。是的,我知道这是c++,使用libc是邪恶和错误的,但这是疯狂的。使用libc很好,特别是对于这样简单的事情。
本质上:只需打开文件,获取它的大小(不一定是按这个顺序),然后读取它。
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <sys/stat.h>
static constexpr char const filename[] = "foo.bar";
int main(void)
{
FILE *fp = ::fopen(filename, "rb");
if (!fp) {
::perror("fopen");
::exit(1);
}
struct stat st;
if (::fstat(fileno(fp), &st) == (-1)) {
::perror("fstat");
::exit(1);
}
// You could simply allocate a buffer here and use std::string_view, or
// even allocate a buffer and copy it to a std::string. Creating a
// std::string and setting its size is simplest, but will pointlessly
// initialize the buffer to 0. You can't win sometimes.
std::string str;
str.reserve(st.st_size + 1U);
str.resize(st.st_size);
::fread(str.data(), 1, st.st_size, fp);
str[st.st_size] = '\0';
::fclose(fp);
}
除了(在实践中)完全可移植之外,这看起来并不比其他一些解决方案更糟糕。当然,也可以抛出异常,而不是立即退出。它严重激怒我,调整std::string总是0初始化它,但这是没有办法的。
请注意,这只适用于c++ 17及以后的版本。早期版本(应该)禁止编辑std::string::data()。如果使用较早的版本,可以考虑使用std::string_view或简单地复制一个原始缓冲区。
就性能而言,我还没有找到比下面的代码更快的代码。
std::string readAllText(std::string const &path)
{
assert(path.c_str() != NULL);
FILE *stream = fopen(path.c_str(), "r");
assert(stream != NULL);
fseek(stream, 0, SEEK_END);
long stream_size = ftell(stream);
fseek(stream, 0, SEEK_SET);
void *buffer = malloc(stream_size);
fread(buffer, stream_size, 1, stream);
assert(ferror(stream) == 0);
fclose(stream);
std::string text((const char *)buffer, stream_size);
assert(buffer != NULL);
free((void *)buffer);
return text;
}
从几个地方提取信息…这应该是最快最好的方法:
#include <filesystem>
#include <fstream>
#include <string>
//Returns true if successful.
bool readInFile(std::string pathString)
{
//Make sure the file exists and is an actual file.
if (!std::filesystem::is_regular_file(pathString))
{
return false;
}
//Convert relative path to absolute path.
pathString = std::filesystem::weakly_canonical(pathString);
//Open the file for reading (binary is fastest).
std::wifstream in(pathString, std::ios::binary);
//Make sure the file opened.
if (!in)
{
return false;
}
//Wide string to store the file's contents.
std::wstring fileContents;
//Jump to the end of the file to determine the file size.
in.seekg(0, std::ios::end);
//Resize the wide string to be able to fit the entire file (Note: Do not use reserve()!).
fileContents.resize(in.tellg());
//Go back to the beginning of the file to start reading.
in.seekg(0, std::ios::beg);
//Read the entire file's contents into the wide string.
in.read(fileContents.data(), fileContents.size());
//Close the file.
in.close();
//Do whatever you want with the file contents.
std::wcout << fileContents << L" " << fileContents.size();
return true;
}
这将宽字符读入std::wstring,但如果您只想要常规字符和std::string,则可以很容易地进行调整。