Skip to content

C 文件 I/O

除了与标准输入(键盘)和标准输出(屏幕)交互之外,C 程序通常还需要从文件中读取数据或向文件中写入数据以进行持久存储。本章介绍用于文件输入/输出 (I/O) 的标准 C 库函数。

在 C 语言中,文件通常被视为一个字节序列,通常称为流(stream)。标准 I/O 库(<stdio.h>)提供用于管理这些流的函数,无论它们代表文本文件还是二进制文件。

所有标准 I/O 操作都使用一个指向 FILE 结构体的指针。这个结构体定义在 <stdio.h> 中,它保存了管理文件流所需的内部信息,例如当前位置、缓冲区详细信息、错误指示符和文件结束状态。您通常不需要直接操作 FILE 结构体的成员;相反,您会将 FILE * 指针传递给各种 I/O 函数。

要处理文件,首先需要使用 fopen() 函数打开它。它尝试将一个流与指定的文件关联起来。

FILE *fopen(const char * restrict filename, const char * restrict mode);

filename:一个字符串,包含要打开的文件名(可能包含路径)。

mode:一个字符串,指定您打算如何使用文件(读、写、追加等)。常用的模式包括:

模式 (Mode)描述 (文本文件)
"r"打开一个现有的文本文件进行读取。文件必须存在。
"w"打开一个文本文件进行写入。如果文件存在,其内容会被丢弃(截断)。如果不存在,则创建一个新文件。
"a"打开一个文本文件进行追加(在文件末尾写入)。如果文件不存在,则创建一个新文件。
"r+"打开一个现有的文本文件进行读写。文件必须存在。
"w+"打开一个文本文件进行读写。截断现有文件或创建一个新文件。
"a+"打开一个文本文件进行读写和追加。如果文件不存在则创建。初始读取位置在文件开头,但写入总是追加到文件末尾。

对于二进制文件,请在模式字符串后附加 b(例如,"rb"、"wb"、"ab"、"rb+"、"wb+"、"ab+")。在类 Unix 系统上,b 通常影响不大,但在像 Windows 这样的系统上,它对于正确处理二进制数据至关重要(防止换行符转换)。

返回值: 如果成功,fopen() 返回一个非 NULL 的 FILE * 指针,该指针与文件相关联。如果发生错误(例如,"r" 模式下文件未找到,权限被拒绝),它返回 NULL 并设置 errno。

关键: 调用 fopen() 后,务必检查返回的指针是否为 NULL。

完成文件操作后,您必须使用 fclose() 关闭文件,以确保所有缓冲的数据都写入磁盘(刷新)并且释放与文件关联的系统资源。

int fclose(FILE *stream);

stream:从 fopen() 获取的 FILE * 指针。

返回值: 成功时返回 0。如果发生错误(例如,最终缓冲区刷新期间出错),返回 EOF(一个定义在 <stdio.h> 中的常量,通常为 -1)。检查 fclose() 的返回值是良好实践,特别是对于以写入模式打开的文件。

有几个函数允许向文本文件写入:

  • int fputc(int c, FILE *stream);:将单个字符 c 写入流。成功时返回写入的字符,错误时返回 EOF。
  • int fputs(const char * restrict s, FILE * restrict stream);:将以空字符结尾的字符串 s 写入流(不包括空终止符)。成功时返回非负值,错误时返回 EOF。
  • int fprintf(FILE * restrict stream, const char * restrict format, ...);:将格式化输出写入流,类似于 printf 但目标是文件。成功时返回写入的字符数,错误时返回负值。

示例:写入文件

#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
// 用于 EXIT_SUCCESS, EXIT_FAILURE
#include <errno.h> // For errno
// 用于 errno
#include <string.h> // For strerror
// 用于 strerror
int main(void) {
FILE *fp = NULL; // Initialize pointer to NULL
// 初始化指针为 NULL
const char *filename = "output.txt"; // Use a relative path
// 使用相对路径
// Open file in write mode ("w+" allows writing and reading)
// 以写入模式打开文件("w+" 允许写入和读取)
fp = fopen(filename, "w+");
if (fp == NULL) {
perror("Error opening file");
// 打开文件错误
return EXIT_FAILURE;
}
// Write using fprintf
// 使用 fprintf 写入
int written = fprintf(fp, "Testing fprintf: line %d\n", 1);
// 测试 fprintf:第 %d 行
if (written < 0) {
perror("Error writing with fprintf");
// 使用 fprintf 写入错误
fclose(fp); // Attempt to close even on error
// 即使出错也尝试关闭
return EXIT_FAILURE;
}
// Write using fputs
// 使用 fputs 写入
if (fputs("Testing fputs: line 2\n", fp) == EOF) {
// 测试 fputs:第 2 行
perror("Error writing with fputs");
// 使用 fputs 写入错误
fclose(fp);
return EXIT_FAILURE;
}
// Write using fputc
// 使用 fputc 写入
if (fputc('3', fp) == EOF) {
perror("Error writing with fputc");
// 使用 fputc 写入错误
fclose(fp);
return EXIT_FAILURE;
}
fputc('\n', fp); // Don't forget newline if needed
// 如果需要,不要忘记换行符
printf("Successfully wrote to %s\n", filename);
// 成功写入 %s
// Close the file and check for errors
// 关闭文件并检查错误
if (fclose(fp) == EOF) {
perror("Error closing file");
// 关闭文件错误
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

运行后,output.txt 将包含:

Testing fprintf: line 1
Testing fputs: line 2
3

用于从文本文件读取的函数包括:

  • int fgetc(FILE *stream);:从流中读取下一个字符。成功时返回读取的字符(作为 int),在文件结束或错误时返回 EOF。使用 feof() 和 ferror() 来区分文件结束和错误。
  • char *fgets(char * restrict s, int n, FILE * restrict stream);:从流中最多读取 n-1 个字符到缓冲区 s 中。读取在遇到换行符(\n)、文件结束或读取了 n-1 个字符后停止。如果读取了换行符,它会存储在缓冲区中。始终附加一个空终止符(\0)。成功时返回 s,在文件结束(且未读取任何字符)或错误时返回 NULL。fgets 通常比 fscanf 读取字符串更安全,因为它能防止缓冲区溢出。
  • int fscanf(FILE * restrict stream, const char * restrict format, ...);:从流中读取格式化输入,类似于 scanf。成功匹配和赋值的输入项数,如果在任何转换之前输入失败则返回 EOF。警告: 在没有宽度限制的情况下使用 fscanf 和 %s 是危险的,如果输入比缓冲区长,可能导致缓冲区溢出。请使用 %<width>s(例如 %254s)或首选使用 fgets。

示例:从先前创建的文件中读取

#include <stdio.h>
#include <stdlib.h> // For EXIT_SUCCESS, EXIT_FAILURE
// 用于 EXIT_SUCCESS, EXIT_FAILURE
#define BUFFER_SIZE 256
int main(void) {
FILE *fp = NULL;
char buffer[BUFFER_SIZE];
const char *filename = "output.txt";
fp = fopen(filename, "r");
if (fp == NULL) {
perror("Error opening file for reading");
// 打开文件进行读取错误
return EXIT_FAILURE;
}
printf("Reading content using fgets:\n---\n");
// 使用 fgets 读取内容:
// Read line by line using fgets (safer)
// 使用 fgets 逐行读取(更安全)
while (fgets(buffer, BUFFER_SIZE, fp) != NULL) {
// Process the line (here, just print it)
// 处理该行(这里只是打印出来)
printf("%s", buffer); // fgets includes the newline, so no extra \n needed
// fgets 包含换行符,所以无需额外的 \n
}
printf("---\n");
// Check if loop ended due to error or EOF
// 检查循环结束是由于错误还是文件结束
if (ferror(fp)) {
perror("Error reading file");
// 读取文件错误
} else if (feof(fp)) {
printf("End of file reached.\n");
// 已到达文件末尾。
} else {
printf("fgets returned NULL without EOF or error set? Unexpected.\n");
// fgets 返回 NULL 但未设置 EOF 或错误?意外情况。
}
if (fclose(fp) == EOF) {
perror("Error closing file");
// 关闭文件错误
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

编译并执行时,这将读取 output.txt 并产生以下输出:

Reading content using fgets:
---
Testing fprintf: line 1
Testing fputs: line 2
3
---
End of file reached.

二进制 I/O 函数(fread 和 fwrite)

Section titled “二进制 I/O 函数(fread 和 fwrite)”

对于读取和写入二进制数据(如原始字节、数组或结构体),请使用 fread 和 fwrite。这些函数处理指定大小的数据块。

size_t fread(void * restrict ptr, size_t size, size_t count, FILE * restrict stream);
size_t fwrite(const void * restrict ptr, size_t size, size_t count, FILE * restrict stream);
  • ptr:指向用于读取数据(fread)或写入数据(fwrite)的缓冲区的指针。
  • size:要读取/写入的每个元素的大小(以字节为单位)。
  • count:要读取/写入的元素数量。
  • stream:FILE * 指针。
  • 返回值: 两个函数都返回成功读取或写入的元素数量。如果发生错误或到达文件末尾(fread),此值可能小于 count。使用 feof() 和 ferror() 来确定原因。

示例:写入和读取一个简单的结构体

#include <stdio.h>
#include <stdlib.h>
// Define a simple structure
// 定义一个简单的结构体
typedef struct {
int id;
double value;
} Record;
int main(void) {
FILE *fp = NULL;
const char *filename = "records.bin";
Record rec_write = {101, 3.14159};
Record rec_read;
size_t items_written, items_read;
// --- Write the structure to a binary file ---
// --- 将结构体写入二进制文件 ---
fp = fopen(filename, "wb"); // Note: "wb" mode
// 注意:“wb”模式
if (!fp) { perror("Cannot open for writing"); return EXIT_FAILURE; }
// 无法打开文件进行写入
items_written = fwrite(&rec_write, sizeof(Record), 1, fp);
if (items_written != 1) {
fprintf(stderr, "Error writing record (wrote %zu elements).\n", items_written);
// 写入记录错误(写入了 %zu 个元素)。
fclose(fp);
return EXIT_FAILURE;
}
printf("Wrote record: ID=%d, Value=%.5f\n", rec_write.id, rec_write.value);
// 写入记录:ID=%d, 值=%.5f
if (fclose(fp) == EOF) { perror("Error closing after write"); return EXIT_FAILURE; }
// 写入后关闭文件错误
// --- Read the structure back from the binary file ---
// --- 从二进制文件读回结构体 ---
fp = fopen(filename, "rb"); // Note: "rb" mode
// 注意:“rb”模式
if (!fp) { perror("Cannot open for reading"); return EXIT_FAILURE; }
// 无法打开文件进行读取
items_read = fread(&rec_read, sizeof(Record), 1, fp);
if (items_read != 1) {
if (feof(fp)) {
fprintf(stderr, "Error reading record: Unexpected end of file.\n");
// 读取记录错误:意外的文件末尾。
} else if (ferror(fp)) {
perror("Error reading record");
// 读取记录错误
}
fclose(fp);
return EXIT_FAILURE;
}
printf("Read record: ID=%d, Value=%.5f\n", rec_read.id, rec_read.value);
// 读取记录:ID=%d, 值=%.5f
if (fclose(fp) == EOF) { perror("Error closing after read"); return EXIT_FAILURE; }
// 读取后关闭文件错误
return EXIT_SUCCESS;
}

输出:

Wrote record: ID=101, Value=3.14159
Read record: ID=101, Value=3.14159