前些时候,咱们学习的C语言程序都是由输入输出和算法组成的控制台程序。咱们在终端上来输入咱们提供的数据,而后程序也会经过终端来告诉咱们最终运行的结果。算法
可是,可能有的同窗已经观察到了,咱们平常使用的别人开发的程序,大多数都是经过文件来提供数据的。好比一个Excel的报表,程序能够直接来分析里面的数据。再好比,一个TXT格式的电子书,程序能够直接分析有多少字、多少个章节,甚至还能够生成出一个目录来。数组
拥有这样能力的程序,是否是感受功能强大了许多?这就要用到咱们今天要讲到的内容——「文件操做」。缓存
关于文件在咱们比较熟悉的Windows系统下,文件类型的区分是用「扩展名」来进行的。但其实扩展名并非指「文件格式」,它只是一个「门牌号」而已。至于它到底对不对,那系统就不知道了。可能有不少的新手,在遇到格式的问题的时候,会认为直接更改扩展名,就能实现格式转换。不瞒大家说,我小时候也有过这种想法。可是后来发现,不行。举个例子,如今有一个 MP3 的文件,要转成 AAC。这两个文件从编码上来说,就是不同的。MP3 只能用 MP3 的方式去读取,AAC 只能用 AAC 的方式去读取。若是你把扩展名直接改为 AAC,那么系统就被你骗了,就会用 AAC 的方式去读取实际仍是 MP3 的文件,固然是不行了。app
不一样的扩展名,就对应了不一样的读取方式。「EXE」 就表明 Windows 系统下的可执行二进制文件,「TXT」是纯文本文件,等等。ide
在 Linux 和 Unix 操做系统下,文件的定义就宽泛多了。不光软件,硬件也能够叫文件。也就是说,硬件实际上也是当作文件的方式来处理的。函数
在C语言中,文件通常分为两种,一种是二进制文件,就是咱们编译出来的那个东西,咱们是看不懂的;另外一种是文本文件,也就是咱们常说的源代码。学习
打开和关闭文件咱们要对一个文件进行操做,首先咱们须要把文件打开,而后才能读或者写。对文件操做完成后,咱们还要将文件关闭。ui
C语言中的打开文件使用fopen
函数,通式以下:编码
fopen("文件路径", "模式")
url
若是打开文件成功,则会返回一个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'
);第三个是用于指定读取的文件流。
函数调用成功后,会返回第一个参数所指向的地址。若是读取到EOF
则eof指示器被设置。若一开始就读取到EOF
,第一个参数的内容不变,返回NULL
。若读取发生错误,则error指示器被设置,函数返回NULL
,第一个参数内容可能会被改变。
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
咱们用fopen
函数能够用二进制的方式来打开一个文件,但实际上咱们要用二进制的方式来读写,还得用相应的函数才行。
C语言提供了fread
和fwrite
两个函数来实现二进制的读取和写入。
#include <stdio.h>
...
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
首先来看fread
。这个函数有四个参数。第一个指向存放数据的地址,第二个指定读取的每一个元素的尺寸,第三个指定准备读取的元素个数,最后一个指向待读取的文件流。
函数调用成功,会返回读取到的元素个数,若是实际读取的比第三个参数小,那么可能会一直读取到文件末尾或者发生错误,这种状况就要经过foef
和ferror
来进一步判断。
而后是fwrite
,也是有四个参数。第一个是指向存放数据的地址,第二个是指定待写入的每一个元素的尺寸,第三个是指定待写入的元素的个数,最后一个是指向待写入的文件流。
刚刚咱们介绍的,都是从文件头开始读写。可是咱们实际生产生活中,不少时候咱们是须要任意修改的。好比改一个文档,颇有多是中间的什么地方错了,或者是表达有不妥。那么这个时候若是你还要从头开始去检索,那样效率就过低了。
因而,C语言也为咱们提供了这个功能,就是随机读写。
首先,咱们要了解光标的位置,才可以更好地运用这个功能。C语言为咱们提供了ftell
函数,它能够告诉咱们如今的光标位置。
#include <stdio.h>
...
long ftell(FILE* stream);
若是将一个文件当作一个数组,那么这个函数返回的就是这个数组的下标。
//Example 01
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
FILE* fp;
if ((fp = fopen("data.txt", "w")) == NULL)
{
printf("文件打开失败!\n");
exit(EXIT_FAILURE);
}
printf("%ld\n", ftell(fp));
fputc('T', fp);
printf("%ld\n", ftell(fp));
fputs("echZone\n", fp);
printf("%ld\n", ftell(fp));
fclose(fp);
return 0;
}
//data.txt中的内容
TechZone
//Consequence 01
0
1
10
若是你想将光标快速移动到文件头,能够用rewind
函数来实现。
...
rewind(fp);
fputs("Hello", fp);
fclose(fp);
...
//data.txt中的内容
Helloone
能够看到,它会覆盖咱们前面的数据。
有的同窗可能会说了,你这不仍是没解决问题吗?
好的,那就来解决下问题吧。C语言给咱们提供了一个函数fseek
,这个函数能够直接把光标跳转到咱们想要的位置。
#include <stdio.h>
...
int fseek(FILE* stream, long int offset, int whence);
第一个参数是指的咱们要读取的文件流,第二个是偏移量(日后走是正数,往前走是负数),第三个是指的开始偏移的位置。
值 | 描述 |
---|---|
SEEK_SET |
文件开头 |
SEEK_CUR |
当前位置 |
SEEK_END |
文件末尾 |
若是我要定位到第一百个字符的位置,那么:
fseek(fp, 100, SEEK_SET)
倒数第 10 个就要这样:
fseek(fp, -10, SEEK_END)标准流
通常C语言程序在执行的时候,都会有 3 个面向终端的文件流,分别是「标准输入」,「标准输出」和「标准错误输出」。咱们以前用printf
的时候,其实就是在往标准输出流中写入字符串;用scanf
的时候,其实就是函数在从标准输入流中读取字符串。固然,咱们写的程序也不可能一直都是正确的,警告和报错的状况时有发生,这个时候其实就是对标准错误输出中写入数据。
这三个流,咱们就将它们称为:「标准流」
C语言分别为这三个标准流提供了对应的文件指针:stdin
,stdout
,stderr
好比打开文件失败的时候,就能够这样显示:
...
fputs("打开文件失败!\n", stderr);
exit(EXIT_FAILURE);
...
这样就不用printf
这种“不专业”的错误指示方法了。
打开文件失败!
每一个流的内部都有两个指示器。一个是「文件结束指示器feof
」,当遇到文件末尾时被设置;另外一个是「错误指示器ferror
」,当读写文件出错时被设置。
...
if (ferror(fp))
{
fputs("出错了!\n", stderr);
}
...
而使用clearerr
能够人为地清除两个指示器的状态:
...
clearerr(fp);
...
错误指示器只能判断是否出了错误,但具体是什么错误,那就要看errno
和perror
了。
首先看errno
。这个函数包含在errno.h
这个头文件中。它会返回一个错误码。
#include <errno.h>
...
printf("打开文件失败:%d\n", errno);
...
举个例子:
打开文件失败:2
可是这个错误代码不是全部人都知道它的含义。因此C语言又提供了一个函数perror
,它能够直接用文字来提示咱们错误的地方。
#include <stdio.h>
...
perror("打开文件失败,缘由是");
...
结果是这样的:
打开文件失败,缘由是:No such file or directory
中间的冒号是自动加上的。
C语言基础内容大致到这里就结束了。咱们也终于算是入门了C语言。或许之后在你的开发生涯中,用的最多的不是C语言,但这门语言对你带来的提高,那是不可忽视的。C语言的文章自此就告一段落,之后还会写一些进阶的内容,但不会连续发布了。若是你有什么好的题材或者是问题,均可以私信提供给我,我会考虑把它们写进文章的。最后,祝各位学有所成!
来自公众号:TechZone