%% # 纲要 > 主干纲要、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` 类型。 作用:**获取**输入缓冲区中 "**下一个字符**"(但不提取,**仍保留该字符在缓冲区中**)。 ![img](_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-63790A9082C5EDDEA49A4D5737BE1F37.jpg) ### `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()` 用法 > ![image-20230921190811464|682](_attachment/02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理.assets/IMG-cpp-输入输出处理-46638B301F334BD7766B4773BB57DBB0.png) `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