在日常的编程练习中读写文件是再平常不过的场景了,虽然这算是基础中的基础了,但以前用起来总是不求甚解,有时用get有时用getline,几乎每次都是面向搜索引擎编程+能跑就行+过后就忘。。。这次整理了二者的用法和区别(其实就是抄了一下cppreference),希望加深理解,以后用的时候能信手拈来。
[What] std::basic_istream<CharT,Traits>::get
首先,这里说的get指的是专门用于读取输入流的 std::basic_istream<CharT,Traits>::get函数,具体一点就是
std::istream::get
、std::ifstream::get
、std::istrstream::get
、std::iostream::get
、std::fstream::get
、std::strstream::get
及相应的宽字符输入流类型的成员函数get。
get的作用是从输入流中读取(并释放)一个或多个字符。它有以下6种重载形式:
其中(3)(5)默认的delim
是'\n'
,(4)(6)则可以指定delim
。
get函数的终止条件如下:
-
读到
eof
:以上所有函数当读到流的结尾触发end of file
条件时,会执行setstate(eofbit)
。 -
读到
delim
:(3)(4)(5)(6)当将要读取的下一个字符c == delim
时,get会将已读取到的字符串存储并从流中释放,但字符c
并不会被释放。如果继续读取,第一个读取到的字符就是c
。 -
读满
s
:(3)(4)至多读取count-1
个字符存储至字符串s
中,因为最后一个字符是\0
。 -
没读到:如果没有读出任何字符,会执行
setstate(failbit)
。
注意:由于get默认的
delim
是'\n'
,使得在读文件时的行为表现出来就是读取一行的内容,但它又不会将'\n'
读出并释放,这也是容易与getline造成混淆之处。
[How] std::basic_istream<CharT,Traits>::get
- 下面演示了get函数的具体用法(基本抄了cppreference):
#include <sstream>
#include <iostream>
static void testGet() {
std::istringstream s1("Hello, world.\nCannot read");
std::istringstream s2("123\n|Cannot read");
// [1]
char c1 = s1.get(); // reads 'H'
std::cout << "[1] After reading " << c1 << ", gcount() == " << s1.gcount() << '\n';
// [2]
char c2;
s1.get(c2); // reads 'e'
// [3]
char str1[5];
s1.get(str1, 5); // reads "llo,"
std::cout << "[3] After reading " << str1 << ", gcount() == " << s1.gcount() << '\n';
std::cout << c1 << c2 << str1;
// [5] 读取剩余字符,读到 '\n' 停止
s1.get(*std::cout.rdbuf()); // reads " world."
std::cout << "\n[5] After the last get(), gcount() == " << s1.gcount() << '\n';
// [4] 读到 '|' 停止,'|' 未释放仍保存在流中
char str2[10];
s2.get(str2, 10, '|'); // reads "123\n"
std::cout << "[4]After reading " << str2 << ", gcount() == " << s2.gcount() << '\n';
// [6] 读到 '|' 停止
s2.get(*std::cout.rdbuf(), '|'); // reads nothing
std::cout << "[6]After the last get(), gcount() == " << s2.gcount() << '\n';
}
- 运行结果:
C++中有两种getline函数: 一种是std::basic_istream<CharT,Traits>::getline,它与上文的get函数都是istream
类的成员函数;另一种是std::getline,定义于头文件<string>
中,是std
命名空间的全局函数。
下面分别介绍这两种getline函数。
[What] std::basic_istream<CharT,Traits>::getline
getline的作用是从流中读取(并释放)字符串,直到遇到指定的delim
。常用的就是(1),它默认的delim
为'\n'
,等效于getline(s, count, widen('\n'))
。
getline函数的终止条件如下:
-
读到流的结尾,会执行
setstate(eofbit)
; -
下一个字符
c == delim
,字符c
会被读取并释放,但不会被存储; -
已经读取了
count-1
个字符,会执行setstate(failbit)
; -
如果没有读出任何字符(e.g.
count < 1
),会执行setstate(failbit)
。
[How] std::basic_istream<CharT,Traits>::getline
#include <iostream>
#include <sstream>
#include <vector>
#include <array>
static void testGetline_istream() {
std::istringstream input1("abc\ndef\ngh");
std::istringstream input2("123|456|\n78");
std::vector<std::array<char, 4>> v;
// [1]
for (std::array<char, 4> a; input1.getline(&a[0], 4);) {
v.push_back(a);
}
for (auto& a : v) {
std::cout << &a[0] << '\n';
}
v.clear();
// [2]
for (std::array<char, 4> a; input2.getline(&a[0], 4, '|'); ) {
v.push_back(a);
}
for (auto& a : v) {
std::cout << &a[0] << '\n';
}
}
-
运行结果:
可以看出,getline函数并不只局限于读取一行的数据,只是默认
delim
为'\n'
使得它的行为表现出来为读取一行而已。
[What] std::getline
std::getline
的功能是从一个输入流中读取字符串,然后存储到一个string
中。常用(2)默认的delim
也是'\n'
,等效于getline(input, str, input.widen('\n'))
。
std::getline的终止条件如下:
-
读到流结尾,会设置
eofbit
并返回; -
下一个字符
c == delim
,字符c
会被读取并释放,但不会被添加到str
中; -
已经读取了
str.max_size()
个字符,会设置failbit
并返回; -
如果没有读出任何字符,会设置
failbit
并返回。
注意:其实
std::getline
与std::basic_istream<CharT,Traits>::getline
大同小异,可以看作是一个功能的两种接口形式:一个针对C++的string
类型,另一个针对C类型字符串char *
。
[How] std::getline
static void testGetline_string() {
// [2]
std::string name;
std::cout << "What is your name? ";
std::getline(std::cin, name);
std::cout << "Hello " << name << ", nice to meet you.\n";
// [1]
std::istringstream input;
input.str("1|2|3|4|5|6|7|");
int sum = 0;
for (std::string line; std::getline(input, line, '|'); ) {
sum += std::stoi(line);
}
std::cout << "\nThe sum is: " << sum << "\n";
}
- 运行结果:
[Warning] 坑点
坑点1:使用getline(s, count)
时字符数组s
必须要预留足够的空间!!!
上文多次提到eofbit
和failbit
这两种指定流状态的标志,详情可见std::ios_base::iostate。
下面使用一个例子验证已经读取了count-1
个字符时,会执行setstate(failbit)
:
static void testGetline_failbit() {
std::istringstream input("123|4567|89");
// note: the following loop terminates when std::ios_base::operator bool()
// on the stream returned from getline() returns false
std::array<char, 4> a;
while (input.getline(&a[0], 4, '|')) {
// 读取 "123" 同时 '|' 也被读出
std::cout << "After reading " << &a[0] << ", gcount() == " << input.gcount() << '\n'; // 4
std::cout << "After reading " << &a[0] << ", tellg() == " << input.tellg() << '\n'; // 4
}
// 读取 "456" 时字符数组读满导致 setstate(failbit) 执行,跳出循环
std::cout << "After reading " << &a[0] << ", gcount() == " << input.gcount() << '\n'; // 3
std::cout << "After reading " << &a[0] << ", tellg() == " << input.tellg() << '\n'; // -1
std::cout << "After reading " << &a[0] << ", rdstate() == " << input.rdstate() << '\n'; // 2 == failbit
// 设置状态为 goodbit 可找到循环退出时文件流指针的确切位置
input.clear();
std::cout << "After clear(), tellg() == " << input.tellg() << '\n'; // 7
// 移到指针到文件开头
input.seekg(0, std::ios::beg);
std::cout << "After seekg(), tellg() == " << input.tellg() << '\n'; // 0
}
- 运行结果(具体的原因分析都已经在注释中标明):
坑点2:当使用流处理的方式读数据时,读到行尾并不会跳行!!!
get和getline读取的结果都是C/C++类型的字符串,有时为了读取其他类型的数据(e.g. int, double),我们通常会结合流处理的方式完成类型转换(流处理可以看作是C++类型转换的通杀法:万物转stream,stream转万物)。
但是,当使用流处理读到行尾时,它并不会主动跳转到下一行!如下面的代码示例:当读出123
后,流指针还是在第一行,需要调用两次getline函数使指针跳到456
所在的行。
static void testGetlineAndIstream() {
std::istringstream input("123\n***\n456");
int a, b;
std::string str;
input >> a;
std::cout << "First reading from stream: " << a << '\n';
std::getline(input, str);
std::getline(input, str);
input >> b;
std::cout << "Second reading from stream: " << b << '\n';
}
- 运行结果:
[Summary] 三者的比较
# | std::basic_istream<CharT,Traits>::get | std::basic_istream<CharT,Traits>::getline | std::getline |
---|---|---|---|
头文件 | <iostream>/<sstream>/<fstream> | <iostream>/<sstream>/<fstream> | <string> |
输出对象 | C类型字符串 | C类型字符串 | C++string对象 |
释放delim | ❌ | ❌ | ✔️ |
[Github] 代码
项目实例均在vs2017上测试,并上传至GitHub,将GetAndGetline设为启动项目即可复现实验结果。
[Reference] 参考
std::basic_istream<CharT,Traits>::get