Menu

C++ getline:读取整行、修复输入被跳过、逐行读文件

std::getline 如何把一整行读入 std::string:配合 cin 使用,为什么在 cin >> 之后它会返回空字符串,cin.ignore 和 std::ws 两种修复方法,自定义分隔符,以及逐行读取文件直到文件末尾。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

读取一整行

cin >> text 只读取一个单词:遇到第一个空格就停止。要读取用户在按下 Enter 之前输入的全部内容(包括空格),请使用 <string> 头文件中的 std::getline:

#include <iostream>
#include <string>

int main() {
    std::string name;
    std::cout << "Full name: ";
    std::getline(std::cin, name);
    std::cout << "Hello, " << name << "!\n";
}

输入 Ada Lovelace 会打印 Hello, Ada Lovelace!。如果用 std::cin >> name,它会打印 Hello, Ada!,而 Lovelace 还留在输入中等待读取。

函数签名如下:

std::istream& std::getline(std::istream& input, std::string& text);
std::istream& std::getline(std::istream& input, std::string& text, char delim);
  • input 可以是任何输入流:std::cin、用 std::ifstream 打开的文件,或由字符串构造的 std::istringstream。
  • text 的内容会被读到的字符替换(而不是追加)。
  • delim 是结束读取的字符,默认为 '\n'。分隔符会从流中移除,但不会存入 text。
  • 返回值是流本身,因此可以直接在条件中判断它。

由于 getline 在所有输入流上的用法都一样,本页可运行的示例都从保存固定文本的 std::istringstream 中读取。这样不用输入任何内容,输出也是确定的。如果想改为从键盘读取,把流变量换成 std::cin 即可。

输出:

[Ada Lovelace]
[Alan Turing]

方括号表明换行符不属于任何一个字符串。

cin >> 之后 getline 返回空字符串

这是 getline 最常见的问题。你先用 >> 读取一个数字,再用 getline 读取一行,结果 getline 好像被跳过了:

输出:

age: 30
city: []

input >> age 读取字符 30,在第一个不可能属于数字的字符处停下,也就是换行符。这个换行符留在了流中。随后 getline 读到下一个换行符为止,而它立刻就遇到了一个,于是返回空字符串。如果用的是 std::cin,表现就是程序根本没有停下来等你输入第二个答案。

有两种标准的修复方法,都放在 >> 和 getline 之间。

方法 1:用 ignore 丢弃这一行剩下的内容。

输出:

age: 30
city: [Paris]

ignore(n, '\n') 会一直丢弃字符,直到移除了一个换行符或已丢弃 n 个字符,以先到者为准。传入 std::streamsize 的最大值,意思就是“一直丢到换行符,不管它有多远”。它还会移除用户在同一行数字后面输入的其他内容,比如末尾的空格。从键盘读取时,这一行写作 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');,并且需要 #include <limits>。

不带参数的 std::cin.ignore() 只丢弃一个字符。当换行符紧跟在数字后面时它能用,但只要数字和换行符之间多了一个空格就会失效。

方法 2:用 std::ws 跳过开头的空白字符。

输出:

age: 30
city: [Paris]

std::ws 是一个操纵符,它会吞掉当前位置的所有空白字符(空格、制表符、换行符)。input >> std::ws 返回流本身,所以可以直接传给 getline。它还会跳过空行和开头的空格,读取姓名、城市时很方便,但如果开头的空格或空行本身就是有意义的输入,这样做就错了。这种情况下请用 ignore。

一直读取到输入结束

getline 返回流本身,而读取失败后流会转换为 false。没有更多行时 getline 就会失败,所以用 while 循环包住它,就能读完每一行并在末尾停下:

输出:

1: [apples 3]
2: [bread 1]
3: []
4: [milk 2]
read 4 lines

从这段输出可以看出两个细节:

  • 空行也算一次成功的读取。它得到一个空字符串,循环继续。只有到达输入末尾才会让循环停止。
  • 最后一行末尾不需要换行符。如果文本以 milk 2 结尾且没有 \n,getline 仍然会返回这一行,下一次调用才会失败。

对 std::cin 使用同样的循环,会一直读到输入结束。输入结束的情况包括:从文件重定向输入(./program < data.txt),或者用户在 macOS 和 Linux 上按 Ctrl+D,在 Windows 上按 Ctrl+Z 再按 Enter。

不要使用 while (!input.eof()) { std::getline(input, line); ... } 这种写法。eof() 只有在某次读取已经碰到末尾之后才变为 true,所以循环体会多执行一次,此时读取失败,line 还是旧值。应该把 getline 调用直接放进条件里。

逐行读取文件

读取文件用的是同样的循环。用 <fstream> 中的 std::ifstream 打开文件,检查是否打开成功,然后读取:

输出:

1. Ada 92
2. Grace 88
3. Alan 95

if (!file) 能在循环之前发现文件不存在或路径写错的情况。如果没有这个检查,循环只会读到零行,看起来像是一个空文件,而不是一个错误。file 在 main 结束时离开作用域,文件会自动关闭。

在 Windows 上保存的文件,每行以 \r\n 结尾。在 Linux 或 macOS 上读取这样的文件时,getline 会移除 \n 但保留 \r,于是每一行末尾都多了一个看不见的回车符,导致 line == "done" 这样的比较失败。读取之后把它去掉:

if (!line.empty() && line.back() == '\r') {
    line.pop_back();
}

使用自定义分隔符

第三个参数决定用哪个字符结束每次读取。常见的用法是按逗号拆分一行。先读取这一行,再通过 std::istringstream 拆分它:

输出:

fields: 3
[Ada Lovelace]
[36]
[London]

逗号会从流中移除且不会被保存,和默认形式中换行符的处理方式一样。注意,使用自定义分隔符时,换行符就是普通字符。如果直接对多行文件调用 std::getline(file, field, ','),一行的最后一个字段会和下一行的第一个字段连在一起返回,比如 "London\nGrace Hopper"。每次读取一行、再分别拆分每一行,就能避免这个问题。字符串流页面更详细地介绍了 std::istringstream,包括如何把 "36" 这样的字段转换成数字。

std::getline 与 cin.getline

还有一个名字相近的成员函数:

char buffer[20];
std::cin.getline(buffer, 20);        // member function, fixed-size char array

std::string text;
std::getline(std::cin, text);        // free function, std::string

cin.getline 写入一个 char 数组,最多存储 size - 1 个字符,外加结尾的 '\0'。如果一行更长,流会进入失败状态,之后的每次读取都会被跳过,直到你调用 std::cin.clear()。使用 std::string 的 std::getline 则不需要操心长度限制。除非你在使用要求 char 数组的 API,否则请选用它。

常见错误

  • 混用 >> 和 getline,却没有清除换行符。 在两者之间加上 ignore(...) 或使用 std::ws。输入和输出页面展示了 std::cin 中的同一个问题,以及数字读取失败后如何恢复。
  • 用 int 调用 getline(std::cin, number)。 getline 只能读入 std::string。先读取这一行,再用 std::stoi 或 std::istringstream 转换。
  • 以 eof() 作为循环条件。 直接判断 getline 调用本身:while (std::getline(input, line))。
  • 忘记 #include <string>。 用于字符串的 std::getline 声明在 <string> 中。有些编译器通过 <iostream> 间接包含了它,这样的代码换一个编译器就会报错。
  • 没有检查文件是否打开成功。 路径错误时 std::ifstream 会静默失败,读取循环一次也不执行。

常见问题

C++ 中 getline 是做什么的?

std::getline(stream, text) 从 stream 读取字符到 std::string 类型的 text 中,直到遇到换行符,然后把这个换行符从流中移除,但不存入字符串。和 cin >> text 不同,它会保留空格,所以能读取一整行,比如完整的姓名或一句话。

为什么在 cin >> 之后 getline 会被跳过?

cin >> number 读到你在数字后面敲的换行符就停下,并把这个换行符留在输入缓冲区里。接下来的 getline 立刻遇到它,于是返回空字符串。先用 std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); 丢掉它,或者调用 std::getline(std::cin >> std::ws, text) 跳过开头的空白字符。

如何让 getline 使用换行符以外的分隔符?

把分隔符作为第三个参数传入:std::getline(stream, field, ',')。它会读到下一个逗号,保存逗号之前的文本,并丢弃这个逗号。此时换行符被当作普通字符,所以通常会先读一行,再配合由这一行构造的 std::stringstream 使用。

C++ 中如何逐行读取文件?

用 std::ifstream file("data.txt"); 打开文件,然后循环 while (std::getline(file, line)) { ... }。getline 返回流本身,没有更多行时流会转换为 false,所以循环在文件末尾停止。

cin.getline 和 getline 有什么区别?

std::getline(std::cin, text) 是一个自由函数,读入 std::string,字符串会自动增长以容纳任意长度的行。std::cin.getline(buffer, size) 是成员函数,读入固定大小的 char 数组,行长度超过 size - 1 个字符时会失败。除非你有理由使用原始数组,否则请用 std::getline 配合 std::string。

Coddy 编程语言插图

和 Coddy 一起学编程

开始