%%
# 纲要
> 主干纲要、Hint/线索/路标
- **底层 IO 函数**:
- 单字节 IO、多字节 IO
- 流操作
- **输入相关函数总结**
- `cin >>`、`cin.get()`、`cin.getline()`、`std::getline()`
- ACM 模式下的输入输出(参见另一份笔记)
- **fstringstream 文件流**
- **stringstream 字符串流**
# Q&A
#### 已明确
![[02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理#^t26vol]]
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 底层 IO 函数
IO 类对象重载的 `>>` 与 `<<` 运算符提供的是 "**格式化 IO**",根据**读取或写入的数据类型**进行格式化。
IO 对象同时提供了一组**底层操作**,用以实现 "**未格式化的 IO**",将一个流当做一个 **无解释的==字节序列==** 进行处理。
## 单字节 IO
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-18460BB45B2BF1EE495B72B59C6194F9.png|751]]
## 多字节 IO
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-82032D8594FE0CAC799C48902EBFFC6A.png|758]]
<br><br><br>
# 流辅助操作函数总结
### `istream::peek()` 用法
函数原型:`int std::istream::peek()`,返回 `int` 类型。
作用:**获取**输入缓冲区中 "**下一个字符**"(但不提取,**仍保留该字符在缓冲区中**)。

### `istream::ignore()` 用法
函数原型:`istream& ignore(streamsize n = 1, int delim = EOF);`
作用:从输入缓冲区中 **"忽略"(提前并丢弃)==最多 n 个字符==,直至遇见指定分隔符(默认 EOF),忽略包括分隔符**。
> [!caution] 当缓冲区中剩余字符时不满足 "`cin.ignore()`" 忽略条件时,将导致 `cin.ignore()` 阻塞
>
> 例如,**未忽略满指定字符数,且未遇见分隔符**。
> [!caution] `cin.ignore()` 仅当 `cin.good()` 为真时有效,否则 `cin.ignore()` 将不执行任何操作。
```cpp
cin.ignore(); // 忽略缓冲区中下一个字符
cin.ignore(80, '\n'); // 忽略缓冲区中长度最大为80字符的下一行.
cin.ignore(100, ';'); // 等待用户输入直到输入至少 100 个字符或遇到 ';'
```
### `istream::gcount()` 用法
`std::istream::gcount()` 用于**返回上一次非格式化输入操作成功读取的字符数**,通常与以下函数搭配使用:
- `std::istream::read()`
- `std::istream::getline()`
- `std::istream::igrnoe()`
- `std::istream::get()` (多字符读取的版本)
```cpp
std::cin.ignore(10);
std::cout << "Characters ignored: " << std::cin.gcount() << std::endl; // 获取实际忽略的字符数
```
<br><br><br>
# 输入流读取函数总结
> <font color="#c0504d">❓有哪些输入流读取函数?区别是什么?</font> ^t26vol
C++ 中用于**从 "输入流" 中读取内容**的函数有:
- `std::getline(istream, string)` 函数
- `istream` 对象的 **`>>` 操作符**
- `istream` 对象的**成员函数**:
- `std::istream::get()` 函数
- `std::istream::getline()` 函数
- `std::istream::ignore()` 函数
- `std::istream::read()` 函数
- `std::istream::peek()` 函数
### 使用总结
> [!NOTE] 空白字符(Whitespace Characters)
>
> C++中定义的空白字符包括三个:**空格 `\s`、制表符 tab `\t`、换行符 `\n`**,定义于 `std::isspace` 中。
- 读取**空白字符分割的 token**:应使用 `cin >> var`
- **逐字符**读取:应使用 `cin.get(ch)` 或 `ch = cin.get()`;
- **逐行**读取:应使用 `cin.getline(ptr, count)` 或 `getline(cin, std::string)`
#### 逐行读取 API 的区别
| | `cin.get(ptr, count)` | `cin.getline(ptr, count)` | `getline(cin, string)` |
| ------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------- |
| 作用 | 读取一行内容到**字符数组** 中 | 读取一行内容到**字符数组**中 | 读取一行内容到 **`string` 对象**中 |
| 写入对象 | `char* ptr` | `char* ptr` | `string` 对象 |
| 终止条件 | - 遇见**分隔符 `\n`** <br>- 或 **EOF** <br>- 或 **已读取 `count-1`个字符** | - 遇见**分隔符 `\n`** <br>- 或 **EOF** <br>- 或 **已读取 `count-1`个字符** | - 遇见**分隔符 `\n`** <br>- 或 **EOF** |
| | | | |
| 写入内容是否 <br>包含分隔符 | ❌ | ❌ | ❌ |
| **缓冲区**中是否 <br>**保留分隔符** | ✔️ | ❌ (提取并丢弃) | ❌(提取并丢弃) |
| **末尾自动添加 <br>结束符 `\0`** | ✔️ | ✔️ | ✔️ |
| 触发 `failbit` 的情况 | **空行** <br>(缓冲区中**首字符即为分隔符**) | **缓冲区溢出 ptr 大小时** <br>(缓冲区中**剩余超过 `count - 1` 个有效字符**) | 无 |
具体说明:
- `cin >>`:
- 以 "**空白字符**" 作为分隔符(空白字符:空格`\s`、制表符 tab `\t`、换行符 `\n`;)
- 忽略缓冲区中起始的**空白字符**,从**第一个非空白字符开始读取**,直到**再次遇见空白字符**时结停止,**不读取空白字符**。**空白字符保留在缓冲区中**。
- `cin.get(ptr, count) `
- 读取一行,遇见**结束符**则停止,结束符**保留**在输入缓冲区中;
- `cin.getline(ptr, count)`
- 读取一行,遇见**结束符**则停止,从缓冲区中**抽取并丢弃结束符**。
- `getline(cin, std::string)`: 位于头文件 `<string>` 中
- 读取一整行内容赋给`s`,遇见**分隔符`\n`** 时结束读取,**抽取换行符并丢弃**。
> [!info] 四个 API 均返回 `istream&` 输入流对象
---
<br><br>
### (1)`istream >>` 用法
从 `istream` 流中读取内容,写入到指定变量中。
- **忽略、丢弃==前导==空白字符**(不留于缓冲区),从**首个非空白字符**开始读取。如果**无有效字符**,则**不执行任何操作**;
- 读取直至**再次遇见空白字符**时终止,不提取空白字符,**空白字符==保留==于缓冲区中**。
> [!note] 对于 `cin >> target`,如果缓冲区中无有效字符,**不会写入任何内容到目标变量 `target`**。
>
> ```cpp
> string str = "25 477 ";
> istringstream iss(str);
> string token;
> while (iss >> token) {}
> cout << token << endl; // 输出"477"
> ```
> [!NOTE] 说明
> 当键盘输入敲击回车后,**回车对应的换行符 `\n` 存在于缓冲区中的末尾。**
> 当 `cin>>` 读取完这一输入行的数据后:
>
> - 若下一步继续使用 `cin>>` 则无影响(**跳过头部空白并丢弃**)
> - 若下一步使用`cin.get(ch)` 则将能读取到缓冲区中的这一换行符`\n`。
> [!example]
>
> 以输入 `ABC CB BCD` 为例:
>
> - 第一个 `cin>>`语句首先读取`ABC`后遇见空格而结束,此时缓冲区中还有剩余空格,为`_CB BCD\n`;
> - 第二个 `cin>>`语句跳过缓冲区头部的空格,读取 `CB` 后遇见空格而结束,此时缓冲区为`_BCD\n`;
> - 第三个 `cin>>`语句跳过缓冲区头部的空格,读取`BCD` 后遇见换行符而结束,此时缓冲区剩余有`\n`;
> - 使用`cin.get(ch)`,将能获取到最后的换行符;
<br><br>
### (2)`istream::get()` 用法
> 
`istream.get()` 有多个重载版本,用法分为三种:
- 读取 **==单个字符==**:
- `cin.get()` :无参数,读取成功时**返回==字符的 ASCII 码==**(`int`类型),到达文件尾时**返回 EOF 宏常量(负值)**。
- `cin.get(ch)`:读取单个字符赋给变量 `ch`,**返回 ==`cin` 输入流对象==**,到达文件尾时 `cin` 将可隐式转换为 `false`。
- 读取 **==整行==内容**:
- `cin.get(char* s, streamsize n, char delim)`;
- **结束条件**: 遇见**分隔符 `\n`** 或 **EOF** 或 **已读取 `count-1`个字符**
- **最多读取 `n-1` 个字符**,**自动补充==空字符`\0`==到末尾**;
- **==不提取分隔符符==**,**==保留==换行符在输入缓冲区中**。
- 当读取到**空行**(缓冲区中下一字符为终止符)时触发流对象的 **failbit 置位**。
> [!caution] `cin.get()` 无参数版本的返回值应当赋给 `int` 类型!
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-F3F5ACD4349DCFC4678BE2B28466ED5F.png|674]] ^l59uyb
#### 使用示例
**逐字符读取**:
```cpp
// 逐字符读取的两种使用方式:
void byte_by_byte_1() {
char ch;
while (cin.get(ch)) { // 返回cin对象.
cout.put(ch);
}
}
void byte_by_byte_2() {
int ch; // 必须是int, 而不能是char
while ((ch = cin.get()) != EOF) { // `cin.get()`读取成功时返回字符ASCII码, 或者遇见文件尾时返回EOF宏的值
cout.put(ch);
}
}
```
**逐行读取**:
```cpp
// 逐行读取: `cin.get(char*, size n, char delim='\n')`
// 最多读取"n-1"个字符;
// 遇见结束符或EOF时停止,结束符保留在输入缓冲区中;
// 会自动补上'\0';
// 当读取到空行(缓冲区中下一字符为终止符)时触发流对象的 **failbit 置位**。
// 返回cin对象.
void cin_get_by_line() {
char buffer[BUF_SIZE];
while (cin.get(buffer, sizeof buffer)) { // EOF或空行时结束循环.
cout << buffer << endl;
// 丢弃此行中超出数量`sizeof buffer-1`的字符, 以及最后的分隔符. 以免再次调用cin.get()时读取到空行而触发failbit;
while (cin && cin.get() != '\n') {
continue;
}
// 或者只丢弃结束符, 不丢弃多余字符
// if (cin.peek() == '\n') {
// cin.get();
// }
}
if (cin.fail()) cout << "cin.fail() return true" << endl;
}
```
<br><br>
### (3)`istream::getline()` 用法
> ![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-5468446C665A301539644BF40CC1FD1E.png|862]]
`cin.getline(char*s, streamsize n, char delim)` 读取 **==整行== 内容**:
- **结束条件**: 遇见**分隔符 `\n`** 或 **EOF** 或 **已读取 `n-1`个字符**
- **最多读取 `n-1` 个字符**;
- **自动==补充空字符==`\0`到末尾**;
- **从输入缓冲区中==提取并丢弃==分隔符**。
- 若缓冲区中剩余"**有效字符数**"(不计分隔符) 大于 `n-1`,则**读取完 `n-1` 个字符后将触发 `setstate(failbit)`**。
> [!caution] `cin.getline()` 不能直接作为 "**==循环条件==**",因为当读取完最大字符数 `count - 1`后,若缓冲区中还有字符时将**触发输入流对象的 `failbit` 置位**。
```cpp
// cin.getline(ch, n)读取整行字符串, 遇见"EOF"或"已读取n-1个字符"或"结束符"时停止,结束符被抽取并丢弃, 不保留在输入缓冲区中;
// 最多读取n-1个字符
// 遇见结束符或EOF时停止,从缓冲区中提取结束符并丢弃.
// 会自动补上'\0'.
// 若缓冲区中剩余"有效字符数"(不计分隔符)大于n-1, 则读取完n-1个字符后将触发setstate(failbit).
void cin_getline_func() {
char buffer[BUF_SIZE];
while (true) {
cin.getline(buffer, sizeof buffer);
cout << buffer << endl;
if (cin.eof()) {
break;
} else if (cin.fail()) { // 检查是否发生缓冲区溢出.
cin.clear(); // 清除failbit置位.
// 可选: 丢弃一行中多余字符.
// while (cin && cin.get() != '\n');
}
}
}
```
<br><br>
### (4)`std::getline()` 用法
> 位于头文件 `<string>` 中
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-E68237F40DF49F3E03D7898968545F09.png|525]]
从一个 `istream` 流中**读取一行数据**,存入到一个指定 **`string` 对象**中。
- **结束条件**: 遇见**分隔符 `\n`** 或 **EOF**
- **从输入缓冲区中==提取并丢弃==分隔符**。
> [!NOTE] `getline()` 也可以对 `fstream`,`stringstream` 使用!
示例:
```cpp
void str_getline_func() {
string line;
while (getline(cin, line)) {
cout << line << endl;
}
}
```
<br><br><br>
# 流定位
> 参见 [^1]
> [!caution] 流定位只能对 `fstream` 与 `sstream` 类型使用,`iostream` 不支持流定位与随机访问!
> [!caution] 在一个流中**只维护一个 "==单一标记=="**,当一个流可 "==同时读写==" 时,**每次在读写操作间切换前,都应当 seek 重定位!**
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-0E87D897D19D0E295CFC51E67488235A.png|692]]
> [!danger] `seek/tell` 函数是基于 "**==字节偏移量==**" 的定位,只能用于以 "**==二进制模式==**" 打开的 `fstream` 文件流!
>
> **如果以 "文本模式" 打开可能导致错误结果**!甚至连 `fs.seekg(fs.tellg())` 都是错误的。
>
> 原因在于:
>
> 在**文本模式**下,文件流会对文件内容进行**自动处理和转换**,例如**不同平台下的换行符转换、EOF 特殊标记、字符编码转换**等。
> 该情况下,`fs.tellg()` 返回的是将是**处理后的文本**中的 "**逻辑字节位置**",而不是文件中原始字节位置。
>
> - POSIX API 里中 `open()` 只有 "**二进制**" 模式,`lseek()` 也是基于 "**当前文件的字节偏移量**";
> - C API `<stdio.h>` 中 `fopen()` 可选 "二进制 or 文本模式" 打开文件,仅当 "**二进制模式**" 打开时可应用 `ftell()` 与 `fseek()`;
>
> 参见:
>
> - [\<fstream\>: seekg(tellg()) fails for text files containing unix line endings · Issue #1784 · microsoft/STL](https://github.com/microsoft/STL/issues/1784)
> - ["seek/tell functions are for files opened in binary mode, not for text mode."](https://cplusplus.com/forum/general/67355/)
> ^lef93r
> [!NOTE] "文本模式" 与 "二进制模式" 打开文件的区别
>
> - 在==**文本模式**==下,文件流会对文件内容进行一些**自动处理**:
> 1. **换行符转换**:
> - 在 **Windows** 平台上,文件中的 **`\r\n` 会被转换为单个 `\n`**。
> - 在 **Linux/Unix** 平台上,文件中的 `\n` 保持不变。
> 2. **EOF(文件结束符)处理**:
> - 一些平台会对文件末尾的 EOF 进行特殊标记或处理。
> 3. **字符编码转换(某些实现)**:
> - 某些库(如 Windows 下的 C++ 标准库)可能对字符编码进行转换,例如 **UTF-8 到本地编码**。
>
> >
>
> - 在==**二进制模式**==下,所有内容都以 "**原始字节**" 形式存在,文件流不会进行任何额外解释或修改,文件流以 "逐字节" 地方式处理。 ^6dtlfy
<br><br><br>
# fstream 文件流
> 参见 [^2] (P284)
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-5CAC033306DA57DF20D147DF17BA0D2A.png|774]]
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-39E89247F2F35691FF7F8E07DEB930A1.png|611]]
使用示例:
```cpp
// 示例: 对传递给程序的每个文件进行读取, 执行循环操作
int main(int argc, char** argv) {
for (auto p = argv + 1; p != argv + argc; ++p) {
ifstream input(*p); // 创建一个只读文件流, 打开该文件并关联.
if (input) { // 文件流有效
process(input);
} else {
cerr << "couldn't open: " << string(*p) << endl;
}
}
}
```
<br><br><br>
# stringstream 字符串流
> 位于 `<sstream>` 头文件中
`<sstream>` 头文件提供了下列三个 "**字符串流**" 类,支持内存 IO——**以 "流" 的形式来提供对==内存中 `string` 对象==的字符串处理功能** [^3]。
- `istringstream` 类:从 `string` 中**读取**数据——使用 **string 初始化`iss`**,随后**支持 `iss >> data`**;
- `ostringstream` 类:向 `string` 中**写入**数据——支持 `oss << data`,再**调用 `oss.str()` 获取字符串**。
- `stringstream` 类: 对 `string` 读写数据
### 主要作用
主要作用是:「**解析输入字符串**」或「**格式化输出字符串**」
- **解析输入**:从字符串中解析数据
- 将输入字符串存入到 `stringstream` 中后,可通过流操作符 `>>` **从 `stringstream` 中提取数据**,并转换为相应的数据类型(如 `int`、`double` 等)
- **格式化输出**:
- 可通过**标准的流操作符 `<<`** 将非字符串类型的数据(如 `int`,`double`)插入到 `stringstream` 对象中,将**不同类型的数据**转换为**字符串** ,最后可通过 `stringstream.str()` 获取对应的 `string` 对象。
### 相关操作
![[_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-84E7201AC03C6AA794C879E631B55C43.png|783]]
常用操作:(一部分是 iostream 共有的,如 `.clear()`、`.unsetf()` 等)
- `<<`:将数据插入到 `stringstream` 字符串流中
- `>>`:从 `stringstream` 字符串流中提取数据
- `.str()`:
- 不带参数:**获取**流中的字符串内容,返回 `string` 对象
- 带参数:**设置**流中的字符串内容
- `.clear()`:**清除流的错误状态标志**,恢复流为 "**有效状态**"
- 注意,只是清除状态标志,并未清除流的字符串内容,重用流之前应搭配 `.str("")` 来清空流的内容
- `.unsetf()`:清除固定的格式化标志
- 例如 `ss.unsetf(std::ios::fixed)`
- `.precision()`:设置流的精度(小数点后保留的位数)
### 使用示例
#### 重复写入
对 `stringstream` 对象进行**多次写入操作**时,需要在每次写入前 "**清空流的状态**" 并 "**重置流的内容**"。
> [!caution] `stringstream` 对象**写出**后,会将流状态置为 `EOF`。此时,再次写入需要 "**清空流的状态**"
```cpp
#include <iostream>
#include <sstream>
using namespace std;
int main() {
stringstream ss;
string str;
// 第一次写入
ss << "Hello";
cout << "First write: " << ss.str() << endl;
// 读取流的内容
cout << "EOF(before): "<< ss.eof() << endl;
ss >> str;
cout << "EOF(after): " << ss.eof() << endl;
// 清理流的状态并重置内容
ss.clear(); // 清除流的状态标志
ss.str(""); // 清空流的内容
// 第二次写入
ss << "World";
cout << "Second write: " << ss.str() << endl;
return 0;
}
```
#### 清除 EOF 位而保留格式位
```cpp
// 先获取完整标志位, 再将eof位置0, 从而保留其它标志位.
stringstream ss;
ss.clear(ss.rdstate() & ~ss.eofbit) // ss.eofbit 也可换为 std::ios::eofbit;
```
#### 示例
```cpp title:stringstream_exam.cpp
#include <iostream>
#include <iomanip>
#include <sstream>
#include <string>
using namespace std;
// stringstream: 字符串流
// 解析输入: 从字符串中解析数据
void example1() {
// 输入字符串
string data = "123 45.64 Hello";
// 创建一个 stringstream对象, 使用字符串data对其进行初始化.
stringstream ss(data);
int x;
double db;
string word;
// 从字符串流中解析数据:
ss >> x >> db >> word;
cout << x << endl;
cout << db << endl;
cout << word << endl;
}
// 格式化字符串: 将非字符串数据转换为字符串
void example2() {
// 创建一个 stringstream对象
stringstream ss;
int a = 10;
double b = 3.14;
string c = "Hello, world";
// 将数据输入到字符串流
ss << "Interger: " << a << " double: " << b << " String: " << c << endl;
// 获取经字符串流格式化的字符串
string ans = ss.str();
cout << ans << endl;
}
// 示例: 复用同一个stringstream对象
void example3() {
double input = 32.141576924;
stringstream ss;
ss << fixed << setprecision(2) << input; // 保留2位小数点
double num;
ss >> num;
cout << "First number: " << num << endl;
// 复用stringstream对象
ss.unsetf(std::ios::fixed); // 清除固定格式化标志
ss.precision(6); // 重置精度为默认值
ss.clear(); // 清空流的状态标志
ss.str(""); // 清空流的内容
// 第二次使用
ss << input;
double num2;
ss >> num2;
cout << "Second number: " << num2 << endl;
}
int main() {
example1();
example2();
example3();
return 0;
}
```
<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)
- [C++ cin 详解之终极无惑](https://blog.csdn.net/K346K346/article/details/48213811)
- [C++中关于输入cin的一些总结](https://www.cnblogs.com/mini-coconut/p/9041925.html)
- [C++ cin的使用,看这一篇就够了](https://zhuanlan.zhihu.com/p/583646330)
- [c++ cin整数以,(逗号)分割读取](https://blog.csdn.net/weixin_39956356/article/details/118053985)
- [关于输入流状态函数cin.eof()的问题(转)](https://blog.csdn.net/liyang2010dd/article/details/18792419)
# Footnotes
[^1]: 《C++ Primer》P676
[^2]: 《C++ Primer》P279
[^3]: 《C++ Primer》P287