知识点

Python与C的文件操作

程序读写持久化数据要经过操作系统提供的文件接口。Python在open() 之上封装了文件对象,C用标准库 <stdio.h> 的流和FILE * 指针。两套接口解决同一个问题,但打开失败、关闭时机、文本解码和定位方式都不同。本页按“打开、读写、定位、关闭”的顺序逐项对照。模式规则分别见Python文件打开模式和C文件流的打开模式,二者最终都通过系统调用进入内核,系统调用层见 系统调用。

定义

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的读取与写入

操作行为
read(size=-1)读取至多size个字符(文本)或字节(二进制),省略时读到文件末尾。
readline(size=-1)读取一行,返回的字符串保留行尾换行符;最后一行没有换行符时原样返回。
readlines(hint=-1)一次读完,返回由各行组成的列表。
write(s)写入字符串或字节,返回写入的字符数;不会自动补换行符。

文件对象本身可以迭代,for line in f: 逐行读取,比readlines() 更省内存:后者会把整个文件装进列表。

C的读写

C的stdio按处理单位分成几层,各有对应的函数:

层次读写典型用途
字符fgetc(stream)fputc(c, stream)逐字符处理
行fgets(s, n, stream)fputs(s, stream)按行处理文本,不解析格式
格式化fscanf(stream, format, ...)fprintf(stream, format, ...)字段格式已知的结构化文本
块fread(ptr, size, nmemb, stream)fwrite(ptr, size, nmemb, stream)二进制数据、结构体数组

几个容易误读的返回值:

  • fscanf返回成功赋值的输入项个数,不是读到的字节数。fscanf(fp, "%d %s", &id, name) 成功读到两个字段时返回2。
  • fread和fwrite返回成功读写的元素个数,每个元素占size字节。只有返回值等于nmemb才算完整成功,返回0可能表示读到末尾,也可能表示出错。

定位

Python用tell() 取当前位置,用seek(offset, whence=0) 移动,whence取0、1、2表示相对文件开头、当前位置和文件末尾。文本模式下的offset不是可靠的字节偏移,而是编码器给出的位置值;可靠的做法是只使用0、tell() 的返回值和文件末尾。要按字节精确定位,改用二进制模式。

C用ftell(fp) 取位置,用fseek(fp, offset, whence) 移动,whence取SEEK_SET、SEEK_CUR、SEEK_END。rewind(fp) 等价于移到文件开头并清除文件结束标志。fseek成功返回0,失败返回非0。

两种接口都带缓冲,写到文件的数据可能还留在内存里。需要将语言库中的输出缓冲交给底层文件接口时,Python调用flush(),C调用fflush(fp);关闭文件也会尝试刷新缓冲区。刷新库缓冲并不保证数据已经持久写入存储介质。

一次读写操作的顺序

  1. 打开并检查结果

    Python把打开放在with里,失败时直接抛出异常;C判断fopen是否返回NULL,失败时读取errno再决定退出还是重试。

  2. 确定起始位置

    新打开的文件按模式决定起点。要从头读刚写过的内容,用seek(0) 或rewind回到开头。

  3. 读写并检查数量

    Python按字符或字节计数,短读要自己循环补足;C检查fread、fwrite的返回值是否等于nmemb,用feof与ferror区分读到末尾和出错。

  4. 刷新并关闭

    需要刷新输出缓冲时先flush() 或fflush(),再关闭文件。Python的with在退出时自动完成关闭,C必须显式fclose() 并检查返回值。

两种接口的差异

维度PythonC
打开结果文件对象FILE *
打开失败抛出OSError的子类,如FileNotFoundError返回NULL,原因记录在errno
关闭close() 或withfclose(),手动调用
文本处理按encoding解码和编码只做换行转换,不处理字符编码
读写单位字符、行、整块等高层方法字符、行、格式化、块四层函数
定位tell() / seek()ftell() / fseek()
错误检查异常返回值、feof()、ferror()

例子

Python:逐行读取文件

read.py
f = open("./Task1.txt", "r")
fls = f.readlines()
for line in fls:
line = line.strip()
print(line)
f.close()

readlines() 一次读出所有行,strip() 去掉两端空白后再打印。这段代码依赖末尾的f.close() 释放文件。

Python:用上下文管理器保证关闭

read_with.py
with open("./Task1.txt", "r") as f:
fls = f.readlines()
for line in fls:
line = line.strip()
print(line)

逻辑和上例相同,但离开with块时无论是否发生异常都会关闭文件,不需要手写close()。

Python:读取后计算并写回

update.py
f = open("./Task1.txt", "r+")
fls = f.readlines()
for line in fls:
line = line.strip()
lstr = line.split()
if lstr[0] == "3":
res = 0
for e in lstr[1:]:
res += int(e)
f.write("\n4 " + str(res))
f.close()

文件以 "r+" 打开,可以读也可以写。readlines() 读完后位置已经在文件末尾,所以后面的write是追加,而不是覆盖开头的内容。

C:写入后回读

scores.c
#include <stdio.h>
int main(void) {
FILE *fp = fopen("scores.txt", "w+");
if (fp == NULL) {
perror("fopen");
return 1;
}
fprintf(fp, "%d %s\n", 1, "Alice");
rewind(fp);
int id;
char name[32];
if (fscanf(fp, "%d %31s", &id, name) == 2) {
printf("%d %s\n", id, name);
}
fclose(fp);
return 0;
}

"w+" 先截断再以读写方式打开;写入后必须rewind把位置移回开头,才能读到刚写的内容。fopen的返回值先判空,fclose放在最后释放流。格式化读入字符串时用 %31s限定宽度,避免超出name的容量。

探索

读操作如何推进文件位置?

逐次读取固定字节并回到开头。

正在准备演示…

题集

练习

  1. open("data.txt") 等价于下列哪种写法?

    选择答案
    参考解答

    默认模式是 "r",即只读文本模式,等价于 "rt"。

  2. 文件data.txt恰好有两行。len(open("data.txt").readlines()) 的值是 。

    参考解答

    readlines() 为每一行返回一个列表元素,两行就是两个元素,元素中的字符串保留行尾换行符。

  3. "w" 与 "w+" 的区别是什么?

    选择答案
    参考解答

    两种模式都会清空已有文件、在文件不存在时创建;+ 只增加读取能力。

  4. C的fopen打开失败时返回什么?

    选择答案
    参考解答

    fopen失败返回空指针,具体原因记录在errno中,可以用perror打印。EOF是fclose失败等场景的返回值。

  5. 关于C的文件定位与状态判断,哪些叙述正确?

    题 c-position · choice
    参考解答

    ftell返回当前位置指示值,不是元素个数;文本流的位置值不应一概当作字节偏移。元素个数是fread、fwrite的返回值。

  6. fread(ptr, size, nmemb, stream) 的返回值表示什么?

    选择答案
    参考解答

    返回值以元素为单位,元素大小由size指定。只有返回值等于nmemb才表示完整读到了请求的数量。

  7. Python中with open(...) as f: 的主要作用是什么?

    选择答案
    参考解答

    with保证退出代码块时调用close(),包括因异常退出的情况;它不加锁,也不负责读取或创建文件。

  8. 关于两套接口的文本与二进制处理,哪些叙述正确?

    题 text-binary · choice
    参考解答

    文本模式要求写入str,写bytes会抛TypeError;需要按字节读写时应显式使用二进制模式。