1. 文件定义
文件分为程序文件和数据文件。本文讨论的是数据文件。
1.1 程序文件
程序文件包括:源程序文件(后缀为.c),目标文(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
1.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
1.3 文件名
一个文件要有唯一的文件标识,以便用户进行识别。文件名包含三部分:文件路径+文件名主干+文件后缀。
例如:C:\code\text.txt 为了方便起见,文件标示被称为i文件名。
2.文件操作
为了能够将我们输入的数据永久的存储起来,C语言提供了一系列的库函数,帮助我们来完成数据的永久存储与读取工作。
2.1 文件指针
顾名思义:指向文件的指针。
指针类型为FILE
FILE * pf;
当我们想要打开文件时,OS会根据我们提供的文件标识,去硬盘里将文件部分信息加载到内存中,并且声明一个FILE类型的文件信息区,存取当前文件的各种信息。并且会返回给用户一个指向该文件信息区的指针pf,用户通过pf对文件进行各种操作。
如图:每个指针变量都指向各自的文件。
2.2 文件打开和关闭
和动态内存开辟相似,用户在对文件进行操作时,首先要打开文件,结束操作后,要将文件关闭。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
打开方式如下:
文件使用方式 | 含义 | 如果指定文件不存在 |
“r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 建立一个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
“ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 建立一个新的文件 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
文件打开示例:用只读的方式打开一个不存在的文件(注意要用\\进行转义)
以"w"的方式打开,如果文件不存在会创建一个text.txt文件。
3. 文件顺序读写
打开文件后我们就可以对文件进行操作,下面介绍顺序读写的一些函数。
功能 | 函数名 | 适用于 |
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
3.1 输入输出函数对比
流:为了便于简化操作,封装出流的概念,流是一个高度的抽象,表示着数据流动。有了这个概念后,我们就可以统一的对各种设备进行IO,而防止差异性。
3.1.1 fgetc和fputc
int fgetc( FILE *stream );
从流中读一个字符,函数返回读到的字符。返回EOF代表输入错误或者读到文件末尾。
int fputc( int c, FILE *stream );
写一个字符到流中,返回EOF代表输出错误。
示例1:在键盘上输入,在屏幕上输出
示例2:将a~z写入文件中。
示例3:将文件中的信息读出来,输出到屏幕上。
3.1.2 fgets和fputs
char *fgets( char *string, int n, FILE *stream );
从流中读取n个数据到字符指针中,返回值为NULL,意味着文件结束,或者错误。
int fputs( const char *string, FILE *stream );
把字符串的内容输出到流中。
示例1:向文件中写入两个字符串
示例2:从文件中读取一个字符串(读取n-1个),要留一个位置给‘\0’,读到\n就停止了
3.1.3 scanf和printf
格式化的输入输出函数
int scanf( const char *format [,argument]… );
int printf( const char *format [, argument]… );
比较熟悉,不再讲解。
3.1.4 fscanf 和fprintf
针对所有输入流的格式化输入函数
int fscanf( FILE *stream, const char *format [, argument ]… );
针对所有输出流的格式化输出函数
int fprintf( FILE *stream, const char *format [, argument ]…);
用法和scanf和printf极其类似。
示例1:将结构体数据输出到文件中。
示例2:将数据从文件读到结构体中。
3.1.5 sscanf和sprintf
把一个字符串转化成格式化的数据
int sscanf( const char *buffer, const char *format [, argument ] … );
把一个格式化的数据转化成字符串
int sprintf( char *buffer, const char *format [, argument] … );
下面是使用示例:
3.1.6 fread和fwrite
从buf中写count个大小为size的数据到流中。返回值为写入的数量。
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
从流中读取count个大小为size的数据到buf,返回值为读出的数量
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
示例1:将数据写入文件中
示例2:将文件中的数据读出来。
4. 文件随机读写
4.1 fseek
根据文件指针的位置和偏移量来定位文件指针。
int fseek ( FILE * stream, long int offset, int origin );
origin有三种格式:SEEK_CUR当前位置 SEEK_END末尾位置 SEEK_SET开始位置。
示例:调整pf的指针位置
4.2 ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
示例:读取了两个字符,偏移量应为2。
4.3 rewind
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
示例:让文件指针回到起始位置
5. 文本文件和二进制文件
根据数据的组织形式,数据被分为文本文件和二进制文件。数据在内存中以二进制的方式进行存储,如果不加以转换存储到外存,就是二进制文件。如果在存储前转化成ASCII码的形式,就是文本文件。
其中:字符一律以ASCII码形式存储,数值型数据既可以以ASCII码形式存储,也可以用二进制形式存储。
示例:
6. 文件读取结束的判定
在判断文件是否读取结束时,不能单单判断文件是否读取到末尾结束,还应该判断文件是否读取出错而结束。
- 文本文件是否读取结束判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
- 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数(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); }
要先用ferror判断文件是否错误结束,在判断是否读到末尾正常结束。
二进制文件示例:
#include <stdio.h> enum { SIZE = 5 }; int main(void) { double a[SIZE] = {1.,2.,3.,4.,5.}; FILE *fp = fopen("test.bin", "wb"); // 必须用二进制模式 fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组 fclose(fp); double b[SIZE]; fp = fopen("test.bin","rb"); size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组 if(ret_code == SIZE) { puts("Array read successfully, contents: "); for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]); putchar('\n'); } else { // error handling if (feof(fp)) printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) { perror("Error reading test.bin"); } } fclose(fp); }
同样需要判断文件是否正常结束或是异常结束
7. 文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
通俗来讲文件写入磁盘时,要先写入输出缓冲区,然后再写入磁盘;读入数据时,先从磁盘读到输入缓冲区,再读到内存中。
简单证明示例:
#include <stdio.h> #include <windows.h> //VS2013 WIN10环境测试 int main() { FILE*pf = fopen("test.txt", "w"); fputs("abcdef", pf);//先将代码放在输出缓冲区 printf("睡眠10秒-已经写数据了,打开test.txt文件,发现文件没有内容\n"); Sleep(10000); printf("刷新缓冲区\n"); fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘) //注:fflush 在高版本的VS上不能使用了 printf("再睡眠10秒-此时,再次打开test.txt文件,文件有内容了\n"); Sleep(10000); fclose(pf); //注:fclose在关闭文件的时候,也会刷新缓冲区 pf = NULL; return 0; }
结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文
件。
如果不做,可能导致读写文件的问题
以上是文件相关知识介绍,后续还会继续更新,如有问题,恳请大佬指点💖