目录
一、文件的简单介绍
1.1 什么是文件
从广义上来说,磁盘上的文件是文件。在C语言阶段我们在执行一段程序过后,它的内容随着进程的结束而消失,而我们要想把数据持久化,数据存放在磁盘文件、存放到数据库等方式。今天简单介绍数据保存到文件这种方式。在程序设计中,我们一般谈的文件有两种:程序文件和数据文件(从文件功能的角度来分析).
1.2 程序文件
包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
1.3 数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。在以前各章所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上文件。
1.4 文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt为了方便起见,文件标识常被称为文件名。
二、文件的打开和关闭
2.1 文件指针
1、缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
2、每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
例如,VS2013编译环境提供的 stdio.h 头文件中有以下的文件类型申明:
typedef struct _iobuf
{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
}FILE;
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量填充其中的信息,一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
比如我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
该类型的意义是:
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说通过文件指针变量能够找到与它关联的文件。
2.2 文件的打开与关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件。
//打开文件
FILE* fopen(const char* filename,const char* mode);
//关闭文件
int fclose(FILE* stream);
我们这里可以简单介绍一下 fopen 和 fclose函数。
①fopen
FILE* fopen(const char* filename,const char* mode);
打开一个文件以什么形式打开,mode就是打开的方式。如果打开成功就返回文件指针的起始地址,如果失败就返回NULL,而这也暗示我们要进行检查。
②fclose
int fclose(FILE* stream);
这个函数比较简单,就是关闭之前打开的文件。如果打开成功,就返回0,如果失败,就返回EOF.
上面对于mode(打开方式)介绍的比较含糊,那么具体的打开方式有如下这些方式:
常用的就是标红的那些,需要注意的是:
1.以"r"(只读的方式)来打开文件,需要打开一个原本已经存在的文本文件,否则会打开失败。
2.如果以"w"(只写的方式)来打开文件,无论文本文件是否存在,无论文本里面有没有内容,都会开辟一个文件,而且需要注意如果已经存在的文件,那么它会把文件里原本的内容清除,然后存入数据。如果不想清除原本文件内的数据,可以使用第三种方式
3."a"(追加的方式),向文本文件尾添加数据。
4.至于这些字母后面加个b,"wb","rb","ab"意思是以二进制的形式。上述是以文本的形式。
使用演示:
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
FILE* pf = fopen("test.txt", "r");//pf的类型是FILE*
//文件打开可能失败,所以要判断
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//"r" "w" "a" "r+" "w+" "a+" "rb" "wb"
//写文件
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里是和之前我讲述的动态开辟比较相似,那里需要对malloc或者calloc开辟的动态内存进行判断是否为NULL,然后在结束的时候需要free掉动态内存,和这里的文件操作是比较相似的。可以类比着记忆。
2.3 文件的流程展示
通过我简单的介绍,大家对于文件有了基本了解,而具体是如何操作的,那是下面要介绍的内容,而这里笔者简单画了个图帮助大家理解:
三、文件的顺序读写
3.1 fputc(写字符)
int fputc ( int character, FILE * stream );1、Charcater: 要编写的字符的整型升级。写入时,该值在内部转换为无符号字符。
2、Stream:指向标识输出流的 FILE 对象的指针。该函数的意义就是将字符character写入文件中去。
3、如果成功后,将返回写入的字符。如果发生写入错误,则返回 EOF 。
应用展示:
3.2 fgetc(读字符)
int fgetc ( FILE * stream );1、在默认状态下,fgetc从stream指向的文件首个字符开始读取,当读取过后,自动指向下一个字符,如果指向末尾或者读取失败则返回EOF。
2、如果成功,则返回当前指向的字符,如果失败,则返回EOF。
应用展示:
①一个一个读取
注意:这里之所以能读出a b c d是因为在上个函数fpuc时笔者写入了一个txt文件里面是a到c的字符,所有各位朋友在使用这个函数的时候记得不是空的文件,否则会出错。
②全部读取
如果仅能一个一个读取,那么效率有点低了,如果我们想全部读取文件里的内容,那么我们根据fgetc函数的返回值可以设计,当指向文件尾时返回EOF。
展示:
3.3 fputs(写字符串到文件)
int fputs ( const char * str, FILE * stream );1、意义:将 str 所指向的字符串写入流(stream)。
2、该函数开始从指定的地址 (str) 复制,直到到达终止空字符 ('0')。此终止空字符不会复制到流中。
3、请注意,之前fputc和putc以及fgetc和getc是非常相似的,而这里fputs 不仅与 puts 不同,因为可以指定目标流,而且 fputs 不会写入其他字符,而 puts 会自动在末尾追加换行符。
4、如果成功,则返回一个非0的数,如果失败,则返回EOF。
应用展示:
这里我之前写入的"abcdef..."已经消失了,是因为笔者以"w"(只写)的方式打开了文本,会自动清除里面原本的内容,要想保留下来原本的内容,可以以"a"(追加)的方式打开。
3.4 fgets(读字符串)
char * fgets ( char * str, int num, FILE * stream );1、意义:从流(stream)中读取(num-1)字符并将其作为字符串存储到 str 中。
2、当读取 (num-1) 字符或达到换行符或文件末尾(以先发生者为准)时停止,需要注意换行符(\n)使fgets停止读取,但它被视为有效字符,并包含在复制到str的字符串中。
3、终止空字符('\0')会自动追加到复制到 str 的字符之后。
4、请注意,fgets 与 gets 相同之处在于:fgets和gets都会自动追加终止空字符,不同:fgets 不仅接受流参数(stream),还允许指定读取字符个数,并且fgets遇到换行符会把换行符当作字符拷贝到str中。
5、如果成功,返回str,如果失败,则返回NULL指针.
应用展示:
这里要求打印5个字符只打印了4个字符的原因是,它自动追加'\0'算作一个字符。
接下来这两个函数是针对所有输出输入流的:
3.5 fscanf(从文件中读取数据)
int fscanf ( FILE * stream, const char * format, ... );1、这个函数和scanf:(int scanf (const char* format,...);)是非常相似的,只不过这个函数多了一项从流(stream)中获取数据,后面基本相同的。如果想让这个函数发挥和printf一样的功能,那么只需将流改为 stdin(标准输入).
2、而且这个函数不仅仅可以读取字符串,对于其他类型的整型,结构体,浮点型等都是可以读取的。
3、如果成功,则返回成功读取的项数,如果失败,则返回EOF。
应用展示:
3.6 fprintf(写数据到文件)
int fprintf ( FILE * stream, const char * format, ... );1、同理,这个函数和printf(int printf(const char* format,...)),非常相似,这个函数多了一项向流(stream)中写入数据,后面基本相同的。如果想让这个函数发挥和printf一样的功能,那么只需将流改为 stdout(标准输出).
2、这个函数也是不仅仅可以写入字符串,对于其他类型的整型,结构体,浮点型等都是可以写入的。
3、如果成功,则返回成功写入的项数,如果失败,则返回EOF。
应用展示:
阐述:通过上面对fscanf、fprintf的介绍,我们知道了标准输入流(stdin)、标准输出流(stdout),那么为什么在使用printf和scanf的时候不需要打开那两个流呢?
因为任何一个C程序,只要运行起来就会默认打开3个流:
FILE* stdin---标准输入流(键盘)
FILE* stdout-- 标准输出流(屏幕)
FILE* stderr--标准错误流(屏幕)
上面介绍的fputc,fgetc只能一个个写入或读取字符,而fputs,fgets也只能写入或读取字符串,fprintf和fscanf可以写入或读取任意类型的数据,但是必须设置格式,而且一次只能读取或写入一个。如果我们既想写入或读取任意类型的数据,还不想要设置格式的繁琐,还想要一次可以写入或读取多个数据,上面的几个文件操作函数是无法满足这样的条件的,所以接下来我要介绍的fwrite和fread可以满足这些需求。
注意:fwrite和fread与上述文件操作函数的区别还在于,它们只能以二进制的形式写入或读取二进制文件。
3.7 fwrite(可写多个数据到文件)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );1、意义:从 ptr 指向的当前位置将count个大小为size(字节)的数据写入流(stream)。
2、ptr是要写入的数据,size是指定一次写入多少个字节,count意思是写入count次size个字节。
3、返回成功写入的字节总数。如果此数字与 count 参数不同,则会出现写入错误,导致函数无法完成。
应用展示:
3.8 fread(可从文件读取多个数据)
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );1、意义:从流stream中读取 count个大小为size个字节的数据到ptr中。
2、ptr是读取成功后存放数据的地址,size是每次读取字节的大小,count是读取多少次。
3、如果成功,返回的是读取的总字节数为(大小*计数)。
应用展示:
3.9 sscanf 和sprintf
这两个函数比较有意思
1、sscanf -- int sscanf( const char *buffer, const char *format,... );
从字符串里读并将其转化为格式化数据。从 buffer 读取数据,并根据参数格式将它们存储到附加参数给出的位置,就像使用 scanf 一样,但从 buffer 而不是标准输入 (stdin) 读取。返回值:成功则返回成功读取的项目数,如果失败,则返回EOF。
2、sprintf --int sprintf( char *buffer, const char *format,...);
把一组格式化的数据写到字符串中,本质是把一组格式化数据转换成字符串。它的使用与 printf 相似,但内容不是打印,而是将格式化数据转换成字符串存储在buffer 中。终止空字符会自动追加到内容之后。
返回值:成功后,将返回写入的字符总数。此计数不包括自动追加在字符串末尾的其他空字符。失败时,返回负数。
使用展示:
四、文件的随机读写
4.1 fseek(重定位文件指针的位置)
int fseek ( FILE * stream, long int offset, int origin );
1、作用:根据文件指针的位置和偏移量来定位文件指针,这样说可能比较抽象,编译器默认从初始位置开始读取文件,但是这个函数可以重新定位文件指针。2、stream是要重新定位的文件,offset是要偏移的量,origin是从哪里开始偏移。
origin的选取:
origin的选取有三个:
SEEK_SET,文件的起始位置; SEEK_CUR,文件指针的当前位置,SEEK_END,文件尾
3、成功则返回0,失败则返回非0的数。
4.2 ftell(偏移量是多少)
long int ftell ( FILE * stream );作用:计算文件指针当前位置偏移量。
返回值:成功则返回当前偏移量。
4.3 rewind(回到起点)
void rewind ( FILE * stream );作用:将指针的位置设为开头。
结合应用展示:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//读文件
//定位文件指针
fseek(pf, 2, SEEK_SET);
int ch = fgetc(pf);
printf("%c\n", ch);
//偏移量要看定位处
//int fseek(FILE * stream, long offset, int origin);
//fseek(pf, 2, SEEK_CUR);
fseek(pf, -1, SEEK_END);
ch = fgetc(pf);
printf("%c\n", ch);
//ftell
//返回文件指针相对于起始位置的偏移量
//ftell用来计算文件指针相对于当前位置的偏移量
//long int ftell(FILE* stream)
printf("%d\n", ftell(pf));
//rewind
rewind(pf);//让文件指针回到起始位置
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
五、二进制文件和文本文件
1、数据文件被称为文本文件或者二进制文件
2、数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换,以ASCII字符的形式
存储的文件就是文本文件。
3、一个数据在内存中是怎样存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节,每个字符一个字节,而以二进制形式输出则在磁盘上只占4个字节.
六、文件读取结束的判定
6.1 被错误使用的feof
int feof ( FILE * stream );在文件读取的过程中,不能用feof函数的返回值直接用来判断文件的是否结束,这个函数使用的前提是已经设置了与流关联的文件结束指示符,如果设置了,则返回与零不同的值。也就是成功。如果没有设置,就会返回0.
通俗的讲,这个函数是为了判断是不是因为没有设置结束符而导致失败。如果设置了就排除这一可能。
文本文件使用案例:
1. 文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
fgetc 判断是否为 EOF .
fgets 判断返回值是否为 NULL
int main()
{
int c;//注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp)
{
perror("File opening failed");
return 1;
}
//fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp) != EOF))//标准C I/O读取文件循环
{
printf("%c ", c);
}
if (ferror(fp))
{
puts("I/O error when reading");
}
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
return 0;
}
二进制文件使用案例:
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
fread判断返回值是否小于实际要读的个数
enum {SIZE=5};
int main()
{
double a[SIZE] = { 1.,2.,3.,4.,5. };
FILE* fp = fopen("test.bin", "wb");//必须使用二进制模式
fwrite(a, sizeof(*a), SIZE, fp);//写double的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin", "rb");
size_t ret_code = fread(b, sizeof(*b), SIZE, fp);//读double的数组
if (ret_code == SIZE)
{
puts("Array read successfully ,contents: ");
for (int n = 0; n < SIZE; ++n)
{
printf("%f ", b[n]);
}
putchar('\n');
}
else
{
//error handling
if (feof(fp))//读取遇到文件尾而结束
printf("Error reading test.bin:unexpected end of file\n");
else if (ferror(fp))//读取错误
perror("Error reading test.bin");
}
fclose(fp);
fp = NULL;
}
七.文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
fflush:
int fflush ( FILE * stream );用途:如果给定的流(stream)已打开以进行写入,则其输出缓冲区中的任何未写入数据都将写入该文件。
简单来说就是清理缓冲区上未写入文件的内容,将这些输出到文件。
返回值:0表示成功,EOF表示失败。
效果展示:
注意:如果缓冲区没有填满,也没有刷新缓冲区,那么在文件操作结束的时候,缓冲区的s数据会自动输出到文件中。