第11章 文件输入/输出

文件的基本概念

文件是存储在外部存储设备(如硬盘、U盘、SD卡等)上的数据流,具有唯一的名称和路径。文件是持久化存储数据的重要方式,也是程序与外部世界交互的重要媒介。

文件系统

文件系统是操作系统用于管理存储设备上文件的方法和数据结构,它负责:

  • 文件组织 - 将文件组织成目录结构
  • 空间管理 - 分配和回收存储空间
  • 文件访问 - 控制对文件的读写操作
  • 文件保护 - 提供文件权限和安全机制

常见的文件系统包括:

  • FAT32 - 适用于移动设备的文件系统
  • NTFS - Windows 系统的主要文件系统
  • EXT4 - Linux 系统的主要文件系统
  • APFS - macOS 系统的主要文件系统

文件分类

在 C 语言中,文件可以分为以下两种类型:

  1. 文本文件

    • 以字符序列形式存储
    • 每行以换行符结束(不同系统换行符可能不同:Windows 是 \r\n,Linux/Unix 是 \n,macOS 是 \n
    • 可以用文本编辑器直接查看和编辑
    • 适合存储人类可读的数据,如代码、配置文件、文档等
  2. 二进制文件

    • 以字节序列形式存储
    • 直接存储数据的二进制表示
    • 不能用文本编辑器直接查看(会显示乱码)
    • 适合存储程序数据、图像、音频、视频等

文件路径

文件路径是指文件在文件系统中的位置,有两种表示方式:

  • 绝对路径 - 从根目录开始的完整路径,如 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; // 声明文件指针

文件指针的特性

  1. 唯一性 - 每个打开的文件都有一个唯一的文件指针
  2. 状态管理 - 文件指针内部管理文件的状态,如当前位置、错误状态等
  3. 缓冲区 - 文件指针通常使用缓冲区来提高读写效率
  4. 自动管理 - 标准文件指针(stdin、stdout、stderr)由系统自动管理

文件指针的生命周期

  1. 创建 - 通过 fopen 等函数创建文件指针
  2. 使用 - 通过文件指针进行各种文件操作
  3. 关闭 - 通过 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+"二进制读写模式保留原有内容创建新文件文件末尾

模式说明

  1. 文本模式 vs 二进制模式

    • 文本模式 - 不使用 b 后缀,会进行换行符转换(Windows 下 \n 转换为 \r\n
    • 二进制模式 - 使用 b 后缀,直接读写二进制数据,不进行任何转换
  2. 读写权限

    • r - 只读
    • w - 只写
    • a - 追加写
    • + - 读写
  3. 文件存在性处理

    • r 模式 - 要求文件必须存在
    • w 模式 - 无论文件是否存在,都会创建或截断
    • a 模式 - 如果文件不存在则创建,存在则追加

文件的关闭

使用 fclose 函数关闭文件,释放文件指针和相关资源。文件关闭是文件操作的重要环节,必须确保每个打开的文件都被正确关闭。

fclose 函数原型

1
int fclose(FILE *stream);

参数和返回值

  • 参数 - stream - 要关闭的文件指针
  • 返回值 - 成功返回 0,失败返回 EOF

文件关闭的重要性

  1. 释放资源 - 关闭文件可以释放操作系统为文件分配的资源
  2. 刷新缓冲区 - 确保所有缓冲区中的数据都被写入文件
  3. 避免资源泄漏 - 长时间运行的程序如果不关闭文件,会导致资源泄漏
  4. 文件锁定 - 关闭文件可以释放对文件的锁定,允许其他程序访问

错误处理

文件关闭失败通常是由于磁盘错误或权限问题导致的,应该妥善处理:

1
2
3
4
5
if (fclose(fp) != 0)
{
perror("关闭文件失败");
// 处理错误
}

打开和关闭文件的最佳实践

  1. 始终检查文件打开是否成功
1
2
3
4
5
6
FILE *fp = fopen("file.txt", "r");
if (fp == NULL)
{
perror("无法打开文件");
return EXIT_FAILURE;
}
  1. 使用 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;
}
  1. 使用 perrorstrerror 打印错误信息
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. 处理多个文件时的关闭顺序
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 标准库还提供了其他文件打开函数:

  1. freopen - 重新打开文件,常用于重定向标准输入/输出
1
FILE *freopen(const char *filename, const char *mode, FILE *stream);
  1. fdopen - 从文件描述符创建文件指针
1
FILE *fdopen(int fd, const char *mode);
  1. tmpfile - 创建临时文件
1
FILE *tmpfile(void);

文件的读写操作

文件读写是文件操作的核心,C 标准库提供了多种级别的文件读写函数,从字符级到块级,满足不同场景的需求。

字符级读写

字符级读写是最基本的文件读写方式,以单个字符为单位进行操作。

读取单个字符

1
int fgetc(FILE *stream);

参数和返回值:

  • stream - 文件指针
  • 返回值 - 读取的字符(转换为 int 类型),文件结束或出错返回 EOF

内部工作原理:

  1. 检查文件是否打开且可读
  2. 检查缓冲区是否有数据
  3. 如果缓冲区为空,从文件读取一批数据到缓冲区
  4. 从缓冲区中取出一个字符
  5. 更新文件位置指示器
  6. 如果文件已结束或发生错误,返回 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

内部工作原理:

  1. 检查文件是否打开且可写
  2. 检查缓冲区是否已满
  3. 如果缓冲区未满,将字符写入缓冲区
  4. 如果缓冲区已满,将缓冲区数据写入文件
  5. 更新文件位置指示器
  6. 如果发生错误,返回 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

内部工作原理:

  1. 检查文件是否打开且可读
  2. 从文件读取字符,直到遇到换行符 '\n'、文件结束或已读取 size-1 个字符
  3. 在缓冲区末尾添加终止符 '\0'
  4. 如果读取到换行符,将其包含在缓冲区中
  5. 更新文件位置指示器

注意事项:

  • fgets 会保留换行符 '\n'
  • 如果一行长度超过 size-1,会读取部分行,剩余部分需要再次调用 fgets 读取
  • 当返回 NULL 时,需要使用 feofferror 区分文件结束和错误

示例:

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

内部工作原理:

  1. 检查文件是否打开且可写
  2. 将字符串中的字符逐个写入文件(不包括终止符 '\0'
  3. 更新文件位置指示器
  4. 如果发生错误,返回 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 - 文件指针
  • 返回值 - 成功读取的数据项数量

内部工作原理:

  1. 计算要读取的总字节数:size * count
  2. 检查文件是否打开且可读
  3. 从文件读取数据到缓冲区
  4. 更新文件位置指示器
  5. 返回实际读取的数据项数量

注意事项:

  • 如果返回值小于 count,可能是文件已结束或发生错误
  • 需要使用 feofferror 区分文件结束和错误
  • 对于二进制文件,建议使用块级读取

示例:

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 - 文件指针
  • 返回值 - 成功写入的数据项数量

内部工作原理:

  1. 计算要写入的总字节数:size * count
  2. 检查文件是否打开且可写
  3. 将数据从缓冲区写入文件
  4. 更新文件位置指示器
  5. 返回实际写入的数据项数量

注意事项:

  • 如果返回值小于 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

内部工作原理:

  1. 解析格式控制字符串
  2. 从文件读取字符,按照格式控制字符串的要求进行解析
  3. 将解析结果存储到对应的变量中
  4. 更新文件位置指示器
  5. 返回成功读取的数据项数量

格式控制字符串:

  • %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. 返回成功写入的字符数

格式控制字符串:

  • fscanf 相同,但用于输出

示例:

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 操作次数不适合处理格式化文本处理二进制文件和大文件
格式化读写适合处理结构化数据性能较低,解析开销大处理结构化文本数据

读写操作的最佳实践

  1. 根据文件类型选择合适的读写方式

    • 文本文件:使用行级或格式化读写
    • 二进制文件:使用块级读写
  2. 使用适当的缓冲区大小

    • 对于块级读写,选择合适的缓冲区大小(如 4096 字节)
    • 避免使用过小的缓冲区导致频繁 I/O 操作
  3. 妥善处理错误

    • 始终检查读写函数的返回值
    • 使用 feofferror 区分文件结束和错误
  4. 关闭文件前刷新缓冲区

    • 使用 fflush 函数刷新缓冲区
    • 或确保通过 fclose 函数关闭文件(会自动刷新缓冲区)
  5. 避免混合使用不同级别的读写函数

    • 同一文件最好使用同一级别的读写函数
    • 如果必须混合使用,需要调用 fflush 或调整文件位置指示器

文件定位

文件定位是指在文件中移动读写位置的操作,对于需要随机访问文件的场景非常重要。C 标准库提供了一系列文件定位函数,用于获取和设置文件位置指示器。

文件位置指示器

每个打开的文件都有一个文件位置指示器(File Position Indicator),也称为文件指针(注意与 C 语言中的文件指针 FILE * 不同),它指向当前读写操作的位置。

文件位置指示器的特性

  1. 初始位置 - 根据文件打开模式的不同,文件位置指示器的初始位置也不同:

    • rr+rbr+b 模式:指向文件开头
    • ww+wbw+b 模式:指向文件开头(文件被截断为 0 长度)
    • aa+aba+b 模式:指向文件末尾
  2. 自动更新 - 当执行读写操作时,文件位置指示器会自动更新:

    • 读取操作后,指示器移动到已读取数据的后面
    • 写入操作后,指示器移动到已写入数据的后面
  3. 随机访问 - 通过文件定位函数,可以手动设置文件位置指示器的位置,实现随机访问

文件定位函数

C 标准库提供了以下文件定位函数:

获取当前位置

1
long ftell(FILE *stream);

参数和返回值:

  • stream - 文件指针
  • 返回值 - 当前文件位置(相对于文件开头的字节偏移量),失败返回 -1L 并设置 errno

内部工作原理:

  1. 检查文件是否打开
  2. 获取文件位置指示器的当前值
  3. 返回该值

注意事项:

  • 对于二进制文件,返回值是准确的字节偏移量
  • 对于文本文件,返回值可能不是准确的字节偏移量,因为文本模式下会进行换行符转换

设置文件位置

1
int fseek(FILE *stream, long offset, int whence);

参数和返回值:

  • stream - 文件指针
  • offset - 偏移量(以字节为单位)
  • whence - 起始位置:
    • SEEK_SET - 文件开头
    • SEEK_CUR - 当前位置
    • SEEK_END - 文件末尾
  • 返回值 - 成功返回 0,失败返回非 0 并设置 errno

内部工作原理:

  1. 检查文件是否打开
  2. 根据 whenceoffset 计算新的文件位置
  3. 检查新位置是否有效
  4. 设置文件位置指示器到新位置
  5. 清除文件结束标志
  6. 如果是文本模式,可能需要进行特殊处理

注意事项:

  • 对于二进制文件,offset 是准确的字节偏移量
  • 对于文本文件,offset 必须是之前通过 ftell 获取的值,或者是 0
  • 在追加模式(aa+)下,无论 fseek 如何设置,写入操作总是从文件末尾开始

重置文件位置到开头

1
void rewind(FILE *stream);

参数:

  • stream - 文件指针

内部工作原理:

  1. 调用 fseek(stream, 0L, SEEK_SET) 设置文件位置到开头
  2. 清除文件结束标志和错误标志

注意事项:

  • 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. 随机访问二进制文件
1
2
3
// 读取二进制文件中的第 n 个记录
fseek(fp, (n-1) * sizeof(Record), SEEK_SET);
fread(&record, sizeof(Record), 1, fp);
  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
// 注意:C 标准库不直接支持在文件中间插入数据
// 需要通过临时文件实现
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. 倒序读取文件
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);

文件定位的最佳实践

  1. 二进制文件使用 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);
  1. 文本文件谨慎使用 fseek
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 文本文件中,只使用 SEEK_SET 和偏移量为 0
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. 检查文件定位函数的返回值
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("无法获取文件大小");
// 处理错误
}
  1. 使用 fsetpos 和 fgetpos 进行大文件定位

对于大于 2GB 的文件,long 类型可能不够用,此时可以使用 fsetposfgetpos 函数:

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 标准库提供了一系列文件状态检查函数,帮助我们正确处理文件操作中的各种情况。

检查文件结束

1
int feof(FILE *stream);

参数和返回值:

  • stream - 文件指针
  • 返回值 - 文件结束返回非 0(真),否则返回 0(假)

内部工作原理:

  1. 检查文件是否打开
  2. 检查文件结束标志是否被设置
  3. 返回标志值

使用场景:

  • 当文件读写函数返回错误或特殊值时,用于判断是否是因为到达文件末尾
  • 通常与 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(假)

内部工作原理:

  1. 检查文件是否打开
  2. 检查错误标志是否被设置
  3. 返回标志值

使用场景:

  • 当文件读写函数返回错误或特殊值时,用于判断是否是因为发生了错误
  • 通常与 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);

参数:

  • stream - 文件指针

内部工作原理:

  1. 检查文件是否打开
  2. 清除文件结束标志
  3. 清除错误标志

使用场景:

  • 当需要在同一个文件上继续执行操作,而之前的操作设置了错误标志或文件结束标志时
  • 当需要重新尝试文件操作时

注意事项:

  • 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. 正确处理文件读写返回值
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");
}
  1. 避免使用 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); // 当到达文件末尾时,会多输出一个 EOF
}

// 正确示例
while ((ch = fgetc(fp)) != EOF)
{
putchar(ch);
}

if (ferror(fp))
{
perror("读取文件时出错");
}
  1. 配合使用 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);
// 处理文件结束
}
}
  1. 使用 clearerr 重置文件状态
1
2
3
4
5
6
7
// 重置文件状态示例
if (ferror(fp))
{
perror("文件操作出错");
clearerr(fp);
// 尝试恢复操作
}
  1. 处理多行读取的情况
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");
}

文件状态标志

每个文件指针内部都维护着两个状态标志:

  1. 文件结束标志 - 当文件读写操作尝试读取文件末尾时设置
  2. 错误标志 - 当文件操作发生错误时设置

这些标志只能通过以下方式清除:

  • 调用 clearerr 函数
  • 调用 rewind 函数
  • 调用 fseekfsetpos 函数(对于二进制文件)
  • 关闭并重新打开文件

错误处理策略

  1. 即时错误处理 - 每次文件操作后立即检查返回值和状态
  2. 集中错误处理 - 在关键操作点集中检查文件状态
  3. 恢复策略 - 对于非致命错误,尝试恢复操作
  4. 错误传播 - 将错误信息传递给调用者
  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
#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 是指向标准输入流的文件指针,用于从用户或其他程序读取输入数据。

特性:

  • 默认设备 - 键盘
  • 缓冲类型 - 行缓冲(输入数据直到遇到换行符才被处理)
  • 常用函数 - scanffgetsgetcharfgetc

示例:

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 是指向标准输出流的文件指针,用于向用户或其他程序输出数据。

特性:

  • 默认设备 - 屏幕
  • 缓冲类型 - 行缓冲(输出数据直到遇到换行符才被刷新到设备)
  • 常用函数 - printfputsputcharfputc

示例:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(void)
{
printf("这是标准输出\n");
fprintf(stdout, "这也是标准输出\n");
return 0;
}

标准错误 (stderr)

stderr 是指向标准错误流的文件指针,用于输出错误信息。

特性:

  • 默认设备 - 屏幕
  • 缓冲类型 - 无缓冲(错误信息立即输出,不等待缓冲)
  • 常用函数 - fprintfperror

示例:

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;
}

标准流的重定向

在命令行环境中,可以通过重定向操作符改变标准流的默认设备:

输入重定向 (<)

1
./program < input.txt

stdin 重定向到 input.txt 文件,程序从文件读取输入而不是键盘。

输出重定向 (>)

1
./program > output.txt

stdout 重定向到 output.txt 文件,程序输出到文件而不是屏幕。

错误重定向 (2>)

1
./program 2> error.txt

stderr 重定向到 error.txt 文件,错误信息输出到文件而不是屏幕。

同时重定向

1
./program < input.txt > output.txt 2> error.txt

合并输出和错误 (2>&1)

1
./program > output.txt 2>&1

stderr 重定向到 stdout,两者都输出到 output.txt 文件。

标准流的缓冲机制

标准流使用不同的缓冲策略,这会影响输出的时机:

行缓冲

  • 适用于 stdinstdout
  • 当遇到换行符 '\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 - 要重定向的标准流(stdinstdoutstderr
  • 返回值 - 成功返回 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); // Windows 系统
// freopen("/dev/tty", "w", stdout); // Linux/Unix 系统

printf("这条消息会输出到屏幕\n");
return 0;
}

标准流的最佳实践

  1. 使用适当的流

    • 输入数据使用 stdin
    • 正常输出使用 stdout
    • 错误信息使用 stderr
  2. 错误信息输出到 stderr

1
2
3
4
5
if (error_occurred)
{
fprintf(stderr, "错误:%s\n", error_message);
return EXIT_FAILURE;
}
  1. 避免混合使用 printf 和 puts
1
2
3
4
5
6
7
8
// 不好的做法
printf("Hello");
puts(" World"); // 会添加换行符

// 好的做法
printf("Hello World\n");
// 或
puts("Hello World");
  1. 使用 fflush 确保及时输出
1
2
3
printf("正在处理...");
fflush(stdout); // 确保用户看到提示
// 执行耗时操作
  1. 处理 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 运行时库中实现,其内部结构与普通文件指针类似,但有一些特殊处理:

  1. 自动初始化 - 在程序启动时由 C 运行时库初始化
  2. 特殊设备处理 - 与操作系统的标准设备驱动程序交互
  3. 缓冲管理 - 根据流类型使用不同的缓冲策略
  4. 重定向支持 - 支持命令行重定向和程序内重定向

标准流与操作系统的关系

标准流是 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;

// 使用 stdin 读取输入
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;
}

// 使用 stdout 输出正常信息
printf("您好,%s您的年龄是 %d 岁\n", name, age);

// 使用 stderr 输出错误信息
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 使用 perrorstrerror 打印错误信息

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 指令控制对齐
#pragma pack(push, 1) // 按 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; // 固定 4 字节
float score; // 通常 4 字节
uint64_t id; // 固定 8 字节
} 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); // 1000 次文件操作
}

// 好的做法
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");

// 错误:使用文本模式处理二进制数据
// FILE *fp = fopen("data.bin", "w"); // 可能会进行换行符转换

5. 安全性

5.1 避免缓冲区溢出

1
2
3
4
5
6
7
8
9
10
// 安全的读取方式
char buffer[100];
if (fgets(buffer, sizeof(buffer), fp) != NULL)
{
// 处理数据
}

// 不安全的读取方式(可能导致缓冲区溢出)
// char buffer[100];
// fscanf(fp, "%s", buffer); // 没有限制读取长度

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 // 1MB

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
/**
* 读取文件内容并打印
* @param filename 文件名
* @return 成功返回 0,失败返回非 0
*/
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
// 文本模式下,不同系统的换行符处理
// Windows: \r\n
// Linux/Unix: \n
// macOS: \n
// 二进制模式下,换行符不会被转换
FILE *fp = fopen("file.txt", "rb"); // 二进制模式

7.3 注意文件权限

1
2
3
4
5
6
7
// Windows 下的文件权限
// FILE *fp = fopen("file.txt", "w");

// Linux/Unix 下的文件权限
// 可能需要使用 chmod 函数设置权限
// #include <sys/stat.h>
// chmod("file.txt", 0644); // rw-r--r--

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; // 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 语言编程中的重要部分,掌握文件操作的最佳实践可以帮助你编写更加可靠、安全和高效的程序。以下是文件操作的核心原则:

  1. 错误处理优先 - 始终检查文件操作的返回值
  2. 资源管理 - 确保每个打开的文件都被正确关闭
  3. 性能优化 - 使用块级读写,减少 I/O 操作次数
  4. 安全性 - 避免缓冲区溢出,验证输入
  5. 可维护性 - 使用有意义的变量名,封装文件操作函数
  6. 跨平台考虑 - 注意路径分隔符、换行符和文件权限的差异

通过遵循这些最佳实践,你可以编写出更加专业、可靠的文件操作代码。

临时文件

临时文件是在程序运行过程中临时创建的文件,用于存储中间数据,通常在程序结束或文件关闭时自动删除。临时文件在以下场景中非常有用:

  • 存储大量中间数据 - 当内存不足时
  • 在多个进程或函数之间传递数据
  • 需要原子操作时 - 先写入临时文件,再重命名
  • 备份原始文件 - 在修改文件前创建备份

创建临时文件

C 标准库提供了以下函数创建临时文件:

1. tmpfile 函数

1
FILE *tmpfile(void);

参数和返回值:

  • 返回值 - 临时文件指针,失败返回 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 函数

1
char *tmpnam(char *s);

参数和返回值:

  • 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;
}

临时文件的安全使用

  1. 使用 mkstemp 代替 tmpnam

    • mkstemp 更安全,避免文件名被抢占
    • mkstemp 创建的文件权限更安全(0600)
  2. 及时删除临时文件

    • 使用 unlink 函数删除临时文件
    • 可以在创建文件后立即调用 unlink,这样文件会在所有文件描述符关闭后自动删除
  3. 设置适当的文件权限

    • 临时文件应设置为只有所有者可读写
    • 避免使用全局可写的临时文件
  4. 使用系统临时目录

    • 临时文件应存储在系统的临时目录中
    • 可以通过 TMPDIR 环境变量获取临时目录路径
  5. 避免硬编码临时文件路径

    • 使用 P_tmpdir 宏获取临时目录路径
    • 或使用 getenv("TMPDIR") 获取环境变量中的临时目录

临时文件的最佳实践

  1. 使用 tmpfile 进行简单操作
1
2
3
4
5
6
7
8
9
10
11
12
// 简单场景使用 tmpfile
FILE *tmp = tmpfile();
if (tmp == NULL)
{
perror("无法创建临时文件");
return EXIT_FAILURE;
}

// 使用临时文件
// ...

fclose(tmp); // 自动删除
  1. 使用 mkstemp 进行安全操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 安全场景使用 mkstemp
char tmp_name[] = "temp_XXXXXX";
int fd = mkstemp(tmp_name);
if (fd == -1)
{
perror("无法创建临时文件");
return EXIT_FAILURE;
}

// 立即删除,文件会在关闭时真正删除
unlink(tmp_name);

// 使用文件描述符
// ...

close(fd);
  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
// 原子更新文件
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. 临时文件用于大文件处理
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. 安全性 - 避免在临时文件中存储敏感信息,或确保使用安全的临时文件创建方法

示例:临时文件用于排序大文件

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 语言编程中处理中间数据的重要工具,正确使用临时文件可以提高程序的可靠性和安全性。以下是使用临时文件的核心原则:

  1. 选择合适的临时文件创建函数

    • 简单场景使用 tmpfile
    • 安全场景使用 mkstemp
    • 避免使用 tmpnam
  2. 及时清理临时文件

    • 使用 unlink 函数删除临时文件
    • 或依赖 tmpfile 的自动删除机制
  3. 安全使用临时文件

    • 设置适当的文件权限
    • 避免存储敏感信息
    • 使用系统临时目录
  4. 处理异常情况

    • 检查临时文件创建是否成功
    • 处理磁盘空间不足的情况
    • 确保在异常终止时清理临时文件

通过遵循这些原则,你可以安全、有效地使用临时文件来处理程序中的中间数据。

命令行参数

命令行参数是程序从命令行接收输入的一种方式,允许用户在启动程序时向程序传递数据。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;

// 使用 getopt 处理选项
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. 检查参数数量
1
2
3
4
5
if (argc < 2)
{
fprintf(stderr, "用法:%s <参数>\n", argv[0]);
return EXIT_FAILURE;
}
  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
#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. 使用标准函数处理选项
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':
// 处理 verbose 选项
break;
case 'f':
// 处理 file 选项
break;
case 'h':
// 显示帮助信息
break;
case '?':
// 处理错误
break;
}
}

// 处理剩余参数
for (int i = optind; i < argc; i++)
{
printf("剩余参数:%s\n", argv[i]);
}

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
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. 解析配置文件路径
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. 处理多个文件参数
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. 使用环境变量
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"); // Windows
}

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 语言程序与用户交互的重要方式,通过命令行参数,用户可以:

  1. 向程序传递数据 - 如文件名、配置选项等
  2. 控制程序行为 - 通过选项开关控制程序的不同功能
  3. 指定程序运行模式 - 如 verbose 模式、安静模式等

正确处理命令行参数可以使程序更加灵活、易用和专业。以下是处理命令行参数的核心原则:

  1. 检查参数数量和有效性 - 确保程序接收到正确的参数
  2. 提供清晰的错误信息 - 当参数错误时,向用户提供有用的错误信息
  3. 支持帮助选项 - 提供 --help 选项,显示程序的用法
  4. 使用标准函数处理选项 - 对于复杂的选项,使用 getoptgetopt_long
  5. 考虑环境变量 - 当命令行参数未提供时,使用环境变量作为默认值

通过遵循这些原则,你可以编写更加用户友好的命令行程序。

小结

本章全面介绍了 C 语言的文件输入/输出操作,涵盖了以下核心内容:

1. 文件概念

  • 文件的基本概念:文件是存储在磁盘上的数据流
  • 文件的分类:文本文件和二进制文件
  • 文件系统:文件的组织方式和存储结构
  • 文件路径:绝对路径和相对路径
  • 文件权限:访问控制和安全性
  • 文件句柄:操作系统用于标识文件的唯一标识符

2. 文件指针

  • FILE 结构体:文件指针的内部实现
  • 文件指针的生命周期:创建、使用、关闭
  • 标准文件指针:stdinstdoutstderr
  • 文件指针的安全性:避免空指针和悬垂指针

3. 文件的打开和关闭

  • fopen 函数:打开文件并返回文件指针
  • 文件打开模式:只读、只写、读写、追加等
  • fclose 函数:关闭文件并释放资源
  • 错误处理:检查文件打开和关闭的返回值
  • 最佳实践:确保文件在使用后被正确关闭

4. 文件的读写操作

  • 字符级读写fgetcfputc 函数
  • 行级读写fgetsfputs 函数
  • 块级读写freadfwrite 函数
  • 格式化读写fscanffprintf 函数
  • 二进制文件操作:使用 freadfwrite 处理二进制数据

5. 文件定位

  • 文件位置指示器:跟踪当前读写位置
  • ftell 函数:获取当前文件位置
  • fseek 函数:设置文件位置
  • rewind 函数:将文件位置重置到开头
  • fgetposfsetpos 函数:使用文件位置指示器结构

6. 文件状态检查

  • feof 函数:检查文件是否结束
  • ferror 函数:检查文件操作是否出错
  • clearerr 函数:清除文件错误标志
  • 错误处理策略:及时检查和处理错误

7. 标准输入/输出

  • 标准流:stdinstdoutstderr
  • 重定向:改变标准流的输入/输出目标
  • 缓冲机制:行缓冲、全缓冲、无缓冲
  • 刷新缓冲区:fflush 函数

8. 文件操作示例

  • 文本文件读写:读取和写入文本数据
  • 二进制文件读写:处理二进制数据
  • 文件复制:将一个文件复制到另一个文件
  • 文件追加:向文件末尾添加数据
  • 文件信息统计:统计文件的行数、单词数、字符数
  • 文件加密:使用简单算法加密文件内容
  • 临时文件:创建和使用临时文件

9. 临时文件

  • tmpfile 函数:创建自动删除的临时文件
  • tmpnam 函数:生成临时文件名
  • mkstemp 函数:创建安全的临时文件
  • 临时文件的安全使用:避免文件名被抢占
  • 临时文件的最佳实践:及时删除和错误处理

10. 命令行参数

  • main 函数的参数:argcargv
  • 命令行参数的基本使用:打印和处理参数
  • 命令行参数的类型转换:将字符串转换为其他类型
  • 命令行选项处理:处理以 --- 开头的选项
  • 使用 getopt 函数:处理复杂的命令行选项
  • 命令行参数的最佳实践:检查参数数量和有效性

11. 最佳实践

  • 错误处理:及时检查和处理文件操作错误
  • 资源管理:确保文件在使用后被正确关闭
  • 二进制文件操作:注意数据类型和字节序
  • 性能优化:使用块级读写和适当的缓冲区大小
  • 安全性:避免缓冲区溢出和权限问题
  • 代码可维护性:使用清晰的命名和注释
  • 跨平台考虑:注意路径分隔符和换行符的差异

文件操作是 C 语言中重要的输入/输出方式,掌握文件操作对于编写实际应用程序至关重要。通过本章的学习,你应该能够:

  1. 熟练使用各种文件操作函数
  2. 正确处理文件操作中的错误
  3. 编写安全、高效的文件操作代码
  4. 理解文件系统的基本概念和工作原理
  5. 掌握命令行参数的处理方法

这些技能将为你编写实际的 C 语言应用程序奠定坚实的基础,使你能够处理各种文件操作场景,从简单的文本文件读写到复杂的二进制文件处理。