C语言之文件操作(上)
前些时候,我们学习的C语言程序都是由输入输出和算法组成的控制台程序。我们在终端上来输入我们提供的数据,然后程序也会通过终端来告诉我们最终运行的结果。
但是,可能有的同学已经观察到了,我们日常使用的别人开发的程序,大多数都是通过文件来提供数据的。比如一个Excel的报表,程序可以直接来分析里面的数据。再比如,一个TXT格式的电子书,程序可以直接分析有多少字、多少个章节,甚至还可以生成出一个目录来。
拥有这样能力的程序,是不是感觉功能强大了许多?这就要用到我们今天要讲到的内容——「文件操作」。
关于文件
在我们比较熟悉的Windows系统下,文件类型的区分是用「扩展名」来进行的。但其实扩展名并不是指「文件格式」,它只是一个「门牌号」而已。至于它到底对不对,那系统就不知道了。可能有很多的新手,在遇到格式的问题的时候,会认为直接更改扩展名,就能实现格式转换。不瞒你们说,我小时候也有过这种想法。但是后来发现,不行。举个例子,现在有一个 MP3 的文件,要转成 AAC。这两个文件从编码上来讲,就是不一样的。MP3 只能用 MP3 的方式去读取,AAC 只能用 AAC 的方式去读取。如果你把扩展名直接改成 AAC,那么系统就被你骗了,就会用 AAC 的方式去读取实际还是 MP3 的文件,当然是不行了。
不同的扩展名,就对应了不同的读取方式。「EXE」 就代表 Windows 系统下的可执行二进制文件,「TXT」是纯文本文件,等等。
在 Linux 和 Unix 操作系统下,文件的定义就宽泛多了。不光软件,硬件也可以叫文件。也就是说,硬件实际上也是当做文件的方式来处理的。
在C语言中,文件一般分为两种,一种是二进制文件,就是我们编译出来的那个东西,我们是看不懂的;另一种是文本文件,也就是我们常说的源代码。
打开和关闭文件
我们要对一个文件进行操作,首先我们需要把文件打开,然后才能读或者写。对文件操作完成后,我们还要将文件关闭。
C语言中的打开文件使用fopen
函数,通式如下:
fopen("文件路径", "模式")
如果打开文件成功,则会返回一个FILE结构的指针,通过这个指针,我们就可以对这个文件进行操作;如果打开文件失败,则会返回NULL。
下面是所有的模式:
模式 | 功能 |
---|---|
"r" | 以只读的形式打开文件,并从头开始读取 文件必须存在 |
"w" | 以只写的形式打开文件,从头开始写入 若文件不存在,则创建一个文件 若文件存在,则全部被覆盖 |
"a" | 以追加的形式打开文件,从文件末尾追加内容 若文件不存在,则创建一个新的文件 |
"r+" | 以读写的形式打开文件,从头开始读写 文件必须存在,若原本有内容,则写入的部分被覆盖 |
"w+" | 以读写的形式打开文件,从头开始读写 若文件不存在,则被创建 若文件存在,则被全部覆盖 |
"a+" | 以读取和追加的形式打开文件 若文件不存在,则创建一个新的文件 读取是从头开始,追加是从末尾开始 |
"b" | 表明打开的是二进制文件,使用时与上面的任意一个叠加 如:"wb", "r+b" |
前面几个都好理解,只是最后一个,为啥要区分一个二进制出来呢?
不加「b」的情况下,就是以文本的形式来打开。因为在不同的操作系统中,换行符是不同的。Unix系统用\n
,MacOS用\r
,而Windows用的是\r\n
,那么在文本模式下打开,C语言会根据系统环境的不同,来转化换行符。而在二进制的模式下,就不会进行任何的转换。
当你对文件操作完毕后,一定要记得把文件用fclose()
函数来关闭。其实我们在打开文件后的所有操作,实际上都被记录到了缓存里,只有执行了关闭后,我们的更改才会生效。如果关闭成功,则函数会返回0
;失败的话,就会返回EOF
。关闭成功后,我们创建的文件指针就会失效。
//Example 01
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* f;
int chr;
if ((f = fopen("file1.txt", "r")) == NULL)
{
printf("打开失败!\n");
exit(EXIT_FAILURE);
}
while ((chr = getc(f)) != EOF)
{
putchar(chr);
}
fclose(f);
return 0;
}
//file1.txt中的内容
C programming makes me happy!
//Consequence 01
C programming makes me happy!
顺序读写文件
打开了文件之后,就可以进行我们的操作了。
读写单个字符
读取单个字符,我们可以用fgetc
和getc
这两个来实现。它们的作用,就是读取一个字符,然后将光标移动到下一个位置。
#include <stdio.h>
...
int fgetc(FILE* stream);
int getc(FILE* stream);
函数的参数,是一个FILE
结构体的指针,也就是一个准备读取的文件流。读取成功就会将读取到的unsigned char
内容转化为int
并返回;文件结束或者读取失败就返回EOF
。
这俩函数不同的地方就在于,fgetc
是函数实现,而getc
是用宏实现。宏会产生大量的代码量,但是没有函数调用堆栈的步骤,所以速度会快很多。但是宏的展开可能会多次调用参数,因此如果参数中含有自增、自减这种副作用的的方法,就只能用函数实现的fgetc
了。
写入单个字符,我们可以用fputc
和putc
,带有f
的,就是函数,另一个就是宏的实现的了。
#include <stdio.h>
...
int fputc(int c, FILE* stream);
int putc(int c, FILE* stream);
第一个参数是你要写入的字符,第二个是你要写入的文件流。
读写整个字符串
这里就要用到fgets
和fputs
两个函数了。
#include <stdio.h>
...
char* fgets(char* s, int size, FILE* stream);
int fputs(const chat* s, FILE* stream);
其中,fgets
有三个参数,第一个是一个字符型指针,用来存放读取的数据;第二个用来指定读取的长度(包含'\0'
);第三个是用于指定读取的文件流。
fputs
第一个参数用于存放待写入的数据,第二个是指定待写入的文件流。函数调用成功,返回一个非 0 值,失败则返回EOF
。
格式化读写文件
在文件里,我们就不能用我们熟悉的scanf
和printf
了。但是C语言也提供一组类似的函数:fscanf
和fprintf
。
用法上,第一个参数用于指定文件流,后面的就是照搬的scanf
和printf
中的参数。
//Example 02
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
FILE* fp;
struct tm* p;
time_t t;
time(&t);
p = localtime(&t);
//写入日期到文件
if ((fp = fopen("date.txt", "w")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fprintf(fp, "%d-%d-%d", 1900 + p -> tm_year, 1 + p -> tm_mon, p -> tm_mday);
fclose(fp);
//读取文件日期,输出到终端
int year, month, day;
if ((fp = fopen("date.txt", "r")) == NULL)
{
printf("打开文件失败!\n");
exit(EXIT_FAILURE);
}
fscanf(fp, "%d-%d-%d", &year, &month, &day);
printf("%d-%d-%d\n", year, month, day);
fclose(fp);
return 0;
}
//date.txt中的内容
2020-6-15
//Consequence 02
2020-6-15