%% # 纲要 > 主干纲要、Hint/线索/路标 # Q&A #### 已明确 - [[#(1)`getchar` 返回值为 int,不能赋给 `char` | getchar 等返回值应当赋给 int 类型而非char]] #### 待明确 > 当下仍存有的疑惑 **❓<font color="#c0504d"> 有什么问题?</font>** # Buffer ## 闪念 > sudden idea ## 候选资料 > Read it later %% # C 标准库 IO 接口 > 相关头文件: `<stdio.h>` 参见 [^1](P115,第五章) 在 UNIX 中,标准 I/O 库最终都要调用 POSIX 库提供的 I/O,如 `read`、`write`、`lseek` 等系统调用封装。 <br> ## FILE 对象——文件指针 C 标准 I/O 库的操作围绕 "==**流**==" 进行——**字节流**。 当使用**标准 I/O 库打开或创建一个文件**时,会将一个 "**==流==**" 与该文件关联,**返回指向 `FILE` 对象的指针**(称为 "**==文件指针==**")。 所有标准 I/O 函数均**以 `FILE*` 指针为参数**,`FILE` 对象即代表了这个 "**流**",指示着**当前读写操作的位置**。 > [!info] `FILE` 对象 > > `FILE` 是一个 **==结构体==**,包含了标准 I/O 库为**管理该流需要的所有信息**, > 包括用于实际 I/O 的**文件描述符**、指向**该流缓冲区的指针**、**缓冲区长度**、**当前缓冲区中的字符数**以及**出错标志**等。 > > 在 C 标准库头文件 `<FILE.h>` 中定义有 `typedef struct _IO_FILE FILE;`, > 而在 `<struct_FILE.h>` 中有具体定义: > > ![[_attachment/02-开发笔记/02-c/c-输入输出相关.assets/IMG-c-输入输出相关-DA96D01CC3C70D6CD0D210BC5ABB55D4.png|549]] > ![[02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理#^6dtlfy]] <br> ## API 概览 ![[_attachment/02-开发笔记/99-Unix 环境编程/文件 IO.assets/IMG-文件 IO-AA32DA61E79748479B6430054B8E47C8.png|489]] - 标准 I/O 库**预定义的文件指针**(`FILE*` 类型):`stdin`、`stdout`、`stderr` - **流管理**: - **打开流(打开文件)**: - **打开文件**:`fopen(path, type)`,返回 `FILE*` 指针。 - **以文件描述符打开文件**:`fdopen(fd, type)`,返回 `FILE*` 指针 - **在既有流上打开新文件**:`freopen(path, type, fp)` ,返回 `FILE*` 指针 - **关闭流**:`fclose(fp)` - **获取流关联的文件描述符**:`fileno(fp)` - **设置流宽度**:`fwide`:多字节流或单字节流 - **流错误检测**:`ferror(fp)`(出错)、`feof(fp)`(到达文件尾,EOF) - **清除错误标志位**:`clearerr(fp)` - **读/写流** - **非格式化 I/O** - **格式化 I/O** - **定位流** - **缓冲区相关**: - 设置缓冲区: - `setbuf`:打开 or 关闭缓冲区; - `setvbuf` :设置缓冲类型:全缓冲、行缓冲、无缓冲) - 强制刷新流缓冲区: - `fflush`:将 **stdio 输出流缓冲区**中内容**传送到==内核缓冲区==**) ## 示例 ```c #include <stdio.h> #include <stdlib.h> // C标准库接口提供的是"带缓冲的文件IO", 通过FILE*指针进行操作. void file_write(const char* filename) { // 打开流, 获取FILE*指针 FILE* fp = fopen(filename, "w"); if (fp == NULL) { perror(filename); // 输出 "example.txt: ..." exit(EXIT_FAILURE); } // 向文件写入内容 fprintf(fp, "%s", "Hello, C Standard I/O!\n"); fprintf(fp, "%s", "This is a sample file I/O program.\n"); // 关闭流 fclose(fp); } void file_read(const char* filename) { FILE* fp = fopen(filename, "r"); if (fp == NULL) { perror(filename); // 输出 "example.txt: ..." exit(EXIT_FAILURE); } char buf[256]; // 从文件中每次读取一行. while (fgets(buf, sizeof(buf), fp) != NULL) { printf("%s", buf); // 打印读取的每一行 } fclose(fp); // 关闭文件 } int main() { const char* filename = "example.txt"; file_write(filename); file_read(filename); } ``` <br><br> # 定位流 标准 I/O 库提供三种方法定位流,"**位置**" 表示为相较于文件起始的 "**==字节偏移量==**"。 | | 获取位置 | 设置定位 | "位置值"的类型 | | --- | ------------- | ----------------------------- | ----------- | | (1) | `ftell(fp)` | `fseek(fp, offset, whence)` | `long` 类型 | | (2) | `ftello(fp)` | `fseeko(fp, offset, whence)` | `off_t` 类型 | | (3) | `fgetpos(fp)` | `fsetpos(fp, offset, whence)` | `fpos_t` 类型 | | (4) | | `rewind(fp)` | | 其中 `whence` 即 `SEEK_SET`、`SEEK_CUR`,`SEEK_END` 三项,与 `lseek` 相同。 `rewind(fp)` 将文件指针 FILE 重置到文件开头,等价于 `fseek(fp, 0, SEEK_SET)`。 > [!NOTE] 对于**文本**文件,由于编码格式的差异,因此位置**不能直接以 "字节偏移量" 度量**,故: > > - `offset` 只能有两种值:`0` 或 `ftell` 等所返回的值; > - `whence` 只能是 `SEEK_SET`。 > [!danger] **定位流**只适用于 "**二进制模式**" 打开的文件! > > ![[02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理#^lef93r]] <br><br> # 内存流 内存流:仍使用 `FILE*` 指针进行访问,但实际上**没有底层文件**,所有 I/O 都是通过**在缓冲区与主存之间来回传送字节**来完成的。 - `fmemopen` - `open_memstream` - `open_wmemstream` <br> # 非格式化 I/O 非格式化 I/O 函数包括三类: - **每次==一个字符==的 I/O**: - 读:`getc`、`fgetc`、`getchar`;其中 `getchar(void)` 等价于 `getc(stdin)` - 回压字符:`ungetc` - 写:`putc`、`fputc`、`putchar`;其中 `putchar(c)` 等价于 `fgetc(c, stdout)` - **每次==一行==的 I/O** - 读:以 **"换行符"** 终止 - `gets(buf)` :从 `stdin` 读取,**不会**将换行符写入 `buf`; - `fgets(buf, n, fp)` :以换行符终止,最多读 `n-1` 个字符,会**读取换行符到 `buf`**。 - 写:**以 `'\0'` 终止**,而非换行符 - `puts(str)` 向 `stdout` 输出,最后会**额外输出一个换行符**; - `fputs(str, fp)` 不会输出额外的换行符。 - **==二进制== I/O**(**二进制流读写**) - 读:`fread(ptr, size, nojb, fp)` 从指定流`fp` 中**读取 `nobj` 个 `size` 字节大小的对象到 ptr 所指内存** - 写:`fwrite(ptr, size, nojb, fp)` 向指定流 `fp` 中**写入 ptr 所指内存处的 `nobj` 个 `size` 字节大小的对象** > [!NOTE] 标准 I/O 库中建议始终使用下列函数: > > - 读:`fgetc`、`getchar`、`fgets`、`fread`; > - 写:`fputc`、`putchar`、`fputs`、`fwrite` > > [!tip] `fgets` 与 `fputs` 需要自行处理换行符(去掉 or 补上): > > - `fgets(buf, n, fp)` : > - 遇见**换行符**停止,最多读取 **`n-1` 个字符**到 `buf` 中(保证末尾填入 `'\0'`),**会将读取到的换行符写入`buf`**。 > - `puts(str, fp)` : > - 遇见 **`\0`** 停止,将字符串 `str` 写入到指定流,但**末尾 `\0` 不写出**。 > > <br><br> # 格式化 IO > 参见 [printf, fprintf, sprintf, snprintf, printf\_s, fprintf\_s, sprintf\_s, snprintf\_s - cppreference.com](https://en.cppreference.com/w/c/io/fprintf) ## 格式化输出 5 个格式化输出函数(`fmt` 为 "**==转换说明==**" 字符串) | 格式化输出函数 | 说明 | | ---------------------------- | ------------------------------------------- | | `printf(fmt, ...)` | 写到**标准输出** | | `fprintf(fp, fmt, ...)` | 写到指定**流** | | `dprintf(fd, fmt, ...)` | 写到指定 "**文件描述符**" | | `sprintf(buf, fmt, ...)` | 写到指定 "**缓冲区**"(字符数组),会在尾端自动添加`'\0'`,但不计入返回值 | | `snprintf(buf, n, fmt, ...)` | 写到指定 "**缓冲区**",最多`n-1`个字符, | 返回值为 "**成功输出 or 存入的字符数**"。 此外,还有 **5 种 `printf` 族变体**——将上述函数的 **可变参数列表 `...`** 替换成 **`va_list` 类型** 参数: - `vprintf`、`vfprintf`、`vdprintf`、`vsprintf`、`vsnprintf` ## 格式化输入 3 个格式化输入函数——分析输入字符串,**将字符序列转换成==指定类型的变量==**。 | 格式化输入函数 | 说明 | | ----------------------- | ------------------ | | `scanf(fmt, ...)` | 对 "**标准输入**" 进行转换 | | `fscanf(fp, fmt, ...)` | 对 "**指定流**" 进行转换 | | `sscanf(buf, fmt, ...)` | 对 "**指定字符串**" 进行转换 | 其中,`...` 需传入各变量的 "**地址**",将用转换结果对这些变量**赋值**。 此外,还有 **3 种 `scanf` 族变体**——将上述函数的 **可变参数列表 `...`** 替换成 **`va_list` 类型** 参数: - `vscanf`、`vfscanf`、`vsscanf` ## 格式转换说明 **转换说明(conversion specification)以 `%` 字符起始**: - 输出 "**转换说明**":`%[flags][width][.precision][length]specifier`; - 输入 "**转换说明**":`%[width][length]specifier` ###### 标志 - `flags`:控制输出**对齐方式**,符号与空格,前缀等。 - `-`:**左对齐**(默认右对齐) - `0`:**==右对齐==时** 对于 **整数和浮点数**,**填充==前导 0== 来补足位宽**。 - 标志无效的情况: - 1) 指定了**整数精度**时无效; - 2)指定了**左对齐标志 `-`** 时无效; - `+`:对 "**有符号转换**" 总是**显示符号** `+-`(默认仅在负数时添加负号 `-` ) - ` ` 空格:对于**有符号正数**,在数字前填充一个空格。 - `#`:搭配不同 `specifier`,进行特定形式的转换: - 对于 `x` 或 `X`:**输出 `0x` 或 `0X` 前缀**(若本身值为 0,则无前缀) - 对于 `o`:**输出 `0` 前缀**(如果本身值为 0,则无前缀,只输出 1 个 0) - 对于 `f`, `F`, `e`, `E`:**输出小数点**,即使小数点后没有数字 - 对于其他说明符,为**未定义行为** ###### 宽度 - `width`:以**整数值**或 **字符`*`** 指定 **==最小==宽度**,默认**填充空格** - 当使用 `*` 时,需要由一个**额外的 `int` 型参数**给出具体宽度。 - **值为负时**,等价于 **左对齐标志 `-` 加正数宽度**。 ###### 精度 - `.precision`:以 `.整数值` 或 `.*` 的形式**指定==精度==**(precision)——**==有效数字==位数** - 当使用 **`*`** 时,需要由一个**额外的 `int` 型参数**给出具体宽度 - **位于指定 `width` 的参数之后**,**被转换的参数之前**。 - **为负时无效**,忽略。 - 对于 **==整型==**,控制 "**==最小==输出位数**",默认值为 1——实际数字位数少于 `precision` 时在**数字前面==填充零==**, - 对于 **==浮点型==**,控制 "**==小数点后的数字位数==**",**默认值为 6**——小数点后实际位数小于或大于 `precision` 时均固定保留为指定位数。 - 对于 **==字符串==**,控制 "**==最大==输出字符数**",未指定时则**输出直至遇见首个 `\0`**。 - 例如,`%.5s` 表示只显示前 5 个字符。 ###### 数据长度 - `length`:**长度修饰符**(length Modifiers),**结合 "转换格式说明符 specifier" 来==共同指定传入参数的大小及类型==**。 - `h`:短整型 `short`; - `l`:长整型 `long`——例如 `ld`, `li` 表示 `long int`;`lu`, `lx`, `lo` 均对应 `unsigned long`。 - `ll`:长长整型 `long long` ——例如 `lld`, `lli` 对应 `long long`; `llu`, `llx`, `lld` 均对应 `unsigned long long`。 - `L`:长双精度浮点数 `long double` ###### 格式转换说明符 - `specifier`:**转换格式说明符(Conversion format specifier),控制输出格式**。 - `d` 或 `i` :**有符号十进制整型** - `u`:**无符号十进制整数** - `x` 或 `X`:**无符号十六进制整数**。 **`x` 使用 "小写字母"**,**`X` 使用 "大写字母"** - `f` 或 `F`:**浮点数**(十进制计数法) - `e` 或 `E`:**指数形式的浮点数**。`%e` 使用小写字母,`%E` 使用大写字母。 - `g` 或 `G`:**自动选择 `%f` 或 `%e`(或 `%E`)的格式**来显示浮点数(`double`) - `s`:字符串 - `p`:指针(`void*` 类型) - `%`:输出一个 `%` 字符(即 `%%` 表示输出一个`%`) - `n`:到目前为止,该函数输出或写入的字符数目,写入到指针所指向的整型中。 > [!caution] `d` 与 `i` 仅在 `scanf` 中有区别,在 `printf` 中没有区别。 > > - `%d` 只能接受**十进制输入** > - `%i` 可以接受**十进制、十六进制、八进制**输入,将自动**根据输入前缀进行识别解析** > - 对于 `0x` 或 `0X` 开头 => 识别为十六进制整数。 > - 对于 `0` 开头 => 识别为八进制整数 > - 非 0 数字开头 => 识别为十进制 > <br><br> ## 转换说明表格 ![[_attachment/02-开发笔记/02-c/c-输入输出相关.assets/IMG-c-输入输出相关-A4AFA0345B764C9F2911498D33389F8D.png|867]] ## 示例 ```c %10d // 位宽10(默认右对齐) %-10%d // 位宽10, 左对齐 %010d // 位宽10, 前导0填充 %10.2f:指定浮点数的宽度为10,精度为2。 ``` <br><br><br> # FAQ ##### (1)`getchar` 返回值为 int,不能赋给 `char` ```c #include <stdio.h> int main(void) { char c; // error: 应该为int类型. while ((c = getchar()) != EOF) { putchar(c); } } ``` 上述代码**在一些机器上运行正常,而在另一些机器上运行时出错**。 原因在于: `getchar()` 的返回值是 `int` 类型,从而保证**出错时能够返回负值**, EOF 正是 `<stdio.h>` 中定义的常量 `-1`。 `char` 在部分机器上实现为 "**无符号整型**",这就导致了永远不可能等于 EOF!所以陷入死循环,导致超时! ![[02-开发笔记/01-cpp/输入输出相关/cpp-输入输出处理#^l59uyb]] <br><br> # 参考资料 # Footnotes [^1]: 《Unix 环境高级编程》