C文件操作
目录
1.FILE *fopen( const char *filename, const char *mode );
3.int fputc( int c, FILE *stream );
4.char *fgets( char *string, int n, FILE *stream );
5.int fputs( const char *string, FILE *stream );
6.int fscanf( FILE *stream, const char *format [, argument ]... );
7.int fprintf( FILE *stream, const char *format [, argument ]...);
8.size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
9.size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
1.int fseek( FILE *stream, long offset, int origin );
3.void rewind( FILE *stream );
零.前言
C语言中文件操作似乎并不是很常用,但学习文件操作确实可以帮助我们更好地理解数据在内外存中进行存储时发生的转换。
1.为什么要学习文件操作
我们在编程的时候会遇到这样的问题,比如我们要写一个学生信息管理系统,当程序运行时我们输入学生信息,当程序运行结束时我们录入的信息无论是在栈区存储的,还是在堆区存储的都会被销毁,这样下一次再使用信息管理系统的时候,就需要重新录入信息,这样就很麻烦,所以我们想在程序结束之后这些录入的信息依然可以得到保存方便下一次使用,于是就需要进行文件操作。
2.什么是文件
文件是磁盘上存储数据的单位,我们一般谈论的文件分为两种:程序文件和数据文件。
程序文件:包括源程序文件.c,.h,目标文件.obj,可执行程序.exe等
数据文件:文件的内容不一定是程序,而是程序运行时读取的数据,比如程序运行时需要从中读取
数据的文件,或者输出内容的文件。
我们主要讨论的是数据文件。
我们之前运行的程序会产生一个.c,.exe的文件,这些文件就存储在磁盘上,当使用的时候才会转而调用到内存中。
3.文件名组成
一个文件名由三个部分组成:
文件路径,文件名主干,文件后缀
例如:C:\code\test.txt这个文件,C:\code\为文件路径,test为文件主干,.txt为文件后缀。
4.文件指针
当我们打开一个文件时,内存中会开辟一个文件信息区,并且有相应的指针指向它的首元素地址,用来存放文件的相关信息(如文件的名字,文件状态以及文件的位置等),这些信息保存在一个结构体变量值中,该结构体的类型由系统声明,取名为FILE。
结构体中存放的是什么,我们不需要关心,我们只需要知道这个文件在内存中对应的结构体的类型是FILE就足够了,因此我们可以定义一个这个类型的指针来指向一个文件。
FILE* pf;//定义一个文件类型的指针
通过pf可以找到文件信息区,通过文件信息区就能够访问该文件。所以通过文件指针变量可以找到与之相关的文件。
5.打开关闭文件
1.FILE *fopen( const char *filename, const char *mode );
1.含义
打开文件
2.参数
const char* filename表示的文件名的首元素地址,传入的是文件名。const char* mode指的是要对文件执行的操作,即以何种方式打开。
3.返回值
返回一个指向该文件信息区的指针,这里可以理解成指向该文件的指针。如果打开失败返回NULL
4.打开文件的方式

2.int fclose( FILE *stream );
1.含义
关闭一个已经打开的文件。
2.参数
FILE* stream表示指向该文件信息区的指针。
3.返回值
如果成功关闭返回0,如果关闭失败返回EOF。
3.用法
首先以只读的方式打开一个不存在的文件,那是一定不会成功打开的,返回一个空指针。
FILE* pf = fopen("data.txt","r");//以只读的方式打开一个不存在的文件
if (pf == NULL)
{
perror("fopen");//打印错误信息
return -1;
}
fclose(pf);
pf = NULL;
我们可以看到报错信息为:

当我们以写的方式打开文件时,根据上表,当文件不存在时会新建一个文件。
#include<stdio.h>
int main()
{
FILE* pf = fopen("data.txt","w");//以写的方式打开一个文件
if (fclose(pf) == EOF)//若返回值为EOF则关闭失败
{
perror("fopen");
return -1;
}
return 0;
}
当文件不存在时,以写的方式打开文件会创建一个新的文件。我们打开.c文件的路径就会发现确实多了一个data.txt的文件。

当我们想打开的文件不在这一路径下,比如在桌面时,只需要把路径改成文件本身的路径即可。但要注意转义字符的问题。
打开文件后一定要关闭,然后将文件指针置为NULL。
6.流
流是一种高度抽象的概念。
我们要对数据进行存储或者显示,都是要经过流的作用:

这个就是数据存入或者显示的过程,我们程序员只需要关心如何把数据存到流里,再由流自动分配到各个接收端。比如我们可以把一个数据存到文件里,存到网上,存到光盘里等等都是流帮助分配的。
C语言程序只要运行起来就默认打开了三个流。
1.stdout(屏幕):标准输出流
2.stdin(键盘):标准输入流
3.stderr:标准错误流
有些函数时可以将数据发送到所有流上的比如:fgetc,fputc,fgets,fputs,fscanf,fprintf等。
有些函数只能将数据发送到文件上,比如:fread,fwrite
7.文件的顺序读写(按顺序写入信息)
1.读与写,输入与输出
很多人在学文件操作时搞不懂这几个名词,我当时也是被搞得晕头转向,这里给大家解释一下:

2.int fgetc( FILE *stream );
1.含义
字符输入函数。
2.参数
stream表示指向文件的指针。
3.返回值
返回读取的字符的ascii值,读取失败则返回EOF。
4.应用
假设我们往.txt文件中放的是abcd
FILE* pf = fopen("data.txt","r");//以读的形式打开文件
char a = fgetc(pf);//返回第一个字符的值
printf("%c\n", a);
char b = fgetc(pf);//返回第二个字符的值
printf("%c", b);
if (pf == NULL)
{
perror("fopen");
return -1;
}
fclose(pf);
pf = NULL;
打印的结果为:

我们发现打印完第一个元素'a'之后,再次调用fgetc则返回的是第二个元素'b',所以fgetc还有让函数指针向后移动的作用,即打印b的时候pf指向的是b。
3.int fputc( int c, FILE *stream );
1.含义
字符输出函数。
2.参数
c表示写入字符的ascii值,stream表示指向写入的文件的指针。
3.返回值
返回的是写入字符的ascii值。
4.应用
FILE* pf = fopen("data.txt","w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
fputc('a',pf);//向文件中写入字符
fputc('b',pf);
fputc('c', pf);
fclose(pf);
pf = NULL;
每一次以写的形式打开文件时,文件中原有的内容会被清空,运行之后打开data.txt文件得到的结果是:

即写入文件成功。
4.char *fgets( char *string, int n, FILE *stream );
1.含义
文本行输入函数。
2.参数
string表示的是要将文件中的元素放在字符串中,该字符串的首地址。n表示放入的字符个数,stream表示文件的首元素地址。
3.返回值
返回的是字符串的首地址。
4.用法
假设说文件中放的是abcdefg
FILE* pf = fopen("data.txt","r");//以读的形式打开问文件
if (pf == NULL)
{
perror("fopen");
return -1;
}
char arr[5] = { 0 };
fgets(arr, 5, pf);//读取文件中5个数据
printf("%s", arr);
打印的结果是:

我们发现我们明明规定将文件中前五个字符存入arr中,但是却只存了四个,这是因为在存入这五个字符的同时还需要存放一个‘\0’,'\0'也占5个中的一个位置。
5.int fputs( const char *string, FILE *stream );
1.含义
文本行输出函数。
2.参数
string表示的是输入字符串的首元素地址,stream表示打开的文件的首元素地址。
3.返回值
如果成功输入则返回0,如果发生错误则返回EOF。
4.用法
FILE* pf = fopen("data.txt","w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
char arr[] = "abcdefg";
fputs(arr, pf);//将arr中数据输入到文件中
这时我们打开文件,发现文件中的确存放的是abcdefg

6.int fscanf( FILE *stream, const char *format [, argument ]... );
1.含义
格式化输入数据。
2.参数
我们可以和scanf对比,只是比scanf多了一个文件指针。
3.返回值
返回实际输入数据的个数,或者返回EOF。
4.用法
现在我在文件中放的数据是:

struct S
{
int n;
double d;
};//定义一个结构体方便格式化
FILE* pf = fopen("data.txt","r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
struct S s = { 0 };
fscanf(pf, "%d %lf", &(s.n), &(s.d));//将文件中的数据输入字符串
printf("%d\n", s.n);
printf("%lf", s.d);
打印的结果是:

说明存入成功了。
7.int fprintf( FILE *stream, const char *format [, argument ]...);
1.含义
格式化输出函数。
2.参数
stream表示的是指向文件的指针,后面的和printf的参数是一样的。
3.返回值
返回写入文件的字节数,若失败则返回EOF。
4.用法
FILE* pf = fopen("data.txt","w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
struct S s = { 1000,9.9996 };
fprintf(pf, "%d %lf", s.n, s.d);//在文件中写入s.n和s.d
这时文件中内容被修改为:

8.size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
1.含义
二进制输出。(以二进制的方式进行读写)
2.参数
buffer指的是要被写的数据的首元素地址。size表示一个元素的字节数,count表示的是元素的个数,stream是文件指针。
3.返回值
返回的是写入元素的个数,若失败返回EOF
4.应用
struct S
{
int n;
double d;
char name[20];
};
FILE* pf = fopen("data.txt","w");
if (pf == NULL)
{
perror("fopen");
return -1;
}
struct S s = { 1000,9,"zhangsan"};
fwrite(&s,sizeof(s),1,pf);
我们将这个结构体的内容以二进制的方式写进文件,文件中的内容是:

由于是以二进制的形式写入的,所以我们看不懂,但是不代表计算机看不懂,只需要调用fread函数就可以读懂了。
9.size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
1.含义
二进制输入。
2.参数
buffer表示读入的内容存放位置的首地址,size表示读入的一个元素的大小,count表示的是元素个数,stream表示的是文件指针。
3.返回值
返回的是读入元素的个数,返回值>=count。若失败则返回EOF。
4.应用
我们保留上一个例子中data.txt的二进制文件,用fread读取文件的内容。
struct S
{
int n;
double d;
char name[20];
};
FILE* pf = fopen("data.txt","r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
struct S s = {0};//建立一个struct s大小的结构体
fread(&s,sizeof(s),1,pf);//将文件中内容读到结构体s中
printf("%d %lf %s",s.n, s.d, s.name);
打印的结果是:

说明读入成功。
10.对比函数
1.scanf,fscanf,sscanf
scanf:从标准输入流(键盘)读取格式化的数据。
fscanf:从所有输入流读取格式化的数据。
sscanf:把一个字符串转化成一个格式化的数据。
2.printf,fprintf,sprintf
printf:打印格式化的数据到屏幕上(标准输出流)。
fprintf:把格式化的数据输出到所有输出流上。
sprintf:把格式化的数据转化成对应的字符串。
3.介绍一下sscanf与sprintf
struct S
{
int n;
double d;
char name[20];
};
char arr[100] = { 0 };
struct S s = { 100,3.14,"zhangsan" };
sprintf(arr, "%d %lf %s", s.n, s.d,s.name);//将三者的内容合并转化成一个字符串存入arr中
printf("%s\n", arr);
struct S tmp = { 0 };//定义一个tmp用来接收字符串中信息。
sscanf(arr, "%d %lf %s", &(tmp.n), &(tmp.d), &(tmp.name));//将字符串arr中的数据写入tmp中
printf("%d %lf %s", tmp.n, tmp.d, tmp.name);
第一次打印的是字符串中的信息,第二次打印的是tmp中的信息。

8.文件的随机读写
1.int fseek( FILE *stream, long offset, int origin );
1.含义
根据文件指针的位置和偏移量来定位文件指针。
2.参数
stream表示的是文件指针,offet表示的是偏移量,origin表示的是起始位置
起始位置有三种:SEEK_CUR表示当前位置。
SEEK_SET表示起始位置。
SEEL_END表示末尾位置。
即以什么位置为基准。
3.返回值
若成功返回一个0,若失败返回一个非零值。
4.应用
假设文件中原有的数据为abcdef
FILE* pf=fopen("data.txt", "r");
if (pf == NULL)
{
perror("fopen");
return -1;
}
fseek(pf, 2, SEEK_SET);
char a = fgetc(pf);
printf("%c\n", a);
fseek(pf, -2, SEEK_CUR);
char b = fgetc(pf);
printf("%c\n", b);
fseek(pf, -2, SEEK_END);
char c = fgetc(pf);
printf("%c", c);
打印的结果为:

下面我们来分析一下这段代码:
首先fseek(pf,2,SEEK_SET)的含义是,以文件起始位置为基准,文件指针pf指向偏移量为2的位置,即指向的是c,注意a的偏移量是0.
然后fseek(pf,-2,SEEK_CUR)的含义是,以此时文件指针位置为基准,文件指针pf指向偏移量是-2的位置,注意第一次已将文件指针置于c的位置,所以相对于当前位置c而言,偏移量为-2的位置指向的是a。
最后是fseek(pf,-2,SEEK_END)的含义是,以文件的末尾位置为基准(注意文件末尾的位置在最后一个元素的下一个位置),文件指针指向偏移量为-2的位置,此时指向的是e,同理若为-1则指向的是f。
2.long ftell( FILE *stream );
1.含义
返回文件指针相对于起始位置的偏移量。
2.参数
stream为函数指针。
3.返回值
返回的数偏移量。
4.应用
fseek(pf, 2, SEEK_SET);
long ret = ftell(pf);
printf("%ld", ret);
此时打印的结果是:

fseek将文件指针定义到相对于起始位置为2的位置,ftell计算出文件指针相对于起始位置的偏移量是2。
3.void rewind( FILE *stream );
这个就不多说了,意思是让文件指针回到起始的位置。
9.文件读取结束的判定
都是通过函数的返回值来判定的。
1.fgetc
如果用fgetc来读取文件,则判断fgetc的返回值是否为EOF。
2.fgets
判断返回值是否为NULL。
3.fread
判断返回值与参数count的大小,如果返回值小于count那么读取结束。
4.feof与ferror
ferror:判断是否是遇到文件末尾结束。
feof:判断是否是遇到文件错误而结束。
if(ferror(pf))
{
puts("I/O error when reading")//当ferror返回值为非0时则文件遇到错误而结束。
}
else if(feof(pf))
{
puts("End of file reached successfully");//当feof返回值为非0时则遇到文件结尾发生错误。
}
注意这两个函数不是判断文件是否结束的,而是判断文件为什么结束的。
5.总结
1.文本文件读取是否结束,判断返回值是否是EOF,或者NULL。
2.二进制文件读取是否结实,判断返回值是否小于实际要读的个数。
10.文本文件与二进制文件
1.区别
根据数据的组织形式,数据文件被分为文本文件和二进制文件。
数据在内存中以二进制进行存储,如果发生转换,就可以生成文本文件,如果不发生转换那么就是二进制文件。
如果要求在外存中以ascii存储那么就是文本文件。
2.一个数据的存储
字符一律以ascii值进行存储,数值数据既可以以ascii存储,又可以以二进制的形式进行存储。
假设有一个整数10000,在内存中存储的就是
00000000 00000000 00100111 00010000
如果在外存上以二进制的形式进行存储,那么存储的就是它本身。
如果在外存上以字符的形式进行存储:
就是将1 0 0 0 0分别用ascii存储,其中0对应的ascii为00110000,1对应的ascii为00110001为
00110001 00110000 00110000 00110000 00110000注意这是ascii不是二进制。

我们以二进制的形式打开data.txt文件。
就可以看到10000在磁盘上的二进制输出

11.文件缓冲区
C标准是采用缓冲文件系统处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中,为程序中每一个正在使用的文件开辟一块文件缓冲区。
从内存向磁盘输出数据时,会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上,如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个的将数据送到程序的数据区。缓冲区的大小由C编译系统所决定。

正是由于缓冲区的存在,C在操作文件时需要刷新缓冲区或者在文件操作结束时关闭文件,如果不做可能导致读写文件出现问题。
12.总结
学习了文件操作可以使我们在写代码的时候随心所欲操纵一些文本文件,一定程度上也提高了项目的执行效率,但其实我认为文件操作最大的意义在于我们理解内外存之间的关系,对其中的本质多少了解一些会使我们在写代码的时候越来越严密,越来越细。

浙公网安备 33010602011771号