1、什么是文件
磁盘上的文件是文件
但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件。(从文件功能的角度来分类的)。
1.1、程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.ob),可执行文件(windows环境后缀为.exe)。
1.2、数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本章讨论的是数据文件。
1.3、文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件包含3部分:文件路径+文件名主干+文件后缀。
例如:c:\code\test\aa.txt
为了方便起见,文件标识常被称为文件名
。
2、文件的打开和关闭
2.1、文件指针
缓冲文件系统中,关键的概念是文件类型指针,简称"文件指针"。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名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;
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
打开文件的大致流程就是,如下:
有一个test.txt文件,在使用fopen打开时,会在内存中有一个FILE类型的文件信息区,如何维护这个文件信息区呢?通过一个FILE*的指针,来指向这个文件信息区。而指针指向文件信息区,而文件信息区和文件有关联,因此FILE*指针和文件就有了联系。
C语言中打开文件使用fopen
函数,当fopen打开文件的时候,会在内存中创建文件信息区,并且把文件信息区的起始地址返回来。因为文件信息区是FILE类型的,那既然返回的是文件信息区的地址,所以文件信息区的地址就是FILE*类型的,FILE*就是文件指针。
下面来创建一个FILE*的指针变量:
FILE* pf; //文件指针变量
定义pf是一个指向FILE类型数据的指针变量,可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
2.2、文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用fopen函数打开文件,fclose来关闭文件。
//打开文件
//mode使文件的打开模式,
//"r"---------只读
//"w"---------只写 a、r+、w+、a+
FILE* fopen(const char* filename,const char* mode);
//关闭文件
int fclose(FILE* stream);
#include <stdio.h>
#
int main()
{
FILE* pf = fopen("test.txt", "r");
return 0;
}
但是有可能文件打开失败,比如:此文件不存在。如果文件打开失败,会返回空指针。所以我们需要判断一下:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
return 0;
}
关闭文件:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
2.3、文件打开方式
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 新建一个文件 |
"a"(追加) | 向文本文件末尾添加数据 | 新建一个文件 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个二进制文件 | 新建一个文件 |
"ab"(追加) | 向一个二进制文件末尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个已经存在的文本文件 | 出错 |
"w+"(读写) | 为了读和写,新建一个文件 | 新建一个文件 |
"a+"(读写) | 打开一个文件,在文件末尾进行读写 | 新建一个文件 |
3、关于文件输出和输入
- 写和输出是同一概念,所对应的操作是:内存中的数据到硬盘(或外部设备)中的文件里面区。
- 读和输入时同一概念,所对应的操作是:硬盘(或外部设备)中文件的数据到内存中去。
那文件流
如何理解呢?
外部设备其实有很多:硬盘,屏幕,U盘...
每一种外部设备读和写的方式是不一样的。那我们作为程序员,我们在对文件操作时,我们面对不同的外部设备就需要考虑不同的读写的方式,那这对于程序员来说太麻烦了。为了很好的解决此问题,使得程序员不需要太多关注外部设备的读和写。我们在外部设备前面封装了一层。这一层就是流
。我们先把数据写入流
里面去,这对于程序员来说就够了,而至于流
又是将数据如何写进各种外部设备中去的,这个我们不需要关心,流里面的数据是如何到外部设备中去的,是C语言底层设计好,帮我们自动实现的。
那我们在使用printf
和scanf
时怎么不需要先打开一个流呢?
这是因为C语言在运行时默认打开了三个流:
- FILE* stdin ------ 标准输入流(键盘)。
- FILE* stdout ------ 标准输出流(屏幕)。
- FILE* stderr ------ 标准错误流(屏幕)。
所以说我们在使用printf
和scanf
时不需要先创建一个FILE*,而是直接使用即可。
4、文件的顺序读写
功能 | 函数名 | 适用于 |
---|---|---|
字符输入函数 | fgetc | 所有输入流 |
字符输出函数 | fputc | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
二进制输入 | fread | 文件 |
二进制输出 | fwrite | 文件 |
输入:从文件中读信息到内存中去。
输出:把一个信息写入文件中去。
4.1、fputc---写字符(针对字符)
int fputc ( int character, FILE * stream );
//eg:
fputc('a', pf); //传参字符,以ASCII值存入
代码演示:
//fputc
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//写文件
//在上面的test.txt文件中写入一个字符'a'。
fputc('a', pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.2、fgetc---读字符(针对字符)
int fgetc ( FILE * stream );
//eg:
fgetc(pf);
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//写字符
fputc('a', pf);
//读字符
int ch = 0;
ch = fgetc(pf);
printf("%c ", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
【补充:】fgets读取字符,如果读取失败会返回EOF。
4.3、fputs---写一行数据(针对字符串)
int fputs ( const char * str, FILE * stream );
//eg:
fputs("hello bbb", pf);
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//写一行数据
fputs("hello bbb", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.4、fgets---读一行数据(针对字符串)
char * fgets ( char * str, int num, FILE * stream );
- str:读取的字符串,要存放在那个地址
- num:读取多少个字符
- stream:要操作的文件
代码示例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//读取数据
char arr[20];
fgets(arr, 5, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
输出:
我们从"hello bbb"里面只读取了前4个字符,明明我们指定的5个字符。这是因为fgets会自动在第5个字符位置上添加'\0',所以只读取到前4个字符。
4.5、fprintf---写 格式化数据
上面的函数,读或者写都是指针字符的。
而如果想要写或者读其它的数据呢?比如说:结构体数据。
可以使用fprintf或者fscanf。而写数据需要用fprintf
学习这个函数可以和printf进行对比。
int fprintf ( FILE * stream, const char * format, ... );
下面来看一下prinft的使用:
int printf ( const char * format, ... );
可以发现,fprintf比printf就多个参数:FILE* stream,那我无非就是多个要操作的文件。
一般情况下,printf会这样写:
printf("%s %d %f",a,b,c);
那类比printf,我们就可以写出fprintf的使用,下面来写入结构体数据为例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = { "aaa",12,3.1f };
//打开文件
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("eg");
return 1;
}
fprintf(pf, "%s %d %f", s.arr, s.age, s.b);
fclose(pf);
pf = NULL;
return 0;
}
4.6、fscanf---读 格式化数据
函数使用:
int fscanf ( FILE * stream, const char * format, ... );
学这个函数可以和scanf进行对比:
int scanf ( const char * format, ... );
可以发现,fscanf就是比scanf多个文件流的参数。
我们先来看看scanf的使用方法:
scanf("%s %d %f",&a,&b,&c);
那再来看看fscanf的使用:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = {0};
//打开文件
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("eg");
return 1;
}
fscanf(pf,"%s %d %f", s.arr, &(s.age), &(s.b));
printf("%s %d %f", s.arr, s.age, s.b);
fclose(pf);
pf = NULL;
return 0;
}
输出:
4.7、【补充:】使用以上函数打印数据到屏幕或从屏幕输入数据
fprintf(stdout,"%s %d %f", s.arr, s.age, s.b);
//很简单,只需要在前面添加"stdout"标准输出流即可。
4.8、fwrite---二进制输出
参数说明:
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
- ptr:需要写入的数据来自于哪里。
- size:一个元素有多大。
- count:一共多少个元素。
- stream:文件流。
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = {"zhangsan",18,30.1f};
//以二进制的形式写入文件中。
FILE* pf = fopen("test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以二进制的方式写
fwrite(&s, sizeof(struct S), 1, pf);
fclose(pf);
pf = NULL;
return 0;
}
4.9、fread---二进制输入
参数说明:
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
参数使用和fwrite一样。
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = {"zhangsan",18,30.1f};
//以二进制的形式写入文件中。
FILE* pf = fopen("test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//以二进制的方式读
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.arr, s.age, s.b);
fclose(pf);
pf = NULL;
return 0;
}
5、对比一组函数,并使用sprintf和sscanf
scanf/fscanf/sscanf
printf/fprintf/sprintf
- scanf是针对标准输入的格式化输入语句。
- printf是针对标准输出的格式化输出语句。
- fscanf是针对所有输入流的格式化输入语句。
- fprintf是针对所有输出流的格式化输出语句。
下面介绍sprintf
和sscanf
这两个函数和上面的函数的最大区别就是,不在对文件进行操作了。和文件流没有什么关系了。
5.1、sprintf---把一个格式化的数据写到字符串中。
参数说明:
int sprintf ( char * str, const char * format, ... );
sprintf的作用:把一个格式化的数据写到字符串中,本质是把一个格式化的数据转为一个字符串。
这个函数和上面的函数最大的区别就是,它不再是对文件进行操作了,和FILE*没有什么关系了。
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = {"zhangsan",18,30.1f};
//将结构体数据(格式化数据)转为字符串
char buf[100];
sprintf(buf, "%s %d %f\n", s.arr, s.age, s.b);
printf("%s\n", buf);
return 0;
}
输出:
现在的数据是以字符串的形式:"zhangsan 18 30.100000"存储了。
5.2、sscanf---从一个字符串中转化为一个格式化数据
参数说明:
int sscanf ( const char * s, const char * format, ...);
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>
struct S
{
char arr[20];
int age;
float b;
};
int main()
{
struct S s = {"zhangsan",18,30.1f};
struct S tmp = { 0 };
//将结构体数据(格式化数据)转为字符串
char buf[100];
sprintf(buf, "%s %d %f\n", s.arr, s.age, s.b);
//将结构体数据以字符串的形式打印
printf("%s\n", buf);
//现在buf里面存储的是字符串
//现在使用sscanf从字符串buf中获取一个格式化数据到tmp结构体中。
sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.b));
//将字符串数据以结构体形式打印。
printf("%s %d %f\n", tmp.arr,tmp.age,tmp.b);
return 0;
}
输出:
6、文件的随机读写
6.1、fseek---根据文件指针的位置和偏移量来定位文件指针。
根据文件指针的位置和偏移量来定位文件指针。
int fseek(FILE* stream,long int offset,int origin);
stream:文件流
offset:偏移量
origin:这个有三个取值:
- | SEEK_SET | 文件起始位置 |
| -------- | ------------ |
| SEEK_CUR | 当前指针位置 |
| SEEK_END | 文件结尾 |
- | SEEK_SET | 文件起始位置 |
在上面介绍了一个函数:fgetc,可以输入文件中的字符。
比如现在有字符串:"abcdef"。第一次执行fgetc就会读取字符'a',第二次执行fgetc就会读取字符'b',第三次执行fgetc就会读取字符'c'...
就是只能一个字符一个字符的读取。
那现在有需求:我们直接读取字符'c',然后在读取字符'e',就是我们读取谁就读取谁呢?
答案:可以。使用fseek。
代码演示:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//定位文件指针
fseek(pf, 2, SEEK_SET);
//会直接读取字符'c'
int ch = fgetc(pf);
printf("%c\n", ch);
//SEEK_CUR代表当前的文件指针位置,因为上面文件指针指向了字符c的位置,所以现在从字符c位置开始。
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf); //从c开始,偏移量是2,所以会读取到字符f。
printf("%c\n", ch);
fclose(pf);
pf = NULL;
return 0;
}
6.2、ftell---计算前文件指针相对于起始位置的偏移量。
返回文件指针相对于起始位置的偏移量
long int ftell(FILE* stream);
计算当前文件指针相对于起始位置的偏移量。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//定位文件指针
fseek(pf, 2, SEEK_SET);
//会直接读取字符'c'
int ch = fgetc(pf);
//打印偏移量,由于fgetc把字符c读取了,所以现在文件指针指向了d,因此偏移量应该为3
printf("%d\n", ftell(pf));
fseek(pf, 2, SEEK_CUR);
ch = fgetc(pf);
//打印偏移量,由于fgetc把字符f读取了,所以现在文件指针指向了f后面,因此偏移量应该为6
printf("%d\n", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
输出:
6.3、rewind
让文件指针的位置返回到文件的其实位置。
void rewind(FILE* stream);
例子:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return;
}
//定位文件指针
fseek(pf, 2, SEEK_SET);
//会直接读取字符'c'
int ch = fgetc(pf);
//由于文件指针回到了起始位置,所以下面的偏移量为0。
rewind(pf);
printf("%d\n", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
输出:
7、文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外序,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
一个数据在内存中是怎样存储的呢?
字符一律以ASCII形式存储,数值行数据既可以用ASCII形式存储,也可以使用二进制形式存储。如果有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘占用5个字节(每个字符一个字节)而二进制形式输出,则在磁盘上只占4个字节(VS2013测试)。
8、文件读取结束的判定
8.1、被错误使用的feof
牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件是否结束。
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
- feof(pf)为真,说明是读到文件尾结束,为假说明是读取失败(读取错误)。
- ferror(pf)为真,说明是读取失败(读取错误),并没有成功的读取到文件尾。
1、文本文件读取是否结束,判断返回值是否为EOF(fgetc)
,或者NULL(fgets)
,例如
fgetc
判断是否为EOF
。fgets
判断返回值是否尾NULL。
2、二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
- fread判断返回值是否小于实际要读个数。
9、文件缓冲区
ANSIC标准采用"缓冲文件系统"处理数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块' 文件缓冲区"。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才-起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区) , 然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
代码示例:
//环境VS2022,Win10
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf); //先将代码放在输出缓冲区
printf("睡眠10秒--已经写好数据,打开test.txt文件,发现文件没有内容\n");
Sleep(10000);
printf("刷新输出缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)。
printf("在睡眠10秒--已经写好数据,打开test.txt文件,发现文件有内容\n");
Sleep(10000);
fclose(pf);//注:fclose在关闭文件的时候,也会刷新缓冲区。
return 0;
}
这里可以得出一个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。
如果不做,可能导致读写文件的问题。