前言: 对于在c 语言中,我们通常来说运行之后就会销毁,那么运行结果就无法保留,但是我们同样可以让c语言和文件做一个有机关联,那就是对于文件的各种函数。
Tip:下面的函数定义和参数,都可以到cpulspuls网站上查找到,链接如下:
点击即可 → cplusplus.com
一.文件的打开
首先,对于读写有两种:顺序读写和随机读写。
这两者的区别在哪里我们可以往下去看,但在c语言和文件之间关联起来,首先要打开和关闭文件,而打开文件的方式同样是一个函数,关闭也一样。
在这里就不过多阐述打开和关闭函数,只做简单的说明,如果想有更深刻的了解可以到cpulspuls中查找其定义学习,也可以参照博主的另一篇文章详细学习文件操作,更有利于食用本篇博客:【C语言进阶】从入门到入土(文件操作篇)
对于文件的打开,我们用到的函数是fopen :
FILE * fopen ( const char * filename, const char * mode );
比如说我要打开我的data文本文档进行操作就是:
FILE* pf = fopen("E:\\代码库\\test 10.10\\data.txt", "r");
附录:打开文件操作表
文件使用方式 | 含义 | 如果指定文件不存在 |
---|
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 | “w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 | “a”(追加) | 向文本文件尾添加数据 | 出错 | “rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 | “wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 | “ab”(追加) | 向一个二进制文件尾添加数据 | 出错 | “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 | “w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 | “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 | “rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 | “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 | “ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
然后关闭函数就是fclose :
int fclose ( FILE * stream );
对于打开文件使用后,一定要记得关闭,并且把存储文件地址的指针置为空指针,不然会很危险:
fclose(pf);
pf = NULL;
这里就简单介绍完打开和关闭文件的代码了,如果更详细还是可以去看博主的另一篇文章哦,相信接下来看下面的代码你也可以get到这两个函数的作用。
二. 顺序读写
1.fputc 和 fgetc
在顺序读写中,我们首先要理解对于内存中的数据,可以输出也就是写到文件中,用到的函数是fputc 。而从文件中读取到数据也就是输入,用到的是fgetc 的函数。
然后我们看一下fputc 的参数和定义:
释义:将字符写入流并推进位置指示符。也就是字符写在流的内部位置指示符所指示的位置,然后由一个位置指示符自动前进。
其实意思就是这个函数是写入字符的,每次写入一个字符,然后对于指向它的指针也会移动一个位置,当下一次写入的时候,就可以写入在最后面。
参数:
int fputc ( int character, FILE * stream );
他的定义中写道,前面的int c 是字符写入流,而后面的FILE * stream 就是我们熟悉的写入位置的地址了。所以我们按照前面的打开代码再加上写入:
int main()
{
FILE* pf = fopen("E:\\代码库\\data.txt", "w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
fputc("h", pf);
fputc('e', pf);
fputc('l', pf);
fputc('l', pf);
fputc('o', pf);
fclose(pf);
pf = NULL;
return 0;
}
当我们运行起来之后控制台中没有出现东西,其实就是运行没有错误的意思,然后当我们打开我们的data.txt 文件的时候,我们可以看见,写入的hello已经在里面了:
而顺序读写有以下这些函数:
功能 | 函数名 | 适用于 |
---|
字符输入函数 | fgetc | 所有输入流 | 字符输出函数 | fputc | 所有输出流 | 文本行输入函数 | fgets | 所有输入流 | 文本行输出函数 | fputs | 所有输出流 | 格式化输入函数 | fscanf | 所有输入流 | 格式化输出函数 | fprintf | 所有输出流 | 二进制输入 | fread | 文件 | 二进制输出 | fwrite | 文件 |
在这里,什么是流呢?
其实流是一个高度抽象的概念,你可以把他理解为水流。比如说我们的文件,屏幕,网络,光盘软盘等一些外部设备,就可以由流去对其读写。在c语言中的printf 输出于屏幕,scanf 输入于键盘,而流就在这些程序之间。对于c语言来说,只要运行起来,就会打开三个流:标准输出流(stdout),标准输入流(stdin),标准错误流(stderr),这三个流也都是FILE * 的。
所以对于FILE * 的流,我们可以直接像是上面的函数一样使用,比如想要输出就可以直接输入在屏幕的stdout 。
int main()
{
fputc('h', stdout);
fputc('h', stdout);
fputc('h', stdout);
return 0;
}
然后对于读取文件上的东西的时候我们就用到fgetc 这个函数:
int fgetc ( FILE * stream );
fgetc,从流里面读取数据,返回值是int,也就是说,fgetc在文件中找到字符之后的返回值是int,我们可以用int接收再打印出来。就比如我们刚刚输入进去的hello打印回来:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return -1;
}
int ch = fgetc(pf);
printf("%c", ch);
ch = fgetc(pf);
printf("%c", ch);
ch = fgetc(pf);
printf("%c", ch);
ch = fgetc(pf);
printf("%c", ch);
ch = fgetc(pf);
printf("%c", ch);
return 0;
}
而之前我们的表格中展示写的就是fgetc 适用于使用的输入流,那么标准输入流(stdin)也就可以使用了,我们可以尝试一下:
int main()
{
int ch = fgetc(stdin);
printf("%c", ch);
ch = fgetc(stdin);
printf("%c", ch);
ch = fgetc(stdin);
printf("%c", ch);
ch = fgetc(stdin);
printf("%c", ch);
return 0;
}
这里的代码运行起来的时候,首先我们会看见光标闪烁,输入流所以我们要输入,然后在fgetc(stdin) 和printf() 读取下打印出刚刚输入的字符,这里只写了四个读取,所以输入再多,读到的也是前四个。
2.fputs 和 fgets
但是一个一个的读写是不是太慢了,所以我们后面的函数fgets 和fputs 就是文本行输入输出函数。其实使用的方法和前面的字符输入输出是一样的,函数参数可能略有不同。
fputs函数:
释义:将str 指向的C字符串写入流。函数从指定的地址(Str) 开始复制,直到到达终止空字符(‘\0’)为止。此终止空字符不会复制到流中.
从定义中我们知道,这里写入流的是字符串,所以可以节省我们对于字符写入时只能写一个的不足。
int fputs ( const char * str, FILE * stream );
在这里的参数可以看到,前面是写入的数据也就是常量字符串,后面是打开文件后出现的指针,所以也就是说前面写想输入文件的内容,后面就是写指向打开文件的FILE *的指针就可以了。
所以我们可以尝试写一个代码跑一下:
int main()
{
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fputs("hello world\n", pf);
fputs("hello china\n", pf);
fclose(pf);
pf = NULL;
}
结果显示:
然后我们再来看一下fgets :
释义:从流中获取字符串。从流中读取字符,并将它们作为C字符串存储到str 中,直到(num-1) 字符被读取,或者到达换行符或文件末尾,以先发生者为准。换行符使fgetc 停止读取,但函数认为它是有效字符,并包含在复制到str 的字符串中。
对于fgets来说,其实就是从流中获取字符串,相当于是读取其中的数据,比如文件中的数据。
参数:
char * fgets ( char * str, int num, FILE * stream );
而对于参数来说,这里的str 是什么意思呢?其实在定义中也有,说的是指向复制字符串读取的字符数组的指针,也就是一个指向读取数据的指针。我们直接用代码来说明:
这里的代码是延续上面的fputs,也就是data.txt中已经有两行数据了。
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return -1;
}
char arr[20] = { 0 };
fgets(arr, 20, pf);
printf("%s\n", arr);
fgets(arr, 20, pf);
printf("%s\n", arr);
fclose(pf);
pf = NULL;
}
所以我们看到的内容会是:
3.fprintf和fscanf
那么上面的fgets和fputs只是对于字符串的,如果是其他的类型或者说是不同类型的,就需要用到fprintf 和fscanf 函数了。
我们可以查看一下fscanf-cpulspuls中的解释和参数:
释义:从流中读取数据,并根据参数格式将它们存储到附加参数所指向的位置。附加参数应指向格式字符串中的相应格式说明符指定的类型已分配的对象。
对于fscanf 和fprintf ,也就是格式化格式化输入函数 ,就算按照某一种格式写入按照某一种格式读取。
int fscanf ( FILE * stream, const char * format, ... );
这里的参数如果看不懂,我们就可以进行对比去学习,我们可以打开scanf的说明去看一下对比一下:
实际上fscanf 就多了一个FILE * 的参数,也就是打开的文件的地址参数,所有当我们进行使用的时候,我们可以先像scanf一样写出来,然后再在前面加上一个FILE * 的参数就好了。
比如:
int main()
{
int c = 0;
scanf("%d",&c);
return 0;
}
int main()
{
int c = 0;
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fscanf(pf, "%d", &c);
return 0;
}
而fprintf 函数就是:
把格式化的数据输出到所有输出流(屏幕/文件)上
同样的我们可以先查一下fprintf - cplusplus:
释义:将按格式指向的C字符串写入流。如果格式包括格式说明符(从%开始的子序列),则格式化后的附加参数将被格式化并插入到结果字符串中,替换各自的说明符。
int fprintf ( FILE * stream, const char * format, ... );
同样经过对比之后,我们发现fprintf和printf也就是差了一个FILE *的参数,所以我们同样可以先按照printf的格式写,然后再加上参数,就可以很好的使用fprintf了。
下面是fprintf 和fscanf 的例子:
struct S
{
int n;
double d;
};
int main()
{
struct S s = { 100, 3.14 };
FILE* pf = fopen("data.txt", "w");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fprintf(pf, "%d %lf", s.n, s.d);
fclose(pf);
pf = NULL;
}
struct S
{
int n;
double d;
};
int main()
{
struct S s = {0};
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fscanf(pf, "%d %lf", &(s.n), &(s.d));
printf("%d %lf\n", s.n, s.d);
fclose(pf);
pf = NULL;
}
4.fwrite和fread
fwrite函数:
释义:将数据块写入流。从PTR指向的内存块写入计数元素数组,每个元素的大小为字节,到流中的当前位置。
实际上就是以二进制的形式写入文件中。
参数:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
如果参数读不懂,下面还有关于参数的解释:
大概含义就是:第一个ptr指向的就是要被写的数据。第二个是size,也就是写的一个元素的大小,单位是字节。第三个count就是元素个数,也就是写多少个数据。第四个就是我们熟悉的FILE * 的文件指针。
所以我们直接写一个代码:
struct S
{
int n;
double d;
char name[10];
};
int main()
{
struct S s = {100, 3.14, "zhangsan"};
FILE* pf = fopen("data.txt", "wb");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fwrite(&s, sizeof(s), 1, pf);
fclose(pf);
pf = NULL;
}
运行完之后我们打开记事本看见的是:
注意:因为以二进制写进去,二进制的写法和文本写法不尽相同,所以一些数据在文件中我们是看不懂的,但再以二进制的形式打印出来,我们就可以看懂了。比如使用下面的fread函数:
释义:从流中读取数据块。从流中读取计数元素数组,每个元素的大小为字节,并将它们存储在PTR指定的内存块中。
也就是上面是怎么写入的,现在就怎么读取回来。
参数:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
参数也和上面写入一样,所以我们直接上代码吧:
struct S
{
int n;
double d;
char name[10];
};
int main()
{
struct S s = {0};
FILE* pf = fopen("data.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return -1;
}
fread(&s, sizeof(struct S), 1, pf);
printf("%d %lf %s\n", s.n, s.d, s.name);
fclose(pf);
pf = NULL;
}
而刚刚在文本文件中看不懂的数据,在这里就看到了:
三.随机读写
那么对于上面的函数,以及顺序读写,都是需要从头开始读,一直读下去的,如果我需要读取的是某一段信息的话,怎么办呢,之后就到我们随机读写的主场了。
我们可以想像一下,我们在打开文件的时候,会常见一个FILE * 的指针指向第一个元素,那么如果可以指向的是我们想读取的那一段的话,我们不就可以实现读取某一段信息了?所以有了下面的函数。
随机读写中包含了几个函数,我们一起看一下:
1.fseek函数
首先我们可以拿出fseek 的定义看一下:
释义:重新定位流位置指示器。对于二进制模式下打开的流,新位置是通过向原点指定的参考位置添加偏移量来定义的。
也就是说,对于这个函数,可以通过偏移量,去选择它的读取位置,达到某一部分的读写。
参数:
int fseek ( FILE * stream, long int offset, int origin );
参数中,有我们熟悉的文件指针,也有两个新的参数。其中前面的offset 是偏移量,也就是我们希望从这个位置偏移到哪一个位置开始读取,这就达到了我们可以对起始读取位置的选择。然后后面的一个是源地,也就是指针所指的位置,而这里函数还给了我们三个选择的位置:
第一个是SEEK_SET ,也就是文件起始位置,这就是和顺序读写一样的起始位置。
第二个是SEEK_CUR ,意思是文件指针的当前位置,当我们使用fseek函数读取到文件某一个地方的时候,文件指针再下一次读取不会回到起始位置,而是就在那,所以有了我们这一个选择从哪个位置偏移。
第三个是SEEK_END ,意思就是文件末尾。
如果没有理解的话,我们可以在代码中理解一下:
int main()
{
FILE* pf = fopen( "data.txt","r");
if (pf == NULL)
{
perror(fopen);
return - 1;
}
fseek(pf, 2, SEEK_SET);
int ret = fgetc(pf);
printf("%c\n", ret);
fseek(pf, -2, SEEK_CUR);
ret = fgetc(pf);
printf("%c\n", ret);
return 0;
}
注意:在fgetc 函数中,返回指定流的内部文件位置指示符当前指向的字符。然后将内部文件位置指示符提前到下一个字符。
所以上面的偏移量为-2的时候读到的是b而不是a。
2.ftell函数
对于上面的fseek函数,我们可以用来读取偏移后某一处的位置,那么有时候我们不知道读到哪了怎么办,这时候我们可以使用ftell 函数:
释义:返回流的位置指示符的当前值。对于二进制流,这是文件开头的字节数。
其实这里的意思就是得到当前位置指示符的位置,然后返回一个数值,就是起始位置到当前位置的距离。也就是距离起始位置的偏移量是多少。
返回文件指针相对于起始位置的偏移量
参数:
long int ftell ( FILE * stream );
这里的参数只需要放进文件的FILE * 的指针就可以啦,返回值是long int 。
我们接着上面的代码写一下:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror(fopen);
return -1;
}
fseek(pf, 2, SEEK_SET);
int ret = fgetc(pf);
printf("%c\n", ret);
fseek(pf, -2, SEEK_CUR);
ret = fgetc(pf);
printf("%c\n", ret);
int b = ftell(pf);
printf("%d\n", b);
return 0;
}
这里打印的是2,是我们说错了吗,其实不是,当我们上一次读取的是b之后,位置指示符已经跳到了下一个也就是c处了,所以相对于起始位置a,偏移量是2。
3.rewind函数
当我们用fseek函数读取的时候,有可能走到很远的地方,然后我们需要他回来的时候,就可以使用rewind 函数:
释义:将流的位置设置为开头。将与流关联的位置指示符设置为文件的开头。
其实就是回到起始位置的意思:
让文件指针的位置回到文件的起始位置。
参数:
void rewind ( FILE * stream );
参数同样是传文件的FILE * 的指针过去,并且没有返回值,也就是说直接运行了,就等于首先了文件指针的位置回到文件的起始位置的功能了。
还是紧接上面的代码:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (pf == NULL)
{
perror(fopen);
return -1;
}
fseek(pf, 2, SEEK_SET);
int ret = fgetc(pf);
printf("%c\n", ret);
fseek(pf, -2, SEEK_CUR);
ret = fgetc(pf);
printf("%c\n", ret);
rewind(pf);
ret = fgetc(pf);
printf("%c\n", ret);
return 0;
}
所以这里就很清楚的知道说是回到了起始位置,所以打印的结果是a。
四.读取函数结束标志
如果我们一直用fgetc 函数读,什么时候会停止呢,这时候我们看定义就会发现,当读完了读到的是空的时候,返回的值就是EOF 了。比如下面的代码:
int main()
{
FILE* pf = fopen("data.txt", "r");
if (NULL == pf)
{
perror("fopen");
return -1;
}
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
return 0;
}
对于上面的其他函数也可以同理得到哦,可以接收函数调用的返回值,然后去判断他是否读完整个文件。比如fgets 函数的读取结束再读取返回的是字符指针。还有fscanf ,遇到错误或者文件末尾,也有返回EOF的情况。对于fread 也有返回值是一个size_t,如果你想读5个但是返回的是比你小的数字,说明已经读完了。
五.文件结束的判定
但是!在文件读取过程中,不能用feof 函数的返回值直接用来判断文件的是否结束。还有ferror 函数也是!
int feof ( FILE * stream );
对于feof 函数,是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
int ferror ( FILE * stream );
对于ferror 函数,是应用于文件读取结束,判断是不是遇到错误后而读取结束的。
就好像上面说的读取结束的标志,我们总结出来是这样子的:
- 文本文件读取是否结束,判断返回值是否为EOF (fgetc),或者NULL(fgets)。
例如: fgetc判断是否为EOF. fgets判断返回值是否为NULL.
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如: fread判断返回值是否小于实际要读的个数。
总结来说就是:
feof 的用途:是文件读取结束了,判断是不是遇到文件末尾而结束的 ferror 的用途:文件读取结束,判断是不是遇到错误后而读取结束的
本篇文章就到这里,如果感觉对你有帮助,不妨点个赞。
|