课程笔记

6.6文件

阅读入口

目录与路径

目录树

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

  • 根目录是树的起点。Windows中通常是盘符(如C:\),Unix/Linux中是 /。
  • 目录是可以包含文件和其他目录的容器。
  • 文件是存储数据的基本单元。
  • 路径是定位文件的地址。
一个目录树
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)逐条记录允许和拒绝,概念上同样是“谁能对哪个对象做什么”。

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

Python的打开模式

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

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

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

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

C的打开模式

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

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

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

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

判断“用某个模式打开某个文件会怎样”,依据都在这张表里。下面的“判断顺序”把它压成四个依次回答的问题;路径部分按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。

关闭文件

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

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

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

学习安排

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

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

前面的语言基础在4.1 Python程序设计基础 和4.2 C程序设计基础。文件接口最终落到系统调用,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两行,再填中间的读写,避免最后忘记关闭。