第11章 文件输入/输出 文件的基本概念 文件是存储在外部存储设备(如硬盘、U盘、SD卡等)上的数据流,具有唯一的名称和路径。文件是持久化存储数据的重要方式,也是程序与外部世界交互的重要媒介。
文件系统 文件系统是操作系统用于管理存储设备上文件的方法和数据结构,它负责:
文件组织 - 将文件组织成目录结构空间管理 - 分配和回收存储空间文件访问 - 控制对文件的读写操作文件保护 - 提供文件权限和安全机制常见的文件系统包括:
FAT32 - 适用于移动设备的文件系统NTFS - Windows 系统的主要文件系统EXT4 - Linux 系统的主要文件系统APFS - macOS 系统的主要文件系统文件分类 在 C 语言中,文件可以分为以下两种类型:
文本文件
以字符序列形式存储 每行以换行符结束(不同系统换行符可能不同:Windows 是 \r\n,Linux/Unix 是 \n,macOS 是 \n) 可以用文本编辑器直接查看和编辑 适合存储人类可读的数据,如代码、配置文件、文档等 二进制文件
以字节序列形式存储 直接存储数据的二进制表示 不能用文本编辑器直接查看(会显示乱码) 适合存储程序数据、图像、音频、视频等 文件路径 文件路径是指文件在文件系统中的位置,有两种表示方式:
绝对路径 - 从根目录开始的完整路径,如 C:\\Users\\username\\file.txt(Windows)或 /home/username/file.txt(Linux)相对路径 - 相对于当前工作目录的路径,如 ../docs/file.txt 或 ./file.txt文件权限 文件权限控制谁可以访问文件以及如何访问文件,通常包括:
读取权限 - 查看文件内容写入权限 - 修改文件内容执行权限 - 运行可执行文件在 C 语言中,文件权限通常在文件创建时指定,具体实现依赖于操作系统。
文件句柄 文件句柄是操作系统用于标识和管理打开文件的唯一标识符。在 C 语言中,文件句柄被封装在 FILE 结构体中,通过文件指针来操作。
文件指针 在 C 语言中,文件操作通过文件指针进行。文件指针是指向 FILE 结构体的指针,FILE 结构体定义在 <stdio.h> 头文件中,包含了文件操作所需的各种信息。
FILE 结构体 FILE 结构体是一个复杂的结构体,通常包含以下成员:
文件描述符 - 操作系统用于标识文件的整数文件位置指示器 - 指向文件当前读写位置的指针缓冲区信息 - 用于缓冲文件读写数据的缓冲区错误标志 - 指示文件操作是否出错文件结束标志 - 指示是否到达文件末尾文件模式 - 指示文件的打开模式(只读、只写等)不同编译器和操作系统的 FILE 结构体实现可能略有不同,但基本功能是一致的。
文件指针的声明 1 2 3 #include <stdio.h> FILE *fp;
文件指针的特性 唯一性 - 每个打开的文件都有一个唯一的文件指针状态管理 - 文件指针内部管理文件的状态,如当前位置、错误状态等缓冲区 - 文件指针通常使用缓冲区来提高读写效率自动管理 - 标准文件指针(stdin、stdout、stderr)由系统自动管理文件指针的生命周期 创建 - 通过 fopen 等函数创建文件指针使用 - 通过文件指针进行各种文件操作关闭 - 通过 fclose 函数关闭文件指针,释放资源标准文件指针 C 语言提供了三个标准文件指针,它们在程序启动时自动打开:
stdin - 标准输入(默认是键盘)stdout - 标准输出(默认是屏幕)stderr - 标准错误(默认是屏幕)这些标准文件指针不需要手动打开,也通常不需要手动关闭(由系统自动处理)。
文件指针的安全性 空指针检查 - 始终检查 fopen 的返回值,确保文件指针不为 NULL避免悬垂指针 - 文件关闭后,文件指针变为悬垂指针,不应再使用资源泄漏 - 确保每个打开的文件都被正确关闭,避免资源泄漏并发访问 - 多个线程同时访问同一文件时需要同步处理文件的打开和关闭 文件的打开 使用 fopen 函数打开文件,该函数返回一个文件指针。fopen 函数是文件操作的入口点,所有文件操作都始于此。
fopen 函数原型 1 FILE *fopen (const char *filename, const char *mode) ;
参数解析 filename - 要打开的文件路径,可以是绝对路径或相对路径mode - 文件打开模式,指定文件的访问方式返回值 成功 - 返回指向 FILE 结构体的指针失败 - 返回 NULL,并设置 errno 为相应的错误代码文件打开模式详解 文件打开模式决定了文件的访问方式和行为,是文件操作的重要参数。
模式 描述 文件存在 文件不存在 位置指针 "r"只读模式 打开成功 打开失败 文件开头 "w"只写模式 截断为 0 长度 创建新文件 文件开头 "a"追加模式 保留原有内容 创建新文件 文件末尾 "r+"读写模式 打开成功 打开失败 文件开头 "w+"读写模式 截断为 0 长度 创建新文件 文件开头 "a+"读写模式 保留原有内容 创建新文件 文件末尾 "rb"二进制只读模式 打开成功 打开失败 文件开头 "wb"二进制只写模式 截断为 0 长度 创建新文件 文件开头 "ab"二进制追加模式 保留原有内容 创建新文件 文件末尾 "r+b"/"rb+"二进制读写模式 打开成功 打开失败 文件开头 "w+b"/"wb+"二进制读写模式 截断为 0 长度 创建新文件 文件开头 "a+b"/"ab+"二进制读写模式 保留原有内容 创建新文件 文件末尾
模式说明 文本模式 vs 二进制模式
文本模式 - 不使用 b 后缀,会进行换行符转换(Windows 下 \n 转换为 \r\n)二进制模式 - 使用 b 后缀,直接读写二进制数据,不进行任何转换读写权限
r - 只读w - 只写a - 追加写+ - 读写文件存在性处理
r 模式 - 要求文件必须存在w 模式 - 无论文件是否存在,都会创建或截断a 模式 - 如果文件不存在则创建,存在则追加文件的关闭 使用 fclose 函数关闭文件,释放文件指针和相关资源。文件关闭是文件操作的重要环节,必须确保每个打开的文件都被正确关闭。
fclose 函数原型 1 int fclose (FILE *stream) ;
参数和返回值 参数 - stream - 要关闭的文件指针返回值 - 成功返回 0,失败返回 EOF文件关闭的重要性 释放资源 - 关闭文件可以释放操作系统为文件分配的资源刷新缓冲区 - 确保所有缓冲区中的数据都被写入文件避免资源泄漏 - 长时间运行的程序如果不关闭文件,会导致资源泄漏文件锁定 - 关闭文件可以释放对文件的锁定,允许其他程序访问错误处理 文件关闭失败通常是由于磁盘错误或权限问题导致的,应该妥善处理:
1 2 3 4 5 if (fclose(fp) != 0 ){ perror("关闭文件失败" ); }
打开和关闭文件的最佳实践 始终检查文件打开是否成功 1 2 3 4 5 6 FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ perror("无法打开文件" ); return EXIT_FAILURE; }
使用 RAII 风格的文件管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ perror("无法打开文件" ); return EXIT_FAILURE; } if (fclose(fp) != 0 ){ perror("无法关闭文件" ); return EXIT_FAILURE; }
使用 perror 或 strerror 打印错误信息 1 2 3 4 5 6 7 8 9 10 #include <stdio.h> #include <string.h> #include <errno.h> FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ fprintf (stderr , "无法打开文件:%s\n" , strerror(errno)); return EXIT_FAILURE; }
处理多个文件时的关闭顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FILE *fp1 = fopen("file1.txt" , "r" ); FILE *fp2 = fopen("file2.txt" , "w" ); if (fp1 == NULL || fp2 == NULL ){ perror("无法打开文件" ); if (fp1 != NULL ) fclose(fp1); if (fp2 != NULL ) fclose(fp2); return EXIT_FAILURE; } if (fclose(fp1) != 0 ) perror("无法关闭 file1.txt" ); if (fclose(fp2) != 0 ) perror("无法关闭 file2.txt" );
示例:打开和关闭文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> #include <stdlib.h> int main (void ) { FILE *fp; fp = fopen("example.txt" , "w" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } printf ("文件打开成功\n" ); fprintf (fp, "Hello, File I/O!\n" ); if (fclose(fp) == 0 ) { printf ("文件关闭成功\n" ); } else { perror("无法关闭文件" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
其他文件打开函数 除了 fopen,C 标准库还提供了其他文件打开函数:
freopen - 重新打开文件,常用于重定向标准输入/输出1 FILE *freopen (const char *filename, const char *mode, FILE *stream) ;
fdopen - 从文件描述符创建文件指针1 FILE *fdopen (int fd, const char *mode) ;
tmpfile - 创建临时文件文件的读写操作 文件读写是文件操作的核心,C 标准库提供了多种级别的文件读写函数,从字符级到块级,满足不同场景的需求。
字符级读写 字符级读写是最基本的文件读写方式,以单个字符为单位进行操作。
读取单个字符 1 int fgetc (FILE *stream) ;
参数和返回值:
stream - 文件指针返回值 - 读取的字符(转换为 int 类型),文件结束或出错返回 EOF内部工作原理:
检查文件是否打开且可读 检查缓冲区是否有数据 如果缓冲区为空,从文件读取一批数据到缓冲区 从缓冲区中取出一个字符 更新文件位置指示器 如果文件已结束或发生错误,返回 EOF 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } int ch; while ((ch = fgetc(fp)) != EOF) { putchar (ch); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
写入单个字符 1 int fputc (int c, FILE *stream) ;
参数和返回值:
c - 要写入的字符(转换为 unsigned char 后写入)stream - 文件指针返回值 - 成功返回写入的字符,失败返回 EOF内部工作原理:
检查文件是否打开且可写 检查缓冲区是否已满 如果缓冲区未满,将字符写入缓冲区 如果缓冲区已满,将缓冲区数据写入文件 更新文件位置指示器 如果发生错误,返回 EOF 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "w" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char str[] = "Hello, World!" ; for (int i = 0 ; str[i] != '\0' ; i++) { if (fputc(str[i], fp) == EOF) { perror("写入文件时出错" ); fclose(fp); return 1 ; } } fclose(fp); return 0 ; }
其他字符级读写函数 getc - 与 fgetc 功能相同,通常是宏实现putc - 与 fputc 功能相同,通常是宏实现getchar - 从 stdin 读取一个字符,等价于 getc(stdin)putchar - 向 stdout 写入一个字符,等价于 putc(c, stdout)行级读写 行级读写以行为单位进行操作,适用于处理文本文件。
读取一行 1 char *fgets (char *s, int size, FILE *stream) ;
参数和返回值:
s - 存储读取数据的缓冲区size - 缓冲区大小(包括终止符 '\0')stream - 文件指针返回值 - 成功返回 s,文件结束或出错返回 NULL内部工作原理:
检查文件是否打开且可读 从文件读取字符,直到遇到换行符 '\n'、文件结束或已读取 size-1 个字符 在缓冲区末尾添加终止符 '\0' 如果读取到换行符,将其包含在缓冲区中 更新文件位置指示器 注意事项:
fgets 会保留换行符 '\n'如果一行长度超过 size-1,会读取部分行,剩余部分需要再次调用 fgets 读取 当返回 NULL 时,需要使用 feof 和 ferror 区分文件结束和错误 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char buffer[100 ]; while (fgets(buffer, sizeof (buffer), fp) != NULL ) { printf ("%s" , buffer); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
写入一行 1 int fputs (const char *s, FILE *stream) ;
参数和返回值:
s - 要写入的字符串stream - 文件指针返回值 - 成功返回非负值,失败返回 EOF内部工作原理:
检查文件是否打开且可写 将字符串中的字符逐个写入文件(不包括终止符 '\0') 更新文件位置指示器 如果发生错误,返回 EOF 注意事项:
fputs 不会自动添加换行符 '\n'如果需要换行,需要手动添加 '\n' 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "w" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char *lines[] = { "第一行" , "第二行" , "第三行" }; for (int i = 0 ; i < 3 ; i++) { if (fputs (lines[i], fp) == EOF || fputs ("\n" , fp) == EOF) { perror("写入文件时出错" ); fclose(fp); return 1 ; } } fclose(fp); return 0 ; }
块级读写 块级读写以数据块为单位进行操作,适用于处理二进制文件和大文件,效率比字符级和行级读写更高。
读取数据块 1 size_t fread (void *ptr, size_t size, size_t count, FILE *stream) ;
参数和返回值:
ptr - 存储读取数据的缓冲区指针size - 每个数据项的大小(以字节为单位)count - 要读取的数据项数量stream - 文件指针返回值 - 成功读取的数据项数量内部工作原理:
计算要读取的总字节数:size * count 检查文件是否打开且可读 从文件读取数据到缓冲区 更新文件位置指示器 返回实际读取的数据项数量 注意事项:
如果返回值小于 count,可能是文件已结束或发生错误 需要使用 feof 和 ferror 区分文件结束和错误 对于二进制文件,建议使用块级读取 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> int main (void ) { FILE *fp = fopen("data.bin" , "rb" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } int buffer[100 ]; size_t items_read = fread(buffer, sizeof (int ), 100 , fp); printf ("读取了 %zu 个整数\n" , items_read); if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
写入数据块 1 size_t fwrite (const void *ptr, size_t size, size_t count, FILE *stream) ;
参数和返回值:
ptr - 要写入的数据的指针size - 每个数据项的大小(以字节为单位)count - 要写入的数据项数量stream - 文件指针返回值 - 成功写入的数据项数量内部工作原理:
计算要写入的总字节数:size * count 检查文件是否打开且可写 将数据从缓冲区写入文件 更新文件位置指示器 返回实际写入的数据项数量 注意事项:
如果返回值小于 count,表示发生了错误 对于二进制文件,建议使用块级写入 写入二进制文件时,需要注意数据类型的大小和对齐方式 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <stdio.h> int main (void ) { FILE *fp = fopen("data.bin" , "wb" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } int data[] = {1 , 2 , 3 , 4 , 5 }; size_t items_written = fwrite(data, sizeof (int ), 5 , fp); printf ("写入了 %zu 个整数\n" , items_written); if (items_written != 5 ) { perror("写入文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
格式化读写 格式化读写允许按照指定的格式读取和写入数据,适用于处理结构化数据。
格式化读取 1 int fscanf (FILE *stream, const char *format, ...) ;
参数和返回值:
stream - 文件指针format - 格式控制字符串... - 存储读取数据的变量的地址列表返回值 - 成功读取的数据项数量,文件结束返回 EOF内部工作原理:
解析格式控制字符串 从文件读取字符,按照格式控制字符串的要求进行解析 将解析结果存储到对应的变量中 更新文件位置指示器 返回成功读取的数据项数量 格式控制字符串:
%d - 读取十进制整数%f - 读取浮点数%s - 读取字符串(以空白字符为分隔符)%c - 读取单个字符%x - 读取十六进制整数等等 注意事项:
fscanf 会自动跳过空白字符(除了 %c 格式)当返回 EOF 时,表示文件已结束 当返回值小于格式控制字符串中指定的数据项数量时,表示发生了错误或文件结束 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> int main (void ) { FILE *fp = fopen("data.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } int id; char name[50 ]; float score; while (fscanf (fp, "%d %s %f" , &id, name, &score) == 3 ) { printf ("ID: %d, Name: %s, Score: %.2f\n" , id, name, score); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
格式化写入 1 int fprintf (FILE *stream, const char *format, ...) ;
参数和返回值:
stream - 文件指针format - 格式控制字符串... - 要写入的数据列表返回值 - 成功写入的字符数,失败返回负值内部工作原理:
解析格式控制字符串 将数据按照格式控制字符串的要求转换为字符串 将转换后的字符串写入文件 更新文件位置指示器 返回成功写入的字符数 格式控制字符串:
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> int main (void ) { FILE *fp = fopen("data.txt" , "w" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } struct Student { int id; char name[50 ]; float score; } students[] = { {1 , "Alice" , 95.5 }, {2 , "Bob" , 87.0 }, {3 , "Charlie" , 92.5 } }; for (int i = 0 ; i < 3 ; i++) { int chars_written = fprintf (fp, "%d %s %.2f\n" , students[i].id, students[i].name, students[i].score); if (chars_written < 0 ) { perror("写入文件时出错" ); fclose(fp); return 1 ; } } fclose(fp); return 0 ; }
读写函数性能比较 读写方式 优点 缺点 适用场景 字符级读写 简单直观,适合处理单个字符 性能较低,频繁 I/O 操作 处理文本文件中的单个字符 行级读写 适合处理文本文件的行 对于长行需要多次调用 处理文本文件中的行 块级读写 性能高,减少 I/O 操作次数 不适合处理格式化文本 处理二进制文件和大文件 格式化读写 适合处理结构化数据 性能较低,解析开销大 处理结构化文本数据
读写操作的最佳实践 根据文件类型选择合适的读写方式
文本文件:使用行级或格式化读写 二进制文件:使用块级读写 使用适当的缓冲区大小
对于块级读写,选择合适的缓冲区大小(如 4096 字节) 避免使用过小的缓冲区导致频繁 I/O 操作 妥善处理错误
始终检查读写函数的返回值 使用 feof 和 ferror 区分文件结束和错误 关闭文件前刷新缓冲区
使用 fflush 函数刷新缓冲区 或确保通过 fclose 函数关闭文件(会自动刷新缓冲区) 避免混合使用不同级别的读写函数
同一文件最好使用同一级别的读写函数 如果必须混合使用,需要调用 fflush 或调整文件位置指示器 文件定位 文件定位是指在文件中移动读写位置的操作,对于需要随机访问文件的场景非常重要。C 标准库提供了一系列文件定位函数,用于获取和设置文件位置指示器。
文件位置指示器 每个打开的文件都有一个文件位置指示器(File Position Indicator),也称为文件指针(注意与 C 语言中的文件指针 FILE * 不同),它指向当前读写操作的位置。
文件位置指示器的特性 初始位置 - 根据文件打开模式的不同,文件位置指示器的初始位置也不同:
r、r+、rb、r+b 模式:指向文件开头w、w+、wb、w+b 模式:指向文件开头(文件被截断为 0 长度)a、a+、ab、a+b 模式:指向文件末尾自动更新 - 当执行读写操作时,文件位置指示器会自动更新:
读取操作后,指示器移动到已读取数据的后面 写入操作后,指示器移动到已写入数据的后面 随机访问 - 通过文件定位函数,可以手动设置文件位置指示器的位置,实现随机访问
文件定位函数 C 标准库提供了以下文件定位函数:
获取当前位置 1 long ftell (FILE *stream) ;
参数和返回值:
stream - 文件指针返回值 - 当前文件位置(相对于文件开头的字节偏移量),失败返回 -1L 并设置 errno内部工作原理:
检查文件是否打开 获取文件位置指示器的当前值 返回该值 注意事项:
对于二进制文件,返回值是准确的字节偏移量 对于文本文件,返回值可能不是准确的字节偏移量,因为文本模式下会进行换行符转换 设置文件位置 1 int fseek (FILE *stream, long offset, int whence) ;
参数和返回值:
stream - 文件指针offset - 偏移量(以字节为单位)whence - 起始位置:SEEK_SET - 文件开头SEEK_CUR - 当前位置SEEK_END - 文件末尾返回值 - 成功返回 0,失败返回非 0 并设置 errno内部工作原理:
检查文件是否打开 根据 whence 和 offset 计算新的文件位置 检查新位置是否有效 设置文件位置指示器到新位置 清除文件结束标志 如果是文本模式,可能需要进行特殊处理 注意事项:
对于二进制文件,offset 是准确的字节偏移量 对于文本文件,offset 必须是之前通过 ftell 获取的值,或者是 0 在追加模式(a 或 a+)下,无论 fseek 如何设置,写入操作总是从文件末尾开始 重置文件位置到开头 1 void rewind (FILE *stream) ;
参数:
内部工作原理:
调用 fseek(stream, 0L, SEEK_SET) 设置文件位置到开头 清除文件结束标志和错误标志 注意事项:
rewind 不返回错误信息,无法知道操作是否成功对于需要检查错误的场景,建议使用 fseek 代替 获取文件大小 虽然 C 标准库没有直接提供获取文件大小的函数,但可以通过文件定位函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 long get_file_size (FILE *stream) { long current_pos = ftell(stream); if (current_pos == -1L ) return -1L ; if (fseek(stream, 0L , SEEK_END) != 0 ) return -1L ; long size = ftell(stream); if (size == -1L ) return -1L ; if (fseek(stream, current_pos, SEEK_SET) != 0 ) return -1L ; return size; }
文件定位的高级技巧 随机访问二进制文件 1 2 3 fseek(fp, (n-1 ) * sizeof (Record), SEEK_SET); fread(&record, sizeof (Record), 1 , fp);
在文件中间插入数据 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 FILE *fp = fopen("file.txt" , "r" ); FILE *temp = fopen("temp.txt" , "w" ); char buffer[4096 ];long insert_pos = 100 ; fread(buffer, 1 , insert_pos, fp); fwrite(buffer, 1 , insert_pos, temp); fputs ("Inserted data\n" , temp);while (fgets(buffer, sizeof (buffer), fp) != NULL ){ fputs (buffer, temp); } fclose(fp); fclose(temp); remove("file.txt" ); rename("temp.txt" , "file.txt" );
倒序读取文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 FILE *fp = fopen("file.txt" , "rb" ); if (fp == NULL ){ perror("无法打开文件" ); return 1 ; } if (fseek(fp, 0 , SEEK_END) != 0 ){ perror("无法定位文件" ); fclose(fp); return 1 ; } long file_size = ftell(fp);for (long i = file_size - 1 ; i >= 0 ; i--){ if (fseek(fp, i, SEEK_SET) != 0 ) { perror("无法定位文件" ); fclose(fp); return 1 ; } int ch = fgetc(fp); putchar (ch); } fclose(fp);
文件定位的最佳实践 二进制文件使用 fseek 和 ftell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 FILE *fp = fopen("data.bin" , "rb" ); if (fp == NULL ){ perror("无法打开文件" ); return 1 ; } fseek(fp, 0 , SEEK_END); long size = ftell(fp);fseek(fp, 0 , SEEK_SET); printf ("文件大小:%ld 字节\n" , size);fclose(fp);
文本文件谨慎使用 fseek 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ perror("无法打开文件" ); return 1 ; } char buffer[100 ];fgets(buffer, sizeof (buffer), fp); printf ("第一行:%s" , buffer);fseek(fp, 0 , SEEK_SET); fgets(buffer, sizeof (buffer), fp); printf ("再次读取第一行:%s" , buffer);fclose(fp);
检查文件定位函数的返回值 1 2 3 4 5 6 7 8 9 10 11 12 if (fseek(fp, 0 , SEEK_END) != 0 ){ perror("无法定位到文件末尾" ); } long size = ftell(fp);if (size == -1L ){ perror("无法获取文件大小" ); }
使用 fsetpos 和 fgetpos 进行大文件定位 对于大于 2GB 的文件,long 类型可能不够用,此时可以使用 fsetpos 和 fgetpos 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <stdio.h> int main (void ) { FILE *fp = fopen("large_file.bin" , "rb" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } fpos_t pos; if (fgetpos(fp, &pos) != 0 ) { perror("无法获取文件位置" ); fclose(fp); return 1 ; } if (fsetpos(fp, &pos) != 0 ) { perror("无法设置文件位置" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
示例:文件定位 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <stdio.h> int main (void ) { FILE *fp; long position; fp = fopen("example.txt" , "w+" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } fprintf (fp, "Hello, World!\n" ); position = ftell(fp); if (position == -1L ) { perror("无法获取文件位置" ); fclose(fp); return 1 ; } printf ("当前位置:%ld\n" , position); if (fseek(fp, 0 , SEEK_SET) != 0 ) { perror("无法定位文件位置" ); fclose(fp); return 1 ; } char buffer[50 ]; if (fgets(buffer, sizeof (buffer), fp) == NULL ) { if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } } printf ("读取的数据:%s" , buffer); if (fclose(fp) != 0 ) { perror("无法关闭文件" ); return 1 ; } return 0 ; }
文件状态检查 文件状态检查是文件操作中的重要环节,用于判断文件操作是否成功、是否到达文件末尾等状态。C 标准库提供了一系列文件状态检查函数,帮助我们正确处理文件操作中的各种情况。
检查文件结束 参数和返回值:
stream - 文件指针返回值 - 文件结束返回非 0(真),否则返回 0(假)内部工作原理:
检查文件是否打开 检查文件结束标志是否被设置 返回标志值 使用场景:
当文件读写函数返回错误或特殊值时,用于判断是否是因为到达文件末尾 通常与 ferror 函数配合使用,区分文件结束和错误 注意事项:
feof 只在文件读写操作尝试读取文件末尾之后才会返回真不能用 feof 来预测文件是否即将结束,只能用来确认是否已经结束 对于未执行过读写操作的文件,feof 总是返回假 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } int ch; while (1 ) { ch = fgetc(fp); if (ch == EOF) { if (feof(fp)) { printf ("已到达文件末尾\n" ); } else if (ferror(fp)) { perror("读取文件时出错" ); } break ; } putchar (ch); } fclose(fp); return 0 ; }
检查错误 1 int ferror (FILE *stream) ;
参数和返回值:
stream - 文件指针返回值 - 发生错误返回非 0(真),否则返回 0(假)内部工作原理:
检查文件是否打开 检查错误标志是否被设置 返回标志值 使用场景:
当文件读写函数返回错误或特殊值时,用于判断是否是因为发生了错误 通常与 feof 函数配合使用,区分文件结束和错误 注意事项:
ferror 只检查错误标志是否被设置,不会清除该标志错误标志会一直保持,直到调用 clearerr 函数清除或关闭文件 对于未执行过读写操作的文件,ferror 总是返回假 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char buffer[100 ]; if (fgets(buffer, sizeof (buffer), fp) == NULL ) { if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } else if (feof(fp)) { printf ("文件为空\n" ); } } else { printf ("读取到:%s" , buffer); } fclose(fp); return 0 ; }
清除错误标志 1 void clearerr (FILE *stream) ;
参数:
内部工作原理:
检查文件是否打开 清除文件结束标志 清除错误标志 使用场景:
当需要在同一个文件上继续执行操作,而之前的操作设置了错误标志或文件结束标志时 当需要重新尝试文件操作时 注意事项:
clearerr 不会改变文件位置指示器的位置clearerr 不会修复导致错误的根本原因,只是清除标志示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <stdio.h> int main (void ) { FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char buffer[100 ]; if (fgets(buffer, sizeof (buffer), fp) == NULL ) { if (ferror(fp)) { perror("读取文件时出错" ); clearerr(fp); printf ("错误标志已清除\n" ); } else if (feof(fp)) { printf ("已到达文件末尾\n" ); clearerr(fp); printf ("文件结束标志已清除\n" ); } } fclose(fp); return 0 ; }
文件状态检查的最佳实践 正确处理文件读写返回值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int ch;while ((ch = fgetc(fp)) != EOF){ } if (ferror(fp)){ perror("读取文件时出错" ); } else if (feof(fp)){ printf ("文件读取完毕\n" ); }
避免使用 feof 作为循环条件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 while (!feof(fp)){ ch = fgetc(fp); putchar (ch); } while ((ch = fgetc(fp)) != EOF){ putchar (ch); } if (ferror(fp)){ perror("读取文件时出错" ); }
配合使用 feof 和 ferror 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 size_t bytes_read = fread(buffer, 1 , size, fp);if (bytes_read < size){ if (ferror(fp)) { perror("读取文件时出错" ); } else if (feof(fp)) { printf ("已到达文件末尾,只读取了 %zu 字节\n" , bytes_read); } }
使用 clearerr 重置文件状态 1 2 3 4 5 6 7 if (ferror(fp)){ perror("文件操作出错" ); clearerr(fp); }
处理多行读取的情况 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 char buffer[100 ];while (fgets(buffer, sizeof (buffer), fp) != NULL ){ } if (ferror(fp)){ perror("读取文件时出错" ); } else if (feof(fp)){ printf ("文件读取完毕\n" ); }
文件状态标志 每个文件指针内部都维护着两个状态标志:
文件结束标志 - 当文件读写操作尝试读取文件末尾时设置错误标志 - 当文件操作发生错误时设置这些标志只能通过以下方式清除:
调用 clearerr 函数 调用 rewind 函数 调用 fseek 或 fsetpos 函数(对于二进制文件) 关闭并重新打开文件 错误处理策略 即时错误处理 - 每次文件操作后立即检查返回值和状态集中错误处理 - 在关键操作点集中检查文件状态恢复策略 - 对于非致命错误,尝试恢复操作错误传播 - 将错误信息传递给调用者资源清理 - 发生错误时确保正确清理资源示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <stdio.h> #include <stdlib.h> FILE *open_file (const char *filename, const char *mode) { FILE *fp = fopen(filename, mode); if (fp == NULL ) { perror("无法打开文件" ); exit (EXIT_FAILURE); } return fp; } void read_file (FILE *fp) { char buffer[100 ]; while (fgets(buffer, sizeof (buffer), fp) != NULL ) { printf ("%s" , buffer); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); exit (EXIT_FAILURE); } } int main (void ) { FILE *fp = open_file("file.txt" , "r" ); read_file(fp); fclose(fp); return EXIT_SUCCESS; }
标准输入/输出/错误 C 语言提供了三个标准文件指针,它们在程序启动时自动打开,不需要手动打开,也通常不需要手动关闭(由系统自动处理)。
标准文件指针 标准文件指针 描述 默认设备 缓冲类型 stdin标准输入 键盘 行缓冲 stdout标准输出 屏幕 行缓冲 stderr标准错误 屏幕 无缓冲
标准输入 (stdin) stdin 是指向标准输入流的文件指针,用于从用户或其他程序读取输入数据。
特性: 默认设备 - 键盘缓冲类型 - 行缓冲(输入数据直到遇到换行符才被处理)常用函数 - scanf、fgets、getchar、fgetc示例: 1 2 3 4 5 6 7 8 9 10 11 12 #include <stdio.h> int main (void ) { char name[50 ]; printf ("请输入您的姓名:" ); if (fgets(name, sizeof (name), stdin ) != NULL ) { printf ("您好,%s\n" , name); } return 0 ; }
标准输出 (stdout) stdout 是指向标准输出流的文件指针,用于向用户或其他程序输出数据。
特性: 默认设备 - 屏幕缓冲类型 - 行缓冲(输出数据直到遇到换行符才被刷新到设备)常用函数 - printf、puts、putchar、fputc示例: 1 2 3 4 5 6 7 8 #include <stdio.h> int main (void ) { printf ("这是标准输出\n" ); fprintf (stdout , "这也是标准输出\n" ); return 0 ; }
标准错误 (stderr) stderr 是指向标准错误流的文件指针,用于输出错误信息。
特性: 默认设备 - 屏幕缓冲类型 - 无缓冲(错误信息立即输出,不等待缓冲)常用函数 - fprintf、perror示例: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> int main (void ) { FILE *fp = fopen("nonexistent.txt" , "r" ); if (fp == NULL ) { fprintf (stderr , "无法打开文件\n" ); perror("错误信息" ); return 1 ; } fclose(fp); return 0 ; }
标准流的重定向 在命令行环境中,可以通过重定向操作符改变标准流的默认设备:
输入重定向 (<) 将 stdin 重定向到 input.txt 文件,程序从文件读取输入而不是键盘。
输出重定向 (>) 将 stdout 重定向到 output.txt 文件,程序输出到文件而不是屏幕。
错误重定向 (2>) 将 stderr 重定向到 error.txt 文件,错误信息输出到文件而不是屏幕。
同时重定向 1 ./program < input.txt > output.txt 2> error.txt
合并输出和错误 (2>&1) 1 ./program > output.txt 2>&1
将 stderr 重定向到 stdout,两者都输出到 output.txt 文件。
标准流的缓冲机制 标准流使用不同的缓冲策略,这会影响输出的时机:
行缓冲 适用于 stdin 和 stdout 当遇到换行符 '\n' 时,缓冲区被刷新 当缓冲区满时,也会被刷新 当程序正常结束时,所有缓冲区会被刷新 无缓冲 适用于 stderr 数据立即写入设备,不经过缓冲区 确保错误信息及时显示,即使程序崩溃 全缓冲 适用于普通文件 当缓冲区满时才刷新 可以通过 fflush 函数手动刷新 刷新缓冲区 可以使用 fflush 函数手动刷新缓冲区:
1 int fflush (FILE *stream) ;
参数和返回值:
stream - 文件指针,若为 NULL 则刷新所有输出流返回值 - 成功返回 0,失败返回 EOF使用场景:
确保重要信息及时显示 在长时间运行的程序中定期刷新缓冲区 在可能崩溃的操作前刷新缓冲区 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int main (void ) { printf ("正在处理..." ); fflush(stdout ); for (int i = 0 ; i < 1000000000 ; i++); printf (" 完成!\n" ); return 0 ; }
标准流的重定向函数 C 标准库提供了 freopen 函数,可以在程序中重定向标准流:
1 FILE *freopen (const char *filename, const char *mode, FILE *stream) ;
参数和返回值:
filename - 要重定向到的文件路径mode - 文件打开模式stream - 要重定向的标准流(stdin、stdout 或 stderr)返回值 - 成功返回 stream,失败返回 NULL示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <stdio.h> int main (void ) { if (freopen("output.txt" , "w" , stdout ) == NULL ) { perror("无法重定向标准输出" ); return 1 ; } printf ("这条消息会输出到文件\n" ); fprintf (stdout , "这条消息也会输出到文件\n" ); freopen("CON" , "w" , stdout ); printf ("这条消息会输出到屏幕\n" ); return 0 ; }
标准流的最佳实践 使用适当的流
输入数据使用 stdin 正常输出使用 stdout 错误信息使用 stderr 错误信息输出到 stderr
1 2 3 4 5 if (error_occurred){ fprintf (stderr , "错误:%s\n" , error_message); return EXIT_FAILURE; }
避免混合使用 printf 和 puts 1 2 3 4 5 6 7 8 printf ("Hello" );puts (" World" ); printf ("Hello World\n" );puts ("Hello World" );
使用 fflush 确保及时输出 1 2 3 printf ("正在处理..." );fflush(stdout );
处理 EOF 输入 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <stdio.h> int main (void ) { char buffer[100 ]; printf ("请输入文本(按 Ctrl+D 或 Ctrl+Z 结束):\n" ); while (fgets(buffer, sizeof (buffer), stdin ) != NULL ) { printf ("您输入了:%s" , buffer); } if (feof(stdin )) { printf ("输入结束\n" ); } else if (ferror(stdin )) { perror("输入错误" ); } return 0 ; }
标准流的内部实现 标准流在 C 运行时库中实现,其内部结构与普通文件指针类似,但有一些特殊处理:
自动初始化 - 在程序启动时由 C 运行时库初始化特殊设备处理 - 与操作系统的标准设备驱动程序交互缓冲管理 - 根据流类型使用不同的缓冲策略重定向支持 - 支持命令行重定向和程序内重定向标准流与操作系统的关系 标准流是 C 语言与操作系统交互的桥梁:
Windows 系统 :标准流对应于控制台设备(CON)Linux/Unix 系统 :标准流对应于文件描述符 0(stdin)、1(stdout)和 2(stderr)其他系统 :标准流有相应的实现方式示例:标准流的使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #include <stdio.h> #include <stdlib.h> int main (void ) { char name[50 ]; int age; printf ("请输入您的姓名:" ); if (fgets(name, sizeof (name), stdin ) == NULL ) { fprintf (stderr , "输入错误\n" ); return EXIT_FAILURE; } printf ("请输入您的年龄:" ); if (scanf ("%d" , &age) != 1 ) { fprintf (stderr , "年龄输入错误\n" ); return EXIT_FAILURE; } printf ("您好,%s您的年龄是 %d 岁\n" , name, age); if (age < 0 || age > 150 ) { fprintf (stderr , "错误:年龄值不合理\n" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
文件操作示例 示例 1:文本文件的读写 功能说明: 演示如何创建、写入和读取文本文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <stdio.h> #include <stdlib.h> int main (void ) { FILE *fp; char buffer[100 ]; fp = fopen("text.txt" , "w" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } fprintf (fp, "Hello, World!\n" ); fprintf (fp, "这是一个文本文件。\n" ); fprintf (fp, "C 语言文件操作示例。\n" ); if (fclose(fp) != 0 ) { perror("关闭文件失败" ); return EXIT_FAILURE; } fp = fopen("text.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } printf ("文件内容:\n" ); while (fgets(buffer, sizeof (buffer), fp) != NULL ) { printf ("%s" , buffer); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return EXIT_FAILURE; } if (fclose(fp) != 0 ) { perror("关闭文件失败" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
示例 2:二进制文件的读写 功能说明: 演示如何创建、写入和读取二进制文件,存储结构化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #include <stdio.h> #include <stdlib.h> typedef struct { char name[50 ]; int age; float score; } Student; int main (void ) { FILE *fp; Student s1 = {"Alice" , 18 , 95.5 }; Student s2; fp = fopen("student.dat" , "wb" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } size_t written = fwrite(&s1, sizeof (Student), 1 , fp); if (written != 1 ) { perror("写入文件时出错" ); fclose(fp); return EXIT_FAILURE; } if (fclose(fp) != 0 ) { perror("关闭文件失败" ); return EXIT_FAILURE; } fp = fopen("student.dat" , "rb" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } size_t read = fread(&s2, sizeof (Student), 1 , fp); if (read != 1 ) { if (ferror(fp)) { perror("读取文件时出错" ); } else if (feof(fp)) { fprintf (stderr , "文件意外结束\n" ); } fclose(fp); return EXIT_FAILURE; } printf ("姓名:%s\n" , s2.name); printf ("年龄:%d\n" , s2.age); printf ("分数:%.2f\n" , s2.score); if (fclose(fp) != 0 ) { perror("关闭文件失败" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
示例 3:文件复制 功能说明: 演示如何复制文件,使用块级读写提高效率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <stdio.h> #include <stdlib.h> #define BUFFER_SIZE 4096 int main (void ) { FILE *src, *dest; char buffer[BUFFER_SIZE]; size_t bytes_read, bytes_written; src = fopen("source.txt" , "r" ); if (src == NULL ) { perror("无法打开源文件" ); return EXIT_FAILURE; } dest = fopen("destination.txt" , "w" ); if (dest == NULL ) { perror("无法打开目标文件" ); fclose(src); return EXIT_FAILURE; } while ((bytes_read = fread(buffer, 1 , BUFFER_SIZE, src)) > 0 ) { bytes_written = fwrite(buffer, 1 , bytes_read, dest); if (bytes_written != bytes_read) { perror("写入文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } } if (ferror(src)) { perror("读取文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } if (fclose(src) != 0 ) { perror("关闭源文件失败" ); fclose(dest); return EXIT_FAILURE; } if (fclose(dest) != 0 ) { perror("关闭目标文件失败" ); return EXIT_FAILURE; } printf ("文件复制成功\n" ); return EXIT_SUCCESS; }
示例 4:文件追加 功能说明: 演示如何在文件末尾追加内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <stdlib.h> int main (void ) { FILE *fp; fp = fopen("log.txt" , "a" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } fprintf (fp, "新的日志条目\n" ); if (fclose(fp) != 0 ) { perror("关闭文件失败" ); return EXIT_FAILURE; } printf ("内容追加成功\n" ); return EXIT_SUCCESS; }
示例 5:文件信息统计 功能说明: 演示如何统计文件中的字符数、行数和单词数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <stdio.h> #include <stdlib.h> #include <ctype.h> int main (void ) { FILE *fp; int ch; int char_count = 0 ; int line_count = 0 ; int word_count = 0 ; int in_word = 0 ; fp = fopen("text.txt" , "r" ); if (fp == NULL ) { perror("无法打开文件" ); return EXIT_FAILURE; } while ((ch = fgetc(fp)) != EOF) { char_count++; if (ch == '\n' ) { line_count++; } if (isspace (ch)) { in_word = 0 ; } else if (!in_word) { in_word = 1 ; word_count++; } } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return EXIT_FAILURE; } if (char_count > 0 ) { line_count++; } fclose(fp); printf ("文件统计信息:\n" ); printf ("字符数:%d\n" , char_count); printf ("行数:%d\n" , line_count); printf ("单词数:%d\n" , word_count); return EXIT_SUCCESS; }
示例 6:二进制文件复制 功能说明: 演示如何复制二进制文件,如图片、视频等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 #include <stdio.h> #include <stdlib.h> #define BUFFER_SIZE 4096 int main (void ) { FILE *src, *dest; char buffer[BUFFER_SIZE]; size_t bytes_read, bytes_written; src = fopen("source.jpg" , "rb" ); if (src == NULL ) { perror("无法打开源文件" ); return EXIT_FAILURE; } dest = fopen("destination.jpg" , "wb" ); if (dest == NULL ) { perror("无法打开目标文件" ); fclose(src); return EXIT_FAILURE; } while ((bytes_read = fread(buffer, 1 , BUFFER_SIZE, src)) > 0 ) { bytes_written = fwrite(buffer, 1 , bytes_read, dest); if (bytes_written != bytes_read) { perror("写入文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } } if (ferror(src)) { perror("读取文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } if (fclose(src) != 0 ) { perror("关闭源文件失败" ); fclose(dest); return EXIT_FAILURE; } if (fclose(dest) != 0 ) { perror("关闭目标文件失败" ); return EXIT_FAILURE; } printf ("二进制文件复制成功\n" ); return EXIT_SUCCESS; }
示例 7:文件加密 功能说明: 演示如何对文件进行简单的加密和解密
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <stdlib.h> #define KEY 0x55 void encrypt_file (const char *input_file, const char *output_file) { FILE *in, *out; int ch; in = fopen(input_file, "rb" ); if (in == NULL ) { perror("无法打开输入文件" ); exit (EXIT_FAILURE); } out = fopen(output_file, "wb" ); if (out == NULL ) { perror("无法打开输出文件" ); fclose(in); exit (EXIT_FAILURE); } while ((ch = fgetc(in)) != EOF) { fputc(ch ^ KEY, out); } if (ferror(in) || ferror(out)) { perror("文件操作时出错" ); fclose(in); fclose(out); exit (EXIT_FAILURE); } fclose(in); fclose(out); } int main (void ) { encrypt_file("plain.txt" , "encrypted.bin" ); printf ("文件加密成功\n" ); encrypt_file("encrypted.bin" , "decrypted.txt" ); printf ("文件解密成功\n" ); return EXIT_SUCCESS; }
示例 8:临时文件操作 功能说明: 演示如何使用临时文件进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <stdio.h> #include <stdlib.h> int main (void ) { FILE *tmp; char buffer[100 ]; tmp = tmpfile(); if (tmp == NULL ) { perror("无法创建临时文件" ); return EXIT_FAILURE; } fprintf (tmp, "这是临时文件的内容\n" ); fprintf (tmp, "临时文件会在关闭时自动删除\n" ); rewind(tmp); printf ("临时文件内容:\n" ); while (fgets(buffer, sizeof (buffer), tmp) != NULL ) { printf ("%s" , buffer); } if (ferror(tmp)) { perror("读取临时文件时出错" ); fclose(tmp); return EXIT_FAILURE; } if (fclose(tmp) != 0 ) { perror("关闭临时文件失败" ); return EXIT_FAILURE; } printf ("临时文件操作完成\n" ); return EXIT_SUCCESS; }
文件操作的最佳实践 文件操作是 C 语言编程中的重要部分,正确的文件操作可以提高程序的可靠性、安全性和性能。以下是文件操作的最佳实践:
1. 错误处理 1.1 始终检查文件打开是否成功 1 2 3 4 5 6 FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ perror("无法打开文件" ); return EXIT_FAILURE; }
1.2 检查所有文件操作的返回值 1 2 3 4 5 6 if (fputs ("Hello" , fp) == EOF){ perror("写入文件失败" ); fclose(fp); return EXIT_FAILURE; }
1.3 使用 perror 或 strerror 打印错误信息 1 2 3 4 5 6 7 8 9 10 #include <stdio.h> #include <string.h> #include <errno.h> FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ fprintf (stderr , "无法打开文件:%s\n" , strerror(errno)); return EXIT_FAILURE; }
1.4 区分文件结束和错误 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int ch;while ((ch = fgetc(fp)) != EOF){ } if (ferror(fp)){ perror("读取文件时出错" ); fclose(fp); return EXIT_FAILURE; } else if (feof(fp)){ printf ("文件读取完毕\n" ); }
2. 资源管理 2.1 始终关闭打开的文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 FILE *fp = fopen("file.txt" , "r" ); if (fp == NULL ){ perror("无法打开文件" ); return EXIT_FAILURE; } if (fclose(fp) != 0 ){ perror("关闭文件失败" ); return EXIT_FAILURE; }
2.2 使用 RAII 风格的文件管理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void process_file (const char *filename) { FILE *fp = fopen(filename, "r" ); if (fp == NULL ) { perror("无法打开文件" ); return ; } if (fclose(fp) != 0 ) { perror("关闭文件失败" ); } }
2.3 处理多个文件时的关闭顺序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 FILE *fp1 = fopen("file1.txt" , "r" ); FILE *fp2 = fopen("file2.txt" , "w" ); if (fp1 == NULL || fp2 == NULL ){ perror("无法打开文件" ); if (fp1 != NULL ) fclose(fp1); if (fp2 != NULL ) fclose(fp2); return EXIT_FAILURE; } if (fclose(fp1) != 0 ) perror("关闭 file1.txt 失败" ); if (fclose(fp2) != 0 ) perror("关闭 file2.txt 失败" );
3. 二进制文件操作 3.1 注意数据类型大小和对齐 1 2 3 4 5 6 7 8 9 #pragma pack(push, 1) typedef struct { char name[50 ]; int age; float score; } Student; #pragma pack(pop)
3.2 使用固定大小的数据类型 1 2 3 4 5 6 7 8 9 #include <stdint.h> typedef struct { char name[50 ]; int32_t age; float score; uint64_t id; } Student;
3.3 考虑字节序问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <stdint.h> void write_uint32_be (FILE *fp, uint32_t value) { uint8_t bytes[4 ] = { (value >> 24 ) & 0xFF , (value >> 16 ) & 0xFF , (value >> 8 ) & 0xFF , value & 0xFF }; fwrite(bytes, 1 , 4 , fp); } uint32_t read_uint32_be (FILE *fp) { uint8_t bytes[4 ]; fread(bytes, 1 , 4 , fp); return (uint32_t )bytes[0 ] << 24 | (uint32_t )bytes[1 ] << 16 | (uint32_t )bytes[2 ] << 8 | (uint32_t )bytes[3 ]; }
4. 性能优化 4.1 大文件操作时使用块读写 1 2 3 4 5 6 7 8 9 #define BUFFER_SIZE 4096 char buffer[BUFFER_SIZE];size_t bytes_read;while ((bytes_read = fread(buffer, 1 , BUFFER_SIZE, src)) > 0 ){ fwrite(buffer, 1 , bytes_read, dest); }
4.2 选择合适的缓冲区大小 小文件 - 可以使用较小的缓冲区(如 1024 字节)大文件 - 建议使用较大的缓冲区(如 4096 字节或更大)网络文件 - 可能需要更小的缓冲区4.3 减少文件操作次数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 for (int i = 0 ; i < 1000 ; i++){ fprintf (fp, "%d\n" , i); } char buffer[4096 ];int pos = 0 ;for (int i = 0 ; i < 1000 ; i++){ pos += snprintf (buffer + pos, sizeof (buffer) - pos, "%d\n" , i); if (pos >= sizeof (buffer) - 100 ) { fwrite(buffer, 1 , pos, fp); pos = 0 ; } } if (pos > 0 ){ fwrite(buffer, 1 , pos, fp); }
4.4 使用二进制模式处理二进制数据 1 2 3 4 5 FILE *fp = fopen("data.bin" , "wb" );
5. 安全性 5.1 避免缓冲区溢出 1 2 3 4 5 6 7 8 9 10 char buffer[100 ];if (fgets(buffer, sizeof (buffer), fp) != NULL ){ }
5.2 验证输入文件名 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <string.h> void process_file (const char *filename) { if (filename == NULL || strlen (filename) == 0 ) { fprintf (stderr , "文件名不能为空\n" ); return ; } if (strstr (filename, ".." ) != NULL ) { fprintf (stderr , "文件名无效\n" ); return ; } FILE *fp = fopen(filename, "r" ); }
5.3 限制文件大小 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #define MAX_FILE_SIZE 1024 * 1024 void process_file (const char *filename) { FILE *fp = fopen(filename, "r" ); if (fp == NULL ) { perror("无法打开文件" ); return ; } fseek(fp, 0 , SEEK_END); long size = ftell(fp); fseek(fp, 0 , SEEK_SET); if (size > MAX_FILE_SIZE) { fprintf (stderr , "文件过大\n" ); fclose(fp); return ; } fclose(fp); }
6. 代码可维护性 6.1 使用有意义的变量名 1 2 3 4 5 6 7 FILE *f; char b[100 ];FILE *file_ptr; char buffer[100 ];
6.2 封装文件操作函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 FILE *open_file (const char *filename, const char *mode) { FILE *fp = fopen(filename, mode); if (fp == NULL ) { perror("无法打开文件" ); } return fp; } void close_file (FILE *fp) { if (fp != NULL ) { if (fclose(fp) != 0 ) { perror("关闭文件失败" ); } } }
6.3 添加注释 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int read_and_print_file (const char *filename) { FILE *fp = fopen(filename, "r" ); if (fp == NULL ) { perror("无法打开文件" ); return 1 ; } char buffer[100 ]; while (fgets(buffer, sizeof (buffer), fp) != NULL ) { printf ("%s" , buffer); } if (ferror(fp)) { perror("读取文件时出错" ); fclose(fp); return 1 ; } fclose(fp); return 0 ; }
7. 跨平台考虑 7.1 注意路径分隔符 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <stdio.h> #ifdef _WIN32 #define PATH_SEP "\\" #else #define PATH_SEP "/" #endif int main (void ) { char filename[100 ]; snprintf (filename, sizeof (filename), "dir%sfile.txt" , PATH_SEP); FILE *fp = fopen(filename, "r" ); return 0 ; }
7.2 注意换行符差异 1 2 3 4 5 6 FILE *fp = fopen("file.txt" , "rb" );
7.3 注意文件权限 8. 调试技巧 8.1 使用临时文件进行调试 1 2 3 4 5 6 FILE *debug_fp = fopen("debug.log" , "a" ); if (debug_fp != NULL ){ fprintf (debug_fp, "调试信息:%d\n" , some_value); fclose(debug_fp); }
8.2 打印文件操作的详细信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #define DEBUG 1 #ifdef DEBUG #define LOG_FILE_OPERATION(fmt, ...) fprintf(stderr, "[FILE OP] " fmt "\n" , ##__VA_ARGS__) #else #define LOG_FILE_OPERATION(fmt, ...) do {} while(0) #endif void read_file (const char *filename) { LOG_FILE_OPERATION("打开文件:%s" , filename); FILE *fp = fopen(filename, "r" ); if (fp == NULL ) { perror("无法打开文件" ); return ; } LOG_FILE_OPERATION("关闭文件:%s" , filename); fclose(fp); }
9. 高级技巧 9.1 使用内存映射 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <stdio.h> #include <stdlib.h> #include <sys/mman.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> void memory_map_example (const char *filename) { int fd = open(filename, O_RDONLY); if (fd == -1 ) { perror("无法打开文件" ); return ; } struct stat stat_buf ; if (fstat(fd, &stat_buf) == -1 ) { perror("无法获取文件状态" ); close(fd); return ; } char *addr = mmap(NULL , stat_buf.st_size, PROT_READ, MAP_PRIVATE, fd, 0 ); if (addr == MAP_FAILED) { perror("内存映射失败" ); close(fd); return ; } printf ("文件内容:%.*s\n" , (int )stat_buf.st_size, addr); munmap(addr, stat_buf.st_size); close(fd); }
9.2 使用文件锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #include <stdio.h> #include <fcntl.h> void file_lock_example (const char *filename) { FILE *fp = fopen(filename, "r+" ); if (fp == NULL ) { perror("无法打开文件" ); return ; } struct flock lock ; lock.l_type = F_WRLCK; lock.l_whence = SEEK_SET; lock.l_start = 0 ; lock.l_len = 0 ; if (fcntl(fileno(fp), F_SETLK, &lock) == -1 ) { perror("无法获取文件锁" ); fclose(fp); return ; } lock.l_type = F_UNLCK; fcntl(fileno(fp), F_SETLK, &lock); fclose(fp); }
10. 总结 文件操作是 C 语言编程中的重要部分,掌握文件操作的最佳实践可以帮助你编写更加可靠、安全和高效的程序。以下是文件操作的核心原则:
错误处理优先 - 始终检查文件操作的返回值资源管理 - 确保每个打开的文件都被正确关闭性能优化 - 使用块级读写,减少 I/O 操作次数安全性 - 避免缓冲区溢出,验证输入可维护性 - 使用有意义的变量名,封装文件操作函数跨平台考虑 - 注意路径分隔符、换行符和文件权限的差异通过遵循这些最佳实践,你可以编写出更加专业、可靠的文件操作代码。
临时文件 临时文件是在程序运行过程中临时创建的文件,用于存储中间数据,通常在程序结束或文件关闭时自动删除。临时文件在以下场景中非常有用:
存储大量中间数据 - 当内存不足时在多个进程或函数之间传递数据 需要原子操作时 - 先写入临时文件,再重命名备份原始文件 - 在修改文件前创建备份创建临时文件 C 标准库提供了以下函数创建临时文件:
1. tmpfile 函数 参数和返回值:
返回值 - 临时文件指针,失败返回 NULL 并设置 errno特性:
创建的临时文件在关闭或程序结束时自动删除 以二进制读写模式 (wb+) 打开 文件名由系统生成,确保唯一性 通常存储在系统的临时目录中 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <stdio.h> #include <stdlib.h> int main (void ) { FILE *tmp; char buffer[50 ]; tmp = tmpfile(); if (tmp == NULL ) { perror("无法创建临时文件" ); return EXIT_FAILURE; } fprintf (tmp, "这是临时文件的内容\n" ); fprintf (tmp, "临时文件会在关闭时自动删除\n" ); rewind(tmp); printf ("临时文件内容:\n" ); while (fgets(buffer, sizeof (buffer), tmp) != NULL ) { printf ("%s" , buffer); } if (ferror(tmp)) { perror("读取临时文件时出错" ); fclose(tmp); return EXIT_FAILURE; } if (fclose(tmp) != 0 ) { perror("关闭临时文件失败" ); return EXIT_FAILURE; } printf ("临时文件已关闭并自动删除\n" ); return EXIT_SUCCESS; }
2. tmpnam 函数 参数和返回值:
s - 存储临时文件名的缓冲区,若为 NULL 则返回静态缓冲区的指针返回值 - 成功返回临时文件名,失败返回 NULL特性:
只生成临时文件名,不创建文件 需要手动使用 fopen 创建文件 文件名可能被其他进程抢占,存在安全风险 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <stdio.h> #include <stdlib.h> int main (void ) { char tmp_name[L_tmpnam]; FILE *tmp; if (tmpnam(tmp_name) == NULL ) { perror("无法生成临时文件名" ); return EXIT_FAILURE; } printf ("临时文件名:%s\n" , tmp_name); tmp = fopen(tmp_name, "w+" ); if (tmp == NULL ) { perror("无法创建临时文件" ); return EXIT_FAILURE; } fprintf (tmp, "这是临时文件的内容\n" ); fclose(tmp); if (remove(tmp_name) != 0 ) { perror("无法删除临时文件" ); return EXIT_FAILURE; } printf ("临时文件已删除\n" ); return EXIT_SUCCESS; }
3. mkstemp 函数 1 int mkstemp (char *template) ;
参数和返回值:
template - 文件名模板,末尾必须包含至少 6 个 X 字符返回值 - 成功返回文件描述符,失败返回 -1 并设置 errno特性:
创建并打开临时文件,返回文件描述符 文件名由系统生成,确保唯一性 文件权限为 0600(只有所有者可读写),安全性高 需要手动关闭文件描述符并删除文件 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> int main (void ) { char tmp_name[] = "/tmp/my_temp_XXXXXX" ; int fd; FILE *tmp; char buffer[50 ]; fd = mkstemp(tmp_name); if (fd == -1 ) { perror("无法创建临时文件" ); return EXIT_FAILURE; } printf ("临时文件名:%s\n" , tmp_name); tmp = fdopen(fd, "w+" ); if (tmp == NULL ) { perror("无法获取文件指针" ); close(fd); unlink(tmp_name); return EXIT_FAILURE; } fprintf (tmp, "这是临时文件的内容\n" ); rewind(tmp); printf ("临时文件内容:\n" ); while (fgets(buffer, sizeof (buffer), tmp) != NULL ) { printf ("%s" , buffer); } if (ferror(tmp)) { perror("读取临时文件时出错" ); fclose(tmp); unlink(tmp_name); return EXIT_FAILURE; } if (fclose(tmp) != 0 ) { perror("关闭临时文件失败" ); unlink(tmp_name); return EXIT_FAILURE; } if (unlink(tmp_name) != 0 ) { perror("无法删除临时文件" ); return EXIT_FAILURE; } printf ("临时文件已删除\n" ); return EXIT_SUCCESS; }
临时文件的安全使用 使用 mkstemp 代替 tmpnam
mkstemp 更安全,避免文件名被抢占mkstemp 创建的文件权限更安全(0600)及时删除临时文件
使用 unlink 函数删除临时文件 可以在创建文件后立即调用 unlink,这样文件会在所有文件描述符关闭后自动删除 设置适当的文件权限
临时文件应设置为只有所有者可读写 避免使用全局可写的临时文件 使用系统临时目录
临时文件应存储在系统的临时目录中 可以通过 TMPDIR 环境变量获取临时目录路径 避免硬编码临时文件路径
使用 P_tmpdir 宏获取临时目录路径 或使用 getenv("TMPDIR") 获取环境变量中的临时目录 临时文件的最佳实践 使用 tmpfile 进行简单操作 1 2 3 4 5 6 7 8 9 10 11 12 FILE *tmp = tmpfile(); if (tmp == NULL ){ perror("无法创建临时文件" ); return EXIT_FAILURE; } fclose(tmp);
使用 mkstemp 进行安全操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 char tmp_name[] = "temp_XXXXXX" ;int fd = mkstemp(tmp_name);if (fd == -1 ){ perror("无法创建临时文件" ); return EXIT_FAILURE; } unlink(tmp_name); close(fd);
临时文件用于原子操作 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 char tmp_name[] = "file.txt.XXXXXX" ;int fd = mkstemp(tmp_name);if (fd == -1 ){ perror("无法创建临时文件" ); return EXIT_FAILURE; } FILE *fp = fdopen(fd, "w" ); if (fp == NULL ){ perror("无法获取文件指针" ); close(fd); unlink(tmp_name); return EXIT_FAILURE; } fprintf (fp, "新内容\n" );if (fclose(fp) != 0 ){ perror("关闭临时文件失败" ); unlink(tmp_name); return EXIT_FAILURE; } if (rename(tmp_name, "file.txt" ) != 0 ){ perror("重命名文件失败" ); unlink(tmp_name); return EXIT_FAILURE; } printf ("文件更新成功\n" );
临时文件用于大文件处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 FILE *tmp = tmpfile(); if (tmp == NULL ){ perror("无法创建临时文件" ); return EXIT_FAILURE; } for (int i = 0 ; i < 1000000 ; i++){ fprintf (tmp, "数据 %d\n" , i); } rewind(tmp); char buffer[100 ];while (fgets(buffer, sizeof (buffer), tmp) != NULL ){ } fclose(tmp);
临时文件的注意事项 磁盘空间 - 临时文件可能占用大量磁盘空间,应确保有足够的空间文件描述符限制 - 避免创建过多临时文件,超出系统文件描述符限制异常处理 - 在程序异常终止时,确保临时文件被正确清理跨平台 - 不同平台的临时文件处理方式可能不同,需要考虑跨平台兼容性安全性 - 避免在临时文件中存储敏感信息,或确保使用安全的临时文件创建方法示例:临时文件用于排序大文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_LINE_LENGTH 100 int compare_lines (const void *a, const void *b) { return strcmp (*(const char **)a, *(const char **)b); } int main (void ) { FILE *input, *tmp; char line[MAX_LINE_LENGTH]; char **lines = NULL ; int line_count = 0 ; int capacity = 0 ; input = fopen("large_file.txt" , "r" ); if (input == NULL ) { perror("无法打开输入文件" ); return EXIT_FAILURE; } tmp = tmpfile(); if (tmp == NULL ) { perror("无法创建临时文件" ); fclose(input); return EXIT_FAILURE; } while (fgets(line, sizeof (line), input) != NULL ) { if (line_count >= capacity) { capacity += 1000 ; char **new_lines = realloc (lines, capacity * sizeof (char *)); if (new_lines == NULL ) { perror("内存分配失败" ); free (lines); fclose(input); fclose(tmp); return EXIT_FAILURE; } lines = new_lines; } lines[line_count] = strdup(line); if (lines[line_count] == NULL ) { perror("内存分配失败" ); for (int i = 0 ; i < line_count; i++) free (lines[i]); free (lines); fclose(input); fclose(tmp); return EXIT_FAILURE; } line_count++; } fclose(input); qsort(lines, line_count, sizeof (char *), compare_lines); for (int i = 0 ; i < line_count; i++) { fprintf (tmp, "%s" , lines[i]); free (lines[i]); } free (lines); rewind(tmp); printf ("排序结果:\n" ); while (fgets(line, sizeof (line), tmp) != NULL ) { printf ("%s" , line); } fclose(tmp); return EXIT_SUCCESS; }
总结 临时文件是 C 语言编程中处理中间数据的重要工具,正确使用临时文件可以提高程序的可靠性和安全性。以下是使用临时文件的核心原则:
选择合适的临时文件创建函数
简单场景使用 tmpfile 安全场景使用 mkstemp 避免使用 tmpnam 及时清理临时文件
使用 unlink 函数删除临时文件 或依赖 tmpfile 的自动删除机制 安全使用临时文件
设置适当的文件权限 避免存储敏感信息 使用系统临时目录 处理异常情况
检查临时文件创建是否成功 处理磁盘空间不足的情况 确保在异常终止时清理临时文件 通过遵循这些原则,你可以安全、有效地使用临时文件来处理程序中的中间数据。
命令行参数 命令行参数是程序从命令行接收输入的一种方式,允许用户在启动程序时向程序传递数据。C 语言程序通过 main 函数的参数来接收命令行参数。
main 函数的参数 1 int main (int argc, char *argv[]) ;
参数解析:
argc - 命令行参数的数量(argument count),包括程序名称argv - 命令行参数的数组(argument vector),每个元素是一个字符串指针argv[0] - 程序的名称(包含路径或不包含路径,取决于如何调用程序)argv[1] 到 argv[argc-1] - 用户提供的命令行参数argv[argc] - 始终为 NULL(标准要求)命令行参数的基本使用 示例:打印所有命令行参数
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> int main (int argc, char *argv[]) { printf ("参数数量:%d\n" , argc); for (int i = 0 ; i < argc; i++) { printf ("参数 %d: %s\n" , i, argv[i]); } return 0 ; }
运行示例:
1 2 3 4 5 6 $ ./program hello world 123 参数数量:4 参数 0: ./program 参数 1: hello 参数 2: world 参数 3: 123
命令行参数的类型转换 命令行参数总是以字符串形式传递,需要根据需要转换为其他类型:
示例:计算两个整数的和
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 #include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[]) { if (argc != 3 ) { fprintf (stderr , "用法:%s <整数1> <整数2>\n" , argv[0 ]); return EXIT_FAILURE; } int num1 = atoi(argv[1 ]); int num2 = atoi(argv[2 ]); int sum = num1 + num2; printf ("%d + %d = %d\n" , num1, num2, sum); return EXIT_SUCCESS; }
命令行选项处理 命令行选项通常以 - 或 -- 开头,用于控制程序的行为:
示例:简单的选项处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <stdio.h> #include <stdlib.h> #include <string.h> int main (int argc, char *argv[]) { int verbose = 0 ; char *filename = NULL ; for (int i = 1 ; i < argc; i++) { if (strcmp (argv[i], "-v" ) == 0 || strcmp (argv[i], "--verbose" ) == 0 ) { verbose = 1 ; } else if (strcmp (argv[i], "-f" ) == 0 || strcmp (argv[i], "--file" ) == 0 ) { if (i + 1 < argc) { filename = argv[i + 1 ]; i++; } else { fprintf (stderr , "错误:%s 需要参数\n" , argv[i]); return EXIT_FAILURE; } } else { fprintf (stderr , "错误:未知选项 %s\n" , argv[i]); return EXIT_FAILURE; } } if (verbose) { printf (" verbose 模式启用\n" ); } if (filename) { printf (" 要处理的文件:%s\n" , filename); } else { fprintf (stderr , "错误:必须指定文件\n" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
使用 getopt 函数处理选项 对于复杂的命令行选项,建议使用标准库中的 getopt 函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main (int argc, char *argv[]) { int verbose = 0 ; char *filename = NULL ; int opt; while ((opt = getopt(argc, argv, "vf:" )) != -1 ) { switch (opt) { case 'v' : verbose = 1 ; break ; case 'f' : filename = optarg; break ; case '?' : fprintf (stderr , "错误:未知选项\n" ); return EXIT_FAILURE; case ':' : fprintf (stderr , "错误:选项需要参数\n" ); return EXIT_FAILURE; } } if (filename == NULL ) { fprintf (stderr , "错误:必须指定文件\n" ); return EXIT_FAILURE; } if (verbose) { printf (" verbose 模式启用\n" ); } printf (" 要处理的文件:%s\n" , filename); return EXIT_SUCCESS; }
命令行参数的最佳实践 检查参数数量 1 2 3 4 5 if (argc < 2 ){ fprintf (stderr , "用法:%s <参数>\n" , argv[0 ]); return EXIT_FAILURE; }
验证参数有效性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #include <ctype.h> int is_number (const char *str) { for (int i = 0 ; str[i]; i++) { if (!isdigit (str[i])) return 0 ; } return 1 ; } int main (int argc, char *argv[]) { if (argc != 2 ) { fprintf (stderr , "用法:%s <数字>\n" , argv[0 ]); return EXIT_FAILURE; } if (!is_number(argv[1 ])) { fprintf (stderr , "错误:参数必须是数字\n" ); return EXIT_FAILURE; } return EXIT_SUCCESS; }
使用标准函数处理选项 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <getopt.h> static struct option long_options [] = { {"verbose" , no_argument, 0 , 'v' }, {"file" , required_argument, 0 , 'f' }, {"help" , no_argument, 0 , 'h' }, {0 , 0 , 0 , 0 } }; int main (int argc, char *argv[]) { int opt; int option_index = 0 ; while ((opt = getopt_long(argc, argv, "vf:h" , long_options, &option_index)) != -1 ) { switch (opt) { case 'v' : break ; case 'f' : break ; case 'h' : break ; case '?' : break ; } } for (int i = optind; i < argc; i++) { printf ("剩余参数:%s\n" , argv[i]); } return EXIT_SUCCESS; }
提供帮助信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 void print_help (const char *program_name) { printf ("用法:%s [选项] <参数>\n" , program_name); printf ("选项:\n" ); printf (" -v, --verbose 启用详细输出\n" ); printf (" -f, --file FILE 指定输入文件\n" ); printf (" -h, --help 显示此帮助信息\n" ); } int main (int argc, char *argv[]) { for (int i = 1 ; i < argc; i++) { if (strcmp (argv[i], "-h" ) == 0 || strcmp (argv[i], "--help" ) == 0 ) { print_help(argv[0 ]); return EXIT_SUCCESS; } } return EXIT_SUCCESS; }
命令行参数的高级技巧 解析配置文件路径 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 #include <stdio.h> #include <stdlib.h> #include <string.h> int main (int argc, char *argv[]) { char *config_file = "config.txt" ; for (int i = 1 ; i < argc; i++) { if (strcmp (argv[i], "-c" ) == 0 || strcmp (argv[i], "--config" ) == 0 ) { if (i + 1 < argc) { config_file = argv[i + 1 ]; i++; } else { fprintf (stderr , "错误:%s 需要参数\n" , argv[i]); return EXIT_FAILURE; } } } printf ("使用配置文件:%s\n" , config_file); FILE *fp = fopen(config_file, "r" ); if (fp == NULL ) { perror("无法打开配置文件" ); return EXIT_FAILURE; } fclose(fp); return EXIT_SUCCESS; }
处理多个文件参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[]) { if (argc < 2 ) { fprintf (stderr , "用法:%s <文件1> <文件2> ...\n" , argv[0 ]); return EXIT_FAILURE; } for (int i = 1 ; i < argc; i++) { FILE *fp = fopen(argv[i], "r" ); if (fp == NULL ) { perror(argv[i]); continue ; } printf ("处理文件:%s\n" , argv[i]); fclose(fp); } return EXIT_SUCCESS; }
使用环境变量 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> int main (int argc, char *argv[]) { char *home_dir = getenv("HOME" ); if (home_dir == NULL ) { home_dir = getenv("USERPROFILE" ); } if (home_dir) { printf ("主目录:%s\n" , home_dir); } else { printf ("无法获取主目录\n" ); } return EXIT_SUCCESS; }
示例:文件复制程序 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 #include <stdio.h> #include <stdlib.h> #include <string.h> #define BUFFER_SIZE 4096 void print_help (const char *program_name) { printf ("用法:%s [选项] <源文件> <目标文件>\n" , program_name); printf ("选项:\n" ); printf (" -v, --verbose 启用详细输出\n" ); printf (" -h, --help 显示此帮助信息\n" ); } int main (int argc, char *argv[]) { int verbose = 0 ; int src_index = -1 , dest_index = -1 ; for (int i = 1 ; i < argc; i++) { if (strcmp (argv[i], "-v" ) == 0 || strcmp (argv[i], "--verbose" ) == 0 ) { verbose = 1 ; } else if (strcmp (argv[i], "-h" ) == 0 || strcmp (argv[i], "--help" ) == 0 ) { print_help(argv[0 ]); return EXIT_SUCCESS; } else if (src_index == -1 ) { src_index = i; } else if (dest_index == -1 ) { dest_index = i; } else { fprintf (stderr , "错误:过多的参数\n" ); print_help(argv[0 ]); return EXIT_FAILURE; } } if (src_index == -1 || dest_index == -1 ) { fprintf (stderr , "错误:缺少必要参数\n" ); print_help(argv[0 ]); return EXIT_FAILURE; } FILE *src = fopen(argv[src_index], "rb" ); if (src == NULL ) { perror("无法打开源文件" ); return EXIT_FAILURE; } FILE *dest = fopen(argv[dest_index], "wb" ); if (dest == NULL ) { perror("无法打开目标文件" ); fclose(src); return EXIT_FAILURE; } char buffer[BUFFER_SIZE]; size_t bytes_read, bytes_written; size_t total_bytes = 0 ; if (verbose) { printf ("正在复制 %s 到 %s...\n" , argv[src_index], argv[dest_index]); } while ((bytes_read = fread(buffer, 1 , BUFFER_SIZE, src)) > 0 ) { bytes_written = fwrite(buffer, 1 , bytes_read, dest); if (bytes_written != bytes_read) { perror("写入文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } total_bytes += bytes_written; } if (ferror(src)) { perror("读取文件时出错" ); fclose(src); fclose(dest); return EXIT_FAILURE; } fclose(src); fclose(dest); if (verbose) { printf ("复制完成,共复制 %zu 字节\n" , total_bytes); } return EXIT_SUCCESS; }
总结 命令行参数是 C 语言程序与用户交互的重要方式,通过命令行参数,用户可以:
向程序传递数据 - 如文件名、配置选项等控制程序行为 - 通过选项开关控制程序的不同功能指定程序运行模式 - 如 verbose 模式、安静模式等正确处理命令行参数可以使程序更加灵活、易用和专业。以下是处理命令行参数的核心原则:
检查参数数量和有效性 - 确保程序接收到正确的参数提供清晰的错误信息 - 当参数错误时,向用户提供有用的错误信息支持帮助选项 - 提供 --help 选项,显示程序的用法使用标准函数处理选项 - 对于复杂的选项,使用 getopt 或 getopt_long考虑环境变量 - 当命令行参数未提供时,使用环境变量作为默认值通过遵循这些原则,你可以编写更加用户友好的命令行程序。
小结 本章全面介绍了 C 语言的文件输入/输出操作,涵盖了以下核心内容:
1. 文件概念 文件的基本概念:文件是存储在磁盘上的数据流 文件的分类:文本文件和二进制文件 文件系统:文件的组织方式和存储结构 文件路径:绝对路径和相对路径 文件权限:访问控制和安全性 文件句柄:操作系统用于标识文件的唯一标识符 2. 文件指针 FILE 结构体:文件指针的内部实现文件指针的生命周期:创建、使用、关闭 标准文件指针:stdin、stdout、stderr 文件指针的安全性:避免空指针和悬垂指针 3. 文件的打开和关闭 fopen 函数:打开文件并返回文件指针文件打开模式:只读、只写、读写、追加等 fclose 函数:关闭文件并释放资源错误处理:检查文件打开和关闭的返回值 最佳实践:确保文件在使用后被正确关闭 4. 文件的读写操作 字符级读写 :fgetc、fputc 函数行级读写 :fgets、fputs 函数块级读写 :fread、fwrite 函数格式化读写 :fscanf、fprintf 函数二进制文件操作 :使用 fread 和 fwrite 处理二进制数据5. 文件定位 文件位置指示器:跟踪当前读写位置 ftell 函数:获取当前文件位置fseek 函数:设置文件位置rewind 函数:将文件位置重置到开头fgetpos 和 fsetpos 函数:使用文件位置指示器结构6. 文件状态检查 feof 函数:检查文件是否结束ferror 函数:检查文件操作是否出错clearerr 函数:清除文件错误标志错误处理策略:及时检查和处理错误 7. 标准输入/输出 标准流:stdin、stdout、stderr 重定向:改变标准流的输入/输出目标 缓冲机制:行缓冲、全缓冲、无缓冲 刷新缓冲区:fflush 函数 8. 文件操作示例 文本文件读写 :读取和写入文本数据二进制文件读写 :处理二进制数据文件复制 :将一个文件复制到另一个文件文件追加 :向文件末尾添加数据文件信息统计 :统计文件的行数、单词数、字符数文件加密 :使用简单算法加密文件内容临时文件 :创建和使用临时文件9. 临时文件 tmpfile 函数:创建自动删除的临时文件tmpnam 函数:生成临时文件名mkstemp 函数:创建安全的临时文件临时文件的安全使用:避免文件名被抢占 临时文件的最佳实践:及时删除和错误处理 10. 命令行参数 main 函数的参数:argc 和 argv命令行参数的基本使用:打印和处理参数 命令行参数的类型转换:将字符串转换为其他类型 命令行选项处理:处理以 - 或 -- 开头的选项 使用 getopt 函数:处理复杂的命令行选项 命令行参数的最佳实践:检查参数数量和有效性 11. 最佳实践 错误处理 :及时检查和处理文件操作错误资源管理 :确保文件在使用后被正确关闭二进制文件操作 :注意数据类型和字节序性能优化 :使用块级读写和适当的缓冲区大小安全性 :避免缓冲区溢出和权限问题代码可维护性 :使用清晰的命名和注释跨平台考虑 :注意路径分隔符和换行符的差异文件操作是 C 语言中重要的输入/输出方式,掌握文件操作对于编写实际应用程序至关重要。通过本章的学习,你应该能够:
熟练使用各种文件操作函数 正确处理文件操作中的错误 编写安全、高效的文件操作代码 理解文件系统的基本概念和工作原理 掌握命令行参数的处理方法 这些技能将为你编写实际的 C 语言应用程序奠定坚实的基础,使你能够处理各种文件操作场景,从简单的文本文件读写到复杂的二进制文件处理。