为什么使用文件
如果没有文件,我们写的程序储存在电脑内存中,如果程序退出,内存回收,数据就丢失了。如果要将数据持久化保存,我们可以使用文件。
什么是文件
在程序设计中,按照文件功能来区分,分为数据文件和程序文件
程序文件
程序文件包括.源程序文件(.c),目标文件(win环境下是.obj),可执行程序(win环境下是.exe)
数据文件
用于存放程序运行时所需要的数据,之前我们是从终端键盘上输入数据,运行结果显示到显示器上。我们也可以将信息输入到磁盘上,使用的时候从磁盘将数据读取到内存中,这就是处理磁盘上的文件
文件名(文件标识)
文件的唯一标识,便于识别和引用
包括 文件路径+文件名主干+文件后缀
例如:C:\Users\32355\Desktop\C code\study-c\8.19\8.19\1.c
二进制文件和文本文件
根据数据的组织形式,将数据文件分为二进制文件(数据在内存中以二进制存储(字符则是ASCII码的二进制),不经过转换直接输出到硬盘中,用vs打开此文件,是以16进制的形式展现,大小端并没有转换)和文本文件(经过转换以ASCII字符的形式储存的文件)
字符在文件中一律用ASCII码的形式存储,数值可以用ASCII码和二进制的形式存储
文件的打开和关闭
流
因为不同外部设备输入输出的操作各不相同,为了方便操作,抽象出流的概念。我们可以把流想象成流淌着字符的河。C语言针对文件,画面,键盘等的数据输入输出操作都是通过操作流来完成的
一般来说,我们想从流里读或写数据,都要先打开流
标准流
C语言程序在启动的时候默认打开三个流
stdin:标准输入流,在大多数环境中从键盘输入,scanf函数就是从标准输入流中读取数据
stdout:标准输出流,大多数环境中输出至显示器界面,printf函数就是将信息输出到标准输出流中
stderr:标准错误流,大多数情况输出到显示器界面
stdin,stdout,stderr这三个流的类型是:FILE* ,通常称为文件指针,C语言中就是通过FILE*的文件指针来维护流的各种操作。
文件指针
缓冲区文件系统中,关键概念是“文件类型指针”简称“文件指针”
文件在使用的过程中会在内存中开辟相应的用来存放文件相关信息(文件名,文件状态,文件当前的位置等等)的文件信息区,这些信息存放在一个系统声明的 叫做FILE的结构体变量中
例如在VS2023
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* pf;//⽂件指针变量
通过pf找到文件信息区,通过文件信息区中的信息能够访问文件。也就是说通过文件指针能够间接找到与它相关联的文件。
文件的打开和关闭
文件读写之前应该打开文件,使用结束后关闭文件。在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,建立了指针和文件的关系
ANSI C规定使用fopen函数打开文件,fclose函数关闭文件
FILE * fopen ( const char * filename, const char * mode );
int fclose ( FILE * stream );
mode表示文件打开模式
文件使用方式 含义 如果指定文件不存在
#include<stdio.h>
int main()
{
FILE* fp = fopen("8.22.txt", "r");//打开文件
if (fp == NULL)
{
perror("fopen");
return 1;
}
//...文件操作
int n=fclose(fp);//关闭文件
printf("%d", n);
fp=NULL;
return 0;
}
注意:"w"模式会清楚原有数据
文件的顺序读写
顺序读写函数介绍
fputc(字符输出函数)
int fputc ( int character, FILE * stream );
成功时,将返回写入的字符。
若发生写入错误,则返回 EOF,并设置错误指示器(ferror)。
举个例子(向文件中输入26个英文字母)
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char c = 0;
for (c = 'a'; c <= 'z'; c++)
{
fputc(c, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
fgetc(字符输入函数)
int fgetc ( FILE * stream );
成功时,将返回读取到的字符ASCII码值。
返回类型设计为 int,是为了容纳特殊值 EOF(一个特殊的常量,通常定义为-1,char类型无法储存负数),通常情况:1 光标处于文件末尾返回EOF并设置流(stream)的文件结束指示器(feof) 2 读取错误,同样返回EOF ,但会改为设置流的错误指示器(ferror)
举个例子(向屏幕打印上段代码输出的英文字母)
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r");//打开文件的时候,光标开始指向首个字符
if (pf == NULL)
{
perror("fopen");
return 1;
}
int c = 0;
while (c = fgetc(pf), c != EOF)//每输入一次,光标向后移动一个字符
{
printf("%c ", c);
}
fclose(pf);
pf = NULL;
return 0;
}
输出结果
fputs(文本输出函数)
int fputs ( const char * str, FILE * stream );
把str(首字符地址)指向的字符串输出到stream指向的文件中,不包括\0
成功时,返回一个非负值,发生错误时,该函数返回 EOF(-1),并设置错误指示器(ferror)
fgets(文本输入函数)
char * fgets ( char * str, int num, FILE * stream );
从stream中最多读取num字节个数据并拷贝到str指向的空间
假设num=10,编译器最多读9个,因为要给\0留个位置
一次只读取一行,如果读取的长度过长,会读取末尾的\n,补上\0然后停止读取
读取成功返回str的起始地址,读取失败返回空指针
举个例子(字符串输出输入)
#include<stdio.h>
#include <string.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[26] = {0}; // 初始化一下
int i = 0;
char c = 'a';
//输出
fputs("Hello world\n", pf);
fputs("Hello YY", pf);
//输入
fclose(pf);
pf = fopen("8.22.txt", "r+");//重新打开,使光标移到起始字符
while (fgets(arr, 10, pf) != NULL)
{
printf("%s", arr);
}
fclose(pf);
pf = NULL;
return 0;
}
输出结果
fprintf(格式化输出函数)
int fprintf ( FILE * stream, const char * format, ... );
与printf相比,多了一个文件地址
#include<stdio.h>
struct STU
{
char name[50];
int age;
};
int main()
{
struct STU s1 = {.name="zhangsan",.age=20 };
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fprintf(pf, "%s %d", s1.name,s1.age);
fclose(pf);
return 0;
}
fscanf(格式化输入函数)
int fscanf ( FILE * stream, const char * format, ... );
#include<stdio.h>
struct STU
{
char name[50];
int age;
};
int main()
{
struct STU s1 = {0 };
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fscanf(pf, "%s %d", s1.name, &(s1.age));
printf("%s %d", s1.name, s1.age);
fclose(pf);
return 0;
}
特殊应用
因为以上的函数适用于所有输出流,所以可以通过fputc fputs fprintf向屏幕上打印数据,只需要将地址换成标准输出流stdout
fprintf(stdout,"%s %d", s1.name, s1.age);
fwrite(二进制输出)
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
ptr:需要输出的数据
size:一个数据的大小
count:数据的个数
stream:文件地址
返回成功输出元素的个数
#include<stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6 };
FILE* pf = fopen("8.22.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
size_t sz = sizeof(arr) / sizeof(arr[0]);
fwrite(arr, sizeof(arr[0]), sz, pf);
fclose(pf);
pf = NULL;
return 0;
}
显示格式是16进制,小端。
fread(二进制输入函数)
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
返回成功读取元素总个数
#include<stdio.h>
int main()
{
int arr[6] = {0};
FILE* pf = fopen("8.22.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int i = 0;
while (fread(arr + i, sizeof(int), 1, pf))
{
printf("%d ", arr[i]);
i++;
}
fclose(pf);
pf = NULL;
return 0;
}
对比
scanf:从标准输入流上读取格式化的数据
fscanf:从指定输入流上读取格式化数据
sscanf:在字符串中读取格式化的数据
int sscanf ( const char * s, const char * format, ...);
#include<stdio.h>
struct STU
{
char name[50];
int age;
};
int main()
{
char arr[300] = { 0 };
struct STU s1 = {.name="zhangsan",.age=20};
sprintf(arr, "%s %d", s1.name, s1.age);
struct STU s2 = { 0 };
sscanf(arr, "%s %d",s1.name,&(s1.age));
printf("%s %d", s1.name, s1.age);
return 0;
}
printf:把数据以格式化的形式打印在标准输出流上
fprintf:把数据以格式化的形式打印在指定输出流上
sprintf:把格式化的数据转换成字符串
int sprintf ( char * str, const char * format, ... );
#include<stdio.h>
struct STU
{
char name[50];
int age;
};
int main()
{
char arr[300] = { 0 };
struct STU s1 = {.name="zhangsan",.age=20};
sprintf(arr, "%s %d", s1.name, s1.age);
printf("%s", arr);
return 0;
}
文件随机读写
fseek
int fseek ( FILE * stream, long int offset, int origin );
根据
offset:偏移量
origin:起始位置可以是文件起始位置(SEEK_SET),光标当前的位置(SEEK_CUR),文件末尾(SEEK_END)
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
printf("%c\n", fgetc(pf));
fseek(pf, 4, SEEK_CUR);
printf("%c\n", fgetc(pf));
fseek(pf, 5, SEEK_SET);
printf("%c\n",fgetc(pf));
return 0;
}
ftell
long int ftell ( FILE * stream );
返回文件指针相对起始位置的偏移量
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 0, SEEK_END);
printf("%d ", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
这段代码输出的是文件的长度。
rewind
void rewind ( FILE * stream );
让文件指针回到文件起始位置
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r+");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fseek(pf, 0, SEEK_END);
rewind(pf);
printf("%d ", ftell(pf));
fclose(pf);
pf = NULL;
return 0;
}
文件结束的判定
int feof ( FILE * stream );
int ferror ( FILE * stream );
1 文本文件判断是否结束,返回EOF(fgetc)NULL(fgets)
2 二进制文件,判断fread返回值是否小于要求读取的值
已经读取结束了,feof用于判断是否遇到文件末尾而结束(文件末尾,返回非0,否则返回0)
ferror用于判断是否是因为读取错误而结束(读取错误,返回非0,否则返回0)
#include<stdio.h>
int main()
{
FILE* pf = fopen("8.22.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
int c = 0;
while (c=fgetc(pf),c!=EOF)
{
printf("%c ",c );
}
if (feof(pf))
{
printf("文件读取至结尾,没发生错误");
}
else if (ferror(pf))
{
perror("fgetc");
return 2;
}
fclose(pf);
pf = NULL;
return 0;
}
文件缓冲区
ANSIC 标准采用“缓冲文件系统” 处理的数据⽂件,所谓缓冲⽂件系统是指系统⾃动地在内存中为程序中每⼀个正在使用的文件开辟⼀块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读⼊数据,则从磁盘文件中读取数据输⼊到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的(也可自己定义)
防止系统频繁被打断
因为有缓冲区的存在,C语言操作文件的时候,需要刷新缓冲区或者在文件操作结束的时候关闭文件,防止出现文件读写问题