C++的流简介和简单使用
C语言的输入与输出
C 语言中用到的最频繁的输入输出方式就是 scanf 与 printf 。
scanf :从标准输入设备 ( 键盘 ) 读取数据,并将值存放在变量中。标准输入设备对应程序中的标准输入流 stdin 。
printf :将指定的字符串输出到标准输出设备。标准输出设备对应程序中的标准输出流 stdout 。
这个标准输出设备在 Windows 叫控制台窗口 (Console Window)或命令提示符窗口 (Command Prompt),在 Linux 叫终端 (Terminal)且部分场景也叫终端模拟器 或控制台。
使用 scanf 和 printf 时要注意宽度输出和精度输出控制。
C 语言借助了相应的缓冲区来进行输入与输出。如下图所示,数据从标准输入流代表的 A 进入缓冲区,当代码生成的进程需要时,会从缓冲区获取,若缓冲区没有数据则进程会阻塞在此进行等待;若用户按下组合键 Ctrl 加 C 或 Z ,或 A 从一开始就是一个指定的文件,文件只有一个 EOF ,则程序会停止读取。

对输入输出缓冲区的理解:
1.可以屏蔽掉低级 IO 的实现。简单来说就是同一个代码,放在不同的操作系统都能运行,不必关注底层的 IO 是如何实现的。低级 IO 的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
2.可以使用这部分的内容实现 "行" 读取的行为,对于计算机而言是没有 "行" 这个概念,有了这部分,就可以定义 "行" 的概念,然后解析缓冲区的内容,返回一个 "行" 。
几乎所有的编程语言,最终都会转换成计算机中的进程运行。因此对于 IO ,可以肯定的是这些编程语言都有封装有相关的系统调用,因此学会其中一个,便能通过同样的思路去理解其他编程语言的 IO 操作。
C++的流
"流" 即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是 bit, byte, packet )的抽象描述。计算机里的物质绝大多数情况下指的是数据,因为计算机里的流的本质是数据在不同设备之间流动。
C++ 的流是指信息从外部输入设备 (如键盘)向计算机内部 (如内存)输入 ,和从内存向外部输出设备 (显示器)输出的过程 。这种输入输出的过程被形象的比喻为 "流"。它的特性 是:有序连续 、具有方向性。
为了实现这种流动,C++ 定义了 IO 标准类库,这些每个类都称为流或流类,用以完成某方面的功能。其中 ios 为基类,其他类都是直接或间接派生自 ios 类。同时 IO 类库是使用菱形虚拟继承实现。
设计菱形继承是为了让
ios类和ios_base类同时具有istream类和ostream类的功能。
下图来自 Input/Output - C++ Reference 。

实际上这个库和 C 语言的底层实现没有区别 ,原因是它们都使用同样的系统调用,都和操作系统进行直接交互。或者说这里的类库是将系统调用用 C++ 的方式封装。
在基本熟悉操作系统相关的编程和文件体系后,只需掌握用法即可,除非需要扩展流库、开发编译器和维护标准库,但这些很明显不是一般的开发者学习的东西。
当然,这个地方是 C++ 的难点之一,涉及了虚继承 + 多重继承 + 模板 + 缓冲区管理 + 格式化 + 本地化等,难度源于组件繁多、职责纠缠、设计历史包袱重,说是历史遗留的 "大便" 也不为过。所以这里的方向是先介绍如何使用,再另外开一篇结合操作系统原理进行详细讲解。
C++标准IO流
C++标准库提供了 4 个全局流对象 cin、cout、cerr、clog,使用 cout 进行标准输出,即数据从内存流向控制台 (显示器) 。
使用 cin 进行标准输入即数据通过键盘输入到程序中 ,同时 C++ 标准库还提供了 cerr 用来进行标准错误的输出 ,以及 clog 进行日志的输出。
从上图可以看出,cout、cerr、clog是 ostream 类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同。
这 4 个实现 IO 功能的类在使用时候必须要包含文件并引入 std 标准命名空间。
C 语言的
scanf、printf和perror,和 C++ 的cin、cout和cerr的使用场景类似,它们的底层都封装有所在操作系统的系统调用。
cin和cout使用举例
cin为缓冲流。键盘输入的数据保存在缓冲区中 ,当要提取时从缓冲区中拿 。使用 cin 获取从标准输入流 stdin 得到的数据,需要借助函数 operator>> 。
cpp
#include <cstdio>
#include <iostream>
using namespace std;
int main() {
int x = 3;
cin >> x;
cout << x << '\n';
cin.operator>>(x); // >>重载运算符,2种方式等同
(cout.operator<<(x)).operator<<('\n'); // 等价于cout<<x<<10
(cout.operator<<(endl)).operator<<(x) << endl;
return 0;
}
operator<<没有operator<<(char)的情况,所以operator<<('\n')选择的函数是opeartor<<(int),所以(cout.operator<<(x)).operator<<('\n');输出和cout<<x<<10相同。
若使用 cin 输入时输入错了,必须在回车之前修改,否则回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
输入的数据类型必须与要提取的数据类型一致 ,否则可能会出错。例如这里在 Linux 的 gcc、 Windows 的 gcc 和 VS 的 MSVC 下输入 2 段由英文非数字字符组成的由空格隔开的字符串, x 均被 0 覆盖,但其他编译器生成的程序不确定,这取决于编译器的行为。
空格和回车都可以作为数据之间的分格符 ,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用 cin 输入,字符串中也不能有空格。回车符也无法读入。
流插入<<和流提取>>支持自定义类型
cin 和 cout 可以直接输入和输出内置类型数据 ,因为标准库已经将所有内置类型的输入和输出全部重载了。这里参考流提取 istream::operator>> - C++ Reference 和流插入 ostream::operator<< - C++ Reference 。
cpp
// arithmetic types (1) 即内置类型
istream& operator>> (bool& val);
istream& operator>> (short& val);
istream& operator>> (unsigned short& val);
istream& operator>> (int& val);
istream& operator>> (unsigned int& val);
istream& operator>> (long& val);
istream& operator>> (unsigned long& val);
istream& operator>> (float& val);
istream& operator>> (double& val);
istream& operator>> (long double& val);
istream& operator>> (void*& val);
// stream buffers (2) 即ios类
istream& operator>> (streambuf* sb );
// manipulators (3)
istream& operator>> (istream& (*pf)(istream&));
istream& operator>> (ios& (*pf)(ios&));
istream& operator>> (ios_base& (*pf)(ios_base&));
// C++11 新增
// arithmetic types (1)
istream& operator>> (long long& val);
istream& operator>> (unsigned long long& val);
// arithmetic types (1)
ostream& operator<< (bool val);
ostream& operator<< (short val);
ostream& operator<< (unsigned short val);
ostream& operator<< (int val);
ostream& operator<< (unsigned int val);
ostream& operator<< (long val);
ostream& operator<< (unsigned long val);
ostream& operator<< (float val);
ostream& operator<< (double val);
ostream& operator<< (long double val);
ostream& operator<< (void* val);
// stream buffers (2)
ostream& operator<< (streambuf* sb );
// manipulators (3)
ostream& operator<< (ostream& (*pf)(ostream&));
ostream& operator<< (ios& (*pf)(ios&));
ostream& operator<< (ios_base& (*pf)(ios_base&));
// C++11新增
ostream& operator<< (long long val);
ostream& operator<< (unsigned long long val);
<< 和 >> 的实现还有一个好处:对于自定义类型,如果要支持 cin 和 cout 的标准输入输出,需要对 << 和 >> 进行重载,且自定义类型需要将 >> 和 >> 设置为友元或成员变量设置为公有。
cpp
#include <iostream>
#include <cstdio>
using namespace std;
class A {
private:
friend istream& operator>>(istream&,A&);
friend ostream& operator<<(ostream&, A&);
int x;
int y;
};
istream& operator>>(istream& in, A& aim) {
(in >> aim.x).operator>>(aim.y);
return in;
}
ostream& operator<<(ostream& out, A& aim) {
(out.operator<<(aim.x)) << ' ' << aim.y;
return out;
}
int main() {
A a;
cin >> a;
cout << a;
return 0;
}
cin连续输入和重载类型转换
部分在线 OJ 中的输入和输出,对于 IO 类型的算法,需要循环输入。例如这个输入模式:
cpp
int main(){
string st;
while(cin>>st){
// 算法逻辑
}
return 0;
}
连续输入时,Windows 在输入ctrl + + + Z时结束,而 Linux 则是 ctrl + + + D。
也可以使用ctrl + + + C,但在 Linux 使用这个的本质是发送导致进程终止的信号给操作系统,会强行终止当前进程,需要评估使用环境。
istream 类型对象转换为逻辑条件判断值。这里参考 istream::operator>> - C++ Reference 和 ios::operator bool - C++ Reference 。
cpp
istream& operator>> (int& val);
explicit operator bool() const;
使用 while(cin>>i) 去流中提取对象数据时,调用的是 operator>> ,返回值是 istream 类型的对象,理论上无法直接通过对象判断 bool值,但这里却可以做逻辑判断。
这是因为自 C++11 起, istream 类提供了向 bool 类型的隐式类型转换。简单来说就是 istream 内内置了 operator bool 成员函数,例如类 A 内置了 operator bool ,也可当成 bool 类来使用。
cpp
#include <cstdio>
#include <iostream>
using namespace std;
class A {
public:
A(int _x = 0) : x(_x) {}
operator bool() {
return x == 0 ? false : true;
}
private:
int x;
};
int main() {
A a, b(1);
if (a) {
cout << "xxx.\n";
} else if (b) { // 调用它
cout << "helloworld\n";
} else {
cout << "what up!\n";
}
return 0;
}
所以之前的代码的等价关系:
cpp
while(cin>>st){}\\...
// 等价于
while ((cin >> st).operator bool()) { ... } // C++11 起
即在这个场景下,istream 对象又调用了operator bool,operator bool 调用时如果接收流失败,或者有结束标志 EOF ,则返回 false。而旧标准 C++98 则使用
operator void*() ,返回非空指针(通常为流对象自身地址)表示 true,返回空指针表示 false。
类似的重载类型转换还有 operator int 、 operator double ,以及 operator class name 和 operator classname。
operator class name用于没在本类前声明过的类,operator classname用于在本类前声明过的类。
但这些符号的滥用可能会使隐式类型转换造成歧义,因此推荐使用 explicit 关键字阻止隐式转换。
文件IO流ifstream和ofstream
C++ 根据文件内容的数据格式分为二进制文件 和文本文件。采用文件流对象操作文件的一般步骤:
-
定义一个文件流对象。
ifstream ifile,ofstream ofile,fstream iofile。
-
使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系。
-
使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写。
-
关闭文件。
这和 C 语言的 fopen 打开文件,各种文件操作结束后,使用 fclose 的流程,以及 Linux 的 open 系统调用打开文件,各种文件操作结束后,close 关闭文件的流程基本相似。这里整理一张表格,对比之下即可快速上手 istream 类和 ostream 类。
| 操作 | C(fopen/fclose) | Linux系统调用(open/close) | C++ (文件流) |
|---|---|---|---|
| 对象类型 | FILE* | int(文件描述符) | ifstream, ofstream, fstream 对象 |
| 打开文件 | fopen("file.txt", "r") | open("file.txt", O_RDONLY) | ifstream fin("file.txt")或 fin.open("file.txt") |
| 错误处理 | 检查返回值是否为 NULL(毕竟是结构体指针) | 检查返回值是否为 -1 | 检查流对象状态,如if(!fin)或fin.is_open() |
| 读写操作 | fprintf, fscanf, fread, fwrite 等 | read(), write()等 | 文本: 插入<<和提取>>运算符, 二进制: read(), write()方法 |
| 关闭文件 | fclose(file_ptr) | close(fd) | 主动调用fin.close()或由析构函数自动关闭 |
| 随机访问 | fseek, ftell | lseek | seekg() (读), seekp() (写), tellg(), tellp() |
| 头文件/库 | stdio.h / cstdio | fcntl.h, unistd.h 等 | fstream |
简单使用ifstream
这里简单使用 ifstream 对象读取文件。实际上 cin 本身就是 istream 对象,但它代表的是键盘,因此可以模仿着使用。
cpp
#include <fstream>
#include <iostream>
#include <string>
using std::ifstream;
using std::ofstream;
using std::string;
int main() {
// 按只读的方式打开文件
ifstream test("test.txt"); // 也可以ifstream test;test.open("test.txt");
// if (!test) { // 这里和if (!test.is_open())二选一即可
if (!test.is_open()) {
std::cout << "文件test.txt不存在\n";
return 0;
}
string st;
test >> st; // 欲读取hello, world,奈何不读空格
std::cout << st << '\n';
// 达到和C语言的rewind一样的效果
test.clear(); // 习惯性清除状态位,保证后续操作不受影响
test.seekg(0, std::ios::beg); // 相当于 rewind
std::getline(test, st); // 直接读取整行
std::cout << st << '\n';
return 0;
}
若文件 test.txt 的内容为 "Hello, world! " ,则代码输出
Hello,
Hello, world.
其中 istream 的成员函数 istream::seekg 和 getline 参考:
cpp
istream& seekg (streampos pos);
istream& seekg (streamoff off, ios_base::seekdir way);
// C++98
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
// C++11 新增移动构造
istream& getline (istream&& is, string& str, char delim);
istream& getline (istream&& is, string& str);
代码中的 test.seekg(0, std::ios::beg):0 是从文件开始处偏移的字节数,std::ios::beg 表示从文件头开始计算。这和 fseek(fp, 0, SEEK_SET) 功能相同。
代码中的test.clear():虽然不是每次都需要,但当流之前因为读到文件尾(EOF)或发生格式错误而进入失败状态时,必须先调用 clear() 清除状态,否则 seekg 可能会被忽略。
代码中的getline:从 istream 中读取一行字符串。一般常用 getline(cin,st) 从键盘输入到缓冲区的数据中读取一行。
简单使用ofstream
这里简单使用 ofstream 对象向文件写入。写入分完全覆盖和尾部追加。
完全写入的代码示例:
cpp
#include <fstream>
#include <iostream>
#include <string>
using std::ifstream;
using std::ofstream;
using std::string;
string st = "what up!\n";
void _f1(ofstream &test) {
test << st;
test.close(); // 主动关闭文件,更换打开模式
}
void f1() {
ofstream test("test.txt");
_f1(test);
test.open("test.txt", std::ios::app); // 以追加方式重新打开
test << st; // 这时文件内可以看到2行what up!
}
void f2() {
ofstream test;
test.open("test.txt",
ofstream::out | ofstream::trunc); // 先清空,再写入
test << st;
}
int main() {
// f1(); // f1()之后文件内能看到2行what up!
f2(); // 先清空,再写入
return 0;
}
这里的 app 表示以追加的方式打开文件。使用时类似 Linux 的 open 系统调用,将一堆标识符进行或运算代表文件属性。
这里对常用的属性列举表格进行描述:
| 文件打开模式 | 说明 |
|---|---|
| std::ios::in | 为输入(读)而打开。 |
| std::ios::out | 为输出(写)而打开。 |
| std::ios::app | 所有写入都追加到文件末尾。 |
| std::ios::binary | 以二进制模式打开,用于非文本数据。 |
| std::ios::ate | 打开后立即将读写位置移到文件末尾。 |
| std::ios::trunc | 如果文件已存在,先清空其内容再打开。 |
处理非文本数据时必须使用 read 和 write 成员函数,并配合 std::ios::binary 打开模式。
stringstream
在C语言中,如果想要将一个整形变量的数据转化为字符串格式,如何去做?
-
使用
itoa()函数。原型char* itoa(int value,char* str,int base);。 -
使用C语言自带的
sprintf()函数。原型int sprintf(char* str, const char* format,...);。
但是这 2 个函数在转化时,都得需要先给出保存结果的空间,空间给多大不好界定,而且转化格式不匹配时,可能还会得到错误的结果甚至程序崩溃。
在 C++ 中,可以使用 stringstream 类对象来避开此问题。
在程序中如果想要使用 stringstream,必须要包含头文件 sstream 。在该头文件下,标准库三个类:istringstream、ostringstream 和 stringstream,分别用来进行流的输入、输出和输入输出操作,这里主要介绍stringstream。
若观察
ios_base类的菱形虚拟继承组件,可发现stringstream是最后一层。
stringstream主要可以用来:
- 将数值类型数据格式化为字符串。
- 字符串拼接。
- 序列化和反序列化结构数据。
数值格式化
stringstream 实际是在其底层维护了一个string类型的对象用来保存结果。所以多次数据类型转化时,一定要用clear()来清空,才能正确转化,但 clear()不会将stringstream底层的string对象清空,因此还需要使用s. str("")方法将底层string对象设置为 ""空字符串。
此外可以使用s.str()将让stringstream返回其底层的string对象。
stringstream 其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险。
将数值类型数据格式化为字符串参考:
cpp
#include <iostream>
#include <sstream>
#include <string>
using std::cout;
using std::endl;
using std::string;
using std::stringstream;
void f1() {
int a = 12345678;
string sa;
// 将一个整型变量a转化为字符串,存储到string类对象中
stringstream s;
s << a;
s >> sa;
cout << sa << endl;
}
void f2() {
// f1()的逻辑
int a = 12345678;
string sa;
stringstream s;
s << a;
s >> sa;
s.str("");
// 将stringstream底层管理string对象设置成 "",避免结果堆积
s.clear();
// 注意多次转换时,必须使用clear将上次转换状态清空掉
// 清空s, 不清空会转化失败
double d = 12.34;
s << d;
s >> sa;
cout << sa << endl; // 不做处理
string sValue;
sValue = s.str();
// str()方法:返回stringsteam中管理的string类型
cout << sValue << endl;
}
void f3() {
int a = 12345678;
string sa, sa2;
stringstream s;
s << a;
s >> sa;
cout << sa << endl; // 用完后不做处理
// s.str("");
// s.clear();
double d = 1145.14;
s << d;
s >> sa2;
cout << sa2 << endl; // 读取不到信息
string sValue;
sValue = s.str();
cout << sValue << endl;
}
void f4() {
int a = 12345678;
string sa;
stringstream s;
s << a;
s >> sa;
cout << sa << endl;
// s.str("");
s.clear(); // 只清空
double d = 1145.14;
s << d;
s >> sa;
cout << sa << endl;
string sValue;
sValue = s.str();
// 这里可以看到很明显的堆积
cout << sValue << endl;
}
int main() {
// f1(); // 简单的数字转换成字符串
// f2(); // 多次转换
// f3(); // 演示不处理stringstream的情况
f4(); // 演示不使用空字符串覆盖,只清空的情况
return 0;
}
4 个子程序分别输出:
// f1
12345678
// f2
12.34
12.34
// f3
12345678
12345678
// f4
12345678
1145.14
123456781145.14
字符串拼接
cpp
#include <iostream>
#include <sstream>
#include <string>
using std::cout;
using std::endl;
using std::string;
using std::stringstream;
int main() {
stringstream sstream;
// 将多个字符串放入 sstream 中
sstream << "Hello," << " " << "world";
sstream << ".\n";
cout << "resoult: " << sstream.str() << endl;
// 清空 sstream
sstream.str("");
sstream << "what up!";
cout << "resoult: " << sstream.str() << endl;
return 0;
}
输出:
resoult: Hello, world.
resoult: what up!
这里第 1 次拼接有加 \n ,第 2 次没有。所以第 2 次输出没有换行。
序列化和反序列化
简单理解就是把各种信息转换成字符串 的过程叫序列化 ,把字符串里面的信息提取出来再转换成内存的数据结构 ,这个过程叫做反序列化。
实践当中的序列化和反序列化更多的是用于网络当中。网络可以将多个计算机抽象成一个对象,这些对象需要建立连接进行数据通信,通信的格式是 JSON 数据。JSON 数据由大量键值对组成,将这些键值对翻译成程序可处理的信息叫反序列化,反之将程序内的数据转换成 JSON 数据叫序列化,JSON 格式数据尽管由多个键值对组成,但整体还是一个字符串。
一般情况下 C++ 标准库都不带 JSON 库,都是用的第三方库,使用时还需要安装。详细见网络编程部分。