【C初阶】文件操作
目录
1. 为什么使用文件?
2. 什么是文件?
2.1 程序文件
2.2 数据文件
2.3 文件名
3. 二进制文件和文本文件
4. 文件
4.1 流和标准流
4.1.1 流
4.1.2 标准流
4.2 文件指针
4.3 文件的打开和关闭
4.3.1 fopen
4.3.2 fclose
5. 文件的顺序读写
5.1 fputc
5.2 fgetc
5.3 feof 和 ferror
5.4 fputs
5.5 fgets
5.6 fprintf
5.7 fscanf
5.8 fwrite
5.9 fread
6. 文件的随机读写
6.1 fseek
6.2 ftell
6.3 rewind
7. 文件缓冲区
7.1 fflush
8. 更新文件
1. 为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存收回,数据就丢失了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们就可以使用文件。
2. 什么是文件?
磁盘(硬盘)上的文件是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件,数据文件(从文件功能的角度来分类的。)。
2.1 程序文件
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀.exe)。
2.2 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
在以前各章所处理的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
2.3 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:G:\c++\no free c 59.9\code_exercise\course-code-writing-exercise\test_8_26
为了方便起见,文件标识常被称为文件名。
3. 二进制文件和文本文件
根据数据的组织形式,数据文件被分为:文本文件和二进制文件。
数据在内存中以二进制的形式储存,如果不加转换的输出到外存的文件中,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在储存前转换,以ASCII字符的形式储存的文件就是文本文件。
直观感受一下。
int main()
{int a = 1000;FILE* pf = fopen("test.txt", "wb");fwrite(&a, 4, 1, pf);fclose(pf);pf = NULL;return 0;
}
就是这样操作,就可以看到二进制序列。
4. 文件
4.1 流和标准流
4.1.1 流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同外部设备的输入输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。
C程序针对文件、画面、键盘灯的数据输入输出操作都是通过流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流
那为什么我们从键盘输入数据,向屏幕上输出数据并没有打开流呢?
那是因为C语言程序在启动的时候默认打开3个流:
- stdin - 标准输入流,大多数的环境中从键盘输入,scanf函数就是从标准输入流中读取数据。
- stdout - 标准输出流,大多数环境中输出值显示器界面,printf函数就是将信息输出到标准输出流中。
- stderr - 标准错误流,大多数环境中输出到显示器界面。
这是默认打开了着三个流,我们使用的scanf、printf函数就可以直接进行输入输出操作。
stdin、stdout、stderr三个流的类型是:FILE*,通常称为文件指针。
C语言中,就是通过FILE*的文件指针来维护流的各种操作的。
4.2 文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息取,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等),这些信息是保存在一个结构体变量中的,该结构体类型是由系统声明的,取名FILE。
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会更具文件的情况自动创建一个FILE结构的变量,并在其中填充信息,使用着不必关注细节。
一般情况下,通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
4.3 文件的打开和关闭
文件在读写之前需要先打开文件,使用结束后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSI C规定使用fopen函数来打开文件,fclose来关闭文件。
4.3.1 fopen
代码块
FILE * fopen ( const char * filename, const char * mode );
功能:fopen函数是用来打开参数filename指定的文件,同时将打开的文件和一个流进行关联,后续对流的操作通过fopen函数返回的指针来维护。具体对流(关联文件)的操作是通过参数mode来指定的。
参数:
filename:表示被打开的文件的名字,这个名字可以是绝对路径,也可以是相对路径。
mode: 表示对打开文件的操作方式,具体见下面的表格。
返回值:
- 若文件成功打开,该函数返回一个指向FILE对象的指针,该指针可用于后续操作中对应的流。
- 若打开失败,则返回NULL指针,所以一定要对fopen的返回值做判断。
在进行代码演示前,我们看一看mode指的是什么?
⽂件使⽤⽅式 | 含义 | 如果指定⽂件不存在 |
“r”(只读) | 为了输⼊数据,打开⼀个已经存在的⽂本⽂件 | 出错 |
“w”(只写) | 为了输出数据,打开⼀个⽂本⽂件 | 建⽴⼀个新的⽂件 |
“a”(追加) | 向⽂本⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
“rb”(只读) | 为了输⼊数据,打开⼀个⼆进制⽂件 | 出错 |
“wb”(只写) | 为了输出数据,打开⼀个⼆进制⽂件 | 建⽴⼀个新的⽂件 |
“ab”(追加) | 向⼀个⼆进制⽂件尾添加数据 | 建⽴⼀个新的⽂件 |
“r+”(读写) | 为了读和写,打开⼀个⽂本⽂件 | 出错 |
“w+”(读写) | 为了读和写,建议⼀个新的⽂件 | 建⽴⼀个新的⽂件 |
“a+”(读写) | 打开⼀个⽂件,在⽂件尾进⾏读写 | 建⽴⼀个新的⽂件 |
“rb+”(读写) | 为了读和写打开⼀个⼆进制⽂件 | 出错 |
“wb+”(读写) | 为了读和写,新建⼀个新的⼆进制⽂件 | 建⽴⼀个新的⽂件 |
“ab+”(读写) | 打开⼀个⼆进制⽂件,在⽂件尾进⾏读和写 | 建⽴⼀个新的⽂件 |
代码演示
int main()
{// . -- 表示当前路径// .. -- 表示上一级路径FILE* pf = fopen(".data.txt", "w");// 这里的路径是相对路径,表示在当前工程目录下的data.txtif (pf == NULL){perror("fopen");return 1;}// 读文件// ...// 关闭文件return 0;
}
4.3.2 fclose
代码块
int fclose ( FILE * stream );
功能:关闭参数stream关联的文件,并取消其关联关系。与该流关联的所有内部缓冲区均会解除关联并刷新;任何未写入的输出缓冲区内容将被写入,任何未读取的输入缓冲区内容将被丢弃。
参数:stream:指向要关闭的流的FILE对象的指针
返回值:成功关闭stream指向的流会返回0,否则返回EOF。
代码演示
int main()
{// . -- 表示当前路径// .. -- 表示上一级路径FILE* pf = fopen(".data.txt", "w");// 这里的路径是相对路径,表示在当前工程目录下的data.txtif (pf == NULL){perror("fopen");return 1;}// 读文件// ...// 关闭文件fclose(pf);pf = NULL;return 0;
}
5. 文件的顺序读写
在进行文件解读的时候,会涉及下面4组函数,我们一一学习。
函数名 | 功能 | 适用于 |
fgetc | 从输入流中读取一个字符 | 所有输入流 |
fputc | 向输入流中写入一个字符 | 所有输出流 |
fgets | 从输入流中读取一个字符串 | 所有输入流 |
fputs | 向输出流中写入一个字符串 | 所有输出流 |
fscanf | 从输入流中读取带有格式的数据 | 所有输入流 |
fprintf | 向输出流中写入带有格式的数据 | 所有输出流 |
fread | 从输入流中读取一块数据 | 文件输入流 |
fwrite | 向输出流中写入一块数据 | 文件输出流 |
上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)。
5.1 fputc
代码块
int fputc ( int character, FILE * stream );
功能:将参数character指定的字符写入到stream指向的输出流中,通常用于向文件或标准输出流写入字符。在写入字符之后,还会调整指示器。字符会被写入流内部位置指示器当前指向的位置,随后该指示器自动向前移动一个位置。
参数:
- character:被写入的字符
- stream:是一个FILE*类型的指针,指向了输出流(通常是文件流或stdout)
返回值:
- 成功时返回写入的字符(以int形式)
- 失败时返回EOF,错误指示器会被设置,可通过ferror()检查具体错误
代码演示
int main()
{FILE* pf = fopen("datas.txt", "w");if (pf == NULL){perror("fopen");return 1;}// 写文件fputc('a', pf);fputc('b', pf);fputc('c', pf);fputc('d', pf);fputc('e', pf);// 利用循环写入一串文字int i = 0;for (i = 'a'; i <= 'z'; i++){fputc(i, pf);}// 关闭文件fclose(pf);pf = NULL;return 0;
}
5.2 fgetc
代码块
int fgetc ( FILE * stream );
功能:从参数stream指向的流中读取一个字符。函数返回的是文件指示器当前的指向的字符,读取这个字符之后,文件指示器自动前进到下一个字符。
参数:stream:FILE*类型的文件指针,可以是stdin,也可以是其他输入流的指针。如果是stdin就从标准输入流读取数据。如果是文件流指针,就从文件读取数据。
返回值:
成功时返回读取的字符(以int的形式)。
若调用时流已经处于文件末尾,函数返回EOF并设置流的文件结束指示器(feof)。若发生读取错误,函数返回EOF并设置流的错误指示器(ferror)。
代码演示
int main()
{FILE* pf = fopen("datas.txt", "r");if (pf == NULL){perror("fopen");return 1;}// 读文件int c = fgetc(pf);printf("%c\n", c);putchar(c);fputc(c, stdout);// 关闭文件fclose(pf);pf = NULL;return 0;
}
5.3 feof和 ferror
代码块
int feof ( FILE * stream );int ferror ( FILE * stream );
如果在读取文件的过程中,遇到了文件末尾,文件读取就会结束。这时候读取函数会在对应的流上设置一个文件结束的指示符,这个文件结束指示符可以通过feof函数检测到。如果feof检测到文件结束指示符已经被设置,则返回非0的值,如果没有设置则返回0。
如果在读/写文件的过程中,发生了读/写错误,文件读取就会结束。这时读/写函数会在对应的流上设置一个错误指示符,这个指示符可以通过ferror函数检测到,如果ferror函数检测到错误指示符已经被设置,则返回非0的值,如果没有设置就返回。
代码演示
int main()
{FILE* pf = fopen("datas.txt", "r");if (pf == NULL){perror("fopen");return 1;}int i = 0;for (i = 0; i < 40; i++){int c = fgetc(pf);if (c == EOF){if (feof(pf)){printf("读到文件末尾了\n");}else if (ferror(pf)){printf("读取发生了错误\n");}}else{fputc(c, stdout);}}// 关闭文件fclose(pf);pf = NULL;return 0;
}
ferror函数大家可以自己搭配使用环境进行检测。
5.4 fputs
代码块
int fputs ( const char * str, FILE * stream );
功能:将参数str指向的字符串写入到参数stream指定的流中(不包含结尾的空字符\0),适用于文件流或标准输出。
参数:str -- 指针 -- 指向了要写入的字符串(必须以\0结尾)。
stream -- FILE*类型指针 -- 指向了要写入字符串的流。
返回值:
成功时返回非负整数。
失败时返回EOF,同时设置流的错误指示器,可以使用ferror函数检查错误原因。
代码演示
// fputs 代码演示
int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}// 写文件fputs("abc\0xxxx", pf);fputs("def", pf);// 关闭文件fclose(pf);pf = NULL;return 0;
}
这里data文件中会出现abcdef的内容。
5.5 fgets
代码块
char * fgets ( char * str, int num, FILE * stream );
功能: 从stream指定输入流中读取字符串,至读取到换行符(\n)、文件末尾(EOF)或达到指定字符数(包含结尾的空字符\0),然后将读取到的字符串储存到str指向的空间中。
参数: str -- 指针 -- 指向字符数组的指针 -- str指向的空间用于储存读取到的字符串。
num -- 最大读取字符数(包含结尾的\0)
stream -- 输入流的文件指针
返回值:
成功时返回str指针。
若在尝试读取字符时遇到文件末尾,则设置文件结束指示器,并返回NULL,需要通过feof检测。
若发生读取错误,则设置流错误指示器。并返回NULL,通过ferror函数检测。
代码演示
从两次函数的使用我们不难看出我们读取的字符实际上是num-1个,会自动添加一个‘\0’字符。
使用细节:
若读取到换行符(\n),会将其包含在字符串中除非超出num-1限制,然后以‘\0’结尾
文件末尾没有换行符时,字符串以‘\0’结尾,不包含\n。
5.6 fprintf
代码块
int fprintf ( FILE * stream, const char * format, ... );
功能: fprintf是经格式化数据写入指定文件流的函数,它与printf类似,但可以输出到任意输出流(如磁盘文件、标准输出、标准错误等),而不仅限于控制台。
参数:
stream: 指向FILE对象的指针,表示要写入的文件流
format:格式化字符串,包含要写入的文本和格式说明符
...:可变参数列表,提供与格式字符串中说明符对应的数据
返回值:
成功时,返回写入的字符总数(非负值)
失败时,先设置对应流的错误指示器,再返回赋值,可以通过ferror函数来检测。
在写代码演示之前,我们先来比较一下fprintf和printf这两个函数。
不难看出,我们只要会使用printf就会使用fprintf。
代码演示
struct Stu
{char name[20];int age;float socer;
};
int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}struct Stu s = { "zhangsan", 20, 85.5f };// 写文件fprintf(pf, "%s %d %f", s.name, s.age, s.socer);// 关闭文件fclose(pf);pf = NULL;return 0;
}
5.7 fscanf
代码块
int fscanf ( FILE * stream, const char * format, ... );
功能:fscanf是从指定文件流中读取格式化数据的函数,它类似于scanf,但是可以指定输入源,而非限制于控制台输入。适用于从文件解析结构化数据。
参数:
stream -- 指向FILE对象的指针 -- 表示要读取的文件流
format -- 格式化字符串
... -- 可变参数列表。
返回值:
成功时:函数返回成功填充到参数列表中的项数。该值可能与预期项数一致,也可能因以下原因少于预期,
- 格式和数据匹配失败
- 读取发生错误
- 到达文件末尾
如果在成功读取任何数据之前发生:
读取错误:会在对应流上设置错误指示符,则返回EOF
到达文件末尾:会在对应流上设置文件结束指示符,则返回EOF
这里的代码演示和fprintf时大致一样,就不过多叙述了。感兴趣的小伙伴可以自己尝试的写一写。
以上学习的函数都是操控文本文件的,我们运行后可以直接在文本文档中读取到内容,是我们可以看的懂的,但是下面的这两个函数,我们就不是可以直接看的懂了。
5.8 fwrite
代码块
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
功能:函数用于将数据块写入stream指向的文件流中,是以2进制的形式写入的。
参数:
- ptr -- 指向要写入的数据块的指针
- size -- 要写入的每个数据项的大小(以字节为单位)
- count -- 要写入的数据项的数量
- stream -- 指向FILE类型结构体的指针 -- 指定了要写入数据的文件流
返回值:返回实际写入的数据项数量。如果发生错误,则返回值可能小于count。
使用注意事项:
- 在使用fwrite之前,要确保文件已经以二进制可写方式打开。
- fwrite通常用于二进制数据的写入,如果写入文本数据,请谨慎处理换行符和编码等问题。
代码演示
// fwrite 代码演示
struct Stu
{char name[20];int age;float socer;
};
int main()
{struct Stu s = { "zhangsan",20,95.5f };FILE* pf = fopen("data.text", "wb");if (pf == NULL){perror("fopen");return 1;}// 写文件fwrite(&s, sizeof(struct Stu), 1, pf);// 关闭文件fclose(pf);pf = NULL;return 0;
}
很明显,现在的数据我们是看不懂的。我们以二进制的数据写入的,那么我们以二进制的形式读出来就行,我们使用fread函数。
5.9 fread
代码块
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
功能:函数用于从stream指向的文件流中读取数据块,并将其储存到ptr指向的内存缓冲区中。
参数:
- ptr -- 指向内存区域的指针 -- 用于储存从文件中读取的数据。
- size -- 要读取的每个数据块的大小(以字节为单位)
- count -- 要读取的数据块的数量
- stream -- 指向FILE类型结构体指针 -- 指定了要从中读取数据的文件流。
使用注意事项
- 在使用fread之前,确保文件以二进制可读方式打开
- ptr指向的内存区域必须足够大,以便储存指定数量和大小的数据块
- 如果fread成功读取了指定数量的数据块,则返回值等于count;如果读取数量少于count,则可能已经到达文件末尾了或者发生错误了。
- 在二进制文件读取时,fread是常用的函数,但对于文本文件读取,通常使用fgets或fscanf
代码演示
// fread 代码演示
struct Stu
{char name[20];int age;float socer;
};
int main()
{struct Stu s = { 0 };FILE* pf = fopen("data.text", "rb");if (pf == NULL){perror("fopen");return 1;}// 写文件fread(&s, sizeof(struct Stu), 1, pf);printf("%s %d %f\n", s.name, s.age, s.socer);// 关闭文件fclose(pf);pf = NULL;return 0;
}
所以,我们以二进制的形式读文件就可以了。
6. 文件的随机读写
6.1 fseek
代码块
int fseek ( FILE * stream, long int offset, int origin );
功能:重新定位流位置指示器。
参数:
stream -- 指向FILE类型结构体指针 -- 指定了要从中读取数据的文件流。
offset -- 从源偏移的字节数
origin -- 偏移参考位置
代码演示
// fseek 代码演示
int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}// 写文件fputs("abcdef", pf);// 从规定位置写入内容 fseek(pf, 3, SEEK_SET);fputc('h', pf);// 关闭文件fclose(pf);pf = NULL;return 0;
}
6.2 ftell
代码块
long int ftell ( FILE * stream );
功能:获取流的当前位置
代码演示
int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}// 写文件fputs("abcdef", pf);// 从规定位置写入内容 fseek(pf, 3, SEEK_SET);fputc('h', pf);int r = ftell(pf);printf("%d", r);// 关闭文件fclose(pf);pf = NULL;return 0;
}
6.3 rewind
代码块
void rewind ( FILE * stream );
功能:将流的位置设置为开头。
代码演示
int main()
{FILE* pf = fopen("data.txt", "w");if (pf == NULL){perror("fopen");return 1;}// 写文件fputs("abcdef", pf);// 从规定位置写入内容 fseek(pf, 3, SEEK_SET);fputc('h', pf);int r = ftell(pf);printf("%d", r);rewind(pf);// 关闭文件fclose(pf);pf = NULL;return 0;
}
7. 文件缓冲区
ANSI C标准采用“缓冲文件系统”处理文件,所谓缓冲文件系统是指系统自动地在内存中为程序中的每一个正在使用的文件开辟一块“文件缓冲区”,从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等),缓冲区的大小根据C编译系统决定的。
在学习缓冲区的时候,我们需要关注一个函数,它是用来刷新缓冲区的,具体内容如下:
7.1 fflush
代码块
int fflush ( FILE * stream );
功能:强制刷新参数stream指定流的缓冲区,确保数据写入底层设备。
- 对输出流:将缓冲区中未写入的数据立即写入文件
- 对输入流:行为具体实现决定,非C语言标准行为
- 参数为NULL:刷新所有打开的输出流
参数:stream -- 指向文件流的指针。
返回值:成功返回0,失败返回EOF。.
注意事项:
- 仅对输出流或更新流(最后一次操作作为输出)有明确刷新行为。
- 输入流的刷新行为不可移植
- 程序正常终止或调用fclose时会自动刷新,但程序崩溃时缓冲区数据可能丢失。
这里就不进行代码演示了,在第8小节中一并演示
8. 更新文件
我们在4.3中列了一个表格,其中‘r+、‘w+’’、‘a+’分别表示什么意思?如下表:
⾏为 | "r+" | "w+" | "a+" |
解释 | 可读/可写 | 可读/可写 | 可读/可写 |
⽂件不存在时 | 打开失败 | ⾃动创建新⽂件 | ⾃动创建新⽂件 |
⽂件存在时 | 保留内容 | 清空内容 | 保留内容 |
初始⽂件指针位置 | ⽂件开头 | ⽂件开头 | ⽂件末尾 |
写⼊是否覆盖原有数据 | 是(可定位覆盖) | 是(内容已清空,从头写⼊) | 否(默认是在⽂件末尾写数据) |
典型⽤途 | 修改⽂件部分内容 | 创建新⽂件或完全重写旧⽂件 | 在⽂件末尾追加数据,⽐如记录⽇志 |
关键要点:
1. 在写完文件后,要继续读文件的时候,在读取文件之前一定要使用fflush刷新文件缓冲区、或者使用fseek、rewind重新定位文件指示器的位置。
2. 在读完文件后,需要继续写文件之前,使用fseek、rewind重新定位文件指示器的位置。
代码演示
int main()
{FILE* pf = fopen("test.txt", "w+");if (pf == NULL){perror("fopen");return 1;}// 写文件fputs("abcdef", pf);// 刷新文件缓冲区fflush(pf);// 读文件fseek(pf, 0, SEEK_SET);int c = fgetc(pf);printf("%c\n", c);// 关闭文件fclose(pf);pf = NULL;return 0;
}