在日常开发中,我们处理的配置文件、运行日志或数据清单,通常包含几十行甚至成百上千行文本。调用一次 std::getline() 函数只能读取一行内容,如果靠手动重复编写读取语句,显然没法应付行数不固定的文件。
想要把多行文本从头到尾读完,最常用的做法是将 std::getline() 与 while 循环结合起来。每成功读到一行就进入循环处理,读完最后一行后自动停下,不需要提前知道文件里一共有多少行。
使用 while + getline() 逐行读取
在 C++ 中,逐行读取整个文本文件的标准写法,是直接把 std::getline(file, line) 放进 while 循环的条件括号里。
语法:
std::string line;
while (std::getline(file, line)) {
// 处理当前行 line
}说明:
在上面的语法中,file 是已经成功打开的 std::ifstream 输入流对象,line 则是用来保存当前行文本的 std::string 变量。
为什么 std::getline(file, line) 可以直接当成 while 的判断条件呢?这是因为该函数执行完毕后,会返回文件流对象 file 本身。
只要当前这一行读取成功,文件流状态正常,条件判断就为 true;当读到文件末尾没有新内容可读时,条件判断自动变为 false,循环随之结束。
示例 1:逐行读取整个文件
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 准备 3 行测试文本
std::ofstream outFile("lines.txt");
outFile << "C++ basics\n";
outFile << "lvyenet tutorial\n";
outFile << "file stream\n";
outFile.close();
std::ifstream file("lines.txt");
if (!file.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
std::string line;
while (std::getline(file, line)) {
std::cout << line << '\n';
}
return 0;
}运行结果如下。
C++ basics
lvyenet tutorial
file stream分析:
在这个例子中,前 3 次判断 while (std::getline(file, line)) 时,程序依次读出 lines.txt 中的 3 行文本,每次都会把最新读到的一行存入变量 line 并打印出来。
到了第 4 次判断时,由于已经到达文件末尾,std::getline() 读取失败,循环条件变为 false 并自动退出。在使用 std::getline() 逐行读取时,小伙伴们还需要注意以下 2 个细节:
- 行尾的换行符不会存入变量中:
std::getline()把行尾的\n当作结束分隔标记,读完后会直接丢弃。比如第 1 行读出来是"C++ basics",末尾并不带\n,我们打印时需要手动补上'\n'换行。 - 最后一行没有换行符也能正常读取:有些文本文件的最后一行写完后直接结束,末尾并没有敲回车换行。只要最后一行有字符,
std::getline()依然能把它完整读出来。
逐行读取的常见应用
逐行读取在实际开发中非常实用。我们不仅可以把文件内容打印出来,还能在每读到一行时立即对它做空行过滤、行数统计或关键词筛选。
1. 识别与跳过空行
文本文件中并不是每一行都一定有文字,有时两段内容之间会留有空行。遇到空行时,std::getline() 既不会自动跳过,也不会终止循环,而是依然算作读取成功。只不过这一行没有任何字符,此时 line 中保存的是空字符串 ""。下面我们来看一个例子。
示例 2:读取包含空行的文件
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 准备第 2 行为空行的测试文件
std::ofstream outFile("blank.txt");
outFile << "first line\n";
outFile << "\n";
outFile << "third line\n";
outFile.close();
std::ifstream file("blank.txt");
if (!file.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
std::string line;
int row = 1;
while (std::getline(file, line)) {
std::cout << row << ": [" << line << "]\n";
row++;
}
return 0;
}运行结果如下。
1: [first line]
2: []
3: [third line]分析:
从运行结果中的 2: [] 可以看出,第 2 行的空行也被正常读出来了。此时变量 line 里保存的是空字符串 "",循环并没有因为遇到空行而中断。
如果我们希望在读取文件时自动忽略所有空行,只处理真正有文字的行,应该怎么写呢?我们可以调用字符串的 empty() 函数,并配合 continue 语句跳过当前这一轮循环,代码如下:
while (std::getline(file, line)) {
if (line.empty()) {
continue; // 跳过空行
}
std::cout << line << '\n';
}提示 line.empty() 只能识别完全没有字符的纯空行。如果某一行看起来是空白的,但里面其实敲了几个空格或 Tab 键,line.empty() 会返回 false。遇到这种情况,我们可以先去除字符串首尾空格,再判断它是否为空。
2. 统计行数与筛选关键词
除了过滤空行,我们还可以在逐行读取时统计文件的总行数,或者配合字符串的 find() 函数筛选出包含指定关键词的行。
示例 3:统计行数与筛选关键词
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 准备测试文件 notes.txt
std::ofstream outFile("notes.txt");
outFile << "C++ basics\n";
outFile << "file input\n";
outFile << "lvyenet practice\n";
outFile << "file stream\n";
outFile.close();
std::ifstream file("notes.txt");
if (!file.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
std::string line;
int row = 0;
int matchCount = 0;
while (std::getline(file, line)) {
row++;
if (line.find("file") != std::string::npos) {
matchCount++;
std::cout << "第 " << row << " 行:" << line << '\n';
}
}
std::cout << "总行数:" << row << '\n';
std::cout << "包含 file 的行数:" << matchCount << '\n';
return 0;
}运行结果如下。
第 2 行:file input
第 4 行:file stream
总行数:4
包含 file 的行数:2分析:
在这个例子中,每成功读取一行,row++ 就把行号加 1。紧接着我们通过 line.find("file") 查找当前行是否包含子串 "file"。只要返回值不等于 std::string::npos,说明找到了目标词,程序就会打印当前行号与内容,并将匹配行数 matchCount 加 1。
对于上面这个例子,小伙伴们可以把代码里的 "file" 改成 "lvyenet" 试一下,看看匹配出来的行号和统计数量会发生什么变化。
为什么不要使用 !file.eof() 控制循环?
很多初学的小伙伴会有一个直觉:既然逐行读取是读到文件末尾才停下,那能不能直接调用文件流的 eof() 函数作为 while 的条件呢?其中,eof 是 “end of file(文件末尾)” 的缩写。
实际上,直接使用 while (!file.eof()) 控制读取循环是 C++ 中非常典型的错误写法,很容易导致循环多跑一次。下面我们把正确写法和错误写法放在一起对比一下:
// 正确方式
while (std::getline(file, line)) {
std::cout << line << '\n';
}
// 错误方式
while (!file.eof()) {
std::getline(file, line);
std::cout << line << '\n';
}为什么错误写法会出问题呢?因为 eof 状态不会提前预判,只有在某次读取真正碰上文件末尾并失败之后,才会被设为 true。假设文件里有两行带换行的文本 "line1\n" 和 "line2\n",错误写法的执行过程如下:
- 第 1、2 次循环:程序依次正常读出
"line1"和"line2"。刚读完第 2 行末尾的\n时,程序还没有继续往后读,所以此时file.eof()依然是false。 - 第 3 次循环:由于
!file.eof()仍为true,程序又多进了一次循环。这次std::getline(file, line)先清空line,再尝试读取时碰上文件末尾而失败,但下方的输出语句仍会照常执行,导致多打印出一行空内容。
此外,如果读取中途因为其他原因出错,只要还没到达文件末尾,file.eof() 就会一直是 false,甚至可能引发死循环。因此大家在逐行读取文件时,记住始终使用 while (std::getline(file, line)) 作为循环条件就可以了。
常见问题
1. 在 Linux 或 macOS 下读取 Windows 生成的文本文件时,为什么每行末尾可能会多出一个回车符?
这是因为不同操作系统的默认换行符不同:Linux 和 macOS 使用 \n 换行,而 Windows 文本文件默认使用 \r\n 换行。
当我们在 Linux 或 macOS 下调用 std::getline() 读取 Windows 生成的文本文件时,函数只丢弃了末尾的 \n,字符串 line 的最后一个字符可能还会残留一个 '\r'。
如果需要跨平台兼容这类文件,我们可以在读出 line 后先判断它是否非空,并且检查最后一个字符 line.back() 是不是 '\r'。如果满足条件,调用 line.pop_back() 把末尾残留的 '\r' 删掉即可。
