我如何读整个文件到性病::字符串在c++ ?

如何将文件读入std::string,即一次读取整个文件?

文本或二进制模式应该由调用者指定。解决方案应该是符合标准的、可移植的和高效的。它不应该不必要地复制字符串的数据,并且应该避免在读取字符串时重新分配内存。

一种方法是统计文件大小,将std::stringfread()调整为std::stringconst_cast<char*>()'ed data()。这要求std::string的数据是连续的,这不是标准所要求的,但它似乎是所有已知实现的情况。更糟糕的是,如果以文本模式读取文件,std::string的大小可能不等于文件的大小。

一个完全正确的、符合标准的、可移植的解决方案可以使用std::ifstreamrdbuf()构造为std::ostringstream,再从那里构造为std::string。但是,这可能会复制字符串数据和/或不必要地重新分配内存。

  • 是否所有相关的标准库实现都足够智能以避免所有不必要的开销?
  • 还有别的办法吗?
  • 我是否错过了一些已经提供所需功能的隐藏Boost函数?

< br >

void slurp(std::string& data, bool is_binary)
121812 次浏览

使用

#include <iostream>
#include <sstream>
#include <fstream>


int main()
{
std::ifstream input("file.txt");
std::stringstream sstr;


while(input >> sstr.rdbuf());


std::cout << sstr.str() << std::endl;
}

或者非常接近。我自己没有打开stdlib引用来进行双重检查。

是的,我知道我没有按照要求编写slurp函数。

永远不要写入std::string的const char *缓冲区。从来没有!这样做是一个巨大的错误。

在std::string中为整个字符串保留()空间,将合理大小的文件中的块读入缓冲区,然后追加()它。数据块的大小取决于输入文件的大小。我非常确定所有其他可移植的和与stl兼容的机制都会做同样的事情(但可能看起来更漂亮)。

最短的变体:Live On Coliru

std::string str(std::istreambuf_iterator<char>{ifs}, {});

它需要头文件<iterator>

有一些报告说,这个方法比预分配字符串和使用std::istream::read要慢。然而,在现代的编译器上,这种情况似乎不再存在,尽管各种方法的相对性能似乎高度依赖于编译器。

这样的事情应该不会太糟糕:

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::getline'函数,并指定'eof'作为分隔符。结果代码有点晦涩:

std::string data;
std::ifstream in( "test.txt" );
std::getline( in, data, std::string::traits_type::to_char_type(
std::string::traits_type::eof() ) );

一种方法是将流缓冲区刷新到单独的内存流中,然后将其转换为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而不是seekgtellg获取文件的大小):

#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。如果不支持非const std::basic_string数据,你可能还需要用&result[0]替换result.data()

我没有足够的声誉来直接评论使用tellg()的响应。

请注意tellg()会在错误时返回-1。如果你将tellg()的结果作为分配参数传递,你应该首先检查结果。

这个问题的一个例子:

...
std::streamsize size = file.tellg();
std::vector<char> buffer(size);
...

在上面的例子中,如果tellg()遇到错误,它将返回-1。有符号(即tellg()的结果)和无符号(即vector<char>构造函数的参数)之间的隐式强制转换将导致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();
}

我添加这个答案是因为向原始方法添加错误检查并不像您期望的那样简单。原来的方法使用stringstream的插入操作符(str_stream << file_stream.rdbuf())。问题是,当没有插入字符时,这会设置stringstream的failbit。这可能是由于错误,也可能是由于文件为空。如果通过检查failbit来检查失败,那么在读取空文件时将会遇到误报。如何消除插入任何字符的合法失败和由于文件为空而插入任何字符的“失败”的歧义?

您可能会认为显式地检查空文件,但这是更多的代码和相关的错误检查。

检查失败条件str_stream.fail() && !str_stream.eof()不起作用,因为插入操作没有设置eofbit(在ostringstream或ifstream上)。

所以,解决办法就是改变操作。不要使用ostringstream的插入操作符(<<),而是使用ifstream的提取操作符(>>),它确实设置了eofbit。然后检查失败条件file_stream.fail() && !file_stream.eof()

重要的是,当file_stream >> str_stream.rdbuf()遇到合法的失败时,它不应该设置eofbit(根据我对规范的理解)。这意味着上述检查足以检测出合法的故障。

#include <string>
#include <sstream>


using namespace std;


string GetStreamAsString(const istream& in)
{
stringstream out;
out << in.rdbuf();
return out.str();
}


string GetFileAsString(static string& filePath)
{
ifstream stream;
try
{
// Set to throw on failure
stream.exceptions(fstream::failbit | fstream::badbit);
stream.open(filePath);
}
catch (system_error& error)
{
cerr << "Failed to open '" << filePath << "'\n" << error.code().message() << endl;
return "Open fail";
}


return GetStreamAsString(stream);
}

用法:

const string logAsString = GetFileAsString(logFilePath);

下面是一个使用新文件系统库的版本,它具有相当健壮的错误检查功能:

#include <cstdint>
#include <exception>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>


namespace fs = std::filesystem;


std::string loadFile(const char *const name);
std::string loadFile(const std::string &name);


std::string loadFile(const char *const name) {
fs::path filepath(fs::absolute(fs::path(name)));


std::uintmax_t fsize;


if (fs::exists(filepath)) {
fsize = fs::file_size(filepath);
} else {
throw(std::invalid_argument("File not found: " + filepath.string()));
}


std::ifstream infile;
infile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try {
infile.open(filepath.c_str(), std::ios::in | std::ifstream::binary);
} catch (...) {
std::throw_with_nested(std::runtime_error("Can't open input file " + filepath.string()));
}


std::string fileStr;


try {
fileStr.resize(fsize);
} catch (...) {
std::stringstream err;
err << "Can't resize to " << fsize << " bytes";
std::throw_with_nested(std::runtime_error(err.str()));
}


infile.read(fileStr.data(), fsize);
infile.close();


return fileStr;
}


std::string loadFile(const std::string &name) { return loadFile(name.c_str()); };

基于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()而不是atetellg()会逐位编译到几乎是一样的

#include <iostream>
#include <fstream>
#include <string.h>
using namespace std;
main(){
fstream file;
//Open a file
file.open("test.txt");
string copy,temp;
//While loop to store whole document in copy string
//Temp reads a complete line
//Loop stops until temp reads the last line of document
while(getline(file,temp)){
//add new line text in copy
copy+=temp;
//adds a new line
copy+="\n";
}
//Display whole document
cout<<copy;
//close the document
file.close();
}

由于这似乎是一个广泛使用的实用程序,我的方法是搜索并选择已经可用的库,而不是手工制作的解决方案,特别是如果boost库已经在您的项目中链接(链接器标志-lboost_system -lboost_filesystem)。这里(以及旧的增强版本), 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;
}

你可以使用我开发的rst c++库来做到这一点:

#include "rst/files/file_utils.h"


std::filesystem::path path = ...;  // Path to a file.
rst::StatusOr<std::string> content = rst::ReadFile(path);
if (content.err()) {
// Handle error.
}


std::cout << *content << ", " << content->size() << std::endl;

我知道我迟到了,但现在(2021年)在我的机器上,这是我测试过的最快的实现:

#include <fstream>
#include <string>


bool fileRead( std::string &contents, const std::string &path ) {
contents.clear();
if( path.empty()) {
return false;
}
std::ifstream stream( path );
if( !stream ) {
return false;
}
stream >> contents;
return true;
}
#include <string>
#include <fstream>


int main()
{
std::string fileLocation = "C:\\Users\\User\\Desktop\\file.txt";
std::ifstream file(fileLocation, std::ios::in | std::ios::binary);


std::string data;


if(file.is_open())
{
std::getline(file, data, '\0');


file.close();
}
}

从几个地方提取信息…这应该是最快最好的方法:

#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,你可以很容易地进行调整。

std::string get(std::string_view const& fn)
{
struct filebuf: std::filebuf
{
using std::filebuf::egptr;
using std::filebuf::gptr;


using std::filebuf::gbump;
using std::filebuf::underflow;
};


std::string r;


if (filebuf fb; fb.open(fn.data(), std::ios::binary | std::ios::in))
{
r.reserve(fb.pubseekoff({}, std::ios::end));
fb.pubseekpos({});


while (filebuf::traits_type::eof() != fb.underflow())
{
auto const gptr(fb.gptr());
auto const sz(fb.egptr() - gptr);


fb.gbump(sz);
r.append(gptr, sz);
}
}


return r;
}