# 6.6文件

## 阅读入口

* [目录树与路径](/knowledge/cs/operating-systems/file-system/)
* [Python与C的文件操作](/knowledge/cs/programming/file-io/)

## 目录与路径

### 目录树

文件系统是操作系统中负责管理持久化数据的子系统，它把存储设备上的数据组织成**文件**和**目录**，让程序不必关心数据落在哪个扇区。许多系统将路径组织成以目录为节点的层次结构：

* **根目录**是树的起点。Windows中通常是盘符（如`C:\`），Unix/Linux中是 `/`。
* **目录**是可以包含文件和其他目录的容器。
* **文件**是存储数据的基本单元。
* **路径**是定位文件的地址。

```text title="一个目录树"
C:\ (根)
├── Windows/
├── Program Files/
└── Users/
    └── Alice/
        ├── Documents/
        │   ├── PythonProjects/
        │   │   ├── ProjectA/
        │   │   │   └── main.py
        │   │   └── DataAnalysis/
        │   │       └── data.csv
        └── Downloads/
```

Windows为每个盘符建立一棵独立的树，所以一台机器可以有多个根；Unix只在 `/` 处有一棵树，其他设备通过挂载接到树上。这个差别决定了路径怎么写，但两种系统里的目录都是树的节点。

### 路径

路径由沿途的目录名和最后的文件名拼成，分为两种：

| 类型   | 起点     | 例子                                  |
| :--- | :----- | :---------------------------------- |
| 绝对路径 | 根目录    | `C:\Users\Alice\Documents\file.txt` |
| 相对路径 | 当前工作目录 | `./DataAnalysis/data.csv`           |

`.` 表示当前目录，`..` 表示父目录。分隔符随系统不同：Windows用反斜杠 `\`，Unix用斜杠 `/`。在Python字符串里写Windows路径时，反斜杠要转义，或者改用原始字符串。

相对路径的起点是进程的当前工作目录，不是脚本文件所在的目录。同一个相对路径，从不同目录启动、或运行中切换了工作目录，就可能指向不同甚至不存在的文件。需要稳定定位时用绝对路径，或先根据脚本位置构造路径。

### 访问权限

名字之外，文件系统还为每个对象记录访问控制信息。Unix风格系统常用所有者、所属组和其他用户的读、写、执行权限位，也可能结合访问控制列表等机制。目录上的这三种权限含义与普通文件不同：

| 权限 | 对文件    | 对目录           |
| :- | :----- | :------------ |
| 读  | 读取内容   | 列出目录中的条目      |
| 写  | 修改内容   | 在目录中增删条目      |
| 执行 | 作为程序运行 | 进入该目录并访问其下的文件 |

Windows用访问控制列表（ACL）逐条记录允许和拒绝，概念上同样是“谁能对哪个对象做什么”。

打开文件时，模式说明这次要做什么，权限决定进程是否被允许这样做。因此打开失败不一定代表路径写错：文件不存在、路径中某一级目录不可进入、文件没有相应权限，都会让打开操作失败，区别只在错误码和异常类型。

[来源：目录树与路径](/knowledge/cs/operating-systems/file-system/#definition)

## Python的打开模式

Python的`open`使用模式字符串指定打开方式。以下基础模式互斥：

| 模式  | 文件已存在     | 文件不存在 |
| :-- | :-------- | :---- |
| `r` | 打开供读取     | 抛出异常  |
| `w` | 打开时截断，供写入 | 创建文件  |
| `a` | 写入追加到末尾   | 创建文件  |
| `x` | 抛出异常，不覆盖  | 独占创建  |

`+` 增加读写能力；`b`指二进制模式，`t`指文本模式，二者互斥。文本模式处理字符串与编码，二进制模式处理字节。模式决定请求的操作，实际能否打开仍取决于路径、权限等条件。

`r+` 不会创建缺失文件；`w+` 可以读写，但打开现有文件时仍会截断。`a+` 支持读取和追加写入，写入仍发生在文件末尾；读取前按需要调整文件位置。

[来源：Python文件打开模式](/knowledge/cs/programming/python/file-open-modes/#definition)

## C的打开模式

C标准库的`fopen`用模式字符串指定文件流的访问方式。`r`打开已有文件供读取，`w`打开供写入并在文件存在时截断，`a`打开供追加写入；`w`与`a`在需要时创建文件。打开失败返回空指针，调用方应检查结果。

在基础模式中加入 `+` 可创建**更新流**，允许读取与写入；加入`b`表示二进制模式。文本流的外部表示可能与程序中的字符表示不同，实际转换取决于实现。C的标准模式集合与Python不完全相同，不能把Python的模式表直接当作C的规则。

[来源：C文件流的打开模式](/knowledge/cs/programming/c/file-open-modes/#definition)

更新流在切换读写方向时有次序要求：输出后转为输入，应先刷新或定位；输入后转为输出，应先定位，除非输入已遇到文件结尾。定位操作例如`fseek`；每次操作的返回值仍需检查。

追加模式的写入发生在文件末尾。对于追加更新流，初始读取位置可能由实现决定，跨平台代码应显式定位到希望读取的位置。

[来源：C文件流的打开模式](/knowledge/cs/programming/c/file-open-modes/#properties)

判断“用某个模式打开某个文件会怎样”，依据都在这张表里。下面的“判断顺序”把它压成四个依次回答的问题；路径部分按Windows写法阅读，两种语言的模式集合和文本处理规则分别判断。

## 两套接口

### Python的文件对象

`open(file, mode="r")` 返回一个文件对象，`mode`中含`b`时是二进制文件对象，否则是文本文件对象。对象内部保存打开的文件描述符、缓冲区、当前位置和解码器，后续的读取、写入、定位都作用在这个对象上。

```python
f = open("data.txt", "r")
text = f.read()
f.close()
```

文本模式返回`str`，二进制模式返回`bytes`。文本模式的编码由`encoding`参数决定，省略时使用平台相关的默认编码。

### C的流与FILE指针

C把文件抽象成**流**：一个有序的字节序列，附带缓冲区、当前位置、错误指示器和文件结束标志。`FILE`是保存这些状态的结构体，程序拿到的始终是`FILE *` 类型的**文件指针**。

```c
FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
```

`fopen`成功时返回文件指针，失败时返回`NULL`；`fclose`成功返回0，失败返回`EOF`。

> [!NOTE]
> `FILE`的成员由具体实现决定，C标准只规定库函数的行为。代码不要直接读写结构体成员，一律通过`fgetc`、`fprintf`、`fseek`等函数操作流，否则换一个编译器或平台就会失效。

### 关闭文件

关闭动作把缓冲区里尚未写出的数据刷到设备，并释放文件描述符。Python的`close()` 和C的`fclose()` 都承担这件事，区别在于语言是否替你保证调用：

* **Python**：用`with open(...) as f:` 时，离开代码块会自动调用`close()`，即使块内抛出异常也会执行。手写的`f.close()` 在异常路径上容易被跳过。
* **C**：没有语言级的自动关闭，必须显式`fclose()`。进程正常退出时标准库会刷新并关闭仍打开的流，但依赖这一点会掩盖错误，长期运行的程序还可能耗尽文件描述符。

[来源：Python与C的文件操作](/knowledge/cs/programming/file-io/#definition)

Python的文件对象与C的`FILE *` 在这段里对照。阅读C接口时，把`fopen`的`NULL`和`fclose`的返回值一起记住。

## 学习安排

这一节从[6.5进程](/courses/cs/csintro/6.5/) 的“已打开文件表”接着往下：进程要读写持久化数据，得先把文件系统里的对象打开成本进程可用的句柄。教材的顺序是先用目录树和路径说明文件在哪里，再用一张模式表说明打开时能做什么，最后分别给出Python和C的接口。

建议按这个顺序读：先确认根目录、绝对路径与相对路径的差别，把“当前工作目录决定相对路径”这一点记住；再逐列读模式表，不要只背模式字符串；最后把两段Python示例与C的函数原型对照。C部分只列原型和返回值，没有完整程序，读的时候自己补一个`fopen`判空和`fclose`的骨架。

前面的语言基础在[4.1 Python程序设计基础](/courses/cs/csintro/4.1/) 和[4.2 C程序设计基础](/courses/cs/csintro/4.2/)。文件接口最终落到系统调用，[6.4软件中断与系统调用](/courses/cs/csintro/6.4/) 解释了`fopen`、`open`这些库函数下面发生了什么。

## 课程约定

* 目录树用Windows盘符和反斜杠举例，路径按Windows读；模式字符串按具体语言分别判断。
* Python的文本模式可用`t`表示；C的标准模式不应直接套用Python的`t`写法。
* C11起可以用`x`表示排他创建，也可以与`w`组合成`wx`；答题按题目给出的模式集合判断。
* `fclose`成功返回0，失败返回`EOF`。
* 示例文件名是`Task1.txt`，路径写作 `./Task1.txt`，程序从启动时的工作目录找它。

## 判断顺序

遇到“用某个模式打开某个文件会怎样”的问题，按固定顺序过一遍：

1. 文件是否存在？不存在时看模式是否创建，`r`系列直接报错。
2. 存在时是否被清空？只有`w`系列清空。
3. 读写位置如何确定？追加模式的写入在末尾，更新流的初始读取位置及切换规则要结合语言和实现判断。
4. 能不能读、能不能写？带 `+` 才能同时读写。

按这四步走，`r+` 与`w+`、`a`与`a+` 两组就不会混。做题时常见两个选项分别问位置和读写能力，先定位再判断读写。

## 易错点

* `"w"` 的截断发生在打开时，哪怕一行都没写。
* `"r+"` 不创建文件；要“存在就改、不存在就建”得自己判断，或改用会清空文件的 `"w+"`。
* `"a"` 只是追加写，`"a+"` 才多了读；`a+` 的写入位置仍在末尾，读之前通常要`seek(0)`。
* Python的`readlines()` 读完后位置在文件末尾，紧接着的`write`是追加，不是覆盖。
* `close()` 会影响缓冲数据的写出与资源释放；Python使用`with`可以保证异常时也关闭文件。
* C的`fopen`必须判`NULL`，并注意更新流切换读写方向前需要定位或刷新。

## 复习要点

复习时把模式表的“文件存在时”一列遮住，只看模式和“文件不存在时”，逐个说出另一种情况；说全再反过来。这样能突出`r+` 不会在文件不存在时创建文件这一差异。

写文件相关代码时先写`open`和`close`两行，再填中间的读写，避免最后忘记关闭。
