什么是文件
磁盘上的文件就是文件。
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。
1.程序文件
包括源程序文件(后缀为.c),目标文件(Windows环境后缀为.obj),可执行程序(Windows环境后缀为.exe)。
2.数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本篇讨论的是全是数据文件。
我们通常C语言所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。
其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这里处理的就是磁盘上的文件。
文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包括3部分:文件路径+文件名主干+文件后缀
例如:
c:\code\text.txt
- c:\code\为文件路径
- text为文件名主干
- .txt为文件后缀
为了方便起见,文件标识常被称为文件名。
文件类型
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
简单的理解就是文本文件可以用记事本打开查看,而二进制文件不能用记事本打开查看,如果打开查看看到的将是一堆乱码。 这是因为记事本就是以文本的形式打开文件的。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
如有整数10000,如果以ASCII码的形式输出到磁盘,则在磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
代码如下:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
这段代码我们将10000以二进制的形式存储到文件text.txt中。前面我刚提到过,二进制文件是不能用记事本打开来看的,下面我们用记事本将这个文件打开看看会看到什么。
可以看到这个文件打开后看到的是一个乱码,所以说二进制文件是不能用记事本来查看的的。
那么难道我们就真的不能看看这个文件的二进制信息吗?当然是可以的,我们的编译器就可以很好的帮我们解决这个问题。
第一步:打开源文件添加现有项
第二步:选中我们输出到磁盘的文件test.txt,点击添加到我们的项目中去。
第三步:点击打开方式,选中以二进制编辑器的形式打开,就可以打开我们的二进制文件了。
如下图:
可以看到10000的二进制形式被我们看见到了,只不过这里是以16进制的形式再根据小端字节序打印的。前面的一串0可以假想成一个地址,这里不必深究。
下面我们看看十进制10000在内存中存储形式:
文件缓冲区
ANSIC 标准采用缓冲文件系统处理的数据文件。
所谓缓冲文件系统是指:系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。
如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。
缓冲区的大小根据C编译系统决定的。
文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及 文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE(可以转到定义来查看)。
例如,VS2013编译环境提供的<stdio.h>头文件中有以下的文件类型申明:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异,所以这个不用深究。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便
下面我们可以创建一个FILE*的指针变量:
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
比如:
文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向改文件,也相当于建立了指针和文件的关系。
ANSIC规定使用fopen函数来打开文件,fclose来关闭文件。
1.fopen
FILE * fopen ( const char * filename, const char * mode );
- filename 指的是文件名。
- mode 指的是打开方式。
- FILE *函数的返回值是一个文件指针,是指是一个结构体,存放文件信息。
例:
pfile = fopen("myfile.txt", 'w');
打开文件进行写的操作
2.fclose
int fclose ( FILE * stream );
- stream:打开文件时返回的文件指针。
例如:
fclose(pfile);
将打开的文件关闭。
3.打开方式
注意:这里的"w"是指将该文件的内容清空,然后再进行写操作,如果项目中没有这个文件,这个函数就会自动在这个项目中创建一个文件(这就是建立一个新文件的含义)。
文件的读写
1.fputc(字符输出函数)
函数定义
将一个字符写到一个流里去。
“流”的概念
这里就引出了一个流的概念,下面来解释一下。
电脑上有很多的设备,鼠标,键盘、屏幕,有输入设备有输出设备。比如说我想把我们的相关数据进行输出,可以显示到屏幕上,叫做把数据输出到屏幕上;比如电脑上有硬盘,我把数据输出到硬盘上去,这叫写到硬盘上去,比如电脑上还插了一个U盘,我们还可以把数据写到U盘上去;比如这里放了一个光驱,我们可以把数据写到光驱上去。大家想象一下,这个时候如果我们要通过一定的手段把我们的数据写到外部设备上去,比如U盘,硬盘,屏幕,包括光驱等,这些设备的读写方式一样不一样?肯定不一样,设备不一样,读写方式肯定也不一样。但是C语言操作的话,如果我们给每一种设备都给一个读写方式的话,这个C语言写起来也太复杂了,所以后来做了一件什么事情呢?就是这样,抽象出来一个流的概念,流是一个中间层,我们程序员在写数据的时候统统把它放到流里面去,就像水流一样,把流想象成一个水渠,我们可以从里面舀东西,也可以往里面倒东西,我要输出数据的时候就往流里面放,我要读数据的时候就从流里面读。流自己知道我要怎么往硬盘放东西,怎么往屏幕放东西,我把他写到光驱上又是怎么写数据的。如果C语言把流以下的操作方式处理好,程序员写代码就只关心怎么把数据放到流里面去,这样程序员写代码就会变得更加简单,所以我们在写C语言代码的时候操作的就是流的概念。流是一个高度抽象的概念,是抽象出来的理解方式,需要自己慢慢地感受。
下面介绍三种标准流:
- 标准输入流:stdin
- 标准输出流:stdout
- 标准错误流:stderr
函数声明
int fputc( int c, FILE *stream );
- c:为输入字符的ASCII码值
- stream :为一个文件流
- 返回值:返回写入的字符,如果返回EOF表示输出错误
举例
使用fputc函数,将一组字符写到一个文件中去:
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写文件
fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);
fputc('e', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
写之前首先要打开文件进行写的操作,然后调fputc函数来给文件中写数据,最后再将文件关闭,运行结果:
打开text.c文件可以看到,我们将五个字符abcde成功输入到了该文件中去了。
刚才我们提到了标准输出流,标准输出流就是把数据输出到屏幕上去,平常我们在屏幕上打印数据用的都是printf函数,这次我们用fputc函数操作标准输出流来尝试一下。
#include <stdio.h>
int main()
{
fputc('q', stdout);
return 0;
}
运行结果:
可以看到我们通过fputc函数也可以将字符打印到屏幕上。
2.fgetc(字符输入函数)
函数定义
从流中读取一个字符
函数声明
int fgetc( FILE *stream );
- stream :所要读取的文件流
- 返回值:返回读取为int的字符或返回EOF以指示错误或文件结束。
这里可能会有人疑惑,为什么返回的既然是字符,那么为什么我们不用字符型参数来接收,而要用整型的参数来接收。是这样的,上面函数的返回值我们也看到了,如果获取错误或者文件结束会返回EOF。而这里的EOF大家可能不知道它是什么,转到定义我们来看看:
可以看到EOF的值实际上是-1,所以如果我们用字符型来接收一个整型的话,就会有精度的缺失,而得不到我们想要的结果。
举例
使用fgets函数,从一个文件中读取一串字符。
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里我们使用fgetc函数来从文件text.txt中读取五个字符。首先我们先手动在文件中输入一串字符"hello",然后用fgetc函数读取字符,所读取的字符用一个变量ch来接收,每读取一个字符就将它打印到屏幕上。
手动输入:
运行结果:
可以看到我们读取五次成功读取了这个文件中的5个字符。
通过这个例子大家可以感受一下,fgetc函数在读取完一次字符之后维护该函数读取字符的指针会向后跳一位,但是这个指针可不是文件指针,应该函数内部定义的用来读取字符的指针。
综合fgetc和fputc
学会这两个函数之后,再举一个例子综合运用这两个函数,我们来将一个文件里的内容拷贝到另一个文件中去。
具体的做法是这样的,先用fgetc函数从text1文件中逐个读取字符串,将读取的字符串存放到一个变量中去。然后再将这个字符利用fputc函数拷贝到text2文件中去。
前面我们说过,fgetc读取完一个字符之后,维护文件的指针就会自动向后跳1,而当全部读取结束之后,会返回EOF,所以这里我们可以设置一个循环来实现两个文件之间逐个字符的转换。
代码:
#include <stdio.h>
int main()
{
FILE* pfRead = fopen("test1.txt", "r");
if (pfRead == NULL)
{
perror("open file for reading");
return 1;
}
FILE* pfWrite = fopen("test2.txt", "w");
if (pfWrite == NULL)
{
perror("open file for writting");
fclose(pfRead);
pfRead = NULL;
return -1;
}
//拷贝
int ch = 0;
while ((ch = fgetc(pfRead)) != EOF)
{
fputc(ch, pfWrite);
}
//关闭文件
fclose(pfRead);
pfRead = NULL;
fclose(pfWrite);
pfWrite = NULL;
return 0;
}
这段代码的目的是把text1中的内容拷贝到text2中去,我们先手动的在test1文件中输入一串代码,然后建立text2文件,可以看到这个时候text2文件的大小还是0,说明里面没有内容。
下面我们再来看看程序运行后的结果:
可以看到成功将text1中的内容拷贝到了text2中去了。
3.fputs(文本行输出函数)
函数定义
将字符串写入流
函数声明
int fputs( const char *string, FILE *stream );
- string:要输入的字符串。
- stream :指向文件结构的指针
- 返回值:如果成功,这些函数中的每一个都返回一个非负值。出错时,fputs返回EOF
举例
将两个字符串输出到一个文件中去。
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("open file for writting");
return 1;
}
//写数据
fputs("hello bit\n", pf);
fputs("haha\n", pf);
fclose(pf);
pf = NULL;
return 0;
}
这段代码我们将两个字符串"hello bit\n"和"haha\n"输出到文件test.txt中,运行程序后查看文件的内容。
可以看到成功的将这两个字符串输出到了文件中。
4.fgets(文本行输入函数)
函数定义
从流中获取字符串。
函数声明
char *fgets( char *string, int n, FILE *stream );
- string:数据的存储位置
- n:要读取的最大字符数
- stream :指向文件结构的指针
- 返回值:每个函数都返回字符串。返回NULL以指示错误或文件结束条件。使用feof或feror来确定是否发生了错误。
举例
从文件中读取字符串。
#include <stdio.h>
int main()
{
char input[20] = { 0 };
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("opne file for reading");
return 1;
}
//读数据
fgets(input, 20, pf);
printf("%s", input);
fgets(input, 20, pf);
printf("%s", input);
fclose(pf);
pf = NULL;
return 0;
}
上一段代码中我们将两个字符串输入到了test.txt文件中,接下来我们来把这两个字符串从该文件中读取出来,存放到一个数组中去,再打印到屏幕上。运行结果:
可以看到这个函数在读取字符串的时候一次读取一行的信息,然后维护的指针会跳到下一行。
5.fprintf(格式化输出函数)
函数定义
将格式化数据打印到流。
函数声明
int fprintf( FILE *stream, const char *format [, argument ]...);
- 返回值:fprintf返回写入的字节数。当发生输出错误时,这些函数中的每一个都返回一个负值。
- stream:指向文件结构的指针
- format:格式控制字符串
- [, argument ]… :可选参数
fprintf函数的使用可以参考一下printf函数:
可以看到,fprintf函数和printf函数的不同仅仅是多了一个参数文件流,而我们知道printf函数是将数据打印到屏幕上的,由此可以推断fprintf函数的作用就是将数据打印到对应的文件流上去。
举例
将一个结构体内容输出到文件中
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {"zhangsan", 20, 66.5f};
FILE*pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf, "%s %d %f", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
这段代码我们通过fprintf函数将一个结构体s的内容输出到text.txt文件中去,运行结果:
打开text.txt文件我们可以看到该结构体的内容被输出到该文件中去了。
6.fscanf(格式化输入函数)
函数定义
从流中读取格式化数据。
函数声明
int fscanf( FILE *stream, const char *format [, argument ]... );
- 返回值:每个函数都返回成功转换和分配的字段数;返回值不包括已读取但未分配的字段。返回值0表示未分配任何字段。如果发生错误,或者如果在第一次转换之前到达文件流的结尾,那么fscanf的返回值是EOF
- FILE *stream:指向文件结构的指针
- const char *format:格式控制字符串
- [, argument ]…:可选参数
我们还是拿fscanf函数来和scanf函数对比一下:
可以看到,fscanf函数和scanf函数的不同也仅仅是多了一个参数文件流,而我们知道scanf函数是从屏幕上获取数据的,由此可以推断fscanf函数的功能就是从文件流中获取数据。
举例
从文件中读取一个结构体内容。
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = {0};
FILE*pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
printf("%s %d %f\n", s.name, s.age, s.score);
fclose(pf);
pf = NULL;
return 0;
}
上段代码中我们将一个结构体s的内容输出到了test.txt文件中,这段代码我们再使用fscanf函数将文件中的结构体拿出来打印到屏幕上。
运行结果:
7.fwrite(二进制输出)
函数定义
将数据以二进制形式写入流。
函数声明
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
- 返回值:fwrite返回实际写入的完整项的数目,如果发生错误,该数目可能小于count。此外,如果发生错误,则无法确定文件位置指示器。
- buffer:指向要写入的数据的指针
- size:要写入项目的大小(字节)
- count:要写入的项目数
- stream:指向文件结构的指针
举例
将一个结构体的内容以二进制的形式输出一个到一个文件中去,代码如下:
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { "张三", 20, 99.5f };
FILE* pf = fopen("test.dat", "wb");
if (pf == NULL)
{
perror("open file for writting");
return 1;
}
//写文件
fwrite(&s, sizeof(struct Stu), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
创建一个结构体变量s,以二进制写的方式打开一个文件test.dat,然后用fwrite函数将结构体s的内容写一个写到文件中去,最后我们来看文件里的内容。
其实如果我们真的来看这个文件中的内容你会发现你只能看懂一个"张三",而其他的东西都看不懂。为什么会这样呢,其实不难解释,因为fwrite函数是把数据以二进制的形式写到二进制文件中的,而我们的记事本只能观察文本文件,二进制文件是分辨不出来的,所以这里我们看不到这个文件的内容。但既然我们能看到结构体里的"张三",那也可以简单的说明我们已经成功的把结构体的数据写进去了。
这里肯定有人会疑惑了,那为什么"张三"在二进制文件中可以用文本的方式查看呢?是这样的,"张三"实际上是一个字符串,字符串不论以文本还是二进制写进去内容都是一样的,都是字符串,所以可以看懂。
8.fread(二进制输入)
函数定义
从流中读取二进制数据。
函数声明
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
- 返回值:fread返回实际读取的完整项目数,如果发生错误或在达到count之前遇到文件结尾,则该值可能小于count。使用feof或feror函数来区分读取错误和文件结束条件。如果size或count为0,则fread返回0且缓冲区内容不变。
- buffer:数据的存储位置
- size:项目大小(字节)
- count:要读取的项目数
- stream:指向文件结构的指针
举例
在上一段代码中我们将一个结构体的数据输入到了文件中,接下来我们再利用fread函数将这些数据从文件读到一个结构体中去
#include <stdio.h>
struct Stu
{
char name[20];
int age;
float score;
};
int main()
{
struct Stu s = { 0 };
FILE* pf = fopen("test.dat", "rb");
if (pf == NULL)
{
perror("open file for reading");
return 1;
}
//读文件
fread(&s, sizeof(struct Stu), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
这里我们又重新创建了一个结构体s,初始化里面为空。接下来我们以二进制读取的方式打开文件test.dat,然后利用fread函数从文件中读取一个结构体的数据到结构体s中去。为了方便展示,在读取结束之后我们将结构体s的内容打印出来,运行结果:
这里我总共介绍了八种重要的文件操作函数,大家一定要熟练掌握。
其实这八个函数主要是用来顺序读取文件的,这些文件创建好之后,文件指针就自动指向该文件的起始位置,然后我们从起始位置开始往后操作,这种读写方式称为顺序读写。
但是如果我们在拿到一个文件之后,想随机的来操作文件中的某一个位置,这几个函数显然就不能实现,最后我再向大家提几个有关随机读取的函数。
1. fseek
2. ftell
3. rewind
如果大家有兴趣可以自己查找学习一下。
最后希望这篇文章可以帮助到大家。