前言:在我们每次编写程序的时候,我们所输入的各种数据都伴随程序的结束而消失,那有没有一种方法可以永久的保存我们每一次的数据呢?当让可以,我们只需要将各种数据都存储在文件当中,当我们在想使用的时候,只需打开文件导入数据即可,下面我们来看一下C语言当中的文件操作是怎样的吧。
一.什么是文件
所谓“文件”,就是在我们的计算机中,以实现某种功能、或某个软件的部分功能为目的而定义的一个单位。
在程序设计当中我们将文件分文两类,一类是 程序文件,一类是 数据文件.
1.程序文件
程序文件分为:源程序文件(后缀是" .c "的文件),目标文件(Windows环境下后缀是 '' .obj ''),可执行文件 (Windows环境下后缀是 '' .exe '').
2.数据文件
文件的内容不是程序,而是供程序运行时读写的数据,如在程序运行过程中输出到磁盘(或其他外部设备)的数据,或在程序运行过程中供读入的数据.
二.文件名
文件名是文件存在的标识,操作系统根据文件名来对其进行控制和管理。 不同的操作系统对文件命名的规则略有不同,即文件名的格式和长度因系统而异。 为了方便人们区分计算机中的不同文件,而给每个文件设定一个指定的名称。
例如:
上面的test.c就是在 此电脑目录下的: F盘目录下的: train目录下的: my_strstr目录下的: my_strstr目录中 。
三.文件的操作
1.文件指针
文件指针是一个指向文件的数据结构,用于跟踪文件中的位置。它类似于内存中的指针,但不是指向内存位置,而是指向文件中的位置。每个打开的文件都有一个相关联的文件指针,该指针跟踪文件中的当前位置,以便程序可以在文件中进行读取和写入操作。
在C语言中,文件指针通常表示为指向 FILE 结构体的指针,FILE 结构是C标准库中定义的一个结构,用于表示文件的属性和状态等各种信息。以下VS2013中所定义的FILE类型的结构体:
struct _iobuf {
char* ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
所以文件指针我们就可以如下定义:
FILE* pf;
我们来看一下文件指针跟文件之间的关系示意图:
说明:1->不同的编译器中的FILE类型的结构体里面的各个成员有所差异;
2->每当我们打开一个文件,操作系统会根据文件的情况自动创建一个FILE类型的结构体变量,并将FILE类型的结构体变量中的每个成员进行赋值以存入文件的各种信息,使用者不必关心;
3->一般我们通过一个FILE*类型的指针来维护对应的FILE类型的结构体变量,通过FILE类型的结构体变量中所存入的文件信息,操作者就可以找到对应的文件进行读写操作。
2.文件操作函数
在我们想要进行文件的读写时,我们需要打开对应的文件进行操作,所以我们需要用到 fopen 函数来打开文件,在打开文件并且对文件操作完之后,我们还需要用到 fclose 函数来关闭文件,这是为了结束一个实例,来释放内存,每一个程序能打开的文件资源是有限制的;如果只打开文件,用完之后不关闭,则可能造成内存溢出。
下面我们来学习一下文件的打开与关闭;
fopen 与 fclose
1-> 我们来看一下 fopen 函数是如何使用的,下面是标准库中的函数定义:
FILE * fopen ( const char * filename, const char * mode );
我们可以看到fopen有两个参数,一个是 const char* 类型的 filename ,代表的是我们要打开的文件名(可以是相对路径也可以是绝对路径);还有一个参数是 const char* 类型的 mode ,代表的是操作者要以什么样的形式来打开文件;返回的是一个 FILE* 类型的指针,若打开失败则会返回一个 NULL(空指针).
例如下面的 text.txt 就是相对路径:
FILE* pf = fopen("text.txt","w"); // w 表示以写的形式打开
当然我们也可以传入绝对路径:
FILE* pf = fopen("F:\\train\\fopen函数的使用\\fopen函数的使用\\text.txt", "w");
注意:这里要使用 '' \\ '' ,因为单个的 '' \ ''会被编译器理解为转义字符,所以C语言中 '' \\ '' 才表示的是 '' \ ''。
当然,fopen 的第二个参数 mode 的打开方式有许多种,下面列举出了各个打开方式:
注意:当打开的方式为 " w " \ " wb " \ " wb+ "时,会清空文件原有的内容.
2-> 下面我们来看一下fclose函数在标准库中是如何定义的吧:
int fclose ( FILE * stream );
可以看到函数只有一个参数 : FILE*类型的 stream,它代表的意思是 指向指定要关闭的流的 FILE 对象的指针,也就是我们需要传入一个 FILE* 类型的指针;他的返回类型是 int ,如果流(文件)成功关闭,则返回零值;失败时,则返回 EOF(-1)。
有了上面的基础,接下来我们来看一下 fopen 与 fclose 函数具体是如何使用的:
int main()
{
//一、打开文件,"text.txt"是文件名;"w"是write的缩写,表示以 写 的形式打开文件
FILE* pf = fopen("text.txt", "w");
//倘若打开文件失败,pf得到的就是空指针,所以要判断pf的有效性
if (pf == NULL)
{
//perror可以打印出在 fopen 这里发生的错误信息
perror("fopen");
//结束程序
return 1;
}
//二、对文件的操作
//··········
//··········
//三、关闭文件;判断文件是否关闭失败
if (fclose(pf) == EOF)
{
//打印出在 fclose 这里发生的错误信息
perror("fclose");
}
//将指针置空防止野指针出现
pf = NULL;
return 0;
}
3.文件的读写函数
在文件打开之后我们可以对文件进行读(输入)写(输出)操作,在读写文件的时候,分为了两种读写方式,分别是: 顺序读写 与 随机读写.
1.顺序读写
下面是我们顺序读写时常用的函数:
fputc(字符输出) 与 fgetc(字符输入)函数
fputc 函数
我们先来看一下标准库是如何定义此函数的:
int fputc ( int character, FILE * stream );
我们可以看到他的第一个参数是 character ,也就是我们要写入的字符;第二个参数 stream 指向标识输出流的 FILE 对象的指针。
1-> 成功后,将返回所写字符。
2->如果发生写入错误,则返回 EOF 并设置错误指示器 (ferror)。
#include<stdio.h>
int main()
{
//我们要写文件所以用 "w" 的形式打开
FILE* pf = fopen("text.txt", "w");
//判断pf指针的有效性
if (pf == NULL)
{
perror("fopen");
return 1;
}
for (char ch = 'A'; ch <= 'Z'; ch++)
{
//我们将所有大写字母写入 "text.txt" 这个文件
fputc(ch, pf);
}
//关闭文件并检验是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
我们可以在程序文件的目录中查看是否写入成功:
注意:当以 “w”/“wb”/“wb+” 写的形式打开文件时,如果文件不存在,会自动创建一个文件。
我们可以看到确实将 A~Z 之间的字符都写入了 text.txt 这个文件里,那么我们如何将其拿出呢?
接下来就需要用到 fgetc 函数:
int fgetc ( FILE * stream );
stream->表示指向标识输入流的 FILE 对象的指针。
成功后,将返回读取的字符(提升为 int 值)。然后,内部文件位置指示器将前进到下一个字符。
返回类型为 int 以适应特殊值 EOF(-1),该值指示失败:
如果位置指示器位于文件末尾,则函数返回 EOF 并设置流的 eof 指示符 (feof)。
如果发生其他读取错误,该函数也会返回 EOF,但会设置其错误指示器 (ferror)。
下面来看一下如何使用:
#include<stdio.h>
int main()
{
//我们要写文件所以用 "r" 的形式打开
FILE* pf = fopen("text.txt", "r");
//判断pf指针的有效性
if (pf == NULL)
{
perror("fopen");
return 1;
}
char a;
//fgetc如果读取成功返回的字母的ASCII码值,
// 并且内部文件位置指示器将前进到下一个字符,
// 直到读取结束或者读取遇到错误
while ((a = fgetc(pf)) != EOF)
{
printf("%c ", a);
}
//关闭文件并检验是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
运行结果:
fputs 与 fgets函数
fputs 函数
fputs函数可以一次性向文件当中写入一个字符串,下面来看一下他的定义:
int fputs ( const char * str, FILE * stream );
str->指向一个字符串的指针;
stream->指向标识输出流的 FILE 对象的指针。
该函数从指定的地址 (str) 开始复制,直到到达终止 null 字符 ('\0')。此终止 null 字符不会复制到流(文件)中。
成功后,将返回一个非负值。
出错时,该函数返回 EOF(-1) 并设置错误指示器 (ferror)。
下面是具体使用情况:
#include<stdio.h>
int main()
{
//以 "w" (写) 的形式打开文件
FILE* pf = fopen("text.txt", "w");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
//向文件中存入数据
fputs("Hello ", pf);
fputs("Bit", pf);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
运行结果:
可以看到写入文件成功,那么我们如何一次性将写入的字符串读出呢,下面就要用到 fgets 函数:
char * fgets ( char * str, int num, FILE * stream );
str->字符串指针,用于接收从文件中读出的字符串;
nums->要复制到 str 中的最大字符数(包括终止 '\0' 字符);
stream->指向标识输入流的 FILE 对象的指针。
从文件中读取字符,并将它们作为字符串存储到 str 中,直到读取 (num-1) 个字符或到达换行符或文件末尾,以先发生者为准。
换行符("\n")使 fgets 停止读取,但它被函数视为有效字符,并包含在复制到 str 的字符串中。
终止 '\0' 字符会自动附加到复制到 str 的字符之后。
成功后,该函数返回 str。
如果在尝试读取字符时遇到文件末尾,则设置 eof 指示符 (feof)。如果在读取任何字符之前发生这种情况,则返回的指针为空指针(并且 str 的内容保持不变)。
如果发生读取错误,则设置错误指示符 (ferror),并返回 null 指针(但 str 指向的内容可能已更改)。
实际应用:
#include<stdio.h>
int main()
{
//以 "r" (读) 的形式打开文件
FILE* pf = fopen("text.txt", "r");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
char str[20];
//将文件中的数据读出10个字符并存入 str 中
fgets(str,10 ,pf);
printf(str);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
运行结果:
注意:若文件中的字符串有3行, 因为 fgets 函数遇到换行符时会将换行符读取,并且停止读取下一行的字符,所以最少需要读取3次才能读取文件中3行的内容。
fprintf 与 fscanf函数
上面我们所用到的函数都是对字符进行读写操作,但 fprintf 与 pscanf 函数就可以向文件进行读写任意数据.
fprintf 函数 与 printf 函数的用法十分相似,我们来看一下它们定义中的差别:
int printf ( const char * format, ... );
int fprintf ( FILE * stream, const char * format, ... );
format-> 表示格式说明符(以 % 开头的子序列,%d,%f,%c等等);
...->后面的省略号代表的是我们所要输出的各种数据;
stream->指向标识输出流(文件)的 FILE 对象的指针。
成功后,将返回写入的字符总数。
fprintf 如果发生写入错误,则设置错误指示符 (ferror) 并返回负数.
实际使用:
#include<stdio.h>
int main()
{
//以 "w" (读) 的形式打开文件
FILE* pf = fopen("text.txt", "w");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
char name[20] = "张三";
int age = 18;
int hight = 180;
//将数据写输出到pf指向的这个文件里面
fprintf(pf, " %s\n%d\n%d\n", name, age, hight);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
输出结果:
可以看到fprintf 与printf 的用法非常相似,那么fprintf可不可以做到输出到屏幕上呢,当让可以,我们只需要将 传入stream 的参数 pf 改为 stdout (标准输出流,系统自动打开,不需要关闭),即可输出到屏幕上:
int main()
{
char name[20] = "张三";
int age = 18;
int hight = 180;
fprintf(stdout, "%s\n%d\n%d\n", name, age, hight);
return 0;
}
运行结果:
接下来我们来对比一下fscanf 与 scanf 函数之间的差别:
int scanf ( const char * format, ... );
int fscanf ( FILE * stream, const char * format, ... );
可以看到,他们之间也只是相差一个参数 stream,那么对于scanf就不需要再多介绍了,下面我们直接上手使用:
#include<stdio.h>
int main()
{
//以 "r" (读取数据) 的形式打开文件
FILE* pf = fopen("text.txt", "r");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
char name[20];
int age;
int hight;
//将pf指向的这个文件里面数据输入到我们程序的各个变量中来
fscanf(pf, "%s%d%d", &name, &age, &hight);
//我们可以打印一下看是否数据输入成功
printf("%s\n%d\n%d", name, age, hight);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
运行结果:
既然fprintf 函数可以做到跟 printf 函数一样的功能,那么fscanf 可以做到跟 scanf 一样的功能吗?
答案是可以的,与fprintf 的修改方法类似,我们将 传给 stream 的pf 改为 stdin(标准输入流,系统自动打开,不需要关闭) ,就可以做到从键盘上输入,下面看一下效果:
int main()
{
char name[20];
int age;
int hight;
fscanf(stdin, "%s%d%d", &name, &age, &hight);
//我们可以打印一下看是否数据输入成功
printf("%s\n%d\n%d", name, age, hight);
return 0;
}
运行结果:
fwrite 与 fread函数
在上面的文件读写函数中,我们所采用的都是文本格式(我们可以看懂的数据格式)进行输入输出,
而 fread 与 fwrite 函数就可以做到二进制数据进行文件中的输入与输出,下面先看一下 fwrite 函数的定义:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
ptr->指向要写入的元素数组的指针;
size-> 要写入的每个元素的大小(以字节为单位);
count->元素个数;
stream->指向指定输出流的 FILE 对象的指针。
返回成功写入的元素总数。
如果此数字与 count 参数不同,则写入错误会阻止函数完成。在这种情况下,将为流设置错误指示器 (ferror)。
如果 size 或 count 为零,则函数返回零,错误指示器保持不变。
下面看实际运用:
#include<stdio.h>
struct stu
{
char name[20];
int age;
};
int main()
{
//以 "wb" (二进制输出) 的形式打开文件
FILE* pf = fopen("text.txt", "wb");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
struct stu s = { "张三",18 };
//将str里面的数据输入到pf指向的文件当中
fwrite(&s, sizeof(s), 1,pf);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
运行结果:
可以看到,以 fwrite 写进文件里的数据是二进制的,我们是无法看懂的,因此要将其中的二进制数据读出来我们就需要用到 fread 函数:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
ptr->指向大小至少为 (size*count) 字节的内存块的指针,以读出的数据能够存储;
size->要读取的每个元素的大小(以字节为单位);
count-> 元素个数;
stream->指向指定输入流的 FILE 对象的指针。
返回成功读取的元素总数。
如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。在这两种情况下,都设置了正确的指标,可以分别使用 ferror 和 feof 进行检查。
如果 size 或 count 为零,则该函数返回零,并且 ptr 指向的流状态和内容保持不变。
下面看一下实际运用:
#include<stdio.h>
struct stu
{
char name[20];
int age;
};
#include<stdio.h>
int main()
{
//以 "rb" (二进制输入) 的形式打开文件
FILE* pf = fopen("text.txt", "rb");
//判断是否打开成功
if (pf == NULL)
{
perror("fopen");
return 1;
}
struct stu s;
//将pf指向的文件输入到s当中
fread(&s, sizeof(s), 1, pf);
//打印s里面的内容看是否输入成功
printf("%s\n%d\n",s.name,s.age);
//关闭文件并判断是否关闭成功
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
//将指针置空
pf = NULL;
return 0;
}
运行结果:
sprnitf 与 sscanf函数
sprnitf 与 sscanf函数的使用方法与 printf 与 scanf 函数的使用方法极其相似,但前者是对于字符串与各种数据类型的转化,下面先看一下 sprintf 与 sscanf的定义:
int sprintf ( char * str, const char * format, ... );
int sscanf ( const char * s, const char * format, ...);
可以看到 sprintf 与 printf 不相同的地方就是多出了一个参数 str ,代表指向存储生成的字符串的缓冲区的指针,并且此缓冲区应足够大,以包含生成的字符串.也就是说printf 是将各种数据输出到屏幕上,而sprintf 则是将各种数据存储到 一个str 指向的缓冲区中;
而 sscanf 中多出的一个参数是 const char * 类型的 s ,s 指向的是一块缓冲区,而 sscanf 可以将缓冲区的数据转换为各种格式化的数据,下面看一下实际应用:
struct stu
{
char name[20];
char sex[5];
int age;
};
int main()
{
struct stu s1 = { "张三","男",18 };
struct stu s2;
char str[40];
//将 s1 中的内容输出到 str 中
sprintf(str, "%s %s %d", s1.name, s1.sex, s1.age);
//打印 str 中的内容看是否输出成功
printf("%s\n",str);
//将 str 字符串中的数据输入到结构体变量s2中的各个成员中
sscanf(str, "%s %s %d", s2.name, s2.sex, &(s2.age));
//打印s2中的内容看str中的数据是否输入成功
printf("%s %s %d\n", s2.name, s2.sex, s2.age);
return 0;
}
运行结果:
2.随机读写
上面我们所用到的所有文件读写函数都是必须按照顺序来读写的,但C语言也为我们准备了随机读写的函数,可以在我们指定的位置下进行对文件的读写操作,首先我们先熟悉下面三种参数:
SEEK_SET | 文件开头位置 |
SEEK_CUR | 文件指针的当前位置 |
SEEK_END | 文件末尾 |
fseek函数
该函数可以设置文件指针指向的位置,下面看一下此函数的定义:
int fseek ( FILE * stream, long int offset, int origin );
stream->指向标识流的 FILE 对象的指针;
origin->可以是下面参数的某一个
SEEK_SET 文件开头位置 SEEK_CUR 文件指针的当前位置 SEEK_END 文件末尾 offset->相对于origin的偏移量.
如果成功,该函数将返回零。
否则,它将返回非零值。
如果发生读取或写入错误,则设置错误指示符 (ferror)。
下面我们实际运用一下:
首先我们先在相应的程序目录底下手动创建一个 text.txt 文件,里面存有 ABCDEFGH.
int main()
{
//以 "r"(读)的形式打开文件
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//SEEK_SET 表示的是文件的起始位置
fseek(pf, 4, SEEK_SET);
char ch = fgetc(pf);
printf("%c", ch);
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
上面我们设置的是相对起始位置而言偏移量为 4 的地址处,那么此时的文件指针所指向的也就是D,我们看一下打印出来结果是否正确:
ftell函数
ftell函数可以得到此时的文件指针相对于起始位置的偏移量,从而知道文件指针此时所指向的位置,下面是函数定义:
long int ftell ( FILE * stream );
它的返回值就是此时相对起始位置的偏移量,下面看一下实际运用:
int main()
{
//以 "r"(读)的形式打开文件
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//SEEK_SET 表示的是文件的起始位置
fseek(pf, 4, SEEK_SET);
printf("%ld", ftell(pf));
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
运行结果:
rewind函数
成功调用此函数后,文件指针将回到起始位置,下面是函数定义:
void rewind ( FILE * stream );
此函数只用将文件指针传入即可,下面看一下实际运用:
int main()
{
//以 "r"(读)的形式打开文件
FILE* pf = fopen("text.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//SEEK_SET 表示的是文件的起始位置
fseek(pf, 4, SEEK_SET);
//回复文件指针的到起始位置
rewind(pf);
//ftell打印此时的文件指针的偏移量
printf("%ld", ftell(pf));
if (fclose(pf) == EOF)
{
perror("fclose");
return 1;
}
pf = NULL;
return 0;
}
运行结果:
四.文件读取结束的判断
feof 与 ferror 函数
在读取文件的过程中,文件读取结束有两种可能,一种是在读取过程中遇到了文件末尾;另一种就是在读取过程中发生错误而结束。而 feof 函数的作用就是:文件的读取已经结束,判断是否是遇到文件末尾而结束。而 ferror 函数则是:文件读取结束,判断是否是因为读取过程中发生错误而结束读取。
- 在文件读取过程中,不能用feof函数的返回值直接来判断文件的是否结束;
- feof 的作用是:当文件读取结束的时候,判断读取结束的原因是否是:遇到文件尾结束。
每当我们打开的一个流(文件)的时候,这个流上都会有2个标记值:
1.eof 指示符 (feof) :读取时遇到文件末尾该指示符会被设置;
2.错误指示符 (ferror):读取时遇到错误该指示符会被设置。
而 feof 与 ferror 函数就是用来检测这两个指示符是否已经被设置。
下面是两个函数定义:
int feof ( FILE * stream );
如果设置了与流关联的文件结束指示符,则返回非零值。
否则,将返回零。
int ferror ( FILE * stream );
如果设置了与流关联的错误指示器,则返回非零值。
否则,将返回零。
下面我们来看一下实际应用:
程序当中所打开的 text.txt 文件已经手动创建好;
int main()
{
FILE* pfout = fopen("text.txt", "r");
if (pfout == NULL)
{
perror("fopen");
return 1;
}
//读文件
char ch;
while ((ch = fgetc(pfout)) != EOF)
{
printf("%c ", ch);
}
//判断读取结束的原因
if (feof(pfout))
{
printf("读取遇到文件末尾\n");
}
else if(ferror(pfout))
{
printf("读取遇到错误\n");
}
//关闭文件
fclose(pfout);
pfout = NULL;
return 0;
}
运行结果:
可以看到文件结束的原因是因为遇到了文件末尾, eof 指示符被设置,所以feof函数检测到了结束标识符被设置,所以返回一个非0的数。
五.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
下面是示意图: