文件及文件操作
- 前言
- 1. 文件分类
- 1.1 文本文件
- 1.2 二进制文件
- 1.3 文本文件和二进制文件的区别
- 2. 文件的打开和关闭
- 2.1 文件指针
- 2.2 文件的打开与关闭
- 2.3 文件的打开模式(即文件打开后进行的操作)
- 2.4 文件的顺序读写及文件顺序读写函数介绍
- 2.4.1 fputc
- 2.4.2 fgetc
- 2.4.3 fputs
- 2.4.4 fgets
- 2.4.5 fprintf
- 2.4.6 fscanf
- 2.4.7 fwrite
- 2.4.8 fread
- 3. 文件的随机读写
- 3.1 fseek
- 3.2 ftell
- 4. ⽂件读取结束的判定
- 4.1 被错误使用的feof
- 5. 结语
前言
我们在使用VS书写代码的时候会发现,在每一次运行后,代码里的数据都会被清空,好像没有存在过一样,这是因为我们的代码实在内存上运行的,其数据相关的内容也会在内存上暂存,如果需要将数据保存下来,就需要储存到硬盘中,这里我们就可以运用到“文件”了,我们通过打开,读写等方式对文件进行操作,就可以将数据内容存储到文件中,即使程序运行完毕,我们的数据也不会消失,先看一个例子吧。
#include <stdio.h>
int main()
{
//打开文件
FILE* p = fopen("test.txt", "w");
if (p == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputs("C is best!", p);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
在我们运行了这段代码后,在我们本次代码的目录下会生成一个 “ test.txt ” 文件,并将 " C is best! " 写入文件中,在代码运行完毕后,这段文字也不会消失。
在这里插入图片描述
而上文只是做一个演示,下面我们具体来了解C语言中文件操作相关的内容。
1. 文件分类
在我们C语言中,文件分为二进制文件和文本文件。
文本文件:在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的⽂件就是⽂本⽂件。
二进制文件:数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的⽂件中,就是⼆进制⽂件。
1.1 文本文件
我们将数据以ASCII码的形式储存在文件中,此时我们的文件就可以称之为文本文件。
用通俗的大白话来说就是,文本文件我们是可以看得懂的。
例如
1.2 二进制文件
而二进制的文件,是将数据以二进制的形式存储到文件中,这时的文件即使我们打开了,也会发现是一段根本看不懂的乱码,这是因为二进制文件是给机器看的,而不是面向用户的。
1.3 文本文件和二进制文件的区别
在上文中我们看到的两种文件类型,那可能会有未来的资深程序猿问了,写代码就是为了给用户看的,那为何不直接全部以ASCII码的形式存储,这样都能看得懂呢?
这就不得不引入一个例子了。
例如:我们要把数字5存储到文件中。
我们能看到存储5的时候如果用ASCII的形式存储只会使用1个字节的内存,但如果使用二进制存储却会使用四个字节,确实,ASCII的形式会节省空间。
那我们如果要存储10000到文件中呢?
我们发现如果将10000以ASCII的形式存储到文件中用了五个字节的内存空间,反而比二进制的方式更多。
所以,这两种存储数据的方式并无好坏之分,区别就在于存储的数据是什么样的,如果我们能在存储数据的时候选择更加高效的存储方式,那便会提高我们的代码效率。
2. 文件的打开和关闭
在了解文件的打开和关闭时,我们需要先了解一个概念——流。
作为一个高度抽象的概念插入到我们的C语言学习中,我们可以将流理解为一个传送带,我们需要存放数据时,将数据内容放到传送带(即流)上,存储的目标就会从这条传送带上拿取数据。在获取数据时也是一样的道理。
在C语言中,我们在输入输出数据时,并不需要手动的打开流,因为在程序启动时,我们的编译器就会默认的打开三个流。
即
stdin - 标准输⼊流,在⼤多数的环境中从键盘输⼊,scanf函数就是从标准输⼊流中读取数据。
stdout - 标准输出流,⼤多数的环境中输出⾄显⽰器界⾯,printf函数就是将信息输出到标准输出流中。
•stderr - 标准错误流,⼤多数环境中输出到显⽰器界⾯。
而拥有了这三个流,我们就可以将数据通过 scanf 和 printf 进行操作了。
2.1 文件指针
文件在C语言中又是如何定义的呢,其实在我们的头文件 stdio.h 中就有文件的定义。
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
在头文件 stdio.h 中会有一个结构体类型的声明,在结构体中存放了这个文件的各种信息,这里我们先不做了解,但可以看到最后,将文件类型重新定义为了FILE。
所以我们在平时的使用中,就可以通过FILE来创建指针,代表着文件的地址,进而对文件进行一系列的操作,
2.2 文件的打开与关闭
在对文件进行操作的时候,我们需要先将文件打开,在操作完成后,也需要对文件进行关闭,并将代表文件地址的指针置为空指针,避免其成为野指针,
这就好比我们生活中需要洗漱的时候,我们要先打开水龙头,然后洗漱,最后再关闭水龙头,一步一步地进行,而良好的操作文件的习惯,也会使我们的代码更低概率的出现错误。
我们通过fopen来打开文件,通过fclose来关闭文件。
FILE* p = fopen("test.txt", "w");
↑ ↑
需要打开的文件名 打开后进行的操作
fclose(p);//再关闭文件时只需将存放文件地址的指针传入即可
↑
代表文件地址的指针
p = NULL;//对指针进行置空操作。!!良好的习惯!!
2.3 文件的打开模式(即文件打开后进行的操作)
⽂件使⽤⽅式 | 含义 | 如果指定⽂件不存在 |
---|---|---|
“r”(只读) | 为了输⼊数据,打开⼀个已经存在的⽂本⽂件 | 出错 |
“w”(只写) | 为了输⼊数据,打开⼀个⽂本⽂件 | 建⽴⼀个新的⽂件 |
“a”(追加) | 向⽂本⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
“rb”(只读) | 为了输⼊数据,打开⼀个⼆进制⽂件 | 出错 |
“wb”(只写) | 为了输出数据,打开⼀个⼆进制⽂件 | 建⽴⼀个新的⽂件 |
“ab”(追加) | 向⼀个⼆进制⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
“r+”(读写) | 为了读和写,打开⼀个⽂本⽂件 | 出错 |
“w+”(读写) | 为了读和写,建议⼀个新的⽂件 | 建⽴⼀个新的⽂件 |
“a+”(读写) | 打开⼀个⽂件,在⽂件尾进⾏读写 | 建⽴⼀个新的⽂件 |
“rb+”(读写) | 为了读和写打开⼀个⼆进制⽂件 | 出错 |
“wb+”(读写) | 为了读和写,新建⼀个新的⼆进制⽂件 | 建⽴⼀个新的⽂件 |
“ab+”(读写) | 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 | 建⽴⼀个新的⽂件 |
上面是打开文件进行操作的方式,每一次打开文件只能有一种操作方式。
值得注意的是,如果我们在读文件的时候,打开的文件并不存在,那么代码就会报错从而停止运行。
但如果是写文件的时候,如果文件不存在,那么就会在根目录下创建一个新的文件,并进行操作,但如果使用“w” 或 “wb” 进行写操作时,如果原文件中存在数据内容,那么这个操作将会将原文件中的内容清空,并进行新一轮的写操作。
其余的文件使用方式大家可以对照上表进行了解。
2.4 文件的顺序读写及文件顺序读写函数介绍
下面说的适⽤于所有输⼊流⼀般指适⽤于标准输⼊流和其他输⼊流(如⽂件输⼊流);所有输出流⼀般指适⽤于标准输出流和其他输出流(如⽂件输出流)。
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输⼊函数 | 所有输⼊流 |
fputc | 字符输出函数 | 所有输⼊流 |
fgets | ⽂本⾏输⼊函数 | 所有输⼊流 |
fputs | ⽂本⾏输出函数 | 所有输⼊流 |
fscanf | 格式化输⼊函数 | 所有输⼊流 |
fprintf | 格式化输出函数 | 所有输⼊流 |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
下面我们一个一个进行介绍
2.4.1 fputc
int main()
{
FILE* p = fopen("text.txt", "w");
if (p == NULL)
{
perror("fopen");
return 1;
}
fputc('a', p);
fclose(p);
p = NULL;
return 0;
}
我们通过将需要写入的字符和代表文件地址的指针p传给fputc函数,即可将字符输入到文件中。
2.4.2 fgetc
再上一个操作读写函数中,我们已经将字符a存入到文件中,这时我们就可以用fgetc来获取文件中的字符,并将其存储到字符型变量n中,并打印到屏幕上
int main()
{
FILE* p = fopen("text.txt", "r");
if (p == NULL)
{
perror("fopen");
return 1;
}
char n = fgetc(p);
printf("%c\n", n);
fclose(p);
p = NULL;
return 0;
}
2.4.3 fputs
我们可以将一个字符串的首地址或直接在括号内写入一个字符串,以及代表文件地址的指针传给函数fputs,进而将数据存储到文件中。
int main()
{
FILE* p = fopen("text.txt", "w");
if (p == NULL)
{
perror("fopen");
return 1;
}
fputs("C is best!",p);
fclose(p);
p = NULL;
return 0;
}
2.4.4 fgets
上一个操作符在使用的时候我们将“ C is best! ” 存入到了文件中,我们可以通过fgets函数将文件中的num-1个字符存储到字符型数组中,并将其打印出来。
int main()
{
FILE* p = fopen("text.txt", "r");
if (p == NULL)
{
perror("fopen");
return 1;
}
char arr[15] = { 0 };
fgets(arr,15, p);
printf("%s\n", arr);
fclose(p);
p = NULL;
return 0;
}
2.4.5 fprintf
在我们日常输入数据时,会将数据以指定格式输入,例如整型,浮点型等,此函数的功能就是将这些数据都一视同仁,统统格式化为字符型写入到文件之中。
int main()
{
FILE* p = fopen("text.txt", "w");
if (p == NULL)
{
perror("fopen");
return 1;
}
fprintf(p, "%d %d %s", 100, 200, "abcd");
fclose(p);
p = NULL;
return 0;
}
此时文件中的 100 和 200 已经不再是整型了,而是六个字符型。
2.4.6 fscanf
上文中我们将 100 和 200 以及 abcd存储到文件中,但文件中的数据都是字符型的,我们可以将其通过fscanf函数将其以特定格式存储到指定的参数中。例如
int main()
{
FILE* p = fopen("text.txt", "r");
if (p == NULL)
{
perror("fopen");
return 1;
}
int a = 0;
int b = 0;
char arr[10] = { 0 };
fscanf(p, "%d %d %s", &a, &b, arr);
printf("%d %d %s\n", a, b, arr);
fclose(p);
p = NULL;
return 0;
}
2.4.7 fwrite
此函数为二进制写入函数,故写入的数据会转换为二进制存储到文件中,我们可以通过fread来读取二进制文件内容。
int main()
{
FILE* p = fopen("text.txt", "wb");
if (p == NULL)
{
perror("fopen");
return 1;
}
int arr[5] = { 1,2,3,4,5 };
fwrite(arr, sizeof(int), sizeof(arr), p);
fclose(p);
p = NULL;
return 0;
}
2.4.8 fread
这个函数就可以帮助我们将上一个函数存储的信息进行读操作了,并且我们可以将它打印出来。
int main()
{
FILE* p = fopen("text.txt", "rb");
if (p == NULL)
{
perror("fopen");
return 1;
}
int arr[5] = { 0 };
fread(arr, sizeof(int), sizeof(arr), p);
for (int i = 0; i < 5; i++)
{
printf("%d ", arr[i]);
}
fclose(p);
p = NULL;
return 0;
}
到此我们就将文件操作相关的基本函数了解了大概,但还需给看官补充一些有关文件的随机读写的函数。
3. 文件的随机读写
上文中的文件操作函数,只能按照顺序对文件内容进行读写,那我们如果想要制定一个位置来读写呢?
例如:文件中存放了字符串abcde,如果按照顺序读写的话在读取a后只能读取b,那我们如何在读取到a后读取e呢?这就用到了我们下面讲的文件随机读写函数。
3.1 fseek
对于此函数,我们分别传文件的地址和对于指定位置的偏移量(offset)以及指定位置(origin)三个参数。这就要引入下表中的信息。
指定位置名 | 含义 |
---|---|
SEEK_CUR | 文件指针当前位置 |
SEEK_END | 文件指针的结尾 |
SEEK_SET | 文件指针的开头 |
那具体怎么操作呢?
int main()
{
FILE* p = fopen("example.txt", "wb");
if (p == NULL)
{
perror("fopen");
return 1;
}
fputs("This is an apple.", p);
fseek(p, 9, SEEK_SET);
fputs(" sam", p);
fclose(p);
p = NULL;
return 0;
}
我们可以看到原本p中存放了 This is an apple. 的字符串,在我们通过使用fseek函数后,将文件指针位置以文件起始位置为起点,偏移量为9进行移动,从而得到了“ n apple ”的首地址,再将 sam 存入其中,故最后文件中存放的数据就是 This is a sample 的字符串。
3.2 ftell
这个函数的功能就比较简单了,我们在存放完数据后,将p指针传给函数ftell,其返回值就代表着此时文件指针相对于文件开头的偏移量了。
int main()
{
FILE* p = fopen("example.txt", "wb");
if (p == NULL)
{
perror("fopen");
return 1;
}
fputs("This is an apple.", p);
int n = 0;
n = ftell(p);
printf("%d\n", n);
fclose(p);
p = NULL;
return 0;
}
## 3.3 rewind
此函数可以让文件指针无论身处何处,都能重置为文件的开头位置。
int main()
{
FILE* p = fopen("example.txt", "w");
if (p == NULL)
{
perror("fopen");
return 1;
}
fputs("Lili love C!", p);
rewind(p);
fputs("Stiv", p);
fclose(p);
p = NULL;
return 0;
}
代码中原本是Lili love C!,在我们完成第一次写入时,使用rewind让文件指针回到起始位置,在进行Stiv的写入。最终,文件中存储的内容就是 Stiv love C!了。
4. ⽂件读取结束的判定
4.1 被错误使用的feof
牢记:在⽂件读取过程中,不能⽤feof函数的返回值直接来判断⽂件的是否结束。
feof 的作⽤是:当⽂件读取结束的时候,判断是读取结束的原因是否是:遇到⽂件尾结束
1. ⽂本⽂件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL .
2. ⼆进制⽂件的读取结束判断,判断返回值是否⼩于实际要读的个数。
例如: fread判断返回值是否⼩于实际要读的个数。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int c; // 注意:int,⾮char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
}
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
}
5. 结语
十分感谢您观看我的原创文章。
本文主要用于个人学习和知识分享,学习路漫漫,如有错误,感谢指正。
如需引用,注明地址。