流¶
该笔记基于课程CS106L的学习,用于记录一些cpp的重要特性以及先前不曾了解的cpp特性。
在C++中,流(stream
)是一个十分重要的概念,它是I/O(Input/Output, 输入输出)的一般抽象,表示数据的流动方向和方式。
Note
抽象(Abstractions)通常为各种操作提供一个统一的接口(Interface)。在这里,stream
就是数据读写的接口。
标准输入输出流¶
最常用的标准输入输出流就是cin
和cout
了,他们工作时分别从控制台读取数据和向控制台输出数据。
在标准输入输出流中,还有两个输出流:
-
cerr
:标准错误输出流,用于输出错误信息。与cout
的不同在于不会被缓冲,会立即输出 -
clog
:标准日志输出流,用于输出非关键日志信息。与cerr
类似,但会进行缓冲
更多信息可参考Difference between cerr and clog | GeeksForGeeks
std::cin
/std::cout
¶
#include <iostream>
int main() {
double pi;
std::cin >> pi;
// verify the value of pi
std::cout << pi << '\n';
return 0;
}
3.14
,终端最终返回1.57
。 这里就会有一个疑问:从终端读取的数据显然是数据的字符表示形式,而程序中的pi
是double
型的,中间是否有什么处理或转换的过程呢?
答案是肯定的。作为I/O的一般抽象,stream
允许以一种通用的方式处理来自外部的数据。
本质上,所有的stream
都可以归为Input stream(I)
和Output stream(O)
中的一种。对于相同类型的输入输出流,它们在数据源/目标是互补的。在后面的章节中,我们还会详细介绍这两个流。
字符串流¶
字符串流将字符串视为流,用于在内存中处理数据,在处理多中数据类型混合的应用场景是一个高效的处理接口。
std::stringstream
示例:
#include <string>
#include <iostream>
#include <sstream>
void foo() {
/// partial Bjarne Quote
std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";
/// create a stringstream
std::stringstream ss(initial_quote);
// another way to insert 'initial_quote'
// std::stringstream ss;
// ss << initial_quote;
/// data destinations
std::string first;
std::string last;
std::string language, extracted_quote;
ss >> first >> last >> language >> extracted_quote;
std::cout << first << " " << last << " said this: "<< language << " " << extracted_quote << std::endl;
}
int main() {
foo();
return 0;
}
initial_quote
创建了一个字符串流ss
,并通过>>
(输出流操作符)将流数据++从原始数据移动到first
、last
等目的地++。这就是流的作用,即将数据从内存中的一个地方移动到另一个地方。将数据比作货物,流就是装载货物的货车,而创建数据流的过程就是将货物装车的操作。 但上面的程序存在一个小小的bug:
这是上述程序编译并执行的结果:
Bjarne Stroustrup said this: C makes
通过数据流,我们将变量字符串变量initial_quote
的第一第二以及第三个单词分别从字符串流ss
移动到了字符串变量first
、last
和language
上。接下来,我们的预期是将initial_quote
的剩余部分全部赋给extracted_quote
,但是>>
(输出流操作符)在读取数据时遇到空格就会停止,因此数据流只转移了一个单词。
解决方法是使用std::getline()
:
#include <iostream>
#include <string>
#include <sstream>
void foo() {
/// partial Bjarne Quote
std::string initial_quote = "Bjarne Stroustrup C makes it easy to shoot yourself in the foot";
/// create a stringstream
std::stringstream ss(initial_quote);
/// data destinations
std::string first;
std::string last;
std::string language, extracted_quote;
ss >> first >> last >> language;
std::getline(ss, extracted_quote);
std::cout << first << " " << last << " said this: \'" << language << " " << extracted_quote + "‘" << std::endl;
}
int main() {
foo();
return 0;
}
下面是std::getline()
的定义:
istream& getline(istream& is, std::string& str, char delim)
std::getline()
读取输入流is
,直到遇到字符型分隔符delim
,并将数据存入字符串型缓存str
中。其中delim
的默认值为\n
。 输出流¶
std::cout
¶
Output Stream
用于将数据写入目标地址或外部设备,例如std::cout
将数据写入控制台。实际操作时,我们使用操作符<<
将数据写入输出流。
输出流的数据在加载至目标区域前会事先存储在中间缓存中:
Buffer
double n = 5.50 ------------------------- ---------
std::cout << n; ====> | 5 | . | 5 | 0 | | | ======> |>_ |
------------------------- | |
---------
std::cout
输出流是行缓冲流。缓冲区中的数据不会显示在控制台上,直到缓冲区执行刷新(flush)操作。
std::endl
¶
std::endl
用于提示cout
当前数据流到达行末,需要进行换行操作。
int main() {
for (int i=0; i < 5; i++) {
std::cout << i << std::endl;
}
return 0;
}
0
1
2
3
4
如果去掉上面的std::endl
,结果就会变成这样:
01234
换行的同时,std::endl
还会提示流进行刷新(flash)操作,下面是该过程的可视化:
Buffer
------------------ flash ------------------ flash
| 1 |'\n'| | | ===> | 2 |'\n'| | | ===> ......
------------------ ------------------
每个数在被放入流后都会立即刷新,直接输出到控制台上。使用\n
的情况相同,详情可参考std::endl | cppreference
文件输出流¶
文件输出流用于将数据流写入文件,其具有数据类型std::ofstream
。在实际操作中,我们使用操作符<<
将数据流传输至文件。
下面是具体用法:
#include <fstream>
int main() {
/// associating file on construction
std::ofstream ofs("hello.txt");
if (ofs.is_open()) {
ofs << "Hello CS106L !" << '\n';
}
ofs.close();
ofs << "this will not get written";
/* try adding a 'mode' argument to the open method, like std::ios:app
* What happens?
*/
ofs.open("hello.txt");
ofs << "this will though! It’s open again";
return 0;
}
要使用文件输出流,我们首先要创建一个具有类型std::ofstream
的流。上面的示例中:
-
ofs(hello.txt)
创建了一个指向hello.txt
的文件输出流ofs
-
使用
is_open()
检查文件输出流是否打开 -
使用
<<
尝试写入数据 -
写入第一行数据后,使用
close()
关闭文件输出流 -
文件关闭后,无法向文件中写入数据
-
使用
open()
再次打开文件输出流ofs
-
打开文件输出流后,可继续向文件写入数据
在关闭文件输出流并进行再次打开的操作时,如不希望已写入文件的数据被覆盖,可在open()
方法的参数中添加追加模式的标签:
ofs.open("hello.txt", std::ios::app)
文件输入流¶
文件输入流用于从文件读取数据,本质与文件输出流相同。
假设有文件input.txt
,其内容如下:
line1
line2
#include <fstream>
#include <iostream>
int main() {
std::ifstream ifs("input.txt");
if (ifs.is_open()) {
std::string line;
std::getline(ifs, line);
std::cout << "Read from the file: " << line << '\n';
}
if (ifs.is_open()) {
std::string lineTwo;
std::getline(ifs, lineTwo);
std::cout << "Read from the file: " << lineTwo << '\n';
}
return 0;
}
Read from the file: line1
Read from the file: line2
输入流¶
在文件流中我们简要了解了文件输入流的用法,下面我们将详细学习输入流的概念与应用。
输入流用于从目标或外部数据源读取数据,其具有数据类型std::istream
。实际操作中,我们使用>>
从输出流中读取数据。
std::cin
¶
与std::cout
相同,std::cin
也是行缓冲流。可将std::cin
的行缓冲区理解为用户暂存数据,随后从中读取数据的区域。
需要注意的是,std::cin
的缓冲区遇到空格时会停止接受数据。
int main() {
double pi;
std::cin;
std::cin >> pi;
std::cout << pi << '\n';
return 0;
}
在上面的示例中:
-
最开始时缓冲区为空,所以首个
std::cin
会提示用户进行输入 -
到第二个
std::cin
时,缓冲区中不为空,所以cin
会从其中读取数据,直到遇到空格,并将数据存入变量pi
在日常开发中,我们通常直接将输入操作与数据流转移写在同一个语句:
int main() {
double pi;
std::cin >> pi;
std::cout << pi << '\n';
return 0;
}
与在了解字符串流时遇到的一个问题类似,std::cin
在从目标读取数据时,遇到空格就会停止读取数据:
#include <iostream>
void cinGetlineBug() {
double pi;
double tao;
std::string name;
std::cin >> pi;
std::cin >> name;
std::cin >> tao;
std::cout << "my name is : " << name << " tao is : " << tao
<< " pi is : " << pi << '\n';
}
int main() {
cinGetlineBug();
return 0;
}
3.14
Benjamin C
my name is : Benjamin tao is : 0 pi is : 3.14
cin
缓冲区不为空,因此它在读取数据时遇到空格后就立刻停止继续读取数据: Buffer
-----------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n|
-----------------------------------
^
stop read data here
那么有了之前字符串流的修复经验,你可能会给出以下修复版本:
#include <iostream>
void cinGetlineBug() {
double pi;
double tao;
std::string name;
std::cin >> pi;
std::getline(std::cin, name);
std::cin >> tao;
std::cout << "my name is : " << name << " tao is : " << tao
<< " pi is : " << pi << '\n';
}
int main() {
cinGetlineBug();
return 0;
}
3.14
Benjamin C
my name is : tao is : 0 pi is : 3.14
事实上,第二个数据并不是“丢失了”,而是getline()
的特性导致的:
在介绍字符串流时,我们曾介绍过std::getline()
的定义,其中提到了,getline()
默认将\n
作为字符分隔符,并在遇到它时“消耗它”并停止继续读取数据,那么针对上面失败的修改我们可以想象出如下可视化过程:
Buffer std::cin >> pi;
-----------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n|
-----------------------------------
^ pi: 3.14
||
\/
std::getline(std::cin, name);
-----------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n|
-----------------------------------
^ pi: 3.14
name: ""
std::cin >> tao;
-----------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n|
-----------------------------------
^^^^^^^^^^^^^^^^^^^^
The buffer is not empty, and cin try to read the part, but 'tao' is double type!
pi: 3.14
name: ""
tao: 🗑
那么应该如何修复这个问题呢?
既然getline()
在遇到\n
时会“消耗它”并停止读取数据,那么我们不妨在第一个getline()
消耗\n
后在添加一个getline()
来读取name
的内容:
#include <iostream>
void cinGetline() {
double pi;
double tao;
std::string name;
std::cin >> pi;
std::getline(std::cin, name);
std::getline(std::cin, name);
std::cin >> tao;
std::cout << "my name is : " << name << " tao is : " << tao
<< " pi is : " << pi << '\n';
}
int main() {
cinGetline();
return 0;
}
3.14
Benjamin C
5
my name is : Benjamin C tao is : 5 pi is : 3.14
其可视化过程如下:
Buffer std::cin >> pi;
----------------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n| | |
----------------------------------------
^ pi: 3.14
||
\/
std::getline(std::cin, name);
----------------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n| | |
----------------------------------------
^ pi: 3.14
name: ""
||
\/
std::getline(std::cin, name);
----------------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n| | |
----------------------------------------
^
pi: 3.14
name: "Benjamin C"
||
\/
std::cin >> tao;
----------------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n| | |
----------------------------------------
^
The stream now is empty, so is going to promot user for input!
pi: 3.14
name: "Benjamin C"
tao:
||
\/
std::cin >> tao;
----------------------------------------
|3|.|1|4|\n|B|e|n|j|a|m|i|n| |C|\n|5|\n|
----------------------------------------
^
pi: 3.14
name: "Benjamin C"
tao: 5(double)
事实上,在实际应用的过程中,由于cin
和getline()
解析数据的方式有所差异,我们并不会在一个场景内同时使用二者。但确有需求的话,像上面的操作也是可行的,但还是不建议这样做。
Assignment1: SimpleEnroll¶
这次作业要求学生补全实现三个函数,用于实现CSV
文件的数据处理,考验学生对文件输入输出流的掌握程度。作业难度不算大,同时还涉及了一小部分容器部分的知识点(虽然但是容器部分比较常用的也就std::vector
)
作业个人实现:CS106L-Assignments