文章目录
文件管理
文件
什么是文件呢?
我们知道,我们磁盘上的文件就是文件,我们平时使用电脑也会做创建文件夹之类的操作。
不过,程序设计中,文件一般会分为:程序文件和数据文件。
程序文件
.c为后缀的源文件、.obj为后缀的目标文件、.exe为后缀的可执行程序(windows环境)
数据文件
文件内容不全是程序,会有程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者是输出内容的文件。
我们这里讨论的文件操作就是对数据文件的操作,操作有对文件的读写操作,说到读和写,我们想到我们之前写的代码,系统都是从键盘读信息,读取的信息打印到屏幕上。学习完文件操作,我们就可以在文件上写读了。
文件名包含三个部分:文件路径、文件名主干、文件后缀
我们为什么要使用文件?
使用文件是为了持久化地保存数据。我们的可执行程序需要置入内存运行,程序运行时的数据是保存在内存当中的,而当我们的程序运行结束,内存回收,程序运行时的数据就消失了。
文本文件和二进制文件
我们要谈的数据文件是有两种组织形式的:一种是文本文件,一种是二进制文件。
文本文件
文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,也就是说存放的是数据的终端形式。文本文件最终是以ASCII码值的形式存储。
二进制文件
二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,也就是说存放的是数据的原形式。
数据在文件中的存储
字符是以ASCII码值存储,而数值型的数据既可以以ASCIIC码值的形式存储,也可以以二进制的形式存储。
举个例子,一个整数10000,以ASCII码值的形式存储,则需要占用5个字节,将10000分为5个字符,一个字符占一个字节;如果以二进制形式存储,则只需要占用4个字节。
介绍文件打开和读写前,我们得先了解一些知识:
流
我们程序的数据需要输出到各种外部设备,也需要从外部设备获取数据,不同的外部设备的输⼊输出操作各不相同,为了方便程序员对各种设备进行方便的操作,我们抽象出了流的概念,我们可以把流 想象成流淌着字符的河。 C程序针对文件、画面、键盘等的数据输入输出操作都是通过流操作的。 ⼀般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
标准流
我们C语言程序在启动时,会自动打开3个流:
- stdin:标准输入流 - 大多数环境从键盘输入,我们经常使用的scanf就是从标准输入流中读取数据。
- stdout:标准输出流 - 大多数环境中输出到屏幕界面,我们经常使用的printf就是将信息输出到标准输出流。
- stderr:标准错误流 - 大多数环境输出到显示器界面。
文件指针
C语言自动打开的三个流的类型就是 FILE* 类型的,FILE*通常称为文件指针。
C语言通过文件指针来维护流的各种操作。
那么,这个文件指针是什么呢?
FILE其实是一个结构体的类型,每个被使用的文件都会在内存中开辟一个相应的文件信息区,用来存放文件的相关信息,这些信息就被保存在FILE结构体中,它是由系统声明的。
VS2013编译环境下:
struct _iobuf{
char* _ptr;
int _cnt;
char* _base;
int _flag;
int _file;
int _charbuf:
int _bufsize;
char* _tmpfname;
};
typedef struct _iobuf FILE;
不同编译器的FILE类型内容有所差异,我们只是为了证明FILE是一个结构体类型。
当我们打开文件时,系统会根据文件情况自动创建一个FILE结构体变量,并填充信息。
说到这,关于文件指针是什么就比较清楚了,文件指针就是一个FILE结构体类型的指针变量,用来维护FILE结构体。
文件指针是可以直接使用的:
FILE* pf;
pf指向某个文件的文件信息区(文件信息区就是结构体变量),而通过文件信息区就能访问到我们的文件。
总结来说,通过文件指针就能间接找到与它关联的文件。
文件的打开/关闭
想要对文件执行读写操作,我们必须首先打开文件,并要及时关闭文件。
fopen
打开文件
FILE* fopen(const char* filename, const char* mode);
-
filename就是我们要打开的文件名
-
mode是打开模式,mode决定了打开文件的方式。
-
返回值是一个FILE*类型的指针变量,这个指针变量指向该文件,我们需要接收这个变量,后续对文件的操作需要通过返回的文件指针来实现。
如果打开错误,会返回NULL。
所以我们调用fopen函数时要对它的返回值进行检查,以判断打开文件的成功与否。
文件打开模式
注意事项:
- 在使用 “r”、“rb”、“r+”、“rb+” 这四个模式时,如果指定的文件不存在,那么就会出错,fopen函数将返回NULL,表示打开错误。
- 以除了上面4个模式外的其他模式打开文件,如果指定的文件不存在,那么会自动创建一个文件。
- 以 “w” 模式打开文件,书写文件时,会覆盖掉原来文件的内容,也就是说,打开时,光标停留在文件起始位置。
- 要选择正确的打开模式,不要出现以读模式打开却进行写操作的行为。
fclose
关闭文件
int fclose(FILE* stream);
- stream:指向指定要关闭的流的 FILE 对象的指针。
- 返回值:如果关闭成功返回0,失败则返回EOF,这个返回值不常用。
- 值得注意的是:当成功关闭文件后,接收fopen返回值的指针不会置空,就像free函数一样,我们要记得置为NULL。
举例:
#include <stdio.h>
int main()
{
//打开文件
FILE* pf = fopen("test.txt","w");
if(pf == NULL)
{
perror("fopen");
return 1;
}
//文件操作
fputs("hello world", pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
在VS2022上运行这一代码,自动生成了一个文件,并执行相关操作,其函数后面会讲:
文件的顺序读/写
文件的顺序读写函数有很多,且读写成对,我们一一介绍。
表格所说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输入流(如文件输出流)。
比如,函数适用于所有输出流表明我们可以用它对文件进行写操作(文件输出流),也可以打印到屏幕上(标准输出流)。
以下函数都是以在文件流中操作为例。
fputc
向流(一般都是文件流)输入一个字符(写)
int fputc(int character, FILE* stream);
-
stream指向是要进行写操作的文件
-
返回值:如果书写成功,返回被写入文件的字符
如果书写时发生了错误,返回EOF
-
这是一个写文件的函数,我们要以写的形式打开
-
适用于所有输出流
#include <stdio.h>
//将字符a写入指定文件
int main()
{
FILE* pf = fopen("test.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
fputc('a', pf);
fclose(pf);
pf = NULL;
return 0;
}
我们打开指定文件:
注意此时的文件的光标,它停留在文件末尾,也就是说如果我们在关闭文件前多次写入,写入的内容会因光标的变化而实现顺序写入。我们举个例子:
#include <stdio.h>
//将a~z写入指定文件
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
return 0;
}
打开文件:
fgetc
从流(一般都是文件流)读取一个字符(读)
int fgetc(FILE* stream);
-
返回值:
成功读取,返回字符ASCII码值
读取遇到文件末尾或读取失败,返回EOF
-
读文件函数,以读的方式打开
-
适用于所有输入流
#include <stdio.h>
//从test.txt文件里读取字符,该文件内容为hello world
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULLhe == pf)
{
perror("fopen");
return 1;
}
char ch = 0;
ch = fgetc(pf);
printf("%c ", ch);
fclose(pf);
pf = NULL;
return 0;
}
在关闭文件前,可以不断读文件,直到读到文件末尾,我们要表达的是,我们每读一个字符,光标会自动后移一个。
#include <stdio.h>
//将内容为hello world的文件的内容全部读取并输出到屏幕
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
char ch = 0;
while ((ch = fgetc(pf)) != EOF)
{
printf("%c", ch);
}
fclose(pf);
pf = NULL;
return 0;
}
在讲下一对函数前,我们写一段代码来感受一下 “适用于所有输出/入流”
前面我们在文件输入/出流中使用,这里我们在标准输入/出流使用
#include <stdio.h>
int main()
{
fputc('a', stdout);
fgetc(stdin);
return 0;
}
fputs
向指定流输出一个字符串(写)
int fputs(const char* str, FILE* stream);
-
str是要写入文件的字符串的地址
-
返回值:成功后,返回一个非负值
写入失败时,返回EOF
-
适用于所有输出流
#include <stdio.h>
//将abcdef写入文件
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fputs("abcdef", pf);
fclose(pf);
pf = NULL;
return 0;
}
如果我们想换行写文件,直接加一个换行符就可以了
#include <stdio.h>
//换行写文件
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fputs("hello world\nok,hi", pf);
fclose(pf);
pf = NULL;
return 0;
}
fgets
从指定流输入一个字符串(读)
char* fgets(char* str, int num, FILE* stream);
-
num:想从文件流读取多少个字节的数据
-
str:从文件流读取的num个字节的内容拷贝到这块空间
值得注意的是,假设我们想要读取10个字符(即num = 10),那么fgets会读取9个字节的数据放在str,最后放一个\0
-
fgets单次不能跨行读取数据,如果某次要读取10(实际只会读9个)个数据,而此行数据太少,假设5个,那么会将5个数据读完,再把\n(如果有)读入,最后再补\0,此时拷贝了5 + \n + \0 共7个数据。
-
返回值:
读取成功:返回目标空间的起始地址
读取失败或遇到文件末尾,会返回NULL
-
适用于所有输入流
我们来通过调试,证明一下第三点:
#include <stdio.h>
//调试代码
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
char ac[20] = "xxxxxxxxxxxxxxx";
fgets(ac, 10, pf);
printf("%s", ac);
fclose(pf);
pf = NULL;
return 0;
}
不难发现,fgets只读取了9个字符放到ac数组,最后一个放了\0
我们修改一下文件内容,看一下文件当前读取行字符不够的情况(调试代码不变):
结果显示,会读入\n
函数的使用
要读取的文件的内容:
#include <stdio.h>
//将上面写的多行数据全部读出,输出到屏幕
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
char ac[20] = { 0 };
while (fgets(ac, 10, pf) != NULL)
{
printf("%s", ac);
}
fclose(pf);
pf = NULL;
return 0;
}
fwrite
以二进制输出数据到文件(向流内输出数据块)
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
- ptr是指向要写入的元素数组的指针
- size:要写入的元素数组的每个元素的大小,单位为字节
- count:元素数量
- 返回值:写入成功则返回成功写入的元素总数。
如果此数字与 count 参数不同,则写入错误会阻止函数完成。
如果 size 或 count 为零,则函数返回零。 - 打开模式选择二进制方式
- 只适用于文件流
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "wb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
int a[10] = { 1,2,3,4,5 };
int sz = sizeof(a) / sizeof(a[0]);
fwrite(a, sizeof(int), sz, pf);
fclose(pf);
pf = NULL;
return 0;
}
对于这样一个二进制文件,我们无法阅读它的内容。
fread
从二进制文件中读取数据(从流中读取数据块)
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
-
ptr指向想存放读取后数据的一块内存空间(如数组)
-
size:每个要被读取的元素的大小,以字节为单位
-
count:元素数
-
返回值:读取成功,返回成功读取的元素总数
如果此数字与 count 参数不同,则表示读取时发生读取错误或到达文件末尾。
如果 size 或 count 为零,则该函数返回零,并且 ptr 指向的流状态和内容保持不变。
#include <stdio.h>
//将上面fwrite函数写入的数据读回
int main()
{
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
int a[10] = {0};
int sz = sizeof(a) / sizeof(a[0]);
fread(a, sizeof(int), sz, pf);
for (int i = 0; i < sz; i++)
{
printf("%d ", a[i]);
}
fclose(pf);
pf = NULL;
return 0;
}
这样我们我们就知道了二进制文本中的内容。
fprintf
以文本形式格式化打印数据到流中(写)
int fprintf(FILE* stream, const char* format, ...);
-
fprintf适用于所有输出流,包括标准输出流和文件输出流
-
与printf的区别:
int printf(const char* format...);
printf只适用于标准输出流,只能向屏幕打印信息。
#include <stdio.h>
//向指定文件格式化写入数据
struct stu
{
char name[20];
size_t age;
double score;
};
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
struct stu s1 = { "xiao ming", 20, 65.5 };
fprintf(pf, "%s %zd %lf", s1.name, s1.age, s1.score);
fclose(pf);
pf = NULL;
return 0;
}
用fprintf实现printf的功能:
只需要把第一个参数传入stdout即可
#include <stdio.h>
int main()
{
fprintf(stdout, "%d %f", 100, 34.5);
return 0;
}
总结:与printf的区别就是fprintf可以适用于所有输出流
fscanf
从流中格式化读取数据(读)
int fscanf(FILE* stream, const char* format, ...);
-
fscanf适用于所有输入流,包括标准输入流和文件输入流
-
与scanf的区别:
int scanf(const char* format, ...);
scanf只适用于标准输入流,只能从标准输入流中读取数据。
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fprintf(pf, "%d %s", 100, "hello");
fclose(pf);
pf = NULL;
pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
int i = 0;
char arr[10] = { 0 };
fscanf(pf, "%d %s", &i, arr);
printf("%d %s", i, arr);
fclose(pf);
pf = NULL;
return 0;
}
用fscanf实现scanf的功能:
只需要把第一个参数写成stdin即可:
#include <stdio.h>
int main()
{
int i = 0;
double b = 0;
fscanf(stdin, "%d %lf", &i, &b);
printf("%d %lf", i, b);
return 0;
}
文件的随机读写
上面介绍的函数都是对文件顺序读写,我们也可以依靠一些函数实现随机读写。
fseek
根据文件指针的位置和偏移量来定位⽂件指针(文件内容的光标)
int fseek(FILE* stream, long int offset, int origin);
-
origin:三个选择:
SEEK_SET 文件起始位置
SEEK_CUR 文件现在位置
SEEK_END 文件末尾位置,注意是末尾位置,如abcdefg|,竖线位置就是末尾位置,而不是abcdef|g在这个位置
-
offset:相对于origin的偏移量,与origin一起决定了最后光标的停留位置
-
返回值:如果成功,返回0;失败则返回非0值,返回值不怎么用得到
我们想从下面文件的第5个位置开始读,读到文件末尾
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
char a[10] = { 0 };
fseek(pf, 4, SEEK_SET);//设置光标
while (fgets(a, 10, pf) != NULL)
{
printf("%s", a);
}
fclose(pf);
pf = NULL;
return 0;
}
如果我们第三个参数选择的是SEEK_END,想要将光标设置在前面,第二个参数传负数即可。
ftell
返回文件指针相对于起始位置的偏移量
long int ftell(FILE* stream);
-
返回值:成功时,返回当前的偏移量
失败时,返回-1
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fseek(pf, 4, SEEK_SET);
int ret = ftell(pf);
printf("%d", ret);
fclose(pf);
pf = NULL;
return 0;
}
rewind
让文件指针的位置回到文件的起始位置
void rewind(FILE* stream);
我们使用ftell检查是否返回起始位置
#include <stdio.h>
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
fseek(pf, 4, SEEK_SET);
rewind(pf);
int ret = ftell(pf);
printf("%d", ret);
fclose(pf);
pf = NULL;
return 0;
}
文件结束标志的判定
当打开一个流的时候,流上有两个标记值:
1.是否遇到文件末尾(feof)
2.是否发生错误(ferror)
这两个标记值可以分别用下面两个函数来检查,检查的是文件结束的原因。
feof
文件已经读取结束,判断文件读取结束的原因是否是:遇到文件尾结束
牢记:不可以用feof的返回值判断文件是否结束,文件结束的原因有很多,如遇到文件尾、读取文件时发生了错误
int feof(FILE* stream);
-
返回值:如果文件结束的原因是遇到文件末尾,那么返回一个非0的值
否则,返回0
ferror
文件已经读取结束,判断文件读取结束的原因是否是:发生了某种错误
int ferror(FILE* stream);
-
返回值:如果文件结束的原因是发生某种错误,那么返回一个非0值
否则,返回0
利用这两个函数,我们就可以判断文件结束的原因了:
#include <stdio.h>
int main ()
{
FILE * pFile;
pFile=fopen("myfile.txt","r");
if (pFile==NULL) perror ("Error opening file");
else {
fputc ('x',pFile);
if (ferror (pFile))
printf ("Error Writing to myfile.txt\n");
fclose (pFile);
}
return 0;
}
总结读取文件的函数的返回值
fgetc
读取成功,返回读取的字符的ASCII码值
读到文件末尾或读取时发生错误,返回EOF
fgets
读取成功,返回存储读取到的数据的字符数组的地址
读到文件末尾或读取时发生错误,返回NULL
fread
读取成功,则返回的值与要读取的数量相同
读到文件末尾或读取时发生错误,返回的值与要读取的数量不一样
补充函数
sprintf
将格式化数据写成字符串
int sprintf(char* str, const char* format, ...);
- str是指向存储生成的 C 字符串的缓冲区的指针。
#include <stdio.h>
int main()
{
char buf[100] = { 0 };
sprintf(buf, "%d %s %f", 22, "hello", 12.4);
printf("%s", buf);
return 0;
}
sscanf
从字符串中读取格式化数据
int sscanf(const char* s, const char* format, ...);
- s是要读取的字符串的起始地址
#include <stdio.h>
int main ()
{
char sentence []="Rudolph is 12 years old";
char str [20];
int i;
sscanf (sentence,"%s %*s %d",str,&i);
printf ("%s -> %d\n",str,i);
return 0;
}
文件缓冲区
ANSIC 标准采用“缓冲文件系统”处理数据文件,所谓缓冲文件系统是指系统自动地在内存中为程序中每个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
缓冲区数据送到磁盘有两种情况:
- 缓冲区满
- 手动将缓冲区的内容刷新到磁盘
我们的关闭文件函数fclose就是手动将缓冲区的内容刷新到磁盘。
以上就是关于文件管理的所有内容了,我们大家一起共勉、进步!
标签:文件,fopen,管理,int,C语言,pf,FILE,NULL From: https://blog.csdn.net/xiaokuer_/article/details/137113337