青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】十一、文件


阅读数: 0次    2026-02-03
字数:9.1k字 | 预计阅读时长:38分钟

1. 输入输出初步认识

文件的读写,标准上称之为输入流(input stream)和输出流(output stream)。

输入流的数据会首先被暂存到一个叫缓冲区(buffer)的内存区域。

缓冲区的作用:

  • 检查安全的一种机制;
  • 一部分数据打包放在缓冲区中,用于批量处理,提高数据处理的效率。

标准输入(stdin):指从键盘输入数据到操作系统中,存储这些数据在标准输入关联的缓冲区中,等待被程序读取。

标准输出(stdout):可以把数据发送到外部,例如输出数据到控制台、文件等。

2. 复习scanf_s

根据前面学过的章节,在对字符串进行修改时,使用带_s的函数,可有效避免缓冲区溢出的问题。

接下来是一些复习。

需要注意的是,只要且只有修改字符或字符串时,必须加第三个参数,传入想要输入的长度,避免缓冲区溢出。对于输入数字,不需要加第三个参数。

输入字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define SIZE 50

int main() {
char name[SIZE];

printf("Enter your name: ");
scanf_s("%49s", name, (unsigned int)sizeof(name));

printf("Hello, %s!\n", name);

return 0;
}

输入字符:

1
2
3
4
5
6
char ch;

printf("Enter a character: ");
scanf_s("%c", &ch, 1);

printf("You entered: %c\n", ch);

输入整数:

1
2
3
4
5
6
int age;

printf("Enter a age: ");
scanf_s("%d", &age);

printf("You entered: %d\n", age);

3. scanf_s的返回值

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
uint32_t number;
int result;

puts("Enter an integer:");

result = scanf_s("%" SCNu32, &number);
if (result == 1) { // 输入成功
printf("You entered: %" PRIu32 "\n", number);
}
else if (result == EOF) { // 输入错误或到达文件末尾
printf("An error occurred or end of file was reached.\n");
return 1;
}
else {
printf("Invalid input for integer.\n");
return 1;
}

EOF是“end of file”的缩写,表示没有更多数据可供读取,其值通常为-1。

scanf_s的返回值为一个整数,只有当返回值为1的时候,代表输入被成功读取。

4. stream流的概述

以在文本编辑器中写入数据为例,没保存之前,所有数据都被暂存在内存的缓冲区中,如果没保存时退出程序,这些临时数据就会丢失。点击保存后,内存缓冲区中的数据就会被转移到硬盘中,变成永久性数据。

流(Stream)包含以下几种:

  1. 文件流:用于读取与写入在磁盘上的文件,这是本章的重点;
  2. 标准I/O流:包括stdin(默认连接到键盘,用于程序输入,例如scanf_s())、stdout(默认连接到控制台或者屏幕,用于程序输出,例如printf())、stderr(默认也连接到控制台或屏幕,专门输出错误信息和警告,使其能够被区分开来或者是重定向到不同的目的地);
  3. 管道流:用于进程之间的通信(IPC),允许一个进程的输出成为另一个进程的输入。例如popen();
  4. 内存流:允许将流与内存缓冲区关联,使得用于可以向内存中读写数据,就像操作文件一样。例如fmemopen();
  5. 网络流:套接字(Sockets);
  6. 设备流:特殊文件或是设备,例如打印机。

在C语言中,使用FILE*类型来定义流指针,例如:FILE* stream。

5. 读取r模式(fopen_s、fgetc、fgets、fclose)

使用FILE*可以创建文件流指针,初始化为NULL。

打开文件时为了保证缓冲区不溢出,使用fopen_s()函数,传入三个参数,第一个是文件流的二级指针,第二个是文件路径,第三个是文件打开模式。

Windows下通常使用\作为路径分隔符,例如C:\Users\HC\Desktop\file.txt。但在输入文件路径时,反斜杠\是转义符,后面需要再加一个反斜杠,使用双反斜杠\\,转义之后才能得到反斜杠,这才是正确的路径,例如C:\\Users\\HC\\Desktop\\file.txt。

5.1 按行读取

读取单行文件时使用fgets()函数,传入三个参数,第一个是定义的内存缓冲区,第二个是缓冲区长度,第三个是打开的文件流。

文件操作完成后,需要调用fclose()关闭文件流,传入想要关闭的文件流即可,避免文件流被一直占用。

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>
#include <inttypes.h>
#include <stdint.h>

int main() {
FILE* file_stream = NULL; // 文件流指针

char buffer[256]; // 行缓冲区

// 打开文件
fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 读取文件中的第一行并放入缓冲区
fgets(buffer, sizeof(buffer), file_stream);

// 打印缓冲区内容
printf("%s\n", buffer);

// 关闭文件
fclose(file_stream);

return 0;
}

值得注意的是,微软的编译器非常智能,如果在打开文件时不对可能出现的错误类型做判断处理,后续使用到文件流的函数会报警告:"file_stream" 可能是 "0": 这不符合函数 "fgets" 的规范。。

此时,虽然程序在文件打开没有出错的情况下可以正常运行,但一旦文件打开出错,例如文件路径错误,文件异常,文件权限错误等,程序在Debug模式下会直接弹窗报错,Release模式下无法正常运行。

这时候就需要在打开文件时,对fopen_s()函数的返回值做判断和处理,保证文件正常打开。fopen_s()的返回值是一个errno_t类型的值,当这个值不为0,或者文件流仍为NULL时,说明文件没有被正常打开,此时使用perror()函数返回错误类型,并退出程序即可。

使用EXIT_FAILURE宏定义需要引入标准库stdlib.h。

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 <inttypes.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
FILE* file_stream = NULL; // 文件流指针

char buffer[256]; // 行缓冲区

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 读取文件中的第一行并放入缓冲区
fgets(buffer, sizeof(buffer), file_stream);

// 打印缓冲区内容
printf("%s\n", buffer);

// 关闭文件
fclose(file_stream);

return 0;
}

可以使用循环,调用fgets()函数读取一个文件中的所有内容,直到fgets()函数的返回值为NULL时,文件读取完毕。

当读取到的数据为NULL或EOF时,代表这个文件结束了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
FILE* file_stream = NULL;	// 文件流指针

char buffer[256]; // 行缓冲区

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 循环读取文件中的每一行,直到文件结束
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
// 每读取一行,打印一次缓冲区
printf("%s", buffer);
}

// 关闭文件
fclose(file_stream);

由于读取的每一行中已经包含了换行符,因此打印缓冲区时候不需要加\n。

5.2 按字符读取

fgetc()函数是按照字符读取文件,只需要传入一个文件流参数,返回的是读取到的字符。

准确来说,fgetc()的返回值是int类型,对应的读取到的字符的ASCII码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
FILE* file_stream = NULL;	// 文件流指针

char ch; // 字符缓冲区

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 读取文件中的第一个字符
ch = fgetc(file_stream);
putchar(ch); // 输出一个字符

// 关闭文件
fclose(file_stream);

同样的,使用fgetc()函数读取字符,也可以使用循环读取整个文件。当读取到的字符为EOF时,文件读取完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
FILE* file_stream = NULL;	// 文件流指针

char ch; // 字符缓冲区

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 循环读取文件中的每一个字符,直到文件结束
while ((ch = fgetc(file_stream)) != EOF) {
putchar(ch);
}

// 关闭文件
fclose(file_stream);

由于文件中每一行结束时都包含换行符,也会被当成字符读取,因此输出的内容会与文件中的内容一样,包含换行。

5.3 同一个文件流多次读取

可以在同一个程序中分别使用fgets()与fgetc()通过行和字符读取整个文件。

需要注意的是,第一次读取完成后,文件流指针已经指向了文件末尾,导致第二次一开始读取到的就是文件末尾,无法正常读取整个文件。

此时需要调用rewind()函数来将文件流指针复位到文件开头。

在行读取结束之后,可以使用memset函数释放缓冲区,这个函数包含在头文件string.h中。

为了文件的安全性,只要涉及到与写文件相关的操作,关闭文件时也需要和打开文件时一样,对函数的返回值做判断和错误返回操作。读取文件时可写可不写。

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 <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

int main() {
FILE* file_stream = NULL; // 文件流指针

char buffer[256]; // 行缓冲区
char ch; // 字符缓冲区

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 循环读取文件中的每一行,直到文件结束
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
// 每读取一行,打印一次缓冲区
printf("%s", buffer);
}

// 释放行缓冲区
memset(buffer, 0, sizeof(buffer));
printf("\n"); // 读完一次后换行
// 复位文件流指针到文件头
rewind(file_stream);

// 循环读取文件中的每一个字符,直到文件结束
while ((ch = fgetc(file_stream)) != EOF) {
putchar(ch);
}

// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

return 0;
}

6. 写入w模式(fputs、fputc、fprintf_s)

fopen_s函数中常用的模式和用途如下:

modeAccess
“r”打开以便读取。如果文件不存在或找不到,fopen_s调用将失败。
“w”打开用于写入的空文件。如果给定文件存在,则其内容会被销毁。
“a”在文件末尾打开以写入(追加),在新数据写入到文件之前不移除文件末尾(EOF)标记。如果文件不存在,则创建文件。
“r+”打开以便读取和写入。文件必须存在。
“w+”打开用于读取和写入的空文件。如果文件存在,则其内容会被销毁。
“a+”打开以进行读取和追加。追加操作包括在新数据写入文件之前移除EOF标记。写入完成后,EOF标记不会还原。如果文件不存在,则创建文件。

因此,使用w模式时,文件中原有的内容会被全部覆盖并替换为新内容。

打开与关闭文件的方式与读取文件时相同,模式改为“w”。

fputs()用于写入一行内容,传入要写入的字符串与文件流指针。

fputc()用于写入一个字符,传入要写入的字符与文件流指针。

fprintf()用于格式化写入,第一个参数是文件流指针,后面的参数与printf()相同。

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
FILE* file_stream = NULL;	// 文件流指针

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "w");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 写入行
fputs("This is a line written by fputs.\n", file_stream);

// 写入字符
fputc('H', file_stream);
fputc('i', file_stream);
fputc('!', file_stream);
fputc('\n', file_stream);

float pi = 3.14f;
// 格式化写入
fprintf_s(file_stream, "Example: %d, %.2f\n%s\n", 10, pi, "End of example.");


// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

puts("File 'file.txt' has been written successfully.");

实际上,调用写入函数写入文件时,也应该对返回值进行判断,防止出现在文件打开模式为读取并调用写入函数时,无任何报错,也没有成功写入的情况。

1
2
3
4
5
// 写入行,并进行错误处理
if (fputs("This is a line written by fputs.\n", file_stream) != 0) {
perror("Error writting file");
return EXIT_FAILURE;
}

如果无法写入,输出:Error writting file: Bad file descriptor。

7. ftell、fseek、rewind

在fopen_s()函数中使用“r+”模式,以便读取和写入。

ftell()用于获取当前的文件流指针的位置,需要传入文件流指针,返回long类型的数据。

fseek()用于将文件流指针移动到指定位置,需要传入文件流指针、相对第三个参数的偏移量,以字节为单位、开始添加偏移的位置。

一些常用偏移位置的常量宏定义:

常量描述
SEEK_SET文件的开头
SEEK_CUR文件指针的当前位置
SEEK_END文件的末尾

rewind()用于将文件流指针复位到文件开始,只需传入文件流指针即可。

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
FILE* file_stream = NULL;	// 文件流指针

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r+");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 获取当前文件指针的位置
printf("初始位置:%ld\n", ftell(file_stream));

// 读取一行数据
char buffer[256];
if (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
printf("读取一行:%s", buffer);
// 获取当前文件指针的位置
printf("读取一行后,当前位置:%ld\n", ftell(file_stream));
}

// 移动文件流指针的位置
fseek(file_stream, 1, SEEK_SET);
// 获取当前文件指针的位置
printf("移动文件流指针后,当前位置:%ld\n", ftell(file_stream));
if (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
printf("读取一行:%s", buffer);
// 获取当前文件指针的位置
printf("再次读取一行后,当前位置:%ld\n", ftell(file_stream));
}

// 复位文件流指针到文件开头
rewind(file_stream);
// 获取当前文件指针的位置
printf("复位后,当前位置:%ld\n", ftell(file_stream));

// 释放buffer
memset(buffer, 0, sizeof(buffer));

// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

需要注意的是,Windows系统采用的换行符是CRLF(\r\n),因此一行的字符数是可以看到的字符数+2。

例如,如果第一行为Hi,则真实读取文件时为Hi\r\n,因此读取完一行后,文件流指针的位置为4。

8. fscanf_s

fscanf_s函数用于从流中格式化读取数据。

因为是读取,打开文件模式选择“r”。

例如准备读取的文件内容为:

1
Hello! 6666 5.34 H

可以使用fscanf_s函数分别读取每一种类型,并把读取到的数据储存在变量或缓冲区中。

fscanf_s函数需要传入的参数与scanf_s类似,区别是需要在最前面加一个文件流指针参数。

可以对fscanf_s函数的返回值做一些判断,当返回值为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
FILE* file_stream = NULL;	// 文件流指针

char string[50];
uint32_t num_1;
double num_2;
char ch;

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 读取字符串
if (fscanf_s(file_stream, "%50s", string, (unsigned int)sizeof(string)) != 1) {
printf("读取字符串失败!\n");
}

// 读取整型
if (fscanf_s(file_stream, "%" SCNu32 "", &num_1) != 1) {
printf("读取整型失败!\n");
}

// 读取浮点型
if (fscanf_s(file_stream, "%lf", &num_2) != 1) {
printf("读取浮点型失败!\n");
}

// 读取字符
if (fscanf_s(file_stream, " %c", &ch, 1) != 1) {
printf("读取字符失败!\n");
}

// 打印格式化读取到的数据
printf("字符串:%s\n", string);
printf("整型:%d\n", num_1);
printf("浮点型:%.2lf\n", num_2);
printf("字符:%c\n", ch);

// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

9. fprintf

fprintf用于将缓冲区的数据写入到文件中。

打开文件模式使用“w”,即覆盖式写入。

使用方式与printf类似,区别是需要在最前面加一个文件流指针参数。

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
FILE* file_stream = NULL;	// 文件流指针

uint32_t id = 1234;
char name[] = "HC";
float grade = 98.2f;
char level = 'A';

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "w");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 向文件写入数据
fprintf(file_stream, "ID: %" PRIu32 "\n", id);
fprintf(file_stream, "name: %s\n", name);
fprintf(file_stream, "grade: %.2f\n", grade);
fprintf(file_stream, "level: %c\n", level);

// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

printf("数据写入完成!\n");

10. ferror、feof、clearerr

这三个函数用来处理文件流中的错误,前两个是错误检查,第三个是清除错误。

ferror:检查文件流中或文件读写中是否有错误发生,如果有错误,返回一个非0值,如果没有错误,返回0;

feof:检查文件流指针是否到达了文件的末尾,如果文件流指针到达了文件的末尾,返回非0值,否则返回0;

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
37
FILE* file_stream = NULL;	// 文件流指针

char buffer[100];

// 打开文件
errno_t err = fopen_s(&file_stream, "C:\\Users\\HC\\Desktop\\file.txt", "r");

// 错误处理,确保文件被成功打开
if (err != 0 || file_stream == NULL) {
perror("Error opening file");
return EXIT_FAILURE;
}

// 读取文件
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
printf("%s", buffer);
}

// 检查是否有读写时错误
if (ferror(file_stream)) {
perror("读取文件时发生错误");
clearerr(file_stream); // 清除错误和标志
}

// 检查文件流指针是否已经到达了文件末尾
if (feof(file_stream)) {
printf("已经到达文件的末尾……\n");
}
else {
printf("文件未达到末尾,可能因为发生了错误!\n");
}

// 关闭文件并错误处理,确保文件被安全关闭
if (fclose(file_stream) != 0) {
perror("Error closing file");
return EXIT_FAILURE;
}

11. 抽离读写函数

可以将读取文件的功能抽离为单独的函数,简化主函数。

抽离出来的函数中,打开文件时的错误处理没有用perror,而是用了另一种企业中常用的更为标准的写法,将错误信息先储存在缓冲区中,再将其输出为标准错误。

strerror_s是安全版本的错误转换函数,将错误码errno转换为可读的错误消息;errno是全局错误码变量,记录最近一次系统调用的错误类型(如权限不足、文件不存在等)。

stderr是标准错误输出流,用于输出错误信息(与stdout分离)。

fprintf可以将错误信息格式化输出到标准错误输出流中。

exit是进程级退出,调用时直接终止整个程序,而return只能终止当前函数。此时打开文件遇到错误时,程序不应该继续执行。

strerror_s函数包含在头文件string.h中。

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
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

// 安全读取文件
void file_reader_safe(const char* file_path);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\file.txt";

// 安全读取文件
file_reader_safe(file_path);

return 0;
}

// 安全读取文件
void file_reader_safe(const char* file_path) {
FILE* file_stream = NULL;
errno_t err = fopen_s(&file_stream, file_path, "r");

// 标准错误处理
if (err != 0 || file_stream == NULL) {
char error_msg[256]; // 错误信息缓冲区
// 获取错误信息
strerror_s(error_msg, sizeof(error_msg), errno);
// 输出错误信息
fprintf(stderr, "Failed to open file for reading: %s\n", error_msg);
exit(EXIT_FAILURE);
}
char buffer[256];
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
printf("%s", buffer);
}

// 由于模式为读取,关闭文件时可以不做错误处理
fclose(file_stream);
}

12. 追加a模式

“a”模式可以在一个文件的末尾追加内容,如果文件不存在,则创建文件。

使用“a”或“a+”模式打开文件时,所有写入操作均在文件的末尾进行。使用fseek或rewind可重新定位文件指针,但在执行任何写入操作前,文件指针将始终被移回文件末尾,以确保不会覆盖现有数据。

可以单独抽离出一个函数,用于在文件末尾追加指定内容。

由于追加包含了文件写入操作,文件关闭时应当做错误处理。

追加模式在追加内容时会自动换行,因此追加内容不需要手动写\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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

// 安全追加文件
void file_appender_safe(const char* file_path, const char* msg);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\file.txt";

// 安全追加文件
file_appender_safe(file_path, "This is a safe append test.\n");

printf("File appended successfully.\n");

// 确保所有文件流已关闭
int numclosed = _fcloseall();
printf("Number of files closed: %d\n", numclosed);

return 0;
}

// 安全追加文件
void file_appender_safe(const char* file_path, const char* msg) {
FILE* file_stream = NULL;
errno_t err = fopen_s(&file_stream, file_path, "a");
// 标准错误处理
if (err != 0 || file_stream == NULL) {
char error_msg[256]; // 错误信息缓冲区
// 获取错误信息
strerror_s(error_msg, sizeof(error_msg), errno);
// 输出错误信息
fprintf(stderr, "Failed to open file for appending: %s\n", error_msg);
exit(EXIT_FAILURE);
}

// 写入数据
fprintf(file_stream, "%s\n", msg);

// 关闭文件时进行错误处理
err = fclose(file_stream);
if (err != 0) {
char error_msg[256]; // 错误信息缓冲区
// 获取错误信息
strerror_s(error_msg, sizeof(error_msg), errno);
// 输出错误信息
fprintf(stderr, "Failed to close file after appending: %s\n", error_msg);
exit(EXIT_FAILURE);
}
}

在主函数的最后可以加一个_fcloseall()函数,确保所有的流都已被关闭,这个函数的返回值是其在调用时关闭了多少个文件流,可以输出用于调试。

13. w模式清空

“w”模式在写入时会销毁并覆盖之前的文件,因此可以用这个模式实现清空文件的功能。

只要打开文件时使用的是“w”模式,不需要做其他任何操作,文件内容会被清空。

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
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

// 清空文件内容
void file_clear_safe(const char* file_path);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\file.txt";

// 清空文件内容
file_clear_safe(file_path);

printf("File cleared successfully.\n");

// 确保所有文件流已关闭
_fcloseall();

return 0;
}

// 清空文件内容
void file_clear_safe(const char* file_path) {
FILE* file_stream = NULL;
errno_t err = fopen_s(&file_stream, file_path, "w");
// 标准错误处理
if (err != 0 || file_stream == NULL) {
char error_msg[256]; // 错误信息缓冲区
// 获取错误信息
strerror_s(error_msg, sizeof(error_msg), errno);
// 输出错误信息
fprintf(stderr, "Failed to open file for clearing: %s\n", error_msg);
exit(EXIT_FAILURE);
}
// 关闭文件时进行错误处理
err = fclose(file_stream);
if (err != 0) {
char error_msg[256]; // 错误信息缓冲区
// 获取错误信息
strerror_s(error_msg, sizeof(error_msg), errno);
// 输出错误信息
fprintf(stderr, "Failed to close file after clearing: %s\n", error_msg);
exit(EXIT_FAILURE);
}
}

14. 企业实际案例(难):修改log,r+模式

这个案例用于将一个文件中第一次出现的特定字符串更新为另一个目标字符串。

打开文件时,使用“r+”模式可以实现文件的读取查找与修改更新。

更新文件的难度较大,这个操作涉及到文件的读和写,以及各种各样的判断。

这个例子的函数设计类似于企业标准写法,难度较大,可以作为学习参考。

Windows系统下的换行符是CRLF(\r\n),在打开文件模式为“r+”时,读取文件时,程序自动在内存中将换行符由\r\n转换为\n,在写入文件时,程序自动在内存中将换行符由\n转换为\r\n。

因此,使用strlen得到读取文件缓冲区的字符串长度时,得到的长度会比实际长度少一个\r。

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
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

#define BUFFER_SIZE 1024 // 缓冲区大小

// 修改文件内容
errno_t file_update_safe(const char* file_path, const char* search_str, const char* replace_str);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\file.txt";

const char* search_str = "error";

const char* replace_str = "err";

// 修改文件内容
errno_t update_err = file_update_safe(file_path, search_str, replace_str);

// 标准错误处理
if (update_err != 0) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), update_err);
fprintf(stderr, "Failed to update file: %s\n", error_msg);
exit(EXIT_FAILURE);
}

printf("File updated successfully.\n");

// 确保所有文件流已关闭
_fcloseall();

return 0;
}

// 修改文件内容
errno_t file_update_safe(const char* file_path, const char* search_str, const char* replace_str) {
if (file_path == NULL || search_str == NULL || replace_str == NULL) {
return EINVAL; // 返回无效参数错误码
}

FILE* file_stream = NULL;

errno_t err = fopen_s(&file_stream, file_path, "r+");
// 标准错误处理
if (err != 0 || file_stream == NULL) {
return err; // 返回打开文件错误码
}

char buffer[BUFFER_SIZE]; // 读取缓冲区
bool found = false; // 标记是否找到搜索字符串
long position = 0; // 记录替换位置
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
if (strstr(buffer, search_str) != NULL) {
found = true;
// -1 是为了补偿 Windows 文本模式下的 \r\n 转换
// ftell() 返回实际位置(\r\n=2字节)
// strlen() 计算转换后长度(\n=1字节)
// 差值就是 \r 的1字节
position = ftell(file_stream) - (long)strlen(buffer) - 1;
break; // 找到第一个匹配项后退出循环
}
}
if (found) {
size_t search_len = strlen(search_str); // 源字符串长度
size_t replace_len = strlen(replace_str); // 替换字符串长度
// 定位到替换位置
if (fseek(file_stream, position, SEEK_SET) != 0) {
fclose(file_stream);
return errno; // 返回定位错误码
}
// 检查源字符串和替换字符串长度
if (replace_len > BUFFER_SIZE - 1 || search_len > BUFFER_SIZE - 1) {
fclose(file_stream);
return ERANGE; // 返回范围错误码
}
// 当源字符串长度大于或等于替换字符串长度时,进行替换,否则返回错误
if (replace_len > search_len) {
fclose(file_stream);
printf("替换字符串长度大于源字符串长度,不进行替换。\n");
return EDOM; // 返回数学域错误码
}
// 清除原来位置的内容
memset(buffer, ' ', strlen(buffer) - 1); // 用空格填充,保持行长度不变,使用-1避免覆盖换行符
buffer[strlen(buffer) - 1] = '\n'; // 保留换行符
fputs(buffer, file_stream);
// 重新定位到替换位置
if (fseek(file_stream, position, SEEK_SET) != 0) {
fclose(file_stream);
return errno; // 返回定位错误码
}
// 写入替换字符串
if (fputs(replace_str, file_stream) == EOF) {
fclose(file_stream);
return errno; // 返回写入错误码
}
}
else {
fclose(file_stream);
return ENOENT; // 返回未找到错误码
}

// 关闭文件流
fclose(file_stream);
return 0; // 成功返回0
}

这个程序是针对一整行进行替换,替换部分字符的逻辑可以自行研究。

当替换字符串的长度大于源字符串的长度时,直接替换可能会导致下一行的内容被覆盖。

此程序简化了操作流程,在这种情况下直接返回错误,不进行替换。

对于这种情况,比较简单的处理方法是对于超出源字符串长度的部分直接截断,但这样可能会丢失一部分内容。

精细处理时,小文件可以先将剩余部分读取到内存中,替换后再写入剩余部分;大文件不方便全部读入内存,可以创建一个临时文件,逐行复制并替换,最后用临时文件替换原文件。

临时文件方案可以参考下一节。

15. 临时文件的方案

本节续上节的问题,使用创建临时文件的思路将一个文件中第一次出现的特定行内容替换为另一行目标内容。

用到了几个新的函数:

tmpnam_s:传入临时文件名缓冲区与缓冲区长度,函数会自动生成一个随机的文件名存放在缓冲区中,一般不会出现文件名重复的问题;

remove:传入想要删除的文件路径,返回值为0表示删除成功,返回非0值表示删除失败;

rename:传入两个文件路径,将第一个参数的文件重命名为第二个参数中的文件名,返回0表示重命名成功,返回非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
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
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>

#define BUFFER_SIZE 1024 // 缓冲区大小

// 修改文件内容
errno_t file_update_safe(const char* file_path, const char* search_str, const char* replace_str);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\file.txt";

const char* search_str = "error";

const char* replace_str = "err";

// 修改文件内容
errno_t update_err = file_update_safe(file_path, search_str, replace_str);

// 标准错误处理
if (update_err != 0) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), update_err);
fprintf(stderr, "Failed to update file: %s\n", error_msg);
exit(EXIT_FAILURE);
}

printf("File updated successfully.\n");

// 确保所有文件流已关闭
_fcloseall();

return 0;
}

// 修改文件内容
errno_t file_update_safe(const char* file_path, const char* search_str, const char* replace_str) {
if (file_path == NULL || search_str == NULL || replace_str == NULL) {
return EINVAL; // 返回无效参数错误码
}

// 1. 创建一个唯一的临时文件名
char temp_file_path[L_tmpnam_s]; // L_tmpnam_s 是标准库定义的临时文件名缓冲区大小
errno_t err = tmpnam_s(temp_file_path, sizeof(temp_file_path)); // 生成临时文件名
if (err != 0) {
return err; // 返回生成临时文件名错误码
}

FILE* file_stream = NULL;
FILE* temp_file_stream = NULL;

// 2. 打开原文件进行读取
err = fopen_s(&file_stream, file_path, "r");
if (err != 0 || file_stream == NULL) {
return err; // 返回打开文件错误码
}

// 3. 打开临时文件进行写入
err = fopen_s(&temp_file_stream, temp_file_path, "w");
if (err != 0 || temp_file_stream == NULL) {
fclose(file_stream); // 关闭已打开的原文件
return err; // 返回打开临时文件错误码
}

char buffer[BUFFER_SIZE]; // 读取缓冲区
bool found = false; // 标记是否找到搜索字符串

// 4. 逐行读取原文件内容,判断处理后写入临时文件
while (fgets(buffer, sizeof(buffer), file_stream) != NULL) {
if (found == false && strstr(buffer, search_str) != NULL) {
found = true; // 标记找到搜索字符串
fprintf(temp_file_stream, "%s\n", replace_str); // 写入替换字符串
}
else {
fputs(buffer, temp_file_stream); // 直接写入原内容
}
}

fclose(file_stream); // 关闭原文件
fclose(temp_file_stream); // 关闭临时文件

// 5. 如果找到匹配项,则用新文件替换原文件
if (found) {
if (remove(file_path) != 0) {
remove(temp_file_path); // 删除临时文件
return errno; // 返回删除原文件错误码
}
if (rename(temp_file_path, file_path) != 0) {
return errno; // 返回重命名错误码
}
}
else {
remove(temp_file_path); // 未找到匹配项,删除临时文件
return ENOENT; // 返回未找到错误码
}

return 0; // 成功返回0
}

此程序只会替换第一次出现目标字符串的行。

只要一行中出现目标字符串,就会将整行替换为新的字符串。

16. fflush简单掠过

fflush用于保存文件,传入一个文件流指针。调用这个函数时,缓冲区中的内容会被立刻写入磁盘。

例如在多线程保存文件或者立即保存错误日志的场景下,需要用到这个功能。

17. 游戏设置案例:bin二进制文件存储与读取,wb与rb模式的使用

读取和写入二进制数据(图片、音频等)一般用到fread与fwrite函数。

本案例演示一个简单的写入与读取游戏设置二进制文件的功能。

打开文件时,"wb"模式用来写入二进制文件,“rb”模式用来读取二进制文件。

fwrite函数传递四个参数:

  • ptr:指向要写入的数据的指针。
  • size:要被写入的每个元素的大小,以字节为单位。
  • nmemb:元素的个数,每个元素的大小为size字节。
  • stream:指向FILE结构的指针。

返回值:如果成功,该函数返回一个size_t对象,表示元素的总数,该对象是一个整型数据类型。如果该数字与nmemb参数不同,则会显示一个错误。

fread函数传递四个参数:

  • ptr:数据的存储位置。
  • size:要读取的每个元素的大小,以字节为单位。
  • nmemb:元素的个数,每个元素的大小为size字节。
  • stream:指向FILE结构的指针。

返回值:成功读取的元素总数会以size_t对象返回,size_t对象是一个整型数据类型。如果总数与nmemb参数不同,则可能发生了一个错误或者到达了文件末尾。

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
#include <stdio.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>

// 游戏设置结构体
typedef struct GameSettings {
float volume;
uint32_t resolution_width;
uint32_t resolution_height;
uint8_t difficulty;
bool fullscreen;
} GameSettings;

// 保存游戏设置到文件
void save_game_settings(const char* file_path, const GameSettings* settings);

// 从文件加载游戏设置
void load_game_settings(const char* file_path, GameSettings* settings);

int main() {
const char* file_path = "C:\\Users\\HC\\Desktop\\game_settings.dat"; // 游戏设置文件路径
GameSettings settings = { 0.75f, 1920, 1080, 2, true }; // 初始化游戏设置

// 保存游戏设置到文件
save_game_settings(file_path, &settings);
printf("Game settings saved to file: %s\n", file_path);

// 从文件加载游戏设置
GameSettings loaded_settings;
load_game_settings(file_path, &loaded_settings);
printf("Game settings loaded from file: %s\n", file_path);
printf("Volume: %.2f\nResolution: %" PRIu32 "x%" PRIu32 "\nDifficulty: %" PRIu8 "\nFullscreen: %s\n",
loaded_settings.volume,
loaded_settings.resolution_width,
loaded_settings.resolution_height,
loaded_settings.difficulty,
loaded_settings.fullscreen ? "Yes" : "No");


// 确保所有文件流已关闭
_fcloseall();

return 0;
}

// 保存游戏设置到文件
void save_game_settings(const char* file_path, const GameSettings* settings) {
FILE* file_stream = NULL;
errno_t err = fopen_s(&file_stream, file_path, "wb"); // 以二进制写入模式打开文件
if (err != 0 || file_stream == NULL) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), err);
fprintf(stderr, "Failed to open file for writing: %s\n", error_msg);
exit(EXIT_FAILURE);
}
fwrite(settings, sizeof(GameSettings), 1, file_stream);
fclose(file_stream);
}

// 从文件加载游戏设置
void load_game_settings(const char* file_path, GameSettings* settings) {
FILE* file_stream = NULL;
errno_t err = fopen_s(&file_stream, file_path, "rb"); // 以二进制读取模式打开文件
if (err != 0 || file_stream == NULL) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), err);
fprintf(stderr, "Failed to open file for reading: %s\n", error_msg);
exit(EXIT_FAILURE);
}
fread(settings, sizeof(GameSettings), 1, file_stream);
fclose(file_stream);
}

18. 复制文件

复制文件本质上就是将一个文件中的所有内容都读取出来,并写入到另一个文件中。

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 <inttypes.h>
#include <stdint.h>
#include <stdlib.h>

int main() {
const char* source_file = "C:\\Users\\HC\\Desktop\\source_file.txt"; // 源文件路径
const char* dest_file = "C:\\Users\\HC\\Desktop\\dest_file.txt"; // 目标文件路径

FILE* source_stream = NULL; // 源文件流
FILE* dest_stream = NULL; // 目标文件流
char buffer[1024]; // 缓冲区

size_t bytes_read; // 读取的字节数

// 打开源文件进行读取
errno_t err = fopen_s(&source_stream, source_file, "rb"); // 以二进制读取模式打开源文件
if (err != 0 || source_stream == NULL) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), err);
fprintf(stderr, "Failed to open source file: %s\n", error_msg);
return EXIT_FAILURE;
}

// 打开目标文件进行写入
err = fopen_s(&dest_stream, dest_file, "wb"); // 以二进制写入模式打开目标文件
if (err != 0 || dest_stream == NULL) {
char error_msg[256];
strerror_s(error_msg, sizeof(error_msg), err);
fprintf(stderr, "Failed to open destination file: %s\n", error_msg);
fclose(source_stream); // 关闭源文件流
return EXIT_FAILURE;
}

// 复制文件内容,每次尝试读取的字节数 = size × nmemb = 1 × sizeof(buffer) = 1024字节
// 返回值 bytes_read 表示实际读取的字节数
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source_stream)) > 0) {
// 每次写入的字节数 = size × nmemb = 1 × bytes_read = 实际读取的字节数
fwrite(buffer, 1, bytes_read, dest_stream);
}

// 确保所有文件流已关闭
_fcloseall();

puts("文件复制完成!");

return 0;
}

19. 第十一章结束语

多思考,多练习!

本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2026/02/03/20260203_C_file/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
旧版本Visual Studio一键下载
  1. 1. 1. 输入输出初步认识
  2. 2. 2. 复习scanf_s
  3. 3. 3. scanf_s的返回值
  4. 4. 4. stream流的概述
  5. 5. 5. 读取r模式(fopen_s、fgetc、fgets、fclose)
    1. 5.1. 5.1 按行读取
    2. 5.2. 5.2 按字符读取
    3. 5.3. 5.3 同一个文件流多次读取
  6. 6. 6. 写入w模式(fputs、fputc、fprintf_s)
  7. 7. 7. ftell、fseek、rewind
  8. 8. 8. fscanf_s
  9. 9. 9. fprintf
  10. 10. 10. ferror、feof、clearerr
  11. 11. 11. 抽离读写函数
  12. 12. 12. 追加a模式
  13. 13. 13. w模式清空
  14. 14. 14. 企业实际案例(难):修改log,r+模式
  15. 15. 15. 临时文件的方案
  16. 16. 16. fflush简单掠过
  17. 17. 17. 游戏设置案例:bin二进制文件存储与读取,wb与rb模式的使用
  18. 18. 18. 复制文件
  19. 19. 19. 第十一章结束语
© 2021-2026 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • C
  • FPGA
  • Homework
  • MATLAB
  • Server
  • Vivado
  • Git

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank