文件的分类
文件我们都已经熟知,但在程序设计的时候文件分为两种,一种是程序文件,一种是数据文件(从文件的功能来分类)。
程序文件:
程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows 环境后缀为.exe)。
数据文件:
文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或 者输出内容的文件。
但是这篇文章主要讨论数据文件。
文件名:
⼀个文件要有⼀个唯⼀的文件标识,以便用户识别和引用。 文件名包含3部分:文件路径+文件名主干+文件后缀 ,例如: c:\code\test.txt 为了方便起见,文件标识常被称为文件名。
数据文件的两种形式:
根据数据的组织形式,数据文件被称为文本文件或者⼆进制文件。
数据在内存中以⼆进制的形式存储,如果不加转换的输出到外存的文件中,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
流的介绍及其包含的内容:
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出 操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流想象成流淌着字符的河。 C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
那为什么我们从键盘输⼊数据,向屏幕上输出数据,并没有打开流呢? 那是因为C语言程序在启动的时候,默认打开了3个流:
1.stdin-标准输⼊流,在大多数的环境中从键盘输入,scanf函数就是从标准输⼊流中读取数据。
2. stdout-标准输出流,大多数的环境中输出至显示器界面,printf函数就是将信息输出到标准输出 流中。
3.stderr-标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。 stdin、stdout、stderr三个流 的类型是: FILE* ,通常称为文件指针。 C语言中,就是通过 FILE* 的文件指针来维护流的各种操作的。
文件的打开和关闭:
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。 在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建立了 指针和文件的关系。 ANSIC规定使用 fopen 函数来打开文件, fclose 来关闭文件。
这里有一个笼统的文件模版:
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );
下面是文件的打开模式:
下面是一个小例子(便于理解):
#include <stdio.h>
int main ()
{
//设置文件指针变量
FILE*pf;
//打开文件
pf=fopen("test.c","w");
//判断文件是否建立,防止野指针的出现
if(pf==NULL)
{
//用perror来判断错误的原因
perror("pf");
//如果错误退出程序
return 1;
}
fclose(pf);
//关闭文件时,不要忘了将指针变为空,防止野指针的出现
pf=NULL;
return 0;
}
读写函数的介绍:
(在这里的输出是指将数据写入文件内,输入是指将数据从文件内写到程序上)
fputc与fgetc的介绍:
通过cplusplus的搜索可以看到fputc的用法,前面我们要输入一个字符,第二个我们要输入一个流。
以下便是一个例子:
在这里我输入完后进行了编译,出现我这样的情况,编译通过,那么他便创建了一个文件叫test.c
打开文件:
我们可以看到这里面有我刚刚传入的字符a。
(公司是翻译器的问题,没有公司二字)
我们可以看到fgetc的定义是int的类型,那么当他输入时会返回整数。
我通过循环来输出文件里面的值。
下面是我们在开始的时候容易犯的错误:
这种编译后就没有出现我们文件中的值,这是因为我们在打开文件的时候,是按照只写("w")的方式打开,所以就不会出现我们的结果。
在这里我又新开了一个文件叫tests.c,当我用只读的方式来写时,即使我有fputc,他也是错误的,因为他不会帮你开辟这个文件。所以会报错,告诉你没有这个文件(这个是perror的功能) 。
fputs与fguts的介绍:
fputs与fputc的区别就是一个输入字符串,一个输入字符。
所以你要输入一个abcdef时,可以一步到位
那么我想输入两段字符串,但是我又不想让他们在同一行,这怎么办?
下面便是解决方法;
fgets有三个变量,第一个是从你所指定的流中读取到的数据的拷贝,第二个是你让我读取多少个数据,第三个是流。(但要注意的是用fgets,如果你要他读取10个,他只会读取9个,最后一个放置\0,这便是第一行为什么他会读取num-1的原因)
那么我怎样来读取两行的数据呢?
我们可以通过循环来完成,我们可以知道fgets是char*类型的,所以当我读取时,只要不是空指针,便会一直读取下去。
fprintf与fscanf的介绍:
接下来我们通过对比的形式来看fprintf
我们可以看到,他与printf就相差一个流,那么我们可以先按照printf的形式来写,最后加上这个流即可 。
下面是一个例子
我们看到流的地方只需要写上你要存入的文件指针即可,其他的地方与printf没有区别。
与fprintf类似,fscanf与scanf也只差一个流。它是将文件中的数据写到程序中。
下面是一个举例:
我从文件中,拿出了其中的数据,然后给了一个新定义的结构体。
fwrite与fread的介绍:
fwrite是二进制的输出函数。第一部分是你要拷贝的内容,第二部分是拷贝文件的每一个大小,第三个是你要拷贝的个数,第四部分便是流(你要把这些拷贝到哪里去)。
下面是一个举例:
sizeof(arr)是数组arr的整体大小,sizeof(arr[0])求的是数组一个是多大,两个相除便可以求出数组一共有多少个。
我们可以看到用二进制写的代码,他的内容我们是看不懂的,所以我们要用特殊的方法 。
下面是fread的介绍,我们可以看到,fread与fwrite的格式是一样的。它是将文件中的数据拷贝到你的第一部分。
下面是一个例子 :
我们可以看到他成功打印出了我们文件的内容。
但是这是我们在知道文件有多少个的情况下打印的,如果我们不知道有多少个呢?
这是fread函数介绍的一部分,我们可以根据这一性质来进行输出
我们每次只读取一个,如果读取成功则返回的值为1,那么就会拷贝到a【i】里面去,我们在输出数组a的内容,如果遇到结尾,则fread的返回值为0,说明拷贝完成。
文件的随机读写:
1.fseek:
指针的偏移
int fseek ( FILE * stream, long int offset, int origin );
int origin 的位置可以填写
SEEK_SET(文件指针起始位置) SEEK_CUR(文件指针当前位置)
SEEK_END(文件指针末尾)
第二部分可以填写负数,指向左移动。
include<stdio.h>
int main ()
{
FILE * pFile;
pFile = fopen ( "example.txt" , "wb" );
fputs ( "This is an apple." , pFile );
fseek ( pFile , 9 , SEEK_SET );
fputs ( " sam" , pFile );
fclose ( pFile );
return 0;
}
2.ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
以下是一个例子:
#include <stdio.h>
int main ()
{
FILE * pFile;
long size;
pFile = fopen ("myfile.txt","rb");
if (pFile==NULL)
perror ("Error opening file");
else
{
fseek (pFile, 0, SEEK_END); // non-portable
size=ftell (pFile);
fclose (pFile);
printf ("Size of myfile.txt: %ld bytes.\n",size);
}
return 0;
}
3.rewind
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
文件读取结束的判定:
1.feof:
当文件读取结束的时候,判断是读取结束的原因是否是:遇到文件尾结束。
int feof ( FILE * stream );
文件结束了,如果为非零值则文件正常读取结束(读取到文件末尾)。如果结果为0,则运算过程中有错误。
2.ferror
检查是否发生错误而结束
int ferror ( FILE * stream );
如果发生错误结束则返回非零的数,否则为零。
#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)!=0)
puts("I/O error when reading");
else if (feof(fp)!=0)
puts("End of file reached successfully");
fclose(fp);
}
文件缓冲区:
ANSIC标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为 程序中每⼀个正在使用的文件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓 冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘文件中读取数据输 入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
以下是一个小例子来说明缓冲区的存在
#include <stdio.h>
#include <windows.h>
//VS2019 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语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。