🍁为什么要使用文件
在上一篇文章里展示了如何构造一个通讯录,可是一旦程序退出后,空间便还给操作系统,不存在了,但是当我我们需要保留下这些文件时又无可奈何,所以这章通过学习文件操作,便可以实现保存信息功能啦。
🍁什么是文件
😶🌫️文件
磁盘上的文件即是文件,但我们从文件功能的角度看又往往分为两类
1.程序文件
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
2.数据文件
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
我们写代码的时候与文件是什么关系呢?
看下图
可能有人又不明白啦,为什么写是输出到文件,读是输入到内存里呢?
首先,可以先问问自己,真的知道什么内存吗?(博主经常听到有人说自己手机内存256G...一提运行内存就说8G)
比如,你买了个手机,是8G/256G的,这两个数据分别是什么呢?实际上前面那个数值小一点的叫内存,也就是咱平时口中的运行内存,,而后面那个较大的数值呢,叫外存,也就是咱平时说的什么C盘D盘...这些磁盘。
理解了这些,再结合上图,就不难理解,从文件中读入数据会存入内存的意思了(实际上就是输入缓冲区)。
😶🌫️文件名
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分:文件路径+文件名主干+文件后缀
例如:c:\code\test.txt
有些人可能会问,为什么我没有看见文件后缀名呢?
那不妨看看下图这个选项打开了吗?
为了方便起见,文件标识常被称为文件名。
😶🌫️文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。
这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
听着是不是有点懵?什么意思呢?
通俗的讲啊,这个文件指针是用来维护一块空间的,怎样的一块空间呢?是一个结构体,在stdio.h定义中被声明为FILE
看以下图解:
假设我的磁盘中有一个文件叫test.txt这样一个文件,如下图:
一旦当我打开这个文件时,他就会在内存中开辟一块文件信息区(一个结构体,类型是FILE),而FILE ptr(假设创建的变量名为ptr)这个结构体就是这个文件信息区的一块空间(如下图)
想看这个结构体的成员,可以通过VS2013,(2019已经不显示了)如下:
struct _iobuf
{ char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file;
int _charbuf;
int _bufsiz;
char *_tmpfname;
};typedef struct _iobuf FILE;
刚刚提到一旦打开test.c文件就会在内存中创建文件信息区
文件信息区那这块内存在什么位置呢?
由什么来维护呢?
在C语言的程序里通过fopen这个函数可以打开文件,打开成功会后就会返回这块空间起始位置的地址,这块空间类型为FILE,所以返回的地址就是FILE*类型,程序员就可以通过创建一个FILE*的变量来维护这块空间,这就是文件指针。
下面演示一下如何创建一个指针变量
FILE* pf;//文件指针变量
定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
如下图:
🍁相关函数
😶🌫️文件的打开和关闭(fopen,fclose)
咱先给出语法格式
//打开文件
FILE * fopen ( const char * filename, const char * mode );
//关闭文件
int fclose ( FILE * stream );
需要注意以下几点:
文件读写前要打开文件,读写后要关闭文件;
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件;
在编写文件,打开文件的同时fopen会返回一个指针,若打开成功,则返回该文件在文件信息区的起始空间地址,若打开失败则返回空指针,所以我们为了防止程序出bug,因该这样写(如下代码):
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//处理
fclose(ptr);
return 0;
}
亦或是这样写,如下(报出错误信息即可)
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("fopen");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
fclose(ptr);
return 0;
}
这样写真的就够了吗?
想象一下,fclose可以释放带哦ptr所指向的那块空间,而一旦ptr所指向的空间被释放掉,ptr便成了野指针,所以将ptr置为空指针,如下:
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("fopen");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
fclose(ptr);
ptr = NULL;
return 0;
}
文件有那些打开方式呢?
最常用的便是只读、只写,以下为代码演示:
例一:只写
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("fopen");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
fprintf(ptr, "hello world");
//关闭文件
fclose(ptr);
return 0;
}
例二:只读
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
char str[15] = { 0 };
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
fprintf(ptr, "hello world");
//关闭文件
fclose(ptr);
//打开文件
FILE* ptr_ = fopen("test.txt", "r");
if (ptr_ == NULL)
{
perror("ptr_");
return 1;
}
//处理
fgets(str, sizeof(str), ptr_);
printf("%s\n", str);
//关闭文件
fclose(ptr_);
ptr_ = NULL;
return 0;
}
😶🌫️文件的顺序读写
1.fputc
先看看要求
怎么用呢?
格式:
int fputc( int c, FILE *stream );
c表示要输入的字符,stream表示要被输出的位置
例一
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
char i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, ptr);
}
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
2. fgetc
先看看要求
怎么用呢?
格式:
int fgetc( FILE *stream );
FILE* stream 表示一个文件指针变量,stream表示流,也就是介于内存和文件信息区的输入缓存区(这里不细讲,不是本篇重点,以后会出相关博客),简而言之就是需要你刚刚打开的文件的指针,返回类型为int。
例二
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");//如果没能打开文件,perror会直接打印错误信息
return 1; //括号里是可以任意写的一个常量字符串
}
//处理
char i = 0;
for (i = 'a'; i <= 'z'; i++)
{
fputc(i, ptr);
}
//关闭文件
fclose(ptr);
ptr = NULL;
//打开文件
FILE* ptr_ = fopen("test.txt", "r");
if (ptr_ == NULL)
{
perror("ptr_");
return 1;
}
//处理
int ch = 0;//要注意,fgetc返回值是int类型,所以用int来接收
while ((ch = fgetc(ptr_)) != EOF)//读取完返回EOF 这里注意优先级,用括号括起来
{
printf("%c ", ch);
}
//关闭文件
fclose(ptr_);
ptr_ = NULL;
return 0;
}
3.fguts
先来看看要求
怎么用呢?
格式
int fputs( const char *string, FILE *stream );
string是要被输出的常量字符串,stream是被输出的文件指针;
例三
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作
fputs("hehe\n", ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
4.fgets
先来看看要求
怎么用呢?
格式
char *fgets( char *string, int n, FILE *stream );
从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
例四
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作
fputs("hehe\n", ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
//打开文件
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("p");
return 1;
}
//操作
char str[5] = { 0 };
fgets(str, 5, p);
printf("%s\n", str);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
5.fprintf
先看看要求
怎么用呢?
格式
int fprintf( FILE *stream, const char *format [, argument ]...);
这里都可以对比printf的格式
int printf(const char *format [, argument ]...);
实际不难看懂,只是多了一个被输出的文件流
例五
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作文件
fprintf(ptr, "hello world\n");
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
6.fscanf
先来看看要求
怎么用呢?
格式
int fscanf( FILE *stream, const char *format [, argument ]... );
同理和scanf函数对比格式
int scanf(const char *format [, argument ]... );
不难理解,也是少了文件指针流
例6
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作文件
fprintf(ptr, "hello world\n");
//关闭文件
fclose(ptr);
ptr = NULL;
//打开文件
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("p");
return 1;
}
//操作文件
char str[20] = { 0 };
fscanf(p, "%s", str);//fscanf遇到空格停止读取
printf("%s ", str);
fscanf(p, "%s", str);
printf("%s\n", str);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
7.fwrite
先看看要求
怎么用呢?
格式
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );
简而言之:将buffer这个任意类型,放入count个以size个字节的大小到stream
例7
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作文件
student s = { "chen",19 };
fwrite(&s, sizeof(s), 1, ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
8.fread
先来看看要求
怎么用呢?
格式
size_t fread( void *buffer, size_t size, size_t count, FILE *stream );
简而言之:将stream这个文件流的count个size大小的数据写入buffer中
例子8
#include<stdio.h>
#include<errno.h>
#include<string.h>
typedef struct student
{
char name[15];
int age;
}student;
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror("ptr");
return 1;
}
//操作文件
student s = { "chen",19 };
fwrite(&s, sizeof(s), 1, ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
//打开文件
FILE* p = fopen("test.txt", "r");
if (p == NULL)
{
perror("p");
return 1;
}
//操作文件
student s1 = { 0 };
fread(&s1, sizeof(s), 1, p);
printf("%s %d\n", , s1.age);
//关闭文件
fclose(p);
p = NULL;
return 0;
}
9.fseek
先看看要求
怎么用呢?
格式
int fseek( FILE *stream, long offset, int origin );
简而言之:在文件stream里,以为起点origin(偏移起始位置:文件头0(SEEK_SET),当前位置1(SEEK_CUR),文件尾2(SEEK_END)),将光标偏移offset个字符;
例9
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror(ptr);
return 0;
}
//处理文件
fputs("hellowcdef", ptr);
fseek(ptr, 6, SEEK_SET);
fputs(" hello world\n", ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
10.rewind
先看看要求
怎么用呢?
格式
//未保存数据丢失//
例10
#include<stdio.h>
#include<errno.h>
#include<string.h>
int main()
{
//打开文件
FILE* ptr = fopen("test.txt", "w");
if (ptr == NULL)
{
perror(ptr);
return 0;
}
//处理文件
fputs("world", ptr);
rewind(ptr);
fputs("hello\n", ptr);
//关闭文件
fclose(ptr);
ptr = NULL;
return 0;
}
11.feof
被许多人误用的函数:feof用来判断文件是否结束
实际上:feof是在文件结束的时候判断文件结束的原因,判断文件是因为遇到文件位结束还是读取失败结束
- 1. 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets)例如:
- fgetc判断是否为EOF.
- fgets判断返回值是否为NULL.
- 2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。例如:
- fread判断返回值是否小于实际要读的个数
例11
#include <stdio.h>#include <stdlib.h>
int main(void)
{
int c; // 注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if(!fp)
{
perror("File opening failed");
return EXIT_FAILURE;
} //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
while ((c = fgetc(fp)) != EOF) // 标准C I/O读取文件循环
{
putchar(c);
}
//判断是什么原因结束的
if (ferror(fp))
puts("I/O error when reading");
else if (feof(fp))
puts("End of file reached successfully");
fclose(fp);
fp = NULL;
return 0;
}
🍁文本文件和二进制文件
- 文本文件
- { 以ASCII字符的形式存储的文件就是文本文件 };
- 二进制文件
- { 数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。 }
数据在文本文件中和在二进制文件中分别是怎么存储的?
如有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节),而二进制形式输出,则在磁盘上只占4个字节。
🍁面试题
对比一组函数
scanf/fscanf/sscanf
printf/fprintf/sprintf
scanf/prinf 是针对标准输入流/标准输出流的 格式化输入/输出
fscanf/fprintf 是针对所有输入流/所有输出流的 格式化输入/输出
sscanf/sprintf sscanf是从字符串中读取格式化的数据 sprintf是把格式化的数据输出成(存储到)字符串