我们之前的程序都是运行完结束,再次运行要重新开始,我们打游戏都会有一个存档,可以让我们回到上次游戏结束时位置,这就需要文件操作。文件有分为好几类,我们重点关注的是数据文件。
在程序设计中,我们一般分两种文件:程序文件和数据文件。
程序文件:包括源程序文件(后缀为.c),目标文件(在windows下为.obj)和可执行程序(在windows下为.exe)。
数据文件:文件内容不一定为程序,是程序运行时读写的数据。
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3个部分:文件路径+文件名主干+文件后缀。
例如:C:\Users\test.txt
为了方便,文件标识被称为文件名。
文件类型根据数据的组织形式,数据文件又可以叫做文本文件或二进制文件。
数据在内存中以二进制的形式存储的,如果不加转换的输出到外存就是二进制文件。如果要求在外存中用ASCLL码的形式存储,则需要在存储前进行转换,以ASCLL码的形式存储的文件就是文本文件。
ANSIC标准采用缓冲文件系统处理数据文件的。所谓的缓冲文件系统是指系统自动地在内存中为程序中每一个使用地文件开辟一块文件缓冲区。从内存向磁盘输出数据会先到内存的缓冲区中,装满缓冲区时才被一起送到磁盘上,如果读入数据,也先将数据读入缓冲区,在从缓冲区把数据送入程序数据区。缓冲区的大小由C编译系统决定。
缓冲区也分为三种类型:
全缓冲:在全缓冲中,只有填满标准I/O缓冲后才进行实际I/O操作(缓冲区填满进行操作)。全缓冲代表就是上面图中对磁盘的读写操作。
行缓冲:在行缓冲中只有遇到换行符时执行操作。行缓冲代表是我们在键盘输入数据通过换行提示程序。
无缓冲:顾名思义也就是没有缓冲区。
缓冲文件系统中,关键的就是文件类型指针,简称文件指针。
每个被使用的文件都在内存开辟一个相应的文件信息区,用来存放文件的相关信息,这些信息保存在一个结构体变量中,这个结构体名为FILE。
结构体在vs2013具体如下:
#ifndef _FILE_DEFINED
struct _iobuf {
char* _ptr; //文件输入的下一个位置。
int _cnt; //当前缓冲区的相对位置。
char* _base; //指针的基础位置(即是文件的起始位置)。
int _flag; //文件标志。
int _file; //文件的有效性验证。
int _charbuf; //检查缓冲区状况,如果无缓冲区则不读取。
int _bufsiz; //缓冲区的大小。
char* _tmpfname; //临时文件名。
};
typedef struct _iobuf FILE;
#define _FILE_DEFINED
#endif
不同的编译器的FILE类型内容不完全相同,但是大同小异。每当打开一个文件时系统会自动创建一个FILE结构体变量,一般是通过FILE的指针来维护FILE的变量。通过定义的FILE类型的指针变量可以找到与它关联的文件。
FILE* fopen(const char* filename, const char* mode);//打开文件
int fclose(FILE* stream);//关闭文件
在运行时,系统会默认打开3个流:
stdin:标准输入 --------------对应键盘
stdout:标准输出 --------------对应显示器
stderr:标准输出错误信息--------------对应显示器
打开一个文件时系统会自动创建的FILE结构体变量在这三个下面开辟空间。
文件也属于流,我们可以进行输出输入重定向把数据输入到文件中。
文件打开方式 | 含义 | 文件是否存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开一个已有文件 | 是 |
“w”(只写) | 为了输出数据,打开一个文本文件 | 否 |
“a”(追加) | 向文本文件尾追加数据 | 是 |
“rb”(只读) | 为了输入数据,打开一个二进制文件 | 是 |
“wb”(只写) | 为了输出数据,打开一个二进制文件 | 否 |
“ab”(追加) | 向一个二进制文件尾追加数据 | 是 |
“r+”(读写) | 为了读和写,打开一个文本文件 | 是 |
“w+”(读写) | 为了读和写,建立一个新的文件 | 否 |
“a+”(读写) | 打开一个文件,在文件尾进行读写 | 否 |
“rb+”(读写) | 为了读和写打开一个二进制文件 | 是 |
“wb+”(读写) | 为了读和写,建立一个新的二进制文件 | 否 |
“ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 否 |
如果打开方式需要文件存在时文件不存在,则会报错。如果打开方式需要文件不是必须存在时,没有文件会进行创建,有文件时会进行截断或追加。
图片所展示的是linux下的打开方式,在vs和linux下打开的方式大同小异。
Linux版本的翻译如下:
r打开文本文件进行阅读。流位于文件的开头。
r+开放阅读和写作。流位于文件的开头。
w将文件截断为零长度或创建用于写入的文本文件。流位于文件的开头。
w+开放阅读和写作。如果文件不存在,则会创建该文件,否则会截断该文件。流位于文件的开头。
a打开以进行追加(在文件末尾写入)。如果文件不存在,则会创建该文件。流位于文件的末尾。
a+打开以进行读取和追加(在文件末尾写入)。如果文件不存在,则会创建该文件。用于读取的初始文件位置在文件的开头。
截断会清空文件内容,并且把文件指针定位在开始处。
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输入函数 | fputc | 所有输入流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输入流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输入流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
int fgetc(FILE* stream);
int fputc(int c, FILE* stream);
标准输入输出代码如下:
int main()
{
int ch;
while ((ch = fgetc(stdin)) != EOF)//从标准输入中获得数据
{
fputc(ch,stdout);//向标准输出中输出数据
}
return 0;
}
文件输入输出代码如下:
我们先在目录下创建一个文本文档,里面写入一些内容。
int main()
{
int ch;
FILE* fre = fopen("test1.txt", "r");
if (fre == NULL)
{
perror("fopen()");//报错并且结束程序
return 0;
}
FILE* fwr = fopen("test2.txt", "w");
if (fwr == NULL)
{
perror("fopen()");
return 0;
}
while ((ch = fgetc(fre)) != EOF)//从标准输入中获得数据
{
fputc(ch,fwr);//向标准输出中输出数据
}
fclose(fre);
fclose(fwr);
return 0;
}
我们发现什么结果都没有,让我们在文件中看看。
我们发现出现了一个文本2,且内容和文本1一模一样。
char* fgets(char* s, int size, FILE* stream);//s存放所获得的数据,size获得数据的大小,stream获得数据的地方
int fputs(const char* s, FILE* stream);
标准输入输出代码如下:
int main()
{
char ch[1024] = { 0 };
fgets(ch,1024,stdin);
fputs(ch, stdout);
return 0;
}
文件输入输出代码如下:
我们先把文本文件1的内容复制10行。
int main()
{
char ch[1024] = { 0 };
FILE* fre = fopen("test1.txt", "r");
if (fre == NULL)
{
perror("fopen()");//报错并且结束程序
return 0;
}
FILE* fwr = fopen("test2.txt", "w");
if (fwr == NULL)
{
perror("fopen()");
return 0;
}
while ((fgets(ch,1024,fre)) != NULL)
{
fputs(ch,fwr);
}
fclose(fre);
fclose(fwr);
return 0;
}
int fscanf(FILE* stream, const char* format, ...);
int fprintf(FILE* stream, const char* format, ...);
标准输入输出代码如下:
struct peo
{
char name[10];
int age;
int id;
};
int main()
{
struct peo p;
fscanf(stdin, "%s %d %d" ,&(p.name), &(p.age), &(p.id));
fprintf(stdout, "%s %d %d", p.name,p.age,p.id);
return 0;
}
struct peo
{
char name[10];
int age;
int id;
};
int main()
{
struct peo p;
FILE* fre = fopen("test1.txt", "r");
if (fre == NULL)
{
perror("fopen()");//报错并且结束程序
return 0;
}
FILE* fwr = fopen("test2.txt", "w");
if (fwr == NULL)
{
perror("fopen()");
return 0;
}
while (fscanf(fre, "%s %d %d", &(p.name), &(p.age), &(p.id)) != EOF)
{
fprintf(fwr, "%s %d %d", p.name,p.age,p.id);
}
fclose(fre);
fclose(fwr);
return 0;
}
让我们看看运行结果吧!
这里我们少加了换行符,从内容上看还是从文件中读向另一个文件写。
size_t fread(void* ptr, size_t size, size_t nmemb, FILE* stream);//ptr存放所获得的数据,size读取字节大小,nmemb读取次数
size_t fwrite(const void* ptr, size_t size, size_t nmemb, FILE* stream);
我们先看一下Linux下的介绍:
翻译如下:
函数fread()从流指向的流中读取nmmb个数据元素,每个大小字节长,并将它们存储在ptr给定的位置。
函数fwrite()将nmmb个数据元素(每个大小字节长)写入流所指向的流,并从ptr给定的位置获取它们。
返回值:
成功后,fread()和fwrite()返回读取或写入的项目数。这个数字等于仅当大小为1时传输的字节数。如果发生错误,或者到达文件末尾,则返回值为短项目计数(或零)。
fread()不区分文件结尾和错误,调用方必须使用feof(3)和ferror(3)来确定发生的错误。
从上面翻译来看,我们有两种形式:
//当只有5个字节时
fread(arr, 1, 10, fp);
// 可以读取5个字节并返回值为5
fread(arr, 10, 1, fp);
//读取不够10个字节,进行报错。
代码测试案例如下:
int main()
{
char ch[1024] = { 0 };
int num = 0;// 用来存储从fread中的返回值
FILE* fre = fopen("test1.txt", "r");
if (fre == NULL)
{
perror("fopen()");
return 0;
}
FILE* fwr = fopen("test2.txt", "w");
if (fwr == NULL)
{
perror("fopen()");
return 0;
}
while ((num = fread(ch, 1, 1024, fre)) != 0)
{
fwrite(ch, 1, num, fwr);
}
fclose(fre);
fclose(fwr);
return 0;
}
注意:fread不一定读够我们所设置的字节的大小,所以我们用fwrite时不应该直接用我们所设置读的大小,最好的方法是用一个变量报存我们所读到的字节数,用这个字节数决定写入多少字符。
int scanf(const char* format, ...);
int fscanf(FILE* stream, const char* format, ...);
int sscanf(const char* str, const char* format, ...);
int printf(const char* format, ...);
int fprintf(FILE* stream, const char* format, ...);
int sprintf(char* str, const char* format, ...);
(scanf/printf)(fscanf/fprintf)(sscanf/sprintf)的区别:
scanf/printf 是针对标准输入输出流的 格式化输入输出语句。
fscanf/fprintf 是针对所有输入输出流的 格式化输入输出语句。
sscanf/sprintf 中 sscanf是从字符串中读取格式化的数据,而sprintf是把格式化数据输出成(存储到)字符串。
int fseek(FILE* stream, long offset, int whence);
long ftell(FILE* stream);
函数的作用是为流所指向的流设置文件位置指示符。以字节为单位测量的新位置是通过将偏移字节添加到由where指定的位置而获得的。如果where设置为SEEK_set、SEEK_CUR或SEEK_END,偏移量分别相对于文件的开始、当前位置指示器或文件的结束。成功调用fseek()函数将清除流的文件结尾指示符,并取消
ungetc(3)在同一个流上起作用。
ftell()函数获取流所指向的流的文件位置指示符的当前值。函数的作用是将流指向的流的文件位置指示器设置为文件的开头。它相当于:(无效)fseek(流,0L,SEEK_SET)
我们看一个具体的例子:
int main()
{
FILE* fre = fopen("test1.txt", "r");
if (fre == NULL)
{
perror("fopen()");
return 0;
}
fseek(fre, -4, SEEK_END);
int pos = ftell(fre);
printf("%d", pos);
fclose(fre);
return 0;
}
文件结束判定中feof
在文件读取过程中,不可以用feof函数的返回值来直接判读是否文件结束。而是应用于当文件读取结束时,判读是读取失败还是遇到了文件尾结束。
例如:
fgetc判读是否为EOF
fgets判读是否为NULL
文件操作可以让我们实现很多的操作,方便存取等,各种设备也可以看作文件,在Linux中,一切皆文件,我们可以通过文件实现各种功能,所以学好文件可以让我们更近一层楼。