第二十讲:文件操作
1.什么是文件
如果没有了文件,我们在电脑上所写的程序会存储在电脑的内存中,程序退出,内存回收,我们所写的东西就丢失了,要将数据进行持久保存,就使用了文件
1.1什么是文件
存放在磁盘(硬盘)上的·文件就是文件
但在程序设计中,我们谈论的文件一般有两种:程序文件和数据文件(根据文件功能分类)
1.1.1程序文件
包括源程序文件(.c后缀),目标文件(.obj后缀),可执行程序(.exe后缀)
1.1.2数据文件
文件包含的内容不一定是程序,还可能是数据运行时读写的数据,在本文中讨论的是数据文件
在之前各讲中处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,然后将结果返回到显示器上
但是我们有时候会把信息输出到磁盘上,当需要的时候再从磁盘中把数据读取到内存中使用,这里处理的就是磁盘上的文件
1.2二进制文件和文本文件
根据数据的组织形式,数据文件被分为二进制文件和文本文件
数据在内存中以二进制的形式存储,如果不加转化地输出到外存中,就是二进制文件
如果要求在外存中以ASCII码形式进行存储,就需要在存储前进行转化,以ASCII码形式存储的文件就是文本文件
数据的存储方式
那么一个数据在文件中是怎么存储的呢?
字符:一律按照它的ASCII码进行存储
数值:即可以使用ASCII码进行存储,也可以使用二进制进行存储
比如整数10000,如果以ASCII码形式进行存储,需要占用5个字节,而以二进制形式进行输出,在磁盘上只占用4个字节
2.流和标准流
2.1流
我们程序的数据需要输出到各种外部设备,也需要不断从外部设备获取数据,不同外部设备之间输入和输出方式各不相同,为了方便程序员对各种设备进行方便的操作,我们引用了流的概念,也就是说,流的概念允许开发者以统一的方式处理不同类型的输入输出操作,而不需要关心数据的来源或目的地的具体细节
C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的
⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作
2.2标准流
C语言系统在启动的时候,会自动打开三个流:
1.stdin:标准输入流,大多数情况下从键盘输入,scanf函数就是从标准输入流中读取数据
2.stdout:标准输出流,大多数情况输出到显示器界面,printf函数就是将信息输出到标准输出流中
3.stderr:标准错误流,大多数情况输出到显示器界面,与标准输出流不同,标准错误流常常发送需要立即注意的信息
这三个流的类型是:FILE*,通常称为文件指针
C语言中,就是通过FILE*来维护流的各种操作的
3.文件指针
想要对文件进行操作,文件指针就发挥着重要作用
每个被使用的文件都在内存中开辟了一个文件信息区,用来存放文件的相关信息(文件名称、文件状态、文件所在位置等),结构体有着这种存储能力,该结构体由系统命名,为FILE
如:VS2013中就有类似的声明:
c
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
既然有了这个结构体,那么就可以使用一个指针来指向它,甚至访问、维护它,那么我们就来创建一个结构体指针变量:
c
FILE* pa;//文件指针变量
4.文件的打开与关闭
文件在使用之前需要先打开,使用之后需要记得关闭
ANSI C规定,使用fopen来打开文件,使用fclose来关闭文件
而在打开文件的同时,都会返回一个FILE*类型的指针变量指向该文件,也相当于建立了指针和变量的关系
c
//打开⽂件
FILE* fopen(const char* filename, const char* mode);
//关闭⽂件
int fclose(FILE* stream);
mode表示文件的打开模式,打开模式如下:
我们来看一个示例:
c
//示例
int main()
{
FILE* pa = fopen("myfile.txt", "w");//打开文件
if (pa != NULL)
{
fputs("fopen example", pa);
//使用完要关闭文件
fclose(pa);
}
return 0;
}
5.文件顺序读写
5.1顺序读写函数介绍
上面说的适用于所有输⼊流⼀般指适⽤于标准输⼊流和其他输入流(如文件输入流);
所有输出流⼀般指适⽤于标准输出流和其他输出流(如文件输出流)
5.2打开和关闭函数
它们都被包含在头文件<stdio.h>中
5.2.1fopen函数
函数原型:
c
FILE *fopen( const char *filename, const char *mode );
函数参数:
filename:要打开的文件的名称
mode:打开模式,在上面有
函数返回值:
成功时返回一个指向FILE对象的指针(returns a pointer to the open file)
失败时返回NULL,并设置errno全局变量
函数使用:
c
//函数使用
int main()
{
FILE* pa = fopen("myfile.txt", "w");
//对于文件,要提供正确的文件名称,保证文件是能够被打开的
//使用之前要注意检查返回值是否为空
if (pa == NULL)
{
perror("fopen");//如果为空,可以使用perror函数来进行报错处理
return 1;
}
else
{
fputs("fopen example", pa);
}
//最后要记得关闭文件
fclose(pa);
pa = NULL;
return 0;
}
5.2.2fclose函数
函数原型:
c
int fclose( FILE *stream );
函数参数:
stream:指向FILE类型的指针,这个FILE对象通常由fopen函数返回
函数返回值:
如果流成功关闭,返回0
如果出错,返回EOF表示
函数使用:
c
//函数使用
int main()
{
FILE* pa = fopen("myfile.txt", "w");
//对于文件,要提供正确的文件名称,保证文件是能够被打开的
//使用之前要注意检查返回值是否为空
if (pa == NULL)
{
perror("fopen");//如果为空,可以使用perror函数来进行报错处理
return 1;
}
else
{
fputs("fopen example", pa);
}
//最后要记得关闭文件
if (fclose(pa))
{
perror("Error Closing File");//最好有错误检测,看是否成功关闭了文件
return -1;
}
//fclose(pa); //多次关闭文件是未定义的
pa = NULL;
return 0;
}
5.3fgetc函数和fputc函数
它们都包含在头文件<stdio.h>中
5.3.1fgetc函数
函数原型:
c
int fgetc( FILE *stream );
函数参数:
stream:指向FILE结构的指针,表示要读取的文件流
函数返回值:
成功时,返回读取的字符,然后,将函数相关的文件指针加1,也就是让它指向下一个字符,如果流位于文件末尾,则设置流的文件结束指示符,也就是返回EOF
失败时,返回EOF
函数使用:
c
//fgetc函数
int main()
{
//肯定要先打开一个文件
FILE* pa = fopen("myfile.txt", "r");
//先检查pa是否为空
if(pa != NULL)
{
int a;
while ((a = fgetc(pa)) != EOF)//这时可以进行检查是否为EOF看是否到文件末尾或出错
{
printf("%c ", a);
}
//记得关闭文件
fclose(pa);
pa = NULL;
}
return 0;
}
5.3.2fputc函数
函数原型:
c
int fputc( int c, FILE *stream );
函数参数:
c:表示要写入的字符,作为int类型传递
stream:表示要写入的文件流
函数返回值:
成功时,返回写入的字符,并将文件指针的位置向后移动一个字符,以指向下一个字符
失败时,返回EOF
函数使用:
c
//fputs函数使用
int main()
{
FILE* pa = fopen("myfile.txt", "w");
if (pa == NULL)
return 1;
else
{
int a = fputc('w', pa);
if (a != EOF)
putchar(a);
a = fputc('x', pa);
if (a != EOF)
putchar(a);
}
fclose(pa);
pa = NULL;
return 0;
}
5.4fputs和fgets函数
5.4.1fputs函数
函数原型:
c
int fputs( const char *string, FILE *stream );
函数参数:
string:要写入的字符串
stream:表示要写入的文件流
函数返回值:
成功时,返回非负值(可以为0,且一般为0)
失败时,返回EOF
函数使用:
c
int main()
{
FILE* pa = fopen("example.txt", "w");
if (pa == NULL)
{
perror("fopen");
return 1;
}
else
{
fputs("abcdef", pa);
fputs("ghi", pa);
//文件中存储的是abcdefghi,它们是连在一起的,如果想要它们在两行上,可以进行更改:
fputs("abcdef\n", pa); //要自己添加\n换行符
fputs("ghi\n", pa);
}
fclose(pa);
也可以对此进行判断
//if (fclose(pa) == EOF)
//{
// perror("fclose");
// return -1;
//}
pa = NULL;
return 0;
}
5.4.2fgets函数
函数原型:
c
char *fgets( char *string, int n, FILE *stream );
1.函数参数:
string:指向存储读取出的字符串的指针,用于存储从文件中读取出的字符串
n:指定最多可以从文件中读取出的字符数,包括空字符\0
stream:表示目标文件
返回值:
成功时,返回str的头指针,如果在读取过程中遇到了文件末尾,那么会设置eof指示符
失败时,也就是当未读取字符串时遇到文件结尾,或发生读取错误时,返回NULL
函数使用:
使用1
c
//函数的一般使用
int main()
{
//文件中存储的是hello world
FILE* pa = fopen("example", "r");
char arr[20];
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
fgets(arr, 10, pa);
}
fclose(pa);
pa = NULL;
return 0;
}
当要文件中的总字符数大于设置读取的字符数时,读取的字符数为n-1,因为要存储一个\0
使用2,当文件中这样存储时:
c
int main()
{
FILE* pa = fopen("example", "r");
char arr[20];
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
fgets(arr, 10, pa);
printf("%s", arr);
}
fclose(pa);
pa = NULL;
return 0;
}
当我们要读取10个字符时,函数只会将一整行的数据读取,这也是为什么它叫文本行输出函数了,然后它会读取\n,所以,当我们进行打印时,打印出的效果也会加上\n
所以对于多行,我们需要通过循环多次使用函数来读取:
c
int main()
{
FILE* pa = fopen("example", "r");
char arr[20];
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
while (fgets(arr, 10, pa) != NULL)
printf("%s", arr);
}
fclose(pa);
pa = NULL;
return 0;
}
5.5fprintf和fscanf函数
5.5.1fprint函数
函数原型:
c
int fprintf( FILE *stream, const char *format , ...);
函数参数:
stream:要写入的目标文件流
formate:格式字符串,%d,%s,%c等等都是格式
...:可变参数列表,包含的是要进行输出的数据,包含变量指针
函数返回值:
成功时,返回写入的字符的个数
失败时,返回一个负数
函数的使用:
c
int main()
{
FILE* pa = fopen("example", "w");
char arr[20];
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
fprintf(pa, "Example:%s", "hello world");//文件中存储的是:Example:hello world
}
fclose(pa);
pa = NULL;
return 0;
}
当然,我们可以通过结构体更明显地看出格式化输出的作用:
c
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu S;
FILE* pa = fopen("example.txt", "w");
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
fprintf(pa, "%s %d %f", "zhangsan", 20, 66.6);
}
fclose(pa);
pa = NULL;
return 0;
}
5.5.2fscanf函数
函数原型:
c
int fscanf( FILE *stream, const char *format , ... );
函数参数:
stream:要读取的文件流
formate:格式字符串
...:可变参数列表,包含变量的指针
函数返回值:
成功时,返回读取的数据个数
失败是,也就是没有读取任何数据之前,遇到了文件末尾,或者读取发生错误(非法格式或读取错误),返回EOF
函数使用:
c
int main()
{
struct Stu S = { 0 };
FILE* pa = fopen("example.txt", "r");
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
int ret = fscanf(pa, "%s %d %f", S.name, &S.age, &S.score);//此处要记得&
printf("%s %d %f\n", S.name, S.age, S.score);
printf("%d", ret);
}
fclose(pa);
pa = NULL;
return 0;
}
结果如下:
5.6fwrite和fread函数
5.6.1fwrite函数
函数原型:
函数参数:
buffer:要写入数据的指针
size:接受输出的每个数据大小
count:要输出的数据的数量
stream:要写入的文件流
函数返回值:
成功时,返回写入数据项的个数
如果返回值小于count,表示操作可能发生了错误或未完成
函数使用:
c
int main()
{
FILE* pa = fopen("example.txt", "wb");//注意这里要使用wb,因为写入的是二进制数据
char arr[] = "hello world";
if (pa == NULL)
{
perror("FILE OPEN FILED");
return 1;
}
else
{
fwrite(arr, sizeof(char), strlen(arr), pa);
}
fclose(pa);
pa = NULL;
return 0;
}
使用结构体:
c
struct S
{
int age;
char name[20];
char sex[5];
};
int main()
{
struct S L = { 20,"李四","女" };
FILE* pf = fopen("example.txt", "wb");//二进制输出时用wb
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&L, sizeof(struct S), 1, pf);
if (fclose(pf) == EOF)
{
//关闭失败
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
5.6.2fread函数
函数原型;
函数参数:
1.buffer:要存储数据的指针
2.size:每个元素的大小,单位是字节
3.count:要读取的元素数量
4.stream:文件指针,指向要读取的文件
函数返回值:
函数返回读取成功的元素的数量,如果返回值小于count,可能是由于已经达到了文件末尾或发生错误
函数使用:
c
struct S
{
int age;
char name[20];
char sex[5];
};
int main()
{
struct S L = { 20,"李四","女" };
FILE* pa = fopen("text.txt", "rb");//二进制输入时用rb
if (pa == NULL)
{
perror("fopen");
return 1;
}
fread(&L, sizeof(struct S), 1, pa);
//使用fread函数能够读取二进制值
printf("%d %s %s", L.age, L.name, L.sex);
if (fclose(pa) == EOF)
{
perror("fclose");
return 1;
}
pa = NULL;
return 0;
}
拓展延申:5.7sprintf和sscanf函数
我们直接将两个函数对比着看:
函数原型:
1.sprintf函数用于将格式化的数据写入到字符串中
2.sscanf函数用于从字符串中按照指定的格式解析数据
函数返回值:
1.sprintf函数返回成功写入到字符串中的字符数,不包括字符换末尾的\0,但是,对于字符串的写入,要遇到\0才停止写入,所以\0还是很有必要的
2.sscanf函数返回成功解析的项数,如果返回值小于请求的项数,可能是由于格式不匹配或达到字符串末尾
函数使用:
c
struct S
{
int age;
char name[20];
float point;
};
int main()
{
struct S s = { 15, "lisi", 88.8f };
char arr[20];
//将各种数据转换成字符串
int ret = sprintf(arr, "%d %s %f", s.age, s.name, s.point);
//int sprintf(char* buffer, const char* format[, argument] ...);
//函数参数:
//buffer:用于存储格式化后的字符串
//formate:参数的格式
//...:可变参数列表,包含了要被格式化的数据
printf("%s\n", arr);
printf("%d\n", ret);
//将字符串中的数据按照格式化传入一个新的结构体变量中
struct S m;
sscanf(arr, "%d %s %f", &m.age, m.name, &m.point);//注意:要加上&符号
//int sscanf( const char *buffer, const char *format [, argument ] ... );
//函数参数:
//buffer:指向包含输入数据的字符串指针
//formate:格式化字符串,定义如何分析字符串
//...:可变参数列表
printf("%d %s %f", m.age, m.name, m.point);
return 0;
}
运行结果:
6.文件的随机读写
6.1fseek函数
函数原型:
c
int fseek( FILE *stream, long offset, int origin );
该函数是用来在文件中移动文件指针的位置
函数参数:
1.stream:文件指针
2.offset:要移动的字节数,可正可负,分别表示向前\后进行移动
3.origin:表示offset参数的起始点,有三种类型:
SEEK_SET:表示从文件开头开始计算
SEEK_CUR:表示从当前位置开始计算
SEEK_END:表示从文件末尾开始计算
函数使用:
假设我们的目标文件为:example.txt,文件中存储的数据为:
那么我们看下面的代码:
c
int main()
{
FILE* pa = fopen("example.txt", "r");
if (pa == NULL)
{
perror("fopen");
return 1;
}
int ret1 = fgetc(pa);
printf("ret1 = %c\n", ret1);//结果为a
fseek(pa, 2, SEEK_SET);
int ret2 = fgetc(pa);
printf("ret2 = %c\n", ret2);//结果为c
fclose(pa);
pa = NULL;
return 0;
}
6.2ftell函数
函数原型:
c
long ftell( FILE *stream );
该函数是返回文件指针相对于起始位置的偏移量的函数
函数参数:
该函数只有一个参数,就是文件指针
函数返回值:
成功时,返回一个非负的长整形值,表示文件指针距起始位置的偏移量
失败时,返回-1L
函数使用:
c
int main()
{
FILE* pa = fopen("example.txt", "r");
if (pa == NULL)
{
perror("fopen");
return 1;
}
int ret1 = fgetc(pa);
printf("ret1 = %c\n", ret1);
fseek(pa, 4, SEEK_SET);
int re = ftell(pa);
printf("re = %d\n", re);//结果为4
fclose(pa);
pa = NULL;
return 0;
}
6.3rewind函数
函数原型:
c
void rewind( FILE *stream );
该函数是让文件指针的位置返回到起始位置的函数
函数参数:
很简单,只有一个文件指针
函数返回值:
函数没有返回值
函数使用:
c
int main()
{
FILE* pa = fopen("example.txt", "r");
if (pa == NULL)
{
perror("fopen");
return 1;
}
int ret1 = fgetc(pa);
printf("ret1 = %c\n", ret1);//结果是a
fseek(pa, 4, SEEK_SET);//改变文件指针的位置
rewind(pa);//再将文件指针的位置指向起始位置
int ret2 = fgetc(pa);
printf("ret2 = %c\n", ret2);//结果还应该是a
fclose(pa);
pa = NULL;
return 0;
}
7.文件读取结束的判定
在C语言中,我们可以使用feof函数来判断文件读取结束的原因是否是遇到文件结尾
函数原型:
c
int feof( FILE *stream );
函数参数:
文件指针
函数返回值:
如果stream指向的文件流已经到达文件结尾,返回非0值(通常为1)
如果没有到达文件末尾,返回0
函数使用:
c
int main()
{
FILE* pa = fopen("example.txt", "r");
if (pa == NULL)
{
perror("fopen");
return 1;
}
int c; // 注意:int,⾮char,要求处理EOF
//fgetc 当读取失败的时候或者遇到⽂件结束的时候,都会返回EOF
while ((c = fgetc(pa)) != EOF) // 标准C I/O读取⽂件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(pa))//ferror函数参数和feof函数相同,如果检测到错误,函数返回非0值,否则返回0
puts("I/O error when reading");
else if (feof(pa))
puts("End of file reached successfully");
fclose(pa);
pa = NULL;
return 0;
}
对于ferror函数,检测到错误后会将错误标志设置为非0,如果需要再次使用ferror函数检测后续操作的错误,需要先调用clearerr函数来清除错误标志
8.文件缓冲区
ANSIC标准采用"操作文件系统"来处理数据文件,其功能其实就是系统会自动地为程序中每一个正在使用的文件开辟一块"文件缓冲区",从内存向磁盘输入的数据都会先送到内存中的缓冲区,缓冲区填满后才将缓冲区中的数据写入磁盘
如果从磁盘向计算机读⼊数据,则从磁盘⽂件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的⼤⼩根据C编译系统决定的。原理如下:
我们可以写一个程序观察缓冲区的存在:
c
// VS2022 WIN11环境测试
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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。