%%
# 纲要
> 主干纲要、Hint/线索/路标
- **I/O 库及相关头文件**
- **I / O 标志位**;
- **状态标志位**有哪些?含义是什么?如何**设置/清除**?
- EOF 标志
- **格式标志位**有哪些?含义是什么?如何**设置/清除**?
- 格式化控制:例如保留小数点后几位,输出十进制、十六进制;
- **输入输出缓冲区**
- 缓冲区大小、
# Q&A
#### 已明确
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^no808o]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^snv0v5]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^107nq8]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^ljdupg]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^m99kh1]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^zx67ht]]
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# C++ I/O 库
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-00569A872E9B6A52D23C53211B6B1390.png|800]]
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-9828E9822B276FA30B0B7A4E521E3E3E.png|889]]
C++ 标准库提供的 IO 处理相关的头文件包括:
- `<iostream>` 头文件:定义了读写 "**流**" 的 I/O 类及操作(三种流:**标准输入、标准输出、标准错误**,通常关联到**控制台窗口和键盘**)
- `<fstream>` 头文件:定义了读写 "**文件**" 的 I/O 类及操作
- `<sstream>` 头文件:定义了读写 "**string 对象**(内存中)" 的 I/O 类及操作
> [!NOTE] fstream 类与 stringstream 类都是继承自 iostream 类
> [!caution] IO 对象不支持拷贝和赋值!
<br><br>
### `iostream` 头文件
C++ 标准库的头文件 `<iostream>` 中提供了以下 I/O 相关内容:
- 类定义:
- `istream` 类(输入流),提供输入操作
- `ostream` 类(输出流),提供输出操作
- 全局的 `stream` 对象:
- `cin`:一个 `istream` 类对象,从 "**标准输入**" 读取数据;
- `cout`:一个 `ostream` 类对象,向 "**标准输出**" 写入数据;
- `cerr`:一个 `ostream` 类对象,向 "**标准错误**" 写入数据;
- `clog`:一个 `ostream` 类对象
- 操作符与函数:
- `>>` :**提取**运算符,用于**从一个 "`istream`" 对象**中读取输入数据;
- `<<` :**输出**运算符,用于**向一个 "`ostream`" 对象**中写入数据。
> [!NOTE]
>
> - `ifstream`、`istringstream` 都继承自 `istream`,因此对这两类对象可以使用 `>>` 操作符和 `getline` 函数。
> - `ofstream`、`ostringstream` 都继承自 `ostream`,因此对这两类对象可以使用 `<<` 操作符。
<br><br><br>
# C++ I/O 流类的继承体系
参见 [^1]
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-D390E53E7A57405FCEB815A304B94E12.png|588]]
- `basic_ios` 的派生类( `iostream`等)只处理 "**==数据的格式化==**";
- `basic_ios` 派生类所维护的 **stream buffer** 完成**实际读/写动作**。
- **==Stream Buffer 提供读/写时所使用的字符缓冲区==,并形成对外部数据源(文件或字符串)的抽象**
具体来说:
- `ios_base`:表示流的一般属性比如文件是否打开、是二进制流还是文本流等等
- `ios`:基于 `ios_base`,并且包含了一个指针成员**指向一个 `streambuf` 对象**
- `ostream`:继承自 ios 类并提供了输出方法
- `istream`:继承自 ios 类并提供了输入方法
- `iostream`:继承自 ostream 类和 istream 类
- `streambuf`:该类为缓冲区提供内存(**提供缓冲区**),并提供了**填充缓冲区**、**访问缓冲区内容**、**刷新缓冲区**、**管理缓冲区内存** 的类方法;
> [!NOTE]
> ios 类包含一个指针,**指向 `streambuf` 对象**,而 istream 和 ostream 是其子类,
> 所以 cin 和 cout 其实都是从 **streambuf 表示的缓冲区** 中读取数据。
>
> 创建 istream 或 ostream 对象(如 `cin`、`cout`)时将**打开一个流**,**自动创建缓冲区**,并**将其与流关联起来**。
>
>
<br><br><br>
# I/O 流关联的输入输出缓冲区
每一个 **I/O 流对象**都包含一个 **`streambuf` 对象** 的成员指针。
该 `streambuf` 对象为 I/O 流提供了 "**输入/输出缓冲区**",提供了**填充缓冲区**、**访问缓冲区内容**、**刷新缓冲区**、**管理缓冲区内存**等功能的成员函数。
![[02-开发笔记/99-Unix 环境编程/文件 IO#^015hqk]]
<br><br><br>
# I/O 标志位
C++ 标准库的 **I/O 类**中定义了一些 **==标志位==** ( 值以 "**全局常量**" 的形式给出) 及 **==检查函数==**、**==操控符==**,用于 **==表示和管理 I/O 流的状态==**。
标志位包括两类:
- **==状态==标志位**(State Flag):表示 **I/O 流的状态**,
- 通过对应的「**==检查函数==**」来**获取状态**
- **==格式==标志位**(Format Flag):控制输出输出**格式**,如进制、对齐方式、小数点位数等,
- 通过对应的 「**==操控符==(Manipulator)**」来**进行控制**
<br>
## 「I/O 状态标志位」
**状态标志位**(State Flag):每个 IO 对象都维护有一组 "**状态标志位**",表示 **I/O 流的状态**,指示是否可在该对象上进行 IO 操作。
> [!NOTE] IO 库的条件状态位及操作函数
>
> 参见[^2]
>
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-9C5E6301F0A7FD33B655C902B3D984A9.png|701]]
>
<br>
### 四种状态标志位
| 状态标志位 (常量值) | 标志位含义 | 检查函数 | 函数说明 |
| --------------- | ------------------------------------------ | ---------- | ---------------------------------- |
| `strm::goodbit` | 表示 **流处于良好状态**,没有错误 | `s.good()` | `goodbit` 置位时返回 `true`,表示没有任何错误 |
| `strm::eofbit` | 表示**流已到达文件结束** (EOF) | `s.eof()` | `eofbit` 置位时返回 `true`,表示到达文件结束 |
| `strm::failbit` | 表示**一个 IO 操作失败**<br>(格式错误、逻辑错误等原因) | `s.fail()` | `failbit` 或 `badbit` 置位时返回 `true`; |
| `strm::badbit` | 表示**流已崩溃,发生了不可恢复的错误**<br>(如 I/O 操作被中断硬件故障) | `s.bad()` | `badbit` 置位时返回 `true` |
> [!info] 四个状态标志位是 `std::ios::iostate` 类型
>
> `std::ios::iostate` 是 IO 库中定义的 "**机器无关**" 的类,提供了表达 "**I/O 状态**" 的完整功能,存储了 I/O 类的所有条件状态位情况。
> [!NOTE]
> - `std::ios` 是`std::istream` 和 `std::ostream` 的基类,**包含了用于格式化和控制流状态的常量和类型定义**;
> - `s` 是 "**IO 流**" 对象,例如 `cin`、`cout`,检查函数是 "**IO 流对象**" 的成员函数。
> - 可**直接获取**流对象的标志位值,例如 `cin.goodbit`、`cin.failbit`;
<br>
#### 文件结束标志 EOF
> <font color="#c0504d">❓文件结束标志 EOF 是什么?</font> ^no808o
EOF(End Of File,**文件结束标志**)是一个**指示==文件或流已到达末尾==的状态标志**。
当**输入流**到达文件尾时,会触发流结束标志 EOF。
```cpp
// 下方代码, 只要存在有效输入, 循环将无止境.
// 直到文件尾, cin.get(ch)返回的cin对象带有EOF标志位, 被判定为False,循环终止;
char ch;
while (cin.get(ch)) {
// process input
}
int ch; // 这里应当使用int而不是char, 因为值EOF可能无法用char类型表示;
ch = cin.get();
while ((ch = cin.get()) != EOF) {
//process input
}
```
#### 如何在输入流中触发 EOF 标志
> <font color="#c0504d">❓如何在标准输入流(控制台终端)中触发 EOF 标志?</font> ^snv0v5
对于键盘输入,键盘仿真的文件尾为:
- `DOS` 和 `Windows` 命令提示符模式,为按下`Ctrl+Z`;
- `^Z`(`ctrl+z` 在终端中显示的结果)**不会产生字符,是空字符**。
- **输入流的结束条件是:`^Z`之前没有任何字符输入(回车除外),否则`^Z` 起不到流结束的作用。**
- 对于 Unix,为**在行首**按下 `Ctrl + D`
`cin>>`, `cin.get(ch)` 遇见`EOF` 时结束输入。
> [!NOTE] windows 下通过 `ctrl+z` 模拟 EOF
>
> 
>
### 复位/置位函数
| 函数 | 函数说明 |
| ------------------- | --------------------------------------------- |
| `s.clear()` | **清除(复位)流 `s` 的所有错误状态**,返回 `void` |
| `s.clear(flags)` | **清除(复位)** 所有标志位后,**将 flags 标志位置位**,返回 `void` |
| `s.setstate(flags)` | **置位**流`s` 的 flags 标志位,返回 `void` |
| `s.rdstate()` | **返回流 `s` 的当前条件状态**,返回类型为 `std::ios::iostate` |
#### 示例
```cpp
// 置位EOF
cin.setstate(std::ios::eofbit);
// 清除所有标志位
cin.clear(); // 清除
// 清除fallbit与badbit, 而保留其它所有标志位(包括格式标志位)
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit);
// 清除eof, 而保留其它所有标志位(包括格式标志位)
cin.clear(cin.rdstate() & ~cin.badbit);
```
<br><br>
## 「I/O 格式标志位」
**==格式==标志位**(Format Flag):控制输出输出**格式**,如进制、对齐方式、小数点位数等。
一部分**格式标志位**及对应的**操控器**如下:
| 控制标志位 | 标志位含义 | 操控器 Manipulator |
| ---------------------- | -------------------- | ----------------- |
| `std::ios::dec` | 十进制格式(默认) | `std::dec` |
| `std::ios::hex` | 十六进制格式 | `std::hex` |
| `std::ios::oct` | 八进制格式 | `std::oct` |
| `std::ios::showbase` | 显示进制基数(例如`0x`) | `std::showbase` |
| `std::ios::showpoint` | 强制显示浮点数的小数点 | `std::showpoint` |
| `std::ios::uppercase` | 使用大写字母(例如,十六进制中的A-F) | `std::uppercase` |
| `std::ios::scientific` | 科学计数法格式 | `std::scientific` |
| `std::ios::fixed` | 定点格式 | `std::fixed` |
| `std::ios::left` | 左对齐 | `std::left` |
| `std::ios::right` | 右对齐 | `std::right` |
| `std::ios::internal` | 符号位左对齐,其余部分右对齐 | `std::internal` |
(操控器定义于 `<iomanip>` 头文件中)
> [!NOTE] 格式控制相关的标志位大多 "**成对存在**":设置/复原。
>
> 例如,控制标志位 `std::ios::showbase` 与 `std::ios::noshowbase`,
> 以及对应的操纵器 `std::showbase` 与 `std::noshowbase`。 ^nf6sxe
### 格式标志位访问控制函数
参见 [^3]
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-6C66E49A9BDAC28575DDC6963B56FAD3.png|646]]
#### 应用示例
> ❓<font color="#c0504d"> 如何暂存当前格式标志位、恢复之前的格式标志位?</font> ^ljdupg
示例:**暂存**旧的格式标志位、**修改**格式标志位、**恢复**此前暂存的格式标志位
```cpp title:example.cpp
using std::ios;
using std::cout;
// 暂存当前格式标志位
ios::fmtflags oldFlags = cout.flags();
// 修改
// 同时设置多个格式标志位
cout.setf(ios::showps | ios::showbase | ios::uppercase);
// 清除basefield分组中的所有置位, 并置位hex
cout.setf(ios::hex, ios::basefield);
// 恢复此前的格式标志位
cout.flags(oldFlags);
```
<br><br><br>
# I/O 操纵符(Manipulator)
「**操控器**(Manipulator) 」是 C++标准库提供一类全局的**特殊对象**,用于:
- 改变输入流的 "**输入解释方式**";
- 改变输出流的 "**输出格式化方式**"。
## 操控器的运作原理
> ❓<font color="#c0504d"> I/O 流相关的操纵器 Manipulator 的原理是什么?</font> ^m99kh1
**操纵符**本质上都是 "**==函数==**",这些函数或是**函数原型为 `iostream&(iostrea&)`**,或是返回一个 "**对象**" [^4] [^5]。
I/O 流类中重载了 `operator<<`,包括:
- (1)**以 "函数指针 `ostrea& (*op)(ostream&)`" 为参数**,从而接受 "**操作符**",并在内部通过 "**函数指针**" 进行调用 :`(*op)(*this)` 。
- 例如,`std::cout << std::endl` **实际上是==函数调用==** `std::endl(std::cout)`。
- ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-0E3895EBC8BA1506BBFB4BEF0C76C6DE.png|715]]
- (2)以 "**操纵符函数返回的==特定对象==**" 为参数
- 例如,`std::setw(n)` 返回一个特定的 `_Setw` 类型对象,而 I/O 类重载的 **`operator<<` 具有一个接收 `_Setw` 对象的版本**。
> [!example] `std::endl` 操纵符的函数定义
>
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-D400C8DB33124079A25971EF937E5025.png|450]]
>
> [!example] `std::fixed`、`std::scientific` 操作符的函数定义
>
> 格式控制相关的操纵符本质上都是 "**语法糖**",内部通过调用 **IO 对象的 `.setf()` 方法**完成:
>
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-8B4B5B2F24C436B67ACB7C288DC18E40.png|504]]
>
> ^zfv7z4
> [!example] `std::setw()` 操作符的函数定义
>
> - `std::setw()` 是个函数,**返回一个 `_Setw` 对象**;
> - I/O 流类中为 `operator <<()` **重载了一个 `_Setw` 对象为参数的版本**。
>
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-BEC7154DBCBAC6344903D7BDDA8AC77F.png|479]]
>
<br><br>
## iostream 头文件中的操控器
`<iostream>` 头文件中定义了几个最基本的操控器:
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-77319518C67F9B32555FADDEA7F752A6.png|474]]
`std::endl` 的作用是**向缓冲区中输出`\n`** 并**应用 `std::flush` 强制输出缓冲区所有数据**。
参见 `<ostream>` 头文件中关于 `endl` 的声明:
```cpp
_CRTIMP2_PURE inline basic_ostream<char, char_traits<char> >&
__CLRCALL_OR_CDECL endl(basic_ostream<char, char_traits<char> >& _Ostr)
{ // insert newline and flush byte stream
_Ostr.put('\n'); //换行
_Ostr.flush(); //刷新缓冲
return (_Ostr); }
```
<br>
### 用于 "刷新输出缓冲区" 的操控器
> <font color="#c0504d">❓ C++中用于 "刷新输出缓冲区" 的操控器?</font> ^zx67ht
输出流 `ostream` 会对**输出内容进行缓冲**,因此**输出内容可能不会立即给到输出设备(如显示器)**。
`flush`,`endl` ,`ends` 操控符用于**手动地刷新输出缓冲区**,实现输出。
此外,还有 `unitbuf` 和 `nounitbuf` 操控器:
- `unibuf`:设置后,每次执行 "写操作" 后都将自动进行一次 `flush` 操作;
- `nounitbuf`:重置上述置位,恢复回正常的系统默认的缓冲区刷新机制。
```c++
cout << unitbuf; // 此后所有输出操作后都会立即刷新缓冲区, 任何输出都立即输出, 无缓冲;
cout << nounitbuf; // 回到正常缓冲方式
```
> [!NOTE]
>
> 通常,不使用 `endl`、`flush` 也能正常即时输出,是因为在**系统较为空闲时候,会查看缓存区的内容,如果发现新的内容便会立即进行输出**。
> 然而,这一输出的时机是不确定的,与系统自身的运行状况有关。
> **通过 `endl`、`flush` 刷新缓存区则是强制性的**,为了避免极端情况下系统没有主动刷新输出流,在打印语句调试程序时,应当加入 `endl` 或 `flush`。
<br><br>
## iomanip 头文件中的操控器
`<iomanip>` 头文件中提供了与 "**格式控制**" 相关的操控器。
下图参见 [^4] [^6]
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明.assets/IMG-cpp-IO类说明-7860FE2AAEB8A886B69F2706A9A07112.png|579]]
# I/O 格式化控制
> ❓<font color="#c0504d"> 如何对输出进行格式化?</font> ^107nq8
**格式化控制**,也即设置 "**格式标志位**" 的方式有 2 种:
1. 通过对应的 **==操控器 Manipulator==**;
2. 通过 I/O 流类的 **==格式控制成员函数==**
- 无参格式标志位: `s.setf(flags)`、`s.flag(flags)`、`s.unsetf(flags)`
- 带参格式标志位: `s.precision(n)`
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^zfv7z4]]
![[02-开发笔记/01-cpp/输入输出相关/cpp-IO类说明#^nf6sxe]]
##### 示例
示例:设置 "**固定小数点后两位**"
```cpp
#include <iostream>
#include <iomanip>
using namespace std;
double db = 123.456789;
// 1.通过 "操控器" 设置格式标志位
void exam1() {
cout << fixed << setprecision(2); // 设置定点格式 & 精度为2
cout << db << endl;
}
// 2.通过i/o流对象的成员函数设置格式标志位
void exam2() {
cout.precision(2); // 设置精度为2;
cout.setf(ios::fixed, ios::floatfield); // 设置为定点格式
}
```
<br><br>
# 参考资料
参考:
[进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症](https://www.cnblogs.com/zjuhaohaoxuexi/p/16259442.html)
[scanf 为毛要敲回车?——输入输出缓冲区,键盘缓冲区](https://momentonly.github.io/2020/05/19/C/%E9%94%AE%E7%9B%98%E7%BC%93%E5%86%B2%E5%8C%BA/)
[理解缓冲区,字符 I/O和结束键盘输入](https://bbs.huaweicloud.com/blogs/354764)
[一文带你读懂C/C++语言输入输出流与缓存区](https://www.eet-china.com/mp/a19757.html)
[关于输入流状态函数cin.eof()的问题(转)](https://blog.csdn.net/liyang2010dd/article/details/18792419)
# Footnotes
[^1]: 《C++ 标准库(第二版)》P748
[^2]: 《C++ Primer》P279
[^3]: 《C++ 标准库(第二版)》P779
[^4]: 《C++ 标准库(第二版)》P775
[^5]: 《C++ Primer》P666
[^6]: 《C++ Primer》P670