2408 字
12 分钟
Stream

概述#

Streams(流) 是 C++ 的输入输出抽象。

C 语言主要使用:

printf(...);
scanf(...);

C++ 引入 stream:

cout << x;
cin >> x;

stream 的核心特点:

  • type-safe:根据变量类型自动选择输入输出方式;
  • extensible:可以为用户自定义类型重载 operator<<operator>>
  • object-oriented:把输入输出设备抽象成对象;
  • 支持 chaining:cout << a << b << ccin >> a >> b >> c

目录#


为什么使用 streams#

优点#

C++ stream 相比 C 风格 printf / scanf 的优势:

  1. Type safety

    cout << x 会根据 x 的类型选择正确输出方式,不需要手写 %d%f%s

  2. Extensible

    用户自定义类型也可以支持:

    cout << point;
    cin >> point;

    只要重载 operator<<operator>>

  3. More object-oriented

    输入输出设备被统一抽象成 stream object,例如 cincoutifstreamostringstream

缺点#

stream 也有代价:

  • 语法有时比 printf 更啰嗦;
  • 可能比 C I/O 慢;
  • C++20 后可以用 std::format 改善格式化输出的可读性。

如果需要提高 cin / cout 性能,可以关闭和 C I/O 的同步:

std::ios::sync_with_stdio(false);

常见竞赛写法还会解除 cincout 的绑定:

cin.tie(nullptr);

什么是 stream#

stream 是对输入输出设备的统一逻辑接口。

可以把它理解成:

程序 <-> stream <-> 设备

设备可以是:

  • standard input;
  • standard output;
  • file;
  • string buffer;
  • 其他 I/O source / sink。

stream 的特点:

  • 一维;
  • 单向;
  • cin / cout 通常不能随机访问;
  • 文件流可以支持一定程度的 random access。

Stream 命名习惯#

类型输入输出头文件
Generic streamistreamostream<iostream>
File streamifstreamofstream<fstream>
C string streamistrstreamostrstream<strstream>
C++ string streamistringstreamostringstream<sstream>

命名规律:

  • i 表示 input;
  • o 表示 output;
  • f 表示 file;
  • string 表示以字符串作为流的来源或目标。
TIP

<strstream> 是旧式 C string stream,现代 C++ 更常用 <sstream>


Stream operations#

Extractor#

extractor 从 stream 中读取值。

对应运算符:

operator>>

例子:

cin >> x;

含义:从 cin 中提取一个值,写入变量 x

Inserter#

inserter 把值插入到 stream 中。

对应运算符:

operator<<

例子:

cout << x;

含义:把 x 的文本表示写入 cout

Manipulator#

manipulator 修改 stream 的状态。

例子:

cout << hex << n;
cout << setw(20) << "OK!";

hexsetw(20) 不直接输出普通数据,而是改变后续输入输出的格式。


Text stream 与 binary stream#

stream 可以分成两类。

Text stream:

  • 处理文本字符;
  • 可能进行字符转换;
  • 例如换行符 \n 可能转换成操作系统实际的换行表示。

Binary stream:

  • 处理原始二进制数据;
  • 不做字符层面的转换;
  • 常用于保存结构化二进制文件、图片、模型参数等。

本节课主要讲 text stream。


预定义 streams#

C++ 标准库预定义了几个常用 stream:

stream含义
cinstandard input
coutstandard output
cerrunbuffered error/debug output
clogbuffered error/debug output

cerr 通常用于错误和调试信息,因为它不缓冲,输出更及时。

clog 也用于错误和调试信息,但它是 buffered。


输入:extractors#

operator>> 的基本用法#

#include <iostream>
using namespace std;
int main() {
int i;
float f;
char c;
char buffer[80];
cin >> c; // 读取一个字符
cin >> i; // 读取一个整数,通常会跳过前导空白
cin >> f >> buffer; // 读取 float 和 C-style string
}

常见类型的输入:

表达式类型输入格式类似 C I/O
charcharacter%c
short, intinteger%d
longlong decimal integer%ld
floatfloating point%g
doubledouble precision floating point%lg
char[], char*string%s
void*pointer%p

一般情况下,extractor 会跳过前导 whitespace。

int x;
cin >> x;

如果输入前面有空格或换行,>> 会先跳过,再读取整数。

自定义 extractor#

自定义输入运算符必须是两个参数的 free function:

istream& operator>>(istream& is, T& obj) {
// read obj from is
return is;
}

要求:

  • 第一个参数是 istream&
  • 第二个参数是要写入的对象引用,不能是 const
  • 返回 istream&,用于 chaining。

例如:

cin >> a >> b >> c;

等价于:

((cin >> a) >> b) >> c;

每一步都必须返回同一个 stream,下一步才能继续读取。

其他输入函数#

get():读取下一个字符。

int ch;
while ((ch = cin.get()) != EOF) {
cout.put(ch);
}

get(char* buf, int limit, char delim = '\n')

  • 读取到 limit 个字符,或读到 delimiter 为止;
  • 会在 buf 末尾追加 \0
  • 不会消耗 delimiter。

getline(char* buf, int limit, char delim = '\n')

  • 类似 get
  • 会消耗 delimiter。

其他函数:

int gcount(); // 返回上一次读取的字符数
void putback(char); // 把一个字符放回 stream
char peek(); // 查看下一个字符,但不消耗它

输出:inserters#

operator<< 的基本用法#

#include <iostream>
using namespace std;
int main() {
int i = 10;
double d = 3.14;
cout << i;
cout << d;
cout << "hello";
}

ostream 已经为内置类型提供了输出支持。

常见类型的输出:

表达式类型输出格式类似 C I/O
charcharacter%c
short, intinteger%d
longlong decimal integer%ld
floatfloating point%g
doubledouble precision floating point%lg
char[], char*string%s
void*pointer%p

自定义 inserter#

自定义输出运算符也必须是两个参数的 free function:

ostream& operator<<(ostream& os, const T& obj) {
// write obj to os
return os;
}

要求:

  • 第一个参数是 ostream&
  • 第二个参数通常是 const T&
  • 返回 ostream&,用于 chaining。

例如:

cout << a << b << c;

等价于:

((cout << a) << b) << c;

其他输出函数#

put(char):输出单个字符。

cout.put('A');

flush():强制把缓冲区内容输出。

cout << "Enter a number";
cout.flush();

endl 也会换行并 flush:

cout << "hello" << endl;

如果只需要换行,通常 \n 更轻量:

cout << "hello\n";

Formatting:manipulators#

常见 manipulators#

需要包含:

#include <iomanip>

例子:

#include <iomanip>
#include <iostream>
using namespace std;
int main() {
cout << setprecision(2) << 1230.243 << endl;
cout << setw(20) << "OK!";
}

输出大致为:

1.2e+03
OK!

常见 manipulator:

manipulator作用类型
dec, hex, oct设置整数进制input/output
endl插入换行并 flushoutput
flushflush streamoutput
setw(int)设置字段宽度input/output
setfill(ch)设置填充字符input/output
setbase(int)设置数字进制output
ws跳过 whitespaceinput
setprecision(int)设置浮点数精度output
setiosflags(long)打开指定 flagsinput/output
resetiosflags(long)关闭指定 flagsinput/output

注意:很多 manipulator 的效果会保留一段时间,影响后续 I/O。

int n;
cout << "enter number in hexadecimal" << endl;
cin >> hex >> n;

这里 hex 会让后续输入按十六进制解释。

自定义 manipulator#

可以自己定义不带参数的 output manipulator。

基本形式:

ostream& manip(ostream& out) {
// change or write something to out
return out;
}

Example :

#include <iostream>
using namespace std;
ostream& tab(ostream& out) {
return out << '\t';
}
int main() {
cout << "Hello" << tab << "World!" << endl;
}

这会在 HelloWorld! 中间插入 tab。

带参数的 manipulator 写法更复杂,本节没有展开。

Stream flags#

stream 内部有 flags,用来控制格式。

flag含义
ios::skipws跳过前导空白
ios::left, ios::right左 / 右对齐
ios::internal在符号和值之间填充
ios::dec, ios::oct, ios::hex数字进制
ios::showbase显示进制前缀
ios::showpoint总是显示小数点
ios::uppercase使用大写格式
ios::showpos正数显示 +
ios::scientific, ios::fixed浮点数格式
ios::unitbuf每次输出都 flush

设置方式:

setiosflags(flags);
resetiosflags(flags);

或者使用 stream member functions:

cout.setf(flags);
cout.unsetf(flags);

Example:给 Point 重载输入输出#

为什么 << / >> 要写成 free function#

希望写出:

cout << p;
cin >> p;

左操作数分别是:

cout // ostream
cin // istream

如果把 operator<< 写成 Point 的 member function,调用形式会变成:

p.operator<<(cout);

这不符合我们要的语法。

而且我们不能去修改标准库的 ostream 类,在里面添加一个 ostream::operator<<(Point)

因此用户自定义类型的 stream operator 通常写成 free function:

ostream& operator<<(ostream& out, const Point& p);
istream& operator>>(istream& in, Point& p);

如果需要访问 private fields,可以声明为 friend

Hidden friend 写法#

把 friend 函数直接写在 class 内部。

friend ostream& operator<<(ostream& out, const Point& p) {
return out << "(" << p.x << ", " << p.y << ")";
}

这种函数仍然是 free function,不是 member function。

它被称为 hidden friend 的原因是:

  • 函数声明和定义写在类内部;
  • 可以访问 private fields;
  • 不需要把函数定义散落在外部命名空间里;
  • 对与该类型相关的运算符尤其常用。

完整 Point 示例#

#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend ostream& operator<<(ostream& out, const Point& p) {
return out << "(" << p.x << ", " << p.y << ")";
}
friend istream& operator>>(istream& in, Point& p) {
return in >> p.x >> p.y;
}
private:
int x;
int y;
};
int main() {
Point a(1, 2);
cout << a << endl; // (1, 2)
Point b;
cin >> b; // 输入:7 8
cout << b << endl; // (7, 8)
}

几个细节:

  • operator<<Point 参数是 const Point&,因为输出不修改对象;
  • operator>>Point 参数是 Point&,因为输入要修改对象;
  • 两个 operator 都返回 stream reference,保证可以 chaining;
  • 为了支持 Point b; cin >> b;Point 需要 default constructor,这里通过默认参数实现。

Example:组合类型 LineSegment 的输出#

组合类型:线段由两个点组成。

class LineSegment {
public:
LineSegment(const Point& start, const Point& end)
: start(start), end(end) {}
friend ostream& operator<<(ostream& out, const LineSegment& s) {
return out << s.start << " - " << s.end;
}
private:
Point start;
Point end;
};

完整程序:

#include <iostream>
using namespace std;
class Point {
public:
Point(int x = 0, int y = 0) : x(x), y(y) {}
friend ostream& operator<<(ostream& out, const Point& p) {
return out << "(" << p.x << ", " << p.y << ")";
}
friend istream& operator>>(istream& in, Point& p) {
return in >> p.x >> p.y;
}
private:
int x;
int y;
};
class LineSegment {
public:
LineSegment(const Point& start, const Point& end)
: start(start), end(end) {}
friend ostream& operator<<(ostream& out, const LineSegment& s) {
return out << s.start << " - " << s.end;
}
private:
Point start;
Point end;
};
int main() {
Point a(3, 5);
Point b;
cin >> b; // 例如输入:6 7
cout << a << endl; // (3, 5)
cout << b << endl; // (6, 7)
LineSegment s(a, b);
cout << s << endl; // (3, 5) - (6, 7)
}

这里体现了 composition 的力量。

LineSegment 的输出不需要重新写“点如何输出”的细节:

return out << s.start << " - " << s.end;

因为 Point 已经定义了 operator<<,所以 LineSegment 可以直接复用它。

这说明:

小类型的抽象做完整后,大类型可以一层一层组合上去。

Stream
https://www.lazysheep2031.top/posts/oop/course11/
作者
Lazysheep
发布于
2026-05-07
许可协议
CC BY-NC-SA 4.0