概述
前言
之前编写通讯录程序时,由于数据都是保存在内存上,每次关闭通讯录后,都需要重新再输入信息。有没有办法将数据持久化的保存在硬盘中呢?这就涉及到文件操作的知识。本文将介绍文件操作相关函数的使用,并对之前写过的通讯录程序进行修改。
文件分类
从文件功能的角度来分类:
1. 程序文件
比如在用vs写代码时创建的源文件(.c),目标文件(windows环境后缀为.obj),执行文件(.exe)
2. 数据文件
比如程序运行需要从中读取数据的文件,或者需要写入数据进去的文件。
此前我所写的所有程序都是以终端为数据文件,如printf是在有需要时将数据打印在屏幕上,scanf是将键盘上的输入数据读取进程序文件中。现在不过是将终端变化为数据文件,这样就好理解多了。
文件名
文件名是唯一的文件标识,有了文件名才能准确找到文件的位置并进行操作。
文件名包含三部分:文件路径+文件名主干+文件后缀
比如:D:Study新建文本文档.txt
文件指针
进行文件操作时,我们需要一个桥梁沟通程序和我们希望进行操作的数据文件。这个桥梁就是文件指针FILE*
。
每个被使用的文件都会在内存中开辟一段文件信息区域,用于存放文件的相关信息,如文件名,文件的状态,当前的位置等。而FILE是编译器提供的一个结构体类型,文件信息区的内容被保存在FLIE类型的变量里。
FILE* pf;//文件指针类型变量
文件操作函数
1. 文件的打开和关闭
文件打开,使用函数fopen,关闭使用fclose。
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
当fopen失败时,会返回空指针。因此最好加上判空。
其中,const char * mode
是指输入对文件操作的不同指令字符,即“打开方式”。
打开方式汇总如下表:
比如我要打开文件,并读取文件中数据,则:
FILE* pf = fopen("test.txt","r");//用只读方式打开test.txt文件
FILE* pf = fopen("D:\Studytest.txt","r");//当使用文件路径进行访问时,需要在盘符后多加一个''进行转义
2. 文件的顺序读写
关于文件操作,我们可以使用以下函数:
顾名思义,fgetc和fputc是向文件输出从文件中读取字符的函数,c == char。当成功时,fgetc和fputc会返回其得到写入字符的ASC码。并将文件的位置标识符后移一位。
同理,fgets和fputs中,s == string。
表中函数各功能与返回值均有所不同,在此不一一介绍。
2.1 两组输入/输出函数的对比
为了更深入的理解各个函数的功能,接下来对scanf/fscanf/sscanf;printf/fprintf/sprintf这两组函数的异同之处进行比较测试。
其中,scanf、printf和fscanf、fprintf的区别在于,后两者引入了流。
以scanf和fscanf为例,查看函数定义可知:
scanf函数是从标准输入流——即键盘上读取数据的。
fscanf(stdin,"%d",&a);
和 scanf("%d",&a);
的效果相同,stdin就是标准输入流的意思。
如此一来fscanf的功能就很明确了——从一个输入流中读取数据到目标变量中。
用例如下:
先创建一个文件test.txt。将其内容编辑为15并保存退出。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void test()
{
int a = 0;
//打开文件
FILE* pf = fopen("D:\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//使用
fscanf(pf, "%d", &a);//将文件中信息读取进变量a中
printf("%d", a);
//关闭文件
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行后我们就会惊喜的发现,屏幕上输出了15。
同理,fprintf的功能即为将a中的数据打印进目标文件中。用例如下:
先将test.txt中数据清除,再运行以下代码:
void test()
{
int a = 15;
//打开文件
FILE* pf = fopen("D:\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return;
}
//使用
fprintf(pf, "%d", a);
//关闭
fclose(pf);
pf = NULL;
}
int main() {
test();
return 0;
}
运行后打开文件,发现文件内容已被修改为15。
而sscanf和sprintf两个函数与上述的区别则在于,这两个函数并非从流中读取写入数据,而是从字符串中读取写入格式化的数据。
用例如下:
struct s
{
char name[10];
int age;
double score;
};
void test()
{
char arr[20] = { 0 };
struct s s1 = { "zhang",20,90.1 };
//使用sprintf,将结构体内容转化为字符串并存入字符数组。
sprintf(arr, "%s %d %lf", s1.name, s1.age, s1.score);
//打印arr内容看是否成功存入
printf("%s", arr);
//使用sscanf,从字符串中读取格式化数据
struct s s2 = { 0 };
sscanf(arr, "%s %d %lf", s2.name, &(s2.age), &(s2.score));
//打印s2看是否成功读取
printf("%s %d %lf", s2.name, s2.age, s2.score);
}
int main()
{
test();
return 0;
}
运行程序后得:
说明从字符串读取写入的功能均测试成功。
3. 文件的随机读写
3.1 fseek()
此处介绍文件随机读写所用到的函数:fseek
查看函数定义可知,fseek是用来改变文件指针位置的函数。其第一个参数为文件指针,第二个参数为偏移量,第三个参数表示开始添加偏移 offset 的位置,可选择三个值:
- SEEK_SET 文件的开头
- SEEK_CUR 文件指针的当前位置
- SEEK_END 文件的末尾
当fseek()成功时,返回0,否则返回非零值。
比如,我们可以如此使用fseek:
//文件随机读写
void test()
{
FILE* pf = fopen("D:\test.txt", "r+");//注意此处需要使用读写,r+和 w+都可以。
if (pf == NULL)
{
perror("fopen");
return;
}
fputs("Hello, Mike!", pf);
fseek(pf, -5, SEEK_END);//从文件的末尾向前偏移5个字符
fputs("Jill!", pf);
fseek(pf, 0, SEEK_SET);
//这里也可使用rewind(pf),将文件指针位置回到文件起始位。
char arr[50] = { 0 };
fread(arr, 1, 12, pf);//还有一个'