001、文件的概念
(1)什么是文件
磁盘上的文件就是文件,在程序设计中,从文件功能角度来看,一般谈到的文件有两种:程序文件、数据文件
(2)文件的分类
①程序文件
比如源程序文件、目标文件、可执行文件
②数据文件
单纯存储/输出数据的文件,比如:存放数字、存放字符等,下面要介绍的函数多是处理数据文件的,数据文件又可以分为文本文件和二进制文件
(3)文件名
一个文件要有一个唯一的文件标识符,便于计算机/用户识别和引用,文件名包含三个部分:文件路径+文件名主干+文件后缀,比如:C:\limou\test\文档.txt(注意windows的文件分隔符是“\”,而Linux是“/”)
002、文件指针
(1)文件指针概念FILE
①在缓存文件系统中,最关键概念是“文件类型指针”,简称“文件指针”
②每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息,这些信息保存在一个结构体中,而文件指针就是为了维护这块空间的指针,因此文件指针其本质就是一个结构体指针
③结构体内部主要记录了文件的相关信息:文件名字、文件状态、文件当前的位置
④如果使用一个指向文件空间的指针,来维护FLILE结构的变量,这样在代码中使用起来就会更加方便
(2)在stdio.h中的文件指针定义
但是不同编译器、系统的文件实现机制可能不太相同,以下是windows环境中VS2013的定义FILE
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
003、文件函数(站在程序文件的角度)
(1)打开文件
FILE* fopen(const char* filename, const char* mode);
- 打开文件若成功就会返回一个FILE*类型的指针,若出错就会返回一个NULL,因此需要对指针进行检查
- 有关参数mode的解读和表格总结归纳(站在需要程序文件角度)
(2)关闭文件
int fclose( FILE* stream );
- 关闭文件并不会把原来指向文件的指针置空,因此需要我们手动置空,避免原有指针成为空指针
//测试代码,我在我的桌面放置了一个文件,文件名为:"C:\Users\DELL\Desktop\limou_cache_file_1.txt"
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <errno.h>
int main()
{
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
fclose(pf);
pf = NULL;
return 0;
}
004、文件顺序读写(站在程序文件的角度,请将屏幕和键盘也看做文件)
(1)字符输入函数fgetc(适用于所有输出流)
int fgetc( FILE* stream );
//如果读取成功返回ASCII码值(字符)并且提升为int,以容纳特殊值EOF(其值为-1)
//如果位置指示符位于文件末尾,该函数返回EOF并设置流的EOF指示符(feof)。
//如果发生其他读取错误,该函数也返回EOF,但设置其错误指示符(ferror)。
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//读取文件
int i = 0;
for (i = 0; i < 26; i++)
{
char ch = fgetc(pf);
printf("%c", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(2)字符输出函数fputc(适用于所有输出流)
int fputc( int character, FILE* stream );
//如果读取成功返回ASCII码值,并且提升为int,以容纳特殊值
//如果发生写入错误,则返回EOF,并设置错误指示器(ferror)
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//写入文件
int i = 0;
for (i = 0; i < 26; i++)
{
fputc('a' + i, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(3)文本输入函数fgets(适用于所有输出流)
char* fgets ( char* str, int num, FILE* stream );//(字符串数组名, 最大的个数字符, 文件指针)
//如果读取成功,返回指向字符串的指针
//如果在读取字符时遇到文件结束符,则设置eof指示符(feof)。如果这种情况发生在读取任何字符之前,则返回的指针是空指针(str的内容保持不变)
//如果发生读错误,则设置错误指示符(ferror)并返回NULL(但str所指向的内容可能已更改)。
#include <stdio.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");//这个文件里面我又重新放入一段文本了
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//读取文件
char arr[150];
for (int i = 0; i < 10; i++)
{
fgets(arr, 150, pf);
printf("%s\n", arr);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
注意实际上读取num-1个字符,因为保存多一个’\n’
(4)文本输出函数fputs(适用于所有输出流)
int fputs ( const char* str, FILE* stream );
//如果成功,则返回一个非负值
//在错误时,函数返回EOF并设置错误指示符(ferror)
#include <stdio.h>
#include <errno.h>
int main()
{
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//写入文件
fputs("hello_limou_welcome_file!", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
同时可以看到之前写的数据被替换了,只是没有"w"那么彻底,因此如果多次使用fputs函数会“覆盖显示”在同一行,除非用户输入的字符串本身带有’\n’字符
(5)格式化输入函数fscanf(适用于所有输出流)
int fscanf ( FILE* stream, const char* format, ... );
#include <stdio.h>
int main()
{
typedef struct S
{
int a;
double b;
char c[10];
}S;
S s = { 0 };
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//读文件
fscanf(pf, "%d %f %s", &(s.a), &(s.b), &(s.c));//这里最好加上括号,清晰一点
//打印到屏幕上
printf("%d %lf %s", s.a, s.b, s.c);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(6)格式化输出函数fprintf(适用于所有输出流)
int fprintf ( FILE* stream, const char* format, ... );
#include <stdio.h>
int main()
{
//存储输入文件的数据结构体
typedef struct S
{
int a;
double b;
char c[10];
}S;
S s = { 100, 3.1415926, "limou3434" };
//打开文件
FILE* pf = fopen("C:\\Users\\DELL\\Desktop\\limou_cache_file_1.txt", "r+");
if (pf == NULL)
{
perror("fopen失败!\n");
return 1;
}
//写文件
fprintf(pf, "%d %f %s", s.a, s.b, s.c);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(7)二进制输入fread(适用于文件)
size_t fread ( void* ptr, size_t size, size_t count, FILE* stream );
//返回成功读取的元素总数
//如果这个数字与count参数不同,则要么发生了读取错误,要么在读取时到达了文件末尾。在这两种情况下,都设置了正确的指示器,可以分别使用ferror和feof进行检查。
//如果size或count中有一个为零,函数返回零,流状态和ptr指向的内容都保持不变。
//注意size_t是无符号整型
#include <stdio.h>
typedef struct S
{
int a;
double b;
char arr[10];
}S;
int main()
{
//输入数据
S s1 = { 3, 3.14, "abcde" };
FILE* pf = fopen("limou_txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s1, sizeof(S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
//输出数据
S s2 = { 0 };
pf = fopen("limou_txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fread(&s2, sizeof(S), 1, pf);
printf("%d %f %s\n", s2.a, s2.b, s2.arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(8)二进制输出fwrite(适用于文件)
size_t fwrite ( const void* ptr, size_t size, size_t count, FILE* stream );
//返回成功写入的元素总数
//如果此数字与count参数不同,则写入错误阻止函数完成。在这种情况下,将为流设置错误指示器(ferror)
//如果size或count中有一个为零,则函数返回零,错误指示符保持不变
#include <stdio.h>
typedef struct S
{
int a;
double b;
char arr[10];
}S;
int main()
{
//输入数据
S s1 = { 3, 3.14, "abcde" };
FILE* pf = fopen("limou_txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fwrite(&s1, sizeof(S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
//输出数据
S s2 = { 0 };
pf = fopen("limou_txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fread(&s2, sizeof(S), 1, pf);
printf("%d %f %s\n", s2.a, s2.b, s2.arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
005、区分scanf/printf、fscanf/fprintf、sscanf/sprintf
(1)scanf,针对标准输入流(stdin)的,格式化的输入函数
(2)printf,针对标准输出流(stdout)的,格式化的输出函数
(3)scanf,针对所有输入流的,格式化的输入函数
(4)printf,针对所有输出流的,格式化的输出函数
(5)sscanf,将字符串输入转化为格式化输入
int sscanf ( const char* s, const char* format, ...);
#include <stdio.h>
typedef struct S
{
int a;
double b;
char c[10];
}S;
int main()
{
S s1 = { 200,3.14f,"limou3434" };
//转化为字符串数据
char arr[200] = { 0 };
sprintf(arr, "%d %lf %s\n", s1.a, s1.b, s1.c);
printf("%s", arr);
//转化为格式化数据
S s2 = { 0 };
sscanf(arr, "%d %lf %s", &(s2.a), &(s2.b), s2.c);
printf("%d %lf %s\n", s2.a, s2.b, s2.c);
return 0;
}
(6)sprintf,将格式化输出转化为字符串输出
int sprintf ( char* str, const char* format, ... );
#include <stdio.h>
typedef struct S
{
int a;
double b;
char c[10];
}S;
int main()
{
S s1 = { 200,3.14f,"limou3434" };
//转化为字符串数据
char arr[200] = { 0 };
sprintf(arr, "%d %lf %s\n", s1.a, s1.b, s1.c);
printf("%s", arr);
//转化为格式化数据
S s2 = { 0 };
sscanf(arr, "%d %lf %s", &(s2.a), &(s2.b), s2.c);
printf("%d %lf %s\n", s2.a, s2.b, s2.c);
return 0;
}
006、文件任意读写
(1)fseek
int fseek( FILE* stream, long int offset, int origin );
//(文件指针, 偏移量, 起始位置)根据文件指针的位置和偏移量来定位文件指针
//其中第三个参数的选择有三种:
//①SEEK_SET,即文件开始位置
//②SEEK_CUR,即文件指针的当前位置
//③SEEK_END,即文件末尾位置
#include <stdio.h>
int main()
{
//输入数据
FILE* pf = fopen("limou.txt", "r");//文件指针指向a的地址
if (pf == NULL)
{
perror("fopen");
return 1;
}
//顺序读写
int ch = fgetc(pf);
printf("%c", ch);//打印a,读完后文件指针指向b
ch = fgetc(pf);
printf("%c", ch);//打印b,读完后文件指针指向c
ch = fgetc(pf);
printf("%c", ch);//打印c,读完后文件指针指向d
ch = fgetc(pf);
printf("%c", ch);//打印d,读完后文件指针指向e
//改变偏移量读写
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(2)ftell
long int ftell ( FILE* stream );//返回文件指针相对于起始位置的偏移量
#include <stdio.h>
int main()
{
//输入数据
FILE* pf = fopen("limou.txt", "r");//文件指针指向a的地址
if (pf == NULL)
{
perror("fopen");
return 1;
}
//顺序读写
int ch = fgetc(pf);
printf("%c", ch);//打印a,读完后文件指针指向b
ch = fgetc(pf);
printf("%c", ch);//打印b,读完后文件指针指向c
ch = fgetc(pf);
printf("%c", ch);//打印c,读完后文件指针指向d
ch = fgetc(pf);
printf("%c", ch);//打印d,读完后文件指针指向e
//改变偏移量读写
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c", ch);
//继续顺序读写
ch = fgetc(pf);
printf("%c\n", ch);//打印d,读完后文件指针指向e
//返回偏移量
long int a = ftell(pf);
printf("%d\n", a);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
(3)rewind
void rewind ( FILE* stream );//修改当前的文件指针,指向开头位置
#include <stdio.h>
int main()
{
//输入数据
FILE* pf = fopen("limou.txt", "r");//文件指针指向a的地址
if (pf == NULL)
{
perror("fopen");
return 1;
}
//顺序读写
int ch = fgetc(pf);
printf("%c", ch);//打印a,读完后文件指针指向b
ch = fgetc(pf);
printf("%c", ch);//打印b,读完后文件指针指向c
ch = fgetc(pf);
printf("%c", ch);//打印c,读完后文件指针指向d
ch = fgetc(pf);
printf("%c", ch);//打印d,读完后文件指针指向e
//改变偏移量读写
fseek(pf, -3, SEEK_CUR);
ch = fgetc(pf);
printf("%c", ch);
//继续顺序读写
ch = fgetc(pf);
printf("%c\n", ch);//打印d,读完后文件指针指向e
//返回偏移量
long int a = ftell(pf);
printf("%d\n", a);
//修改文件指针为开头
rewind(pf);
//再次返回偏移量
a = ftell(pf);
printf("%d\n", a);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
007、数据文件的分类
(1)二进制文件
数据在内存中以二进制的形式存储,如果不加转化的输出到外存就是二进制文件
(2)文本文件
如果对二进制数据加以转换,以ASCII字符的形式存储的文件就是文本文件
(3)要点注意
两种文件只是看待数据的视角不一样,实际的访问速度和存储空间大小不能简单进行对比
008、文件读取结束的判定
(1)文本文件判断读取是否结束
- 如果是fgetc判断返回值是否为EOF
- 如果是fgets判断返回值是否NULL
(2)二进制文件判断读取是否结束
- 如果是fread判断返回值是否小于实际要读的个数
(3)判断结束的类型:feof和ferror
int feof ( FILE* stream );
- 注意,在文件的读取过程中,不能用feof的返回值来直接判断文件是否结束!!!
- feof的作用是,当文件读取结束后,判断读取结束的原因是否是“遇到文件尾结束”(也就是,feof是个“马后炮”)
int ferror ( FILE* stream );
- feof的作用是,判断读取结束的原因是否是“遇到错误结束”
(4)具体代码
#include <stdio.h>
int main()
{
//输入数据
FILE* pf = fopen("limou.txt", "r");//文件指针指向a的地址
if (pf == NULL)
{
perror("fopen");
return 1;
}
int ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
//通过feof函数来判断是不是遇到文件末尾结束的
if (feof(pf))
{
printf("到文件结尾\n");
}
else if (ferror(pf))
{
printf("文件读取错误\n");
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
#include <stdio.h>
enum
{
SIZE = 5
};
int main()
{
//创建数据
double a[SIZE] = {1.0, 2.0, 3.0, 4.0, 5.0};
FILE *fp = fopen("test.bin", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//写入文档
fwrite(a, sizeof *a, SIZE, fp);
//关闭文件
fclose(fp);
//创建存储数据的数组
double b[SIZE];
//打开文件
fp = fopen("test.bin","rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读取文件到b数组
size_t ret_code = fread(b, sizeof* b, SIZE, fp);
if(ret_code == SIZE)
{
printf("完全读取成功\n");
for(int n = 0; n < SIZE; ++n)
printf("%f ", b[n]);
putchar('\n');
}
else
{
//如果程序到这里,说明读取异常了
if (feof(fp))//查看是否遇到末尾
{
printf("文件意外结束\n");
}
else if (ferror(fp))//查看是否读取错误,这里还需要判断,所以写出else if
{
perror("读取错误\n");
}
}
//关闭文件
fclose(fp);
}
009、文件缓冲区
ANSIC标准采用“缓冲文件系统”处理的数据文件
(1)缓存文件系统的概念
- 指系统自动地在内存中为程序中,每一个正在使用的文件开辟一块“文件缓冲区”
- 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才被一起送到磁盘上
- 从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区,充满缓冲区后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
- 缓冲区的大小要根据C编译系统决定的
- 缓冲区是为了提高运行效率而存在的
(2)验证缓冲区代码
#include <stdio.h>
#include <windows.h>
int main()
{
//打开文件
FILE* pf = fopen("limou.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//输入数据
fputs("hello limou!", pf);//先将代码放在输出缓冲区
//查看文档这个时候是否存储数据
Sleep(15000);//程序停止运行15秒,这个时候可以打开limou.txt文件来看看是否将数据存入了。可以看到,文件并没有存储数据,因为这段数据还在缓存区域中
//刷新缓冲
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)注意,fflush在高版本的VS上不能使用了,除了这个函数还有其他的方法可以清空缓存区
//查看文档这个时候是否存储数据
Sleep(10000);
//关闭文件
fclose(pf);//注意,fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
010、额外学习补充
在上面的某一段代码中出现了两个宏