HOWTO · C++
在 C++ 中将文件读入到字符串
本文演示了如何在 C++ 中把文件读成字符串。
本页内容
要将文件读入 std::string,如果需要逐行处理,可将 std::ifstream 与 std::getline 配合使用;如果要读取整个流,则可用 std::istreambuf_iterator<char> 构造字符串。下面的示例各自创建临时输入文件,因此每个程序都可以单独编译和运行。
在 C++ 中使用 std::ifstream 和 std::getline 将文件读入字符串
std::getline 会移除每行的分隔符。每次成功调用后追加 "\n" 可以恢复行间分隔符,但即使源文件最后一行没有换行符,这样也会在末尾添加换行符。如果这一差异很重要,请跟踪原行是否带有分隔符,或使用下文的整流读取方法。
下面的完整示例会将两行内容写入临时文件,用 std::ifstream 打开文件,再把每一行追加到字符串中。示例前的说明描述的是通常成功读取时的用法。
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
int main() {
const auto path = std::filesystem::temp_directory_path() / "cpp-string-lines.txt";
{
std::ofstream sample(path);
sample << "alpha\nbeta";
if (!sample) {
std::cout << "Could not create sample file\n";
return 0;
}
}
std::ifstream inputFile(path);
if (!inputFile) {
std::cout << "Could not open sample file\n";
std::filesystem::remove(path);
return 0;
}
std::string fileContent;
std::string line;
while (std::getline(inputFile, line)) {
fileContent += line + "\n";
}
if (inputFile.bad()) {
std::cout << "Read failed\n";
inputFile.close();
std::filesystem::remove(path);
return 0;
}
std::cout << fileContent;
inputFile.close();
std::filesystem::remove(path);
}
当 std::getline 无法再提取一行时,循环就会停止。检查 bad() 可以区分底层 I/O 故障与正常的流末尾。文件默认以文本模式打开;在会转换换行符的系统上,结果字符串中的字节可能与文件中存储的字节不同。若需要逐字节保留内容,请使用二进制模式。
在 C++ 中使用 istreambuf_iterator 将文件读入字符串
要读取整个流,可以用一对 std::istreambuf_iterator<char> 构造字符串。第一个迭代器从流缓冲区读取内容,默认构造的迭代器表示结束位置。该方法不会按行拆分数据,因此会保留流所提供的换行符。
#include <filesystem>
#include <fstream>
#include <iostream>
#include <iterator>
#include <string>
int main() {
const auto path = std::filesystem::temp_directory_path() / "cpp-string-iterator.txt";
{
std::ofstream sample(path, std::ios::binary);
sample << "alpha\nbeta";
if (!sample) {
std::cout << "Could not create sample file\n";
return 0;
}
}
std::ifstream inputFile(path, std::ios::binary);
if (!inputFile) {
std::cout << "Could not open sample file\n";
std::filesystem::remove(path);
return 0;
}
const std::string fileContent{
std::istreambuf_iterator<char>(inputFile),
std::istreambuf_iterator<char>()};
if (inputFile.bad()) {
std::cout << "Read failed\n";
} else {
std::cout << fileContent << '\n';
}
inputFile.close();
std::filesystem::remove(path);
}
alpha
beta
如果整个文件能够放入内存,并且不需要逐行处理,可以使用这种写法。以二进制模式打开文件可以避免文本模式下的换行转换。std::string 可以包含零字节,但将 c_str() 视为以空字符结尾的文本字符串的函数,可能会在第一个零字节处停止。
在 C++ 中使用 rdbuf 将文件读入字符串
另一种标准库方法是将输入流的 rdbuf() 插入 std::ostringstream,然后调用 str(),取得其中内容构成的 std::string。如果周边代码本来就在使用字符串流,这种方式会很方便。如果需要报告读取故障,请在传输完成后检查输入流。
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
int main() {
const auto path = std::filesystem::temp_directory_path() / "cpp-string-rdbuf.txt";
{
std::ofstream sample(path, std::ios::binary);
sample << "alpha\nbeta";
if (!sample) {
std::cout << "Could not create sample file\n";
return 0;
}
}
std::ifstream inputFile(path, std::ios::binary);
if (!inputFile) {
std::cout << "Could not open sample file\n";
std::filesystem::remove(path);
return 0;
}
std::ostringstream stream;
stream << inputFile.rdbuf();
if (inputFile.bad()) {
std::cout << "Read failed\n";
} else {
const std::string fileContent = stream.str();
std::cout << fileContent << '\n';
}
inputFile.close();
std::filesystem::remove(path);
}
插入操作会将文件缓冲区中的字符复制到字符串流;str() 则会返回累积的字符串。若要直接构造整个文件的内容,迭代器方法涉及的步骤更少。无论使用哪种方法,都仍需检查文件是否成功打开,以及读取过程中是否发生错误。
使用 fread 函数将文件读入字符串
fread 是 C 标准库函数,也可以在 C++ 中使用。如果代码已经使用 FILE* 流,或需要面向字节的 C API,它会很实用。它并非天然比 C++ 流方法更快;性能取决于工作负载和具体实现。
此示例使用 fseek 和 ftell 获取可定位示例文件的大小,分配相应大小的字符串,再将字节读入其中。对于不可定位的流,或大到无法放入内存的文件,应改为读取固定大小的数据块。文件大小可能在测量和读取之间发生变化,因此仍需检查实际返回的字节数和 ferror。
#include <cstdio>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
int main() {
const auto path = std::filesystem::temp_directory_path() / "cpp-string-fread.txt";
{
std::ofstream sample(path, std::ios::binary);
sample << "alpha\nbeta";
if (!sample) {
std::cout << "Could not create sample file\n";
return 0;
}
}
std::FILE* file = std::fopen(path.string().c_str(), "rb");
if (file == nullptr) {
std::cout << "Could not open sample file\n";
std::filesystem::remove(path);
return 0;
}
if (std::fseek(file, 0, SEEK_END) != 0) {
std::cout << "Could not seek sample file\n";
std::fclose(file);
std::filesystem::remove(path);
return 0;
}
const long length = std::ftell(file);
if (length < 0 || std::fseek(file, 0, SEEK_SET) != 0) {
std::cout << "Could not determine sample size\n";
std::fclose(file);
std::filesystem::remove(path);
return 0;
}
std::string content(static_cast<std::size_t>(length), '\0');
const std::size_t bytesRead = std::fread(content.data(), 1, content.size(), file);
const bool failed = std::ferror(file) != 0 || bytesRead != content.size();
std::fclose(file);
std::filesystem::remove(path);
if (failed) {
std::cout << "Read failed or file changed while reading\n";
return 0;
}
std::cout << content << '\n';
}
第三个参数表示元素个数,第二个参数表示每个元素的大小;将元素大小设为 1 时,返回值就是字节数。fread 可能返回少于请求数量的字节,因此不要假定一次调用就能填满缓冲区。此示例针对可定位文件,并使用 ftell 所支持的 long 范围;需要处理更大文件的应用应使用适合平台的大文件 API,或分块读取。字符串保存的是字节,不会为 C 字符串函数追加空字符终止符。
使用 read 将文件读入字符串
POSIX 的 read 函数通过文件描述符和调用方提供的缓冲区工作。它不属于标准 C++;请包含相应的 POSIX 头文件,并且只在提供此 API 的平台上使用该方法。一次成功调用返回的字节数可能少于请求数量,因此需要重复调用直到文件结束,并处理调用中断和错误。
下面这个 POSIX 专用程序分块读取数据,不依赖预先测量文件大小。并非所有 C++ 平台都提供这些系统调用,因此应将它视为 POSIX 示例,而非可移植的 C++ 代码。
#include <cerrno>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <fcntl.h>
#include <unistd.h>
int main() {
const auto path = std::filesystem::temp_directory_path() / "cpp-string-read.txt";
{
std::ofstream sample(path, std::ios::binary);
sample << "alpha\nbeta";
if (!sample) {
std::cout << "Could not create sample file\n";
return 0;
}
}
const int fd = ::open(path.c_str(), O_RDONLY);
if (fd < 0) {
std::cout << "Could not open sample file\n";
std::filesystem::remove(path);
return 0;
}
std::string content;
char buffer[4096];
bool failed = false;
for (;;) {
const ssize_t count = ::read(fd, buffer, sizeof buffer);
if (count > 0) {
content.append(buffer, static_cast<std::size_t>(count));
} else if (count == 0) {
break;
} else if (errno != EINTR) {
failed = true;
break;
}
}
if (::close(fd) != 0) {
failed = true;
}
std::filesystem::remove(path);
if (failed) {
std::cout << "Read or close failed\n";
return 0;
}
std::cout << content << '\n';
}
对于普通文件,read 在到达文件末尾时返回零。对于其他类型的文件描述符,具体行为取决于设备或流。每次返回正数时,该数值就是要追加的字节数;返回负数表示发生错误,EINTR 则表示可以重试调用。务必关闭文件描述符,并正确处理部分读取,不要假设一次调用就能填满请求的缓冲区。示例使用 std::string::append,按实际字节数追加内容,包括其中可能存在的零字节。
结论
在可移植的 C++ 代码中,如果要把整个流读成字符串,可选择 std::istreambuf_iterator 或 rdbuf();如果适合逐行处理,则使用 std::getline。请记住,逐行提取会移除分隔符,之后再追加分隔符可能改变原本没有末尾换行符的最后一行。如果周边代码适合使用 C 的 FILE* 接口,可选用 fread;只有在平台提供的文件描述符 API 适用时,才使用 POSIX read。对于大文件,可以考虑分块处理,避免将整个文件留在内存中,并检查文件打开和读取操作的结果。