C_文件

C_文件I/O

在 C 语言中, (stream) 表示任意输入的源或任意输出的目的地。流是一个抽象的概念,即可以表示存储硬盘上的文件,也可以表示网络端口或者打印设备。

Linux哲学:一切皆文件

文件缓冲

操作系统会在内存上为流设置缓冲区。

文件缓冲

缓冲区是以先进先出的方式管理数据的。缓冲区分为三种类型:

  • 满缓冲:当缓冲区空时,从输入流中读取数据;当缓冲区满时,向输出流中写入数据。

  • 行缓冲:每次从输入流中读取一行数据;每次向输出流中写入一行数据 (stdin,stdout)。

  • 无缓冲:顾名思义,就是没有缓冲区 (stderr)。

文件指针

在C语言中,文件指针是用来处理文件读写操作的一种特殊类型的指针。在使用文件指针之前,需要先定义一个指向 FILE 类型的指针变量,用于表示文件指针。 FILE 类型定义在 stdio.h 头文件中。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct
{
short level; // 缓冲区"满"或者"空"的程度
unsigned flags; // 文件状态标志
char fd; // 文件描述符
unsigned char hold; // 如无缓冲区不读取字符
short bsize; // 缓冲区的大小
unsigned char* buffer; // 数据缓冲区的位置
unsigned ar; // 指针,当前的指向
unsigned istemp; // 临时文件,指示器
short token; // 用于有效性的检查
}FILE;

C语言中对文件的各种操作都是基于文件指针来实现的,每操作一个文件都应该让其对应一个文件指针

标准流

C 语言对流的访问是通过文件指针实现的,类型为 FILE* 。并且在 <stdio.h> 头文件中提供了 3 个标准流。这 3 个标准流可以直接使用

文件指针 默认函数
stdin 标准输入 scanf
stdout 标准输出 printf
stderr 标准错误 fprintf

在进行文件操作的时候,定义文件指针的方式如下:

1
FILE *filePointer;

文件文本和二进制文本

C 语言支持两种类型的文件:文本文件和二进制文件。文本文件中存储的是字符数据,人类是可以看懂的;二进制文件中的数据,人类是看不懂的。

文本文件具有两个独特的性质:

  • 文本文件有行的概念:文本文件被划分为若干行,并且每一行的结尾都以特殊字符进行标记。在 Windows 系统中,是以回车符和换行符 (\r\n) 进行标记的;在 Unix和 Mac 系统中是以换行符 (\n) 标记的。
  • 文本文件可能包含一个特殊的文件末尾标记:操作系统允许在文本文件的末尾使用一个特殊的字节作为标记。

文本形式可以方便人类阅读和编辑;二进制形式可以节省空间,并且转换效率高。

打开/关闭文件

fopen 函数

fopen函数的原型如下:

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

参数:

  • filename:指定要打开的文件名,需要加上路径(相对、绝对路径)
  • mode:指定文件的打开模式

返回值:

  • 成功:返回指向打开文件的文件指针
  • 失败:返回 NULL

关于fopen函数第二个参数mode对应的文件打开模式如下表( b 是二进制模式的意思)

打开模式 含义
rrb 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错)
wwb 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
aab 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件
r+rb+ 以可读、可写的方式打开文件(不创建新文件)
w+wb+ 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件)
a+ab+ 以追加方式打开文件,打开文件并在末尾更改文件,若文件不存在则创建文件

写模式和追加模式是不一样的。如果文件存在,写模式会清空原有的数据,而追加模式会在原有数据的后面写入新的内容。

fclose 函数

fclose 可以关闭程序不再使用的文件,函数原型如下::

  • 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
  • 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败
1
int fclose(FILE * stream);

参数:

  • 接受一个文件指针 stream,用于指定要关闭的文件。将缓冲区中的数据写回到文件中,并释放与文件相关的资源。

返回值:返回一个整数值来指示关闭操作的成功与否。

  • 成功:关闭文件,返回0
  • 失败:返回非零值

读/写文件

fgetc/fputc , fgets/fputsfscanf/fprintf 是用来读写文本文件,fread/fwrite 是用来读写二进制文件。

输入与输出的标准函数

读文件

fscanf 用于从文件中读取格式化数据,类似于从键盘输入的 scanf 函数。从指定的文件指针读取数据,并根据提供的格式字符串将其存储在变量中。函数原型如下:

1
int fscanf(FILE *stream, const char *format, ...);

参数说明:

  • FILE *stream: 文件指针,指向要读取的文件。
  • const char *format: 格式化字符串,定义如何读取数据(例如 %d%f%s 等)。
  • ...: 可变参数,存储读取数据的变量地址。

返回值:

  • 成功时,返回成功读取的项数。
  • 如果到达文件末尾,返回 EOF

不同的是, scanf 是从标准输入(stdin)中读取数据,而 fscanf 可以从任何一个流中读取数据。当 fscanf 的第一个参数为 stdin 时,效果等价于 scanf ( sscanf 可以从字符串中读取数据)。

  • fscanf 的读取受文件内容和格式字符串的匹配影响,格式不匹配会导致读取失败。
  • 遇到空格、换行或制表符时,fscanf 会自动忽略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
FILE *file = fopen("data.txt", "r");
if (file == NULL) {
printf("无法打开文件。\n");
return 1;
}

int id;
char name[50];
float score;

while (fscanf(file, "%d %s %f", &id, name, &score) != EOF) {
printf("ID: %d, 姓名: %s, 分数: %.2f\n", id, name, score);
}

fclose(file);
return 0;
}

写文件

fprintfprintf 类似,是用来进行格式化输出的,

1
int fprintf(FILE *stream, const char *format, ...);

参数说明:

  • FILE *stream: 文件指针,指向要写入的文件。
  • const char *format: 格式化字符串,定义如何输出数据。
  • ...: 可变参数,指定要写入的数据。

返回值:

  • 成功时,返回写入字符的总数。
  • 失败时,返回负值。

不同的是, printf 始终是向标准输出( stdout )写入内容的,而 fprintf 可以向任何一个输出流中写入内容。当 fprintf 的第一个参数为 stdout 时,效果等价于 printf ( sprintf 可以将内容写入到一个字符数组中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
FILE *file = fopen("output.txt", "w");
if (file == NULL) {
printf("无法创建文件。\n");
return 1;
}

int id = 101;
char name[] = "张三";
float score = 95.5;

fprintf(file, "ID: %d, 姓名: %s, 分数: %.2f\n", id, name, score);

fclose(file);
printf("数据已成功写入文件。\n");
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
typedef struct{
char name[25];
int age;
char gender;
}Student_t;

int main(int argc, char** argv) {
if (argc != 2) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
Student_t s = { "xixi", 18, 'f' };
FILE* fp = fopen(argv[1], "w");
if (fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
   // 序列化
fprintf(fp, "%s %d %c", s.name, s.age, s.gender);
fclose(fp);
fp = fopen(argv[1], "r");
if (fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
Student_t t;
   // 反序列化
fscanf(fp, "%s %d %c", t.name, &t.age, &t.gender);
fclose(fp);
return 0;
}

按照字符读写文件

读文件

fgetc是一个C标准库函数,用于从文件中读取一个字符。函数原型如下:

1
2
3
#include <stdio.h>
int fgetc(FILE *stream);
int getc(FILE *stream);

参数:

  • stream:文件指针,对应要写入字符的文件

返回值:

  • 成功:返回写入的字符
  • 失败/文件末尾:返回 EOF(end of file)

fgetcgetchargetc 的区别:

  1. int fgetc(FILE *stream): 从指定的文件流 stream 中读取下一个字符,并将其作为 unsigned char 类型返回。如果到达文件末尾或发生读取错误,则返回 EOF
  2. int getchar(): 从标准输入流中读取一个字符,并将其作为 unsigned char 类型返回。与 fgetc(stdin) 是等价的。
  3. int getc(FILE *stream): 从指定的文件流 stream 中读取下一个字符,并将其作为 unsigned char 类型返回。与 fgetc 几乎完全相同,只是可以被实现为宏,并且通常比 fgetc 更高效。

三个函数的返回值都是 int 类型,以便能够返回读取的字符或 EOF (End of File)。

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()
{
FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

char ch;
while ((ch = fgetc(fp)) != EOF)
{
printf("%c", ch);
}
printf("\n");
fclose(fp);

return 0;
}

写文件

fputc是一个C标准库函数,用于将一个字符写入到文件中。函数原型如下:

1
2
3
#include <stdio.h>
int fputc(int character, FILE *stream);
int putc(int character, FILE *stream);

参数:

  • character:要写入的字符,注意参数是整形
  • stream:文件指针,对应要写入字符的文件

返回值:

  • 成功:返回写入的字符
  • 失败:返回 EOF(end of file)

fputcputchar 类似。不同的是 putchar 只能向标准输出流(stdout)中写入字符,而 fputc 可以向任意一个流中写入字符。

在标准C库中,putc函数实际上是一个宏,而不是一个真正的函数。功能是相同的。

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

int main()
{
char* buf = "One world one dream!";
FILE* fp = fopen("example.txt", "w");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

int len = strlen(buf);
for (int i = 0; i < len; ++i)
{
int ch = fputc(buf[i], fp);
printf("%c", ch);
}
printf("\n");
fclose(fp);

return 0;
}

fcopy.c

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(int argc, char** argv) {
   // copy src dest
if (argc != 3) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
// 打开文件流
FILE* source_fp = fopen(argv[1], "r");
if (source_fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}

FILE* dest_fp = fopen(argv[2], "w");
if (dest_fp == NULL) {
printf("Cannot open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}   
// 复制文件
int c;
while ((c = fgetc(source_fp)) != EOF)
fputc(c, dest_fp);
   // 关闭文件流
fclose(source_fp);
fclose(dest_fp);
return 0;
}

按照行读写文件

读文件

fgets是一个C标准库函数,用于从文件中读取一行字符。函数原型如下:

1
2
#include <stdio.h>
char *fgets(char *string, int size, FILE *stream);

参数:

  • string:字符指针,用于存储读取的字符
  • size:指定要读取的最大字符数(包括终止符)
  • stream:文件指针,用于指定要从中读取字符的文件

返回值:

  • 成功:返回参数string的首地址
  • 失败:返回NULL

从输入流 stream 中,最多读取 size-1 个字符,并把读取的字符存入 string 指向的字符数组中。 fgets 遇到换行符 \n,或者文件的末尾就会终止(也就是说,读取的字符数可能不足 size-1 个),并且会存储换行符 \nfgets 会在最后添加空字符\0

fgetsgets 的通用版本,可以从任意输入流中读取数据,而 gets 只能从stdin 中读取数据。 fgets 也比 gets 更为安全,因为它限制了读取字符的最大数目(siez-1)。此外,如果 fgets 是因为读取了换行符而终止,那么它会存储换行符 \n ,而 gets 函数从来不会存储换行符。

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()
{
char line[100]; // 假设一行最多100个字符

FILE* fp = fopen("example.txt", "r");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return 1;
}

while (fgets(line, sizeof(line), fp) != NULL)
{
printf("Read line: %s", line);
}
fclose(fp);

return 0;
}

写文件

fputs是一个C标准库函数,用于将字符串写入文件。函数原型如下:

1
2
#include <stdio.h>
int fputs(const char *string, FILE *stream);

参数:

  • string:字符串的指针,表示要写入的内容
  • stream:文件指针,用于指定要写入字符的文件

返回值:

  • 成功:返回一个非负值
  • 失败:返回EOF(end of file)

fputsputs 的通用版本,可以将字符串写入到任意的输出流中,而 puts 只能写入到 stdout 中。此外, fputs 是原样输出字符串,而 puts 会在字符串后面而外输出一个换行符 \n 。在每个字符串之间添加换行符或其他分隔符,需要在调用fputs之后手动添加。

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()
{
const char* string = "Hello, world!";
FILE* fp = fopen("example.txt", "w");
if (fp == NULL)
{
printf("Failed to open the file.\n");
return -1;
}

if (fputs(string, fp) == EOF)
{
printf("Failed to write the string.\n");
return -1;
}
fclose(fp);

return 0;
}

fcopy.c

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
int main(int argc, char** argv) {
// copy src dest
if (argc != 3) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
   // 打开文件流
FILE* source_fp = fopen(argv[1], "r");
if (source_fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
FILE* dest_fp = fopen(argv[2], "w");
if (dest_fp == NULL) {
printf("Cannot open %s\n", argv[2]);
fclose(source_fp);
exit(EXIT_FAILURE);
}
// 复制文件
char str[1024];
while (fgets(str, sizeof(str), source_fp) != NULL)
fputs(str, dest_fp);
   // 关闭文件流
fclose(source_fp);
fclose(dest_fp);
return 0;
}

按照块读写文件

freadfwrite 主要是用来处理二进制文件的。还可以用于序列化和反序列化过程中。

读文件

fread是一个C标准库函数,用于从文件中以二进制形式读取数据。函数原型如下:

1
2
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

参数:

  • ptr:指向用于存储读取数据的缓冲区的指针
  • size:每个元素的字节数(以字节为单位)
  • count:要读取的元素数量
  • stream:文件指针,用于指定要从中读取数据的文件

返回值:

  • 成功:返回实际读取的元素数量
  • 失败/文件末尾:返回的元素数量与count不相等,可以通过feofferror函数来判断,到底是读到了文件末尾,还是发生了错误。

fread函数会从文件中读取指定数量的元素,每个元素占据size个字节,将它们存储在ptr指向的缓冲区中。

写文件

fwrite是一个C标准库函数,用于以二进制形式将数据写入文件。函数原型如下:

1
2
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);

参数:

  • ptr:指向要写入数据的指针
  • size:要写入的每个元素的字节数(以字节为单位)
  • count:要写入的元素数量
  • stream:文件指针,用于指定要写入数据的文件

返回值:

  • 成功:返回写入的元素数量,即count的值
  • 失败:返回一个小于count的值

fwrite函数会将指针ptr指向的数据写入到文件中。写入的总字节数是sizecount相乘的积。

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
//序列化/反序列化
typedef struct{
char name[25];
int age;
char gender;
}Student_t;

int main(int argc, char** argv) {
if (argc != 2) {
printf("Error: invalid arguments\n");
exit(EXIT_FAILURE);
}
Student_t s = { "xixi", 18, 'f' };
FILE* fp = fopen(argv[1], "wb");
if (fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
   // 序列化
fwrite(&s, sizeof(Student_s), 1, fp);
fclose(fp);
fp = fopen(argv[1], "rb");
if (fp == NULL) {
printf("Cannot open %s\n", argv[1]);
exit(EXIT_FAILURE);
}
Student_t t;
// 反序列化
fread(&t, sizeof(Student_s), 1, fp);
fclose(fp);
return 0;
}

文件定位

每个流都有相关联的文件位置。在执行读写操作时,文件位置会自动推进,并按照顺序访问文件。<stdio.h> 提供了几个函数来支持:

1
2
3
int fseek(FILE* stream, long int offset, int whence);
long int ftell(FILE* stream);
void rewind(FILE* stream);

fseek 函数

fseek 函数用于将文件指针移动到文件中的指定位置。函数原型如下:

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

参数说明

  • stream:文件指针,指向已打开的文件。

  • offset:偏移量,表示相对于 whence 的位置的偏移量(可以是正值、负值或零)。

  • whence:定位起始点,取值如下:

  • SEEK_SET:文件的开头

  • SEEK_CUR:当前文件指针位置

  • SEEK_END:文件的末尾

返回值

​ • 成功时返回 0,失败时返回非零值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

// 将文件指针移动到文件开头的第 10 个字节位置
if (fseek(file, 10, SEEK_SET) == 0) {
printf("文件指针移动到文件开头的第 10 个字节。\n");
} else {
perror("fseek 失败");
}

fclose(file);
return 0;
}

ftell 函数

ftell 函数用于获取当前文件指针的位置。函数原型如下:

1
long ftell(FILE *stream);

参数说明

  • stream:文件指针,指向已打开的文件。

返回值

  • 返回文件指针的当前偏移量(相对于文件开头),失败时返回 -1L。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

// 获取当前文件指针位置
long position = ftell(file);
if (position != -1L) {
printf("当前文件指针位置:%ld\n", position);
} else {
perror("ftell 失败");
}

fclose(file);
return 0;
}

rewind 函数

rewind 函数用于将文件指针移动到文件的开头。函数原型如下:

1
void rewind(FILE *stream);

参数说明

  • stream:文件指针,指向已打开的文件。

返回值

  • 无返回值。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
FILE *file = fopen("example.txt", "r");
if (file == NULL) {
perror("无法打开文件");
return 1;
}

// 将文件指针移动到文件末尾
fseek(file, 0, SEEK_END);
printf("文件指针已移动到文件末尾。\n");

// 使用 rewind 将文件指针移动到文件开头
rewind(file);
printf("文件指针已移动到文件开头。\n");

fclose(file);
return 0;
}

总结

  • fseek:用于将文件指针移动到指定位置。

  • ftell:用于获取当前文件指针的位置。

  • rewind:将文件指针移动到文件开头。

错误处理

C 语言往往是通过函数的返回值,或者是测试 errno 变量来检测错误的;并且需要程序员自己编写代码来处理错误。

errno的简介

errno是C语言标准库中定义的全局变量,用于指示最近发生的错误。该变量由头文件<errno.h>声明,并在出现错误时由系统函数进行设置。errno的初始值为零,每当系统调用或库函数出现错误时,它会被设置为相应的错误代码。

常用的errno错误码

errno变量通常与各种错误码配合使用,错误码预定义在<errno.h>中。以下是常见的错误码:

错误码 含义
EPERM 操作不允许
ENOENT 文件或目录不存在
ESRCH 无效的进程
EINTR 被中断的系统调用
EIO 输入/输出错误
ENOMEM 内存不足
EACCES 权限被拒绝

使用errno的步骤

步骤1:包含必要的头文件

1
2
3
#include <stdio.h>
#include <errno.h>
#include <string.h>

步骤2:清除 errno

errno 是一个全局变量,默认不会被自动清零。建议在调用可能发生错误的函数之前,手动将 errno 设置为 0,以便准确检测函数调用是否发生错误:

1
errno = 0;

步骤3:在函数调用后检测错误

函数调用或系统调用后,可以通过检查errno是否被设置为非零值来判断是否发生了错误。

1
2
3
4
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
printf("Error opening file: %s\n", strerror(errno));
}

如果fopen函数失败,errno会被设置为相应的错误码,并且strerror(errno)将返回描述错误的字符串。

错误处理函数

perror()函数

perror()函数可以用来打印带有错误信息的描述,用于根据当前 errno 的值打印错误消息。perror 会打印一条包含自定义前缀和错误消息的完整错误信息:

示例代码:

1
2
3
4
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
perror("File opening failed");
}

输出结果: perror 自动将 errno 的值转换为错误消息,并将自定义前缀(如 “File opening failed”)一起打印。

1
File opening failed: No such file or directory

strerror()函数

strerror()函数根据错误码返回一个描述错误的字符串。示例如下:

1
2
3
4
FILE *file = fopen("nonexistent.txt", "r");
if (file == NULL) {
printf("Error opening file: %s\n", strerror(errno));
}

注意事项

  • 在每次调用可能出错的函数前,建议将errno设置为0,以便区分新错误与之前的错误。
  • 并非所有函数都会设置errno,因此仅在函数说明中明确表示会设置errno的函数中使用它。
  • 在处理多个函数调用时,及时检查并记录 errno,以免其值被后续的函数调用覆盖。
  • 使用 perror 和 strerror 提供更友好的错误信息。

示例:处理文件操作错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
FILE *file = fopen("nonexistent.txt", "r");

if (file == NULL) {
printf("Error code: %d\n", errno);
printf("Error message: %s\n", strerror(errno));
return 1; // 返回非零表示错误
}

// 如果成功打开文件,执行文件操作
printf("File opened successfully\n");
fclose(file);

return 0;
}

总结

  • errno 是一个全局变量,用于指示最近一次错误发生的类型。
  • 使用 perror()strerror() 可以将 errno 变量值转换为可读的错误信息。
  • 始终检查函数返回值,并在必要时处理错误。