%%
# 纲要
> 主干纲要、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 环境高级编程》