当前位置 博文首页 > HyDraZya的博客:【C语言进阶】文件数据操作详解(万字教你真正
目录
??????一、我们为什么需要文件?
二、究竟什么才是文件
程序文件
数据文件
三、文件名
四、文件类型
五、文件缓冲区
六、文件指针
七、打开文件和关闭文件
fopen
fclose
八、文件顺序读写表
流(stream)
fputc
fgetc
? ? ? ? ?fgets
fputs
fprintf
fscanf
对比一组函数
fwrite
fread
九、文件随机读写
fseek
ftell
rewind
十、文件读取结束的判定
????????由于我们之前所编写的程序在执行过程中所产生的数据及结果都只是临时存放在内存区域,一旦程序运行结束后,该程序所开辟的内存空间将全部返回给操作系统。那么此时我们再想要去查看之前的数据和执行结果,显示是不可能的。
那该如何解决这个问题呢??
我们需要用到的就是——文件。 没错,文件 的作用就是可以将程序运行过程中所产生的数据结果都存储下来,即便程序结束运行,我们依然可以通过 文件 来找到这些数据,甚至再次运行程序时依然能使用这些数据。
准确的来说:计算机文件就是存储在计算机硬盘、磁盘上的信息集合。例如熟悉的图片文件、视频文件、程序等等。
文件有各种各样的类型,但是在程序设计中,我们一般谈的文件有两种:程序文件、数据文件
程序文件
包括源程序文件(后缀为.c ), 目标文件(windows环境后缀为.obj), 可执行程序(windows环境后缀为.exe)。
数据文件
文件的内容不是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
这次我们便围绕数据文件进行讨论说明。
一个文件要有一个唯一的文件标识,以便用户识别和引用。
文件名包含3部分∶文件路径 + 文件名主干 + 文件后缀
例如︰ c : \code\text.txt
文件路径: c : \code
文件名主干: test
文件后缀: .txt当然为了方便起见,文件标识常被成为 文件名。文件名主干 的命名规则遵循标识符的命名规则。
但是我们要了解此时所称呼的文件名,实际上包括以上3部分内容,而不仅仅是文件名主干。
这边我来举例一些常见的文件后缀方便大家去了解文件的性质:
txt(文本文件)dat(数据文件)doc(Word生成的文件)xlsx(excel生成的文件)exe(可执行文件)jpg(图片文件)等……
那么我们如何来区分文件类型呢?
首先根据数据的组织形式,数据文件被称为了 文本文件 或 二进制文件。
而数据在内存中以二进制的形式存储时,如果不加转换的输出到外存中去,那么就是 二进制文件。
反之,如果在外存上以ASCII码的形式进行存储,如果在存储到外存前就转换,那么就是 文本文件。
一个数据如何在内存中存储的?
字符在内存中一律是以ASCII码值形式进行存储的,而数值型数据既可以用ASCII码值形式存储,也可以使用二进制形式进行存储。
?例:
#include <stdio.h>
int main()
{
int a = 10000;
FILE *pf = fopen("eg.txt", "wb");
//fopen 打开名为test.txt的文件
//wb 以二进制的形式写入文件
fwrite(&a, 4, 1, pf);//写一个四个字节的数据放到以pf维护的文件中去
fclose(pf);
pf = NULL;
return 0;
}
我们以二进制编辑器打开我们所创建的 eg.txt 文件中去:
我们可以看出这是以16进制形式显示的,并且由于编译器是按照小端进行存储的。
所以应该为 00 00 27 10
转换成二进制就是 00000000 00000000 00100111 00010000
图解:
我们来了解一下什么是文件缓冲区:
按照ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中微程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。而缓冲区的大小根据C编译系统决定的。
图解:
缓冲文件系统中, 关键的概念是?“文件类型指针”?, 简称?“文件指针”?。每个被使用的文件都在内存中开辟一个相应的文件信息区,用来存放文件的有关信息(如文件的名字、文件状态及文件当前位置等)。这些信息是保存在一个结构体变量中的。
该结构体类型是由系统声明的,取名为 FILE 。
例:
//vs2013编译环境提供的stdio.h头文件的文件类型申明
struct _iobuf
{
char* _ptr;
int _cnt;
char* _base;
int _f1ag;
int _file;
int _charbuf;
int _bufsiz;
char* _tmpfname;
};
typedef struct _iobuf FILE;
- 当然不同的C编辑器它所对应的FILE类型包含的内容也不完全相同,但从根本上大同小异。
- 每当打开一个文件时,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用时我们不必在意这些细节。
- 一般情况下通过FILE指针来维护这个FILE结构的变量,这样使用起来会更加便捷。
例:(创建FILE*的指针变量)
FILE *pf;//文件指针变量
定义 pf 是一个指向 FILE 类型数据的指针变量。可以使 pf 指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件
图解:
我们都知道,文件在读写之前应该想先打开文件,在使用结束后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个 FILE* 的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC规定使用 fopen 函数来打开文件,fclose 来关闭文件。
//函数原型
FILE* fopen(const char* filename, const char* mode);
函数功能: Open a file.(打开文件)
返回类型: Each of these functions returns a pointer to the open file. A null pointer value indicates an error.(如果打开成功,返回指向文件信息区的指针,如果返回失败,返回空指针NULL)
函数参数1:filename(文件名,实际上包括3部分内容,而不仅仅是文件名主干。如果文件路径未写,则默认本路径)
函数参数2:Type of access permitted(文件打开方式)
文件打开方式表:
文件使用方式 | 含义 | 如果指定文件不存在 |
"r"(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
"w"(只写) | 为了输出数据,打开一个文本文件 | 建立一个新文件 |
"a"(追加) | 向文本文件尾添加数据 | 出错 |
"rb"(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
"wb"(只写) | 为了输出数据,打开一个文进制文件 | 建立一个新文件 |
"ab"(追加) | 向一个二进制文件尾添加数据 | 出错 |
"r+"(读写) | 为了读和写,打开一个文本文件 | 出错 |
"w+"(读写) | 问了读和写,建立一个新的文件 | 建立一个新文件 |
"a+"(读写) | 打卡一个文件,在文件尾进行读写 | 建立一个新文件 |
"rb+"(读写) | 为了读和写打开一个二进制文件 | 出错 |
"wb+"(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新文件 |
"ab+"(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新文件 |
注: ‘w’ 打开的时候如果有同名文件,该文件中的内容会被销毁 |
//函数原型
int fclose(FILE* stream);
函数功能:Closes a stream(fclose) or closes all open streams(_fcloseall).(关闭文件)
返回类型:fclose returns 0 if the stream is successfully closed._fcloseall returns the total number of streams closed.Both functions return EOF to indicate an error(关闭文件成功返回0,关闭文件失败返回EOF(值为 - 1)来报错)
函数参数:Pointer to FILE structure(文件指针)
?例:
#include <stdio.h>
int main()
{
//打开文件mysize.txt
//相对路径
//..表示上一级路径
//fopen("../../mysize.txt", "r");
//.表示当前路径
FILE* pf = fopen("mysize.txt", "r");//相对路径
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功
//读文件
fclose(pf);//关闭文件
return 0;
}
?我们首先新建一个文件:
执行结果:?
?
?我们此时再换成绝对路径:
?刚刚我们是以?“r”(只读)的方式来打开文件,现在我们来尝试用?“w”(只写)的方式来打开文件
#include <stdio.h>
int main()
{
FILE* pf = fopen("eg.dat", "w");//相对路径
if (pf == NULL)
{
perror("fopen");
return 1;
}
//打开成功
//读文件
printf("file open success.\n");
fclose(pf);//关闭文件
return 0;
}
观察文件夹,发现新建了 eg.dat 这么一个文件 :
名称 | 函数名 | 功能(返回值) | |
字符输入函数 | fgetc(fp) | 从文件指针 fp 所指向的文件的当前读指针位置读取一个字符,读取完成后指针自动后移指向下一个字符 成功时返回该字符,否则返回EOF | |
字符输出函数 | fputc(ch,fp) | 将字符 ch?写入到文件指针 fp?所指向的文件的当前写指针位置 成功是返回字符本身,否则返回EOF | |
文本行输入函数 | fgets(str, n, fp) | 从 fp 所指向的文件的当前读指针位置读出 n 个字符放入字符串 str?中成功时返回该字符串地址,否则返回NULL | |
文本行输出函数 | fputs(str, fp) | 将字符串 str 写入到文件指针 fp 所指向的文件的当前写指针位置 成功时返回非0值,否则返回EOF | |
格式化输入函数 | fscanf(fp, 格式控制字符串,地址列表) | 按格式控制串所描述的格式,从 fp 所指向的文件中读取数据,送到指定的变量中。 若输出操作成功,返回实际写入的字符 若输出操作失败,则返回EOF | |
格式化输出函数 | fprintf(fp, 格式控制字符串,输出列表) | 将输出项按照制定的格式写入 fp 所指向的文件中。 若输入操作成功,返回实际读出的数据 若没有读到数据项,则返回0 若文件结束或调用失败,则返回EOF | |
二进制输入 | size_t fread(buffer, size, count, stream) | 读取 [count_num] 个对象(每个对象大小为?size(大小) 指定的字节数),并把它们替换到由?buffer?(缓冲区) 指定的数组,数据来自给出的输入流 函数的返回值是读取的内容数量 | |
二进制输出 | size_t fwrite(buffer, size, count, stream) | 把?buffer?所指向的数组中的数据写入到给定流?stream?中 若成功,该函数返回一个 size_t 对象,表示元素的总数,该对象是一个整型数据类型。 若该数字与 count?参数不同,则会显示一个错误。 |
单看这个表可能没有什么感觉,接下去就让我们来详细的进行每个函数的功能描述,在此之前需要先介绍一个函数。它是什么呢?
概念:输入输出是数据传送的过程, 数据如流水一样从一处流向另一处, 因此常将输入输出形象地称为流(stream), 即数据流。流表示了信息从源到目的端的流动。在输入操作时, 数据从文件流向计算机内存, 在输出操作时, 数据从计算机流向文件(如打印机、磁盘文件)。文件是由操作系统进行统一管理的, 无论是用Word打开或保存文件, 还是C程序中的输入输出都是通过操作系统进行的。“流”是一个传输通道, 数据可以从运行环境(有关设备)流入程序中, 或从程序流至运行环境。
图解:?
注:C语言程序,只要运行起来,就默认打开了三个流,类型均为 FILE*
stdin?--- 标准输入流?--- 键盘
stdout?--- 标准输出流?--- 屏幕
stderr --- 标准错误流 --- 屏幕
例:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE* pf = fopen("mysize.txt", "w");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//写文件
//关闭文件
fputc('H', stdout);//stdout 标准输出流
fputc('y', stdout);
fputc('D', stdout);
fputc('r', stdout);
fputc('a', stdout);
fclose(pf);
return 0;
}
执行结果:
例:
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main()
{
FILE *pf = fopen("mysize.txt","r");
if (pf == NULL)
{
printf("%s\n", strerror(errno));
return 0;
}
//读文件
int ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c", ret);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
执行结果:?
在举例前我们先在 mysize.txt 文本内部加上一行字符,这样能够更好的展现效果:
例:
#include <stdio.h>
int main()
{
char buf[1024] = { 0 };
FILE *pf = fopen("mysize.txt", "r");
if (pf == NULL)
{
return 0;
}
//读文件
fgets(buf, 1024, pf);
printf("%s", buf);//buffer自身拥有换行功能
fclose(pf);
pf = NULL;
return 0;
}
执行结果:
例1:
#include <stdio.h>
int main()
{
char buf[1024] = { 0 };
FILE *pf = fopen("mysize.txt", "r");
if (pf == NULL)
{
return 0;
}
//读文件
fgets(buf, 1024, pf);
puts(buf);
fgets(buf, 1024, pf);
puts(buf);
fclose(pf);
pf = NULL;
return 0;
}
执行结果:
例2:
#include <stdio.h>
int main()
{
//从键盘读取一行文本信息
char buf[1024] = { 0 };
fgets(buf, 1024, stdin);//从标准输入流读取
fputs(buf, stdout);
//上面那种写法等价于下面这种写法
gets(buf);
puts(buf);
return 0;
}
例:
#include <stdio.h>
struct S
{
int n;
float f;
char arr[10];
};
int main()
{
struct S s = { 520, 13.14f, "HSS" };
FILE *pf = fopen("mysize.txt", "w");
if (pf == NULL)
{
return 0;
}
//格式化的形式写文件
fprintf(pf, "%d %f %s", s.n, s.f, s.arr);
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
执行结果:
此时我们再去查看 mysize.txt 记事本就会发现里面的数据已经被格式化并且替换成了刚写的数据:
例:
#include <stdio.h>
struct S
{
int n;
float f;
char arr[10];
};
int main()
{
struct S s = { 0 };
FILE* pf = fopen("mysize.txt", "r");
if (pf == NULL)
{
return 0;
}
//格式化的输入数据
fscanf(pf, "%d %f %s", &(s.n), &(s.f), s.arr);
printf("%d %f %s\n", s.n, s.f, s.arr);
fclose(pf);//关闭文件
pf = NULL;
return 0;
}
执行结果:?
scanf / fscanf / sscanf
printf / fprintf / sprintf
此时我们还未了解过 sscanf 和 sprintf,所以我们先进行举例说明:
例:
#include <stdio.h>
struct S
{
int n;
float f;
char arr[10];
};
int main()
{
struct S s = { 520, 13.14f, "HSS" };
struct S tmp = { 0 };
char buf[1024] = { 0 };
//把格式化的数据转换成字符串存储到buffer
sprintf(buf, "%d %f %s", s.n, s.f, s.arr);
//从buffer中读取格式化的数据到tmp中
sscanf(buf, "%d %f %s", &(tmp.n), &(tmp.f), tmp.arr);
printf("%d %f %s\n", tmp.n, tmp.f, tmp.arr);
return 0;
}
执行结果: