# 《UNIX 环境高级编程》笔记 - 标准 IO 库

不仅是 UNIX，很多其它操作系统都实现了标准 I/O 库，这个库是由 ISO C 标准定义的。

## 流和 FILE 对象

文件 I/O 函数都是围绕着文件描述符进行的。当打开一个文件时，即返回一个文件描述符，然后该文件描述符用于后续的 I/O 操作。

对于标准 I/O 库而言，它们的操作是围绕流（stream）进行的。当用标准 I/O 库打开或者创建一个文件时，我们使用一个流与一个文件相关联。

对于 ASCII 字符集，一个字符用一个字节表示。对于国际字符集（例如 UTF-8），一个字符可用多个字节（宽字节）表示。标准 I/O 文件流即可以用于单字节的，也可用于多字节的字符集。流的方向（orientation of stream）决定了它所读写的字符是单字节还是多字节的。

调用 `fwide` 函数可以设置流的方向。需要注意的是，`fwide` 函数并不会改变已经定向的流的方向。

```c
#include <wchar.h>

int fwide(FILE *stream, int mode);
```

当打开一个流时，标准 I/O 函数 `fopen` 返回一个指向 FILE 对象的指针。FILE 对象通常是一个结构，它包含了标准 I/O 库为管理该流需要的所有信息，包括用于实际 I/O 的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志。

## 标准输入、标准输出和标准错误

每个进程都预定义了 3 个流，并且这 3 个流可以自动地被进程所使用，它们是：标准输入、标准输出、标准错误。

这 3 个标准 I/O 流通过预定义的文件指针 stdin、stdout、stderr 加以引用，文件指针定义在头文件 `<stdio.h>` 中。

## 缓冲

标准 I/O 库提供缓冲的目的是尽可能减少使用 `read` 函数和 `write` 函数的调用次数。标准 I/O 库提供了以下 3 种类型的缓冲：

* 全缓冲：标准 I/O 库将会在缓冲区填满之后，再进行实际的 I/O 操作。术语 flush 表示标准 I/O 缓冲区的写操作；
* 行缓冲：标准 I/O 库将会在输入和输出过程中遇到换行符时，再进行实际的 I/O 操作。
* 不带缓冲：标准 I/O 库不会对字符进行缓冲。标准错误流 stderr 通常是不带缓冲的。

调用 `setbuf` 函数或 `setlinebuf` 函数可以更改流的缓冲类型。

```c
#include <stdio.h>

int setvbuf(FILE *restrict stream, char *restrict buf,
            int mode, size_t size);

void setbuf(FILE *restrict stream, char *restrict buf);
void setbuffer(FILE *restrict stream, char *restrict buf,
            size_t size);
void setlinebuf(FILE *stream);
```

## 打开流

调用 `fopen` 函数、`fdopen` 函数或 `freopen` 函数可以打开一个标准 I/O 流。

```c
#include <stdio.h>

FILE *fopen(const char *restrict pathname, const char *restrict mode);
FILE *fdopen(int fd, const char *mode);
FILE *freopen(const char *restrict pathname, const char *restrict mode,
              FILE *restrict stream);
```

`fopen` 函数打开指定的路径名为 pathname 的文件。

`fdopen` 函数打开指定的文件描述符 fd 上的文件。这个函数通常用于创建管道函数和网络通信函数返回的文件描述符。因为这些特殊类型的文件不能用标准 I/O 库中的 `fopen` 函数打开，因此必须先调用设备专用函数用以获取一个文件描述符，然后用 `fdopen` 函数将标准 I/O 流与该文件描述符相关联。

`freopen` 函数打开指定的路径名为 pathname 的文件，并且关联到指定的流 stream 上。如果指定的流 stream 已经打开，则会先关闭它。

`fopen` 函数和 `freopen` 函数是 ISO C 的一部分，因为 ISO C 并不涉及文件描述符，因此只有 POSIX 才有 `fdopen` 函数。

mode 参数指定对该 I/O 流的读写方式，ISO C 规定 mode 参数可以用 15 种不同的值。

| mode           | 说明                          | 对应的 open(2) 标志                     |
| -------------- | --------------------------- | ---------------------------------- |
| r 或 rb         | 只读打开                        | O\_RDONLY                          |
| r+ 或 r+b 或 rb+ | 读写打开                        | O\_RDWR                            |
| w 或 wb         | 只写打开，当文件不存在时则会创建，将文件长度截断为 0 | O\_WRONLY \| O\_CREAT \| O\_TRUNC  |
| w+ 或 w+b 或 wb+ | 读写打开，将文件长度截断为 0             | O\_RDWR \| O\_CREAT \| O\_TRUNC    |
| a 或 ab         | 只写打开，当文件不存在时则会创建，追加内容       | O\_WRONLY \| O\_CREAT \| O\_APPEND |
| a+ 或 a+b 或 ab+ | 读写打开，追加内容                   | O\_RDWR \| O\_CREAT \| O\_APPEND   |

## 读和写流

一旦打开了流，则可以使用 3 种不同类型的非格式化 I/O 对其进行读、写操作：

1. 每次只操作一个字符的 I/O：`fgetc` 函数和 `fputc` 函数在每次执行 I/O 操作时，只会读写一个字符；
2. 每次操作一行字符的 I/O：`fgets` 函数和 `fputs` 函数在每次执行 I/O 操作时，会以换行符为标记，读取一行的内容；
3. 直接 I/O（二进制 I/O）：`fread` 函数和 `fwrite` 函数在每次执行 I/O 操作时，会读写特定数量的对象，每个对象具有指定的长度。这两个函数通常用于从二进制文件中读写一个结构。

### 输入函数

调用 `fgetc` 函数、`getc` 函数或 `getchar` 函数可以读取一个字符。

```c
#include <stdio.h>

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

int ungetc(int c, FILE *stream);
```

`fgetc` 函数可以作为地址，被当作参数传递给另一个函数。

`getc` 函数等同于 `fgetc` 函数，它可能是通过宏来实现的。

`getchar` 函数等同于 `getc(stdin)` 函数调用。

`fgetc` 函数、`getc` 函数和 `getchar` 函数在返回下一个字符时，会将 unsigned char 类型转换为 int 类型。

### 判断是出错还是达到文件尾端

不管是出错还是达到文件尾端，这 3 个函数都会返回同样的值。为了区分这两种不同的场景，必须调用 `ferror` 函数或 `feof` 函数。

```c
#include <stdio.h>

void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
```

### 压送字符回到流中

从流中读取数据之后，可以调用 `ungetc` 函数将字符再压送回到流中。用 `ungetc` 函数压送回字符时，并没有真正将它们写到底层文件或者设备中，只是将它们写回到标准 I/O 库的缓冲区中。

```c
#include <stdio.h>

int ungetc(int c, FILE *stream);
```

压送回到流中的字符之后可以再从流中被读取，但读取字符的顺序与压送回的字符顺序相反。虽然 ISO C 允许实现库支持任意次数的将字符压送回到流中的操作，但是它要求实现库一次只能压送一个字符。压送回到流中的字符不一定是上次读取的字符，但不能压送 EOF 字符。

当正在读取一个输入流，并进行某种形式的分词或者记号切分操作时，会经常用到 `ungetc` 函数。

### 输出函数

调用 `fputc` 函数、`putc` 函数或 `putchar` 函数可以写入一个字符。

```c
#include <stdio.h>

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
```

## 每次一行 I/O

调用 `fgets` 函数可以读取一行字符。

```c
#include <stdio.h>

char *fgets(char *restrict s, int size, FILE *restrict stream);
```

调用 `fputs` 函数和 `puts` 函数可以写入一行字符。

```c
#include <stdio.h>

int fputs(const char *restrict s, FILE *restrict stream);
int puts(const char *s);
```

## 二进制 I/O

调用 `fread` 函数和 `fwrite` 函数可以读写一个完整的结构。

```c
#include <stdio.h>

size_t fread(void *restrict ptr, size_t size, size_t nmemb,
             FILE *restrict stream);
size_t fwrite(const void *restrict ptr, size_t size, size_t nmemb,
             FILE *restrict stream);
```

`fread` 函数和 `fwrite` 函数通常被用于：读写一个二进制数组、读写一个结构。

## 定位流

调用 `fseek` 函数、`ftell` 函数、`rewind` 函数、`fgetpos` 函数和 `fsetpos` 函数可以查询和设置流的偏移量。

```c
#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);

void rewind(FILE *stream);

int fgetpos(FILE *restrict stream, fpos_t *restrict pos);
int fsetpos(FILE *stream, const fpos_t *pos);
```

对于一个二进制文件，其文件位置指示器是从文件起始位置开始度量，并以字节为度量单位。但对于一个文本文件，它的文件当前位置可能不能简单地以字节偏移量来度量。

`ftell` 函数可以获取流的偏移量，`fseek` 函数可以设置流的偏移量。`rewind` 函数可以重置流的偏移量为文件的起始位置。

`fgetpos` 函数和 `fsetpos` 函数与 `ftell` 函数和 `fseek` 函数类似，不过它们是 ISO C 标准引入的。

`ftello` 函数和 `fseeko` 函数支持以 off\_t 类型来设置流的偏移量。

```c
#include <stdio.h>

int fseeko(FILE *stream, off_t offset, int whence);
off_t ftello(FILE *stream);
```

## 格式化 I/O

### 格式化输出

调用 `printf` 函数、`fprintf` 函数、`dprintf` 函数、`sprintf` 函数和 `snprintf` 函数可以以格式化的方式输出 I/O 流。

```c
#include <stdio.h>

int printf(const char *restrict format, ...);
int fprintf(FILE *restrict stream,
            const char *restrict format, ...);
int dprintf(int fd,
            const char *restrict format, ...);
int sprintf(char *restrict str,
            const char *restrict format, ...);
int snprintf(char *restrict str, size_t size,
            const char *restrict format, ...);
```

`vprintf` 函数、`vfprintf` 函数、`vdprintf` 函数、`vsprintf` 函数和 `vsnprintf` 函数也可以以格式化的方式输出 I/O 流，不过它们的可变参数列表变成了 vs\_list 类型。

```c
#include <stdarg.h>

int vprintf(const char *restrict format, va_list ap);
int vfprintf(FILE *restrict stream,
            const char *restrict format, va_list ap);
int vdprintf(int fd,
            const char *restrict format, va_list ap);
int vsprintf(char *restrict str,
            const char *restrict format, va_list ap);
int vsnprintf(char *restrict str, size_t size,
            const char *restrict format, va_list ap);
```

### 格式化输入

调用 `scanf` 函数、`fscanf` 函数和 `sscanf` 函数可以以格式化的方式输入 I/O 流。

```c
#include <stdio.h>

int scanf(const char *restrict format, ...);
int fscanf(FILE *restrict stream,
           const char *restrict format, ...);
int sscanf(const char *restrict str,
           const char *restrict format, ...);
```

`vscanf` 函数、`vfscanf` 函数和 `vsscanf` 函数也可以以格式化的方式输入 I/O 流，不过它们的可变参数列表变成了 vs\_list 类型。

```c
#include <stdarg.h>

int vscanf(const char *restrict format, va_list ap);
int vfscanf(FILE *restrict stream,
           const char *restrict format, va_list ap);
int vsscanf(const char *restrict str,
           const char *restrict format, va_list ap);
```

## 实现细节

在 UNIX 中，标准 I/O 库最终都要调用文件 I/O 中的函数。每个标准 I/O 流中都有一个与其相关联的文件描述符。

调用 `fileno` 函数可以获取与流相关的文件描述符。

```c
#include <stdio.h>

int fileno(FILE *stream);
```

## 临时文件

调用 `tmpnam` 函数或 `tmpfile` 函数可以创建一个临时文件。

```c
#include <stdio.h>

char *tmpnam(char *s);
char *tmpnam_r(char *s);

FILE *tmpfile(void);
```

## 内存流

调用 `fmemopen` 函数、`open_memstream` 函数或 `open_wmemstream` 函数可以创建一个内存流。

```c
#include <stdio.h>

FILE *fmemopen(void *buf, size_t size, const char *mode);

FILE *open_memstream(char **ptr, size_t *sizeloc);
```

```c
#include <wchar.h>

FILE *open_wmemstream(wchar_t **ptr, size_t *sizeloc);
```

`fmemopen` 函数允许调用者将提供的缓冲区用于内存流。

`open_memstream` 函数可以创建面向字节的内存流， `open_wmemstream` 函数可以创建面向宽字节的内存流。

## 标准 I/O 库的替代软件

标准 I/O 库的一个不足之处是效率不高，这与它需要复制的数据量有关系。当使用每次读写一行的 `fgets` 函数和 `fputs` 函数时，通常需要复制两次数据：第一次是在内核和标准 I/O 库缓冲区之间，第二次是在标准 I/O 缓冲区和用户程序中之间。（源码位于 [iofgets.c](https://sourceware.org/git/?p=glibc.git;a=blob;f=libio/iofgets.c;h=68774aaefd67ec39acf2abc6a4ca21f23cf3de36;hb=refs/heads/release/2.30/master) 和 [iogetline.c](https://sourceware.org/git/?p=glibc.git;a=blob;f=libio/iogetline.c;h=ad1e64b1848ecb87c5ec9b3f60a51e82ecf84f8b;hb=refs/heads/release/2.30/master) 两个源文件中。）

可替代标准 I/O 库的软件包有：

* fio：快速 I/O 库；
* sfio 库；
* ASI（Alloc Stream Interface）：提供了 `mmap` 函数；
* uClibc C 库和 Newlib C 库：适用于内存较小的系统，例如嵌入式系统。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gitbook.fantasticmao.cn/tech/c-and-unix/unix-like/unix-huan-jing-gao-ji-bian-cheng-bi-ji-biao-zhun-io-ku.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
