总言
C语言:文件操作。
文章目录
- 总言
- 1、文件是什么?为什么需要文件?
- 1.1、为什么需要文件?
- 1.2、文件是什么?
- 2、文件的打开与关闭
- 2.1、文件指针
- 2.2、文件打开和关闭:fopen、fclose
- 2.3、文件使用方式
- 3、文件的顺序读写
- 3.1、字符输入输出:fputc、fgetc
- 3.1.1、fputc
- 3.1.2、fgetc
- 3.2、适用于所有流:stream的简单介绍
- 3.3、文本输入输出:fputs、fgets
- 3.3.1、fputs
- 3.3.2、fgets
- 3.4、格式化输入输出:fscanf、fprintf
- 3.4.1、fprintf
- 3.4.2、fscanf
- 3.5、与字符串相关的输入输出:sscanf、sprintf
- 3.5.1、对比说明
- 3.5.2、使用介绍
- 3.6、二进制输入输出:fread、fwrite
- 3.6.1、fwrite
- 3.6.2、fread
- 4、文件的随机读写
- 4.1、fseek
- 4.2、ftell
- 4.3、rewind
- 5、其它相关内容
- 5.1、文本文件和二进制文件
- 5.2、文件读取结束判定
- 5.3、文件缓冲区
1、文件是什么?为什么需要文件?
1.1、为什么需要文件?
模拟实现通讯录时,会面临这样一个实际问题:在通讯录中把信息记录下来后,只有在我们自己选择删除数据的情况下,数据才不复存在,否则需要长久保留数据。
这就涉及到了数据持久化的问题,一般数据持久化的方法有,把数据存放在磁盘文件、存放到数据库等方式。使用文件我们可以将数据直接存放在电脑的硬盘上,做到了数据的持久化。
1.2、文件是什么?
1)、文件分类
磁盘上的文件是文件。但在程序设计中,从文件功能的角度来看,一般文件有两种:程序文件、数据文件。
程序文件:包括源程序文件(后缀为.c
),目标文件(windows环境后缀为.obj
),可执行程序(windows环境后缀为.exe
)。
数据文件: 文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
2)、文件名称
一个文件要有一个唯一的文件标识,以便用户识别和引用。为了方便起见,文件标识常被称为文件名。
文件名包含3部分:文件路径+文件名主干+文件后缀
D:\日常\TIM\Tencent Files\All Users\test.txt
文件路径:D:\日常\TIM\Tencent Files\All Users\
文件名主干:test
文件后缀:.txt
2、文件的打开与关闭
2.1、文件指针
1)、文件指针是什么?
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名FILE
。
不同的C编译器的FILE
类型包含的内容不完全相同,但是大同小异。每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE
结构的变量,并填充其中的信息,使用者不必关心细节。
VS2019下FILE声明:
#ifndef _FILE_DEFINED
#define _FILE_DEFINED
typedef struct _iobuf
{
void* _Placeholder;
} FILE;
#endif
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结构的变量
,这样使用起来更加方便。
2)、文件指针的作用
我们可以创建一个FILE*
的指针变量,如下:
FILE* pf;
定义pf
是一个指向FILE类型数据的指针变量。可以使pf
指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
2.2、文件打开和关闭:fopen、fclose
1)、fopen、fclose函数介绍
相关函数链接:fopen
fopen
FILE * fopen ( const char * filename, const char * mode );
const char * filename
:所需要打开的文件名称。
const char * mode
:打开文件的方式,此处类型为字符指针,用于存放字符串首字符的地址。
FILE *
:在内存中创建一个与打开文件有关的文件信息区,同时返回一个文件指针,该指针指向文件信息区的起始地址(结构体)。
NULL
:如果文件打开失败,则返回空指针,同动态内存一致,需要进行判空操作,若错误则显示错误原因,并结束程序。
相关链接:fclose
fclose
int fclose ( FILE * stream );
FILE * stream
:向指定要关闭的FILE指针
2)、使用举例
int main()
{
//打开文件
FILE* pf = fopen("test.txt", "r");//“r”此处为字符串,用双引号,r是一种文件读取模式
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读文件
//……
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
情形一:
情形二:
情形三:绝对路径与相对路径说明
假如要打开的文件不在当前路径下,需要使用对应路径打开:
2.3、文件使用方式
以下mode会在后续学习中慢慢用到。
3、文件的顺序读写
3.1、字符输入输出:fputc、fgetc
3.1.1、fputc
相关函数链接:fputc
fputc
int fputc ( int character, FILE * stream );
将字符character按顺序写入对应的文件流stream中,此处使用的是文件指针。如果失败则返回EOF。
演示如下:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
for (char ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.1.2、fgetc
相关函数链接:fgetc
fgetc
int fgetc ( FILE * stream );
从对应的文件指针指向的文件中按顺序依次读取字符,若遇到文件结尾/错误则返回EOF,若成功则返回字符读取(提升为 int 值)。
使用演示:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
int ch;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c ", ch);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.2、适用于所有流:stream的简单介绍
我们知道各种各样的外部设备,比如键盘、屏幕、U盘、硬盘、网卡等,数据信息的读写在它们之间进行交换,但不同的设备其读写方式可能存在差异,这样一来使用效率就会大幅降低,因为我们在做数据输入输出的前提是要学习了解它们的原理。因此,为了简化这一过程,我们在这些外设间引入一个中间商,即流,用于输入输出。这样一来,我们在各种数据交换时,就不必关心流与这些外设间是如何做到数据信息交换的,相当于一种封装。
相关演示:
int main()
{
int ch = fgetc(stdin);
printf("%c ", ch);
fputc(ch, stdout);
return 0;
}
3.3、文本输入输出:fputs、fgets
3.3.1、fputs
相关函数链接:fputs
相关演示:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
fputs("落霞与孤鹜齐飞,", pf);
fputs("秋水共长天一色。\n", pf);
fputs("渔舟唱晚,响穷彭蠡之滨;\n", pf);
fputs("雁阵惊寒,声断衡阳之浦。\n", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
可以看到,没加换行符,fputs会将当前内容放置于上此文尾,若加了换行符则另起一行。
3.3.2、fgets
相关函数链接:fgets
若成功,则返回str指针指向地址,若失败,则返回NULL;
相关演示一:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[255];
//读写文件
fgets(arr, 254, pf);
printf("%s\n", arr);
while (fgets(arr, 254, pf)!=NULL)
printf("%s", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
验证fgets会保留一位将\0放入:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
char arr[10]="XXXXXXXXXX";
//读写文件
fgets(arr, 5, pf);
printf("%s\n", arr);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.4、格式化输入输出:fscanf、fprintf
3.4.1、fprintf
相关函数链接:printf
printf是将相关内容打印到标准输出,即屏幕上;
fprintf适用于任意流,可将相关内容打印到指定的stream中。
相关演示:
struct st
{
char name[20];
int age;
double score;
};
int main()
{
//创建一个学生数据,要求:将该学生数据的内容存入指定文件中
struct st s1 = { "张三",25,98.00 };
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
fprintf(pf,"%s %d %lf", s1.name, s1.age, s1.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.4.2、fscanf
相关函数链接:fscanf
scanf是从标准输入流中读取数据,一般是键盘;
fscanf适用于任意流,可从指定的流stream中读取数据。
相关演示:
struct st
{
char name[20];
int age;
double score;
};
int main()
{
//创建一个学生数据,要求:从指定文件中读取相关数据信息填入该结构体中
struct st s1 = {0};
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写文件
fscanf(pf, "%s %d %lf", s1.name, &(s1.age), &(s1.score));
//注意,name为数组名,本身就是地址,故无需取地址操作符,其它二者读取时与scanf别无差异
fprintf(stdout, "%s %d %lf", s1.name, s1.age, s1.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.5、与字符串相关的输入输出:sscanf、sprintf
3.5.1、对比说明
1)、scanf/printf、fscanf/fprintf、sscanf、sprintf对比说明
scanf
:针对标准输入输出流的格式化输入函数,Read formatted data from stdin
printf
:针对标准化输入输出流的格式化输出函数,Print formatted data to stdout
scanf
int scanf ( const char * format, ... );
printf
int printf ( const char * format, ... );
fscanf
:针对所有输入流的格式化输入函数,Read formatted data from stream
fprintf
:针对所有输出流的格式化输出函数,Write formatted data to stream
fscanf
int fscanf ( FILE * stream, const char * format, ... );
fprintf
int fprintf ( FILE * stream, const char * format, ... );
sscanf
:把一个字符串数据转换为格式化数据,Read formatted data from string
sprintf
:把一个格式化的数据转换为字符串数据,Write formatted data to string
sscanf
int sscanf ( const char * s, const char * format, ...);
sprintf
int sprintf ( char * str, const char * format, ... );
3.5.2、使用介绍
相关函数链接:sprintf
相关函数链接:sscanf
使用举例:
struct st
{
char name[20];
int age;
double score;
};
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写操作
struct st s1 = { "李四",23,94.5 };//原数据
struct st stof = { 0 };//未赋有效值的结构体:用于存放从字符串中转换过来的格式化数据
char ftos[255];//字符数组:用于存放从格式化数据中转换得到的字符串
//使用sprintf,将格式化数据转换成字符串
sprintf(ftos, "%s %d %lf", s1.name, s1.age, s1.score);
printf("ftos数组中:%s\n", ftos);
//使用sscanf,将字符串转换为格式化数据
sscanf(ftos, "%s %d %lf", stof.name, &(stof.age), &(stof.score));
printf("stof结构体中:%s %d %lf\n", stof.name, stof.age, stof.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.6、二进制输入输出:fread、fwrite
3.6.1、fwrite
相关函数链接:fwrite
注意理解该函数各参数含义,ptr是要写入的元素数组的指针,size是单个此类数据的大小(每个元素的大小),count是要写入这类数据的数目(元素个数),stream为指定输出的流。
fwrite
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
相关演示:
struct st
{
char name[20];
int age;
double score;
};
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "wb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写操作
struct st s1 = { "李四",23,94.5 };
fwrite(&s1, sizeof(struct st), 1, pf);
//含义:将s1中的数据输出至pf中,个数为1个,每个大小为sizeof(struct st)
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
3.6.2、fread
相关函数链接:fread
相关演示:
struct st
{
char name[20];
int age;
double score;
};
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "rb");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//读写操作
struct st s1 = { 0 };
fread(&s1, sizeof(struct st), 1, pf);
printf("%s %d %lf\n", s1.name, s1.age, s1.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4、文件的随机读写
4.1、fseek
相关函数链接:fseek
根据文件指针的位置和偏移量,重新定位文件指针。
SEEK_SET Beginning of file
SEEK_CUR Current position of the file pointer
SEEK_END End of file *
1)、关于随机位置读取
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "r");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件读写
//
//文件中存储的例句:turn lemon into lemonade.
for (int i = 0; i < 4; i++)
{
printf("%c", fgetc(pf));//turn
}
printf("\n");
//至此,文件指针指向trun lemon中的空白字符
//演示一:以当前文件指针作为参考点
fseek(pf, -1, SEEK_CUR);//当前指向turn中的n
printf("文件向左偏移一位后:%c\n", fgetc(pf));//fgetc读取后后挪一位,到空白字符
fseek(pf, 1, SEEK_CUR);//当前指向为lemon中的l
printf("文件向右偏移一位后:%c\n", fgetc(pf));//fgetc读取后后挪一位,到e
printf("\n");
//演示二:以文件头作为参考点,此时偏移量不能为负数
fseek(pf, 3, SEEK_SET);//当前指向trun中的n
printf("文件向右偏移三位后:%c\n", fgetc(pf));
printf("再次读取一位:%c\n", fgetc(pf));
printf("\n");
//演示三:以文件尾作为参考点,此时偏移量不能为正数
fseek(pf, -3, SEEK_END);//当前指向为lemonade.中的d
printf("文件向左偏移三位后:%c\n", fgetc(pf));
printf("再次读取一位:%c\n", fgetc(pf));
printf("\n");
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
2)、关于随机位置写入
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件读写
//
for (char ch = 'a'; ch <='z'; ch++)
{
fputc(ch,pf);
}
//演示一:以当前文件指针作为参考点
fseek(pf, -22, SEEK_CUR);
fputc('S', pf);
//演示二:以文件头作为参考点,此时偏移量不能为负数
fseek(pf, 6, SEEK_SET);
fputc('O', pf);
//演示三:以文件尾作为参考点,此时偏移量不能为正数
fseek(pf, -3, SEEK_END);
fputc('S', pf);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.2、ftell
返回文件指针相对于起始位置的偏移量
相关函数链接:ftell
代码演示如下:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件读写
//
for (char ch = 'a'; ch <='z'; ch++)
{
fputc(ch,pf);
}
fseek(pf, -22, SEEK_CUR);
fputc('S', pf);
long int pos = ftell(pf);
printf("%ld\n", pos);
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
4.3、rewind
相关函数链接:rewind
让文件指针的位置回到文件的起始位置
相关演示:
int main()
{
//打开文件
FILE* pf = fopen("D:\\Daily\\test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
//文件读写
//
for (char ch = 'a'; ch <='z'; ch++)
{
fputc(ch,pf);
}
fseek(pf, -22, SEEK_CUR);
fputc('S', pf);
long int pos = ftell(pf);
printf("%ld\n", pos);
rewind(pf);
printf("%ld\n", ftell(pf));
//
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
5、其它相关内容
5.1、文本文件和二进制文件
1)、数据文件分类
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换直接输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件。
2)、数据在内存中的存储方式
字符型数据一律以ASCII形式存储;
数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
3)、举例演示
以整数10000为例子:
一个测试代码:
我们以二进制的形式将正数10000写入文件中
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("D:\\Daily\\test.txt", "wb");
fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
fclose(pf);
pf = NULL;
return 0;
}
假如用文本文件的形式打开,可以看到是乱码
现在我们在VS中以二进制的形式打开,需要做一点设置,操作如下:
5.2、文件读取结束判定
1)、关于feof函数说明
在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。feof是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。
2)、关于文件读取结束说明
根据文件类型的区别,判断文件读取结束具有一定区别:
对文本文件读取是否结束,有两种判断方法:1、判断返回值是否为 EOF;2、判断返回值是否为 NULL 。
例如:fgetc
函数中,判断文件读取结束是看是否返回 EOF
,而fgets
判断文件读取结束是看返回值是否为 NULL
。
对二进制文件读取是否结束,可通过返回值是否小于实际要读的个数来判断。
例如:fread
判断返回值是否小于实际要读的个数。
5.3、文件缓冲区
1)、什么是文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理的数据文件。所谓缓冲文件系统,是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。
2)、为什么需要文件缓冲区
文件缓冲区是一个链接内存与硬盘的过渡段,从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。之所以要这样做,是因为内存和硬盘处理数据的速度不一,若直接从磁盘中读取数据输送到内存, 会面临频繁访问、占用内存资源的现象。故而在二者之间搭建出一个中间商。
需要注意的是,缓冲区的大小是根据C编译的系统来决定。