文章目录
1. 为什么使用文件?
如果没有文件,我们写的程序的数据是存储在电脑的内存中,如果程序退出,内存回收,数据就丢失
了,等再次运行程序,是看不到上次程序的数据的,如果要将数据进行持久化的保存,我们可以使用
文件。
2. 什么是文件?
计算机里的文件是一种数据存储单元,用于存储文本、图像、音频、视频、程序以及其他类型的数据。它们通过文件名来进行识别和访问。文件可以存储在计算机的各种存储设备中,其中包括:
1.
硬盘驱动器
(机械硬盘):硬盘驱动器(机械硬盘)是计算机中常见的传统意义上的主要存储设备,它们提供了大容量的存储空间用于存储文件和数据。大多数操作系统和应用程序都安装在硬盘驱动器上。
2.固态硬盘
:固态硬盘(SSD)是一种基于闪存存储介质的存储设备,它比传统机械硬盘(硬盘驱动器)提供更快的数据访问速度和更高的性能。
3.光盘
:光盘包括CD、DVD和蓝光光盘,它们通常用于存储大量数据、音频和视频文件。光盘是一种只读存储介质或可写入一次的存储介质。
4.闪存驱动器
:闪存驱动器包括USB闪存驱动器和存储卡,它们是便携式存储设备,可以用来存储、传输和备份文件。
5.网络存储
:文件也可以存储在网络存储设备上,如网络服务器、云存储服务或其他网络连接的存储设备上。这些文件可以通过网络访问和共享。
无论文件存储在何处,操作系统都提供了文件系统来组织、管理和检索文件。文件系统帮助计算机识别文件的位置、大小、访问权限以及其他相关属性。用户可以使用文件系统来创建、查找、复制、移动、重命名和删除文件。
但是在程序设计中,我们⼀般谈的文件有两种:程序文件、数据文件(从文件功能的角度来分类的)。
2.1 程序文件:
简单来讲,程序文件包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
详细来讲,程序文件是包含计算机程序代码的文件,它们用于存储软件应用程序的源代码或可执行代码。程序文件可以包含不同编程语言编写的指令和命令,这些指令和命令告诉计算机如何执行特定的任务和操作。
程序文件可以分为源代码文件和可执行文件:
1.
源代码文件
:源代码文件包含程序的原始文本形式,通常由程序员使用特定的编程语言编写。源代码文件中包含了程序的逻辑结构、算法、函数和指令,它们需要被编译或解释成可执行文件才能在计算机上运行。
2.可执行文件
:可执行文件是经过编译或解释的源代码文件,可以直接在计算机上运行而无需进一步的处理。它们包含了计算机可直接执行的机器代码指令,可以被操作系统加载和执行。
程序文件可以包括各种类型的应用程序,包括系统软件、应用软件、脚本文件、动态链接库(DLL)等。这些文件在计算机系统中起着关键作用,使得计算机能够运行各种不同的应用程序和程序代码。
2.2 数据文件:
简单来说
,文件的内容不⼀定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
本篇讨论的是数据文件。在前面所学的知识中所处理数据的输入输出都是以终端为对象的,即从终端的键盘输入数据,运行结果显示到显示器上。其实有时候我们会把信息输出到磁盘上,当需要的时候再从磁盘上把数据读取到内存中使用,这⾥处理的就是磁盘上文件。
详细说来
,数据文件是用于存储数据的文件,它们可以包含各种类型的数据,一般存放在硬盘(磁盘)上,如文本、数字、图像、音频、视频等。数据文件通常用于保存和管理计算机系统中的用户数据或应用程序数据。这些文件可以由各种不同的应用程序创建、读取和处理。数据文件可以分为多种类型,包括:
1.
文本文件
:文本文件包含文本字符数据,通常以ASCII或Unicode编码存储。它们可以包含纯文本、代码、配置文件等。
2.数字文件
:数字文件包含数字数据,这些数据可以是数字格式的文档、电子表格、数据库文件等。
3.图像文件
:图像文件包含图像数据,如照片、图形、绘画等。常见的图像文件格式包括JPEG、PNG、GIF等。
4.音频文件
:音频文件包含音频数据,如音乐、声音片段、录音等。常见的音频文件格式包括MP3、WAV、FLAC等。
5.视频文件
:视频文件包含视频数据,如电影、视频片段、动画等。常见的视频文件格式包括MP4、AVI、MOV等。
数据文件通常由特定的应用程序生成和处理,用户可以使用各种软件工具来创建、编辑、查看和管理数据文件。这些文件在计算机系统中起着重要作用,用于存储和管理用户的个人数据、应用程序的配置数据以及各种类型的媒体内容。
2.3 文件名:
一个文件要有一个唯一的文件标识,以便用户识别和引用。文件名包含3部分:文件路径
+文件名主干
+文件后缀
(扩展名)。
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
在某些操作系统中,文件名可能有一些限制,比如长度限制、禁止使用的特殊字符或符号等。例如,Windows 操作系统中,文件名不能包含以下任何字符:\ / : * ? " < > |,也不能以空格结尾或者以点开头(除非文件名中包含多个点以指示文件类型)。而在类 Unix 系统中,文件名可以包含绝大多数字符,但是对于一些特殊字符,需要使用转义字符或引号来处理。
3. 二进制文件和文本文件:
根据数据的组织形式,数据文件
被称为文本文件
或者二进制文件
。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是**二进制文件
**。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文本文件
。
如整数10000,在内存中有两种存储形式:一种是按照字符(ASCII)形式存储,一种是直接按照二进制形式存储该(十进制)数据。
测试代码:
#include <stdio.h>
int main()
{
int a = 10000;
FILE* pf = fopen("test.txt", "wb");
fwrite(&a, 4, 1, pf);//⼆进制的形式写到⽂件中
fclose(pf);
pf = NULL;
return 0;
}
然后进行如下操作:
1.vs打开二进制文件的方法:
4.10000在二进制文件中:
5.通过计算进行验证:
测试完成。
4. 文件的打开和关闭:
4.1 流和标准流:
4.1.1 流:
在计算机科学中,“流
”(stream)是一种用于处理输入和输出的抽象概念。它通常用来表示一系列连续的数据,这些数据可能会被分割成连续的数据块(或者说流)。流可以是输入流或输出流,具体取决于数据是流入(输入)还是流出(输出)。
流的概念被广泛用于文件处理、网络通信以及与外部设备进行交互的情况。流使得可以在不一次性加载所有数据的情况下进行连续的读取或写入操作。这对于处理大型文件或持续性的数据传输特别有用,因为它可以提高内存效率并减少处理时间。
流可以是字节流或字符流。字节流用于处理二进制数据,而字符流用于处理文本数据。流的操作通常包括读取、写入以及流的打开和关闭操作。在不同的编程语言和环境中,流的具体实现方式可能会有所不同。
C程序针对文件、画面、键盘等的数据输入输出操作都是同流操作的。
一般情况下,我们要想向流里写数据,或者从流中读取数据,都是要打开流,然后操作。
4.1.2 标准流:
那为什么我们从键盘输入数据,向屏幕上输出数据,并没有打开流呢?
那是因为C语言程序在启动的时候,默认打开了3个流:
1:
stdin
—标准输入流,大多数的环境中从键盘输入。
2:stdout
—标准输出流,大多数的环境中输出至显示器界面。
3:stderr
—标准错误流,大多数环境中输出到显示器界面。
这是默认打开了这三个流,我们使用scanf、printf等函数就可以直接进行输入输出操作的。
stdin、stdout、stderr三个流的类型是: FILE*
,通常称为文件指针
。
C语言中,就是通过 FILE*
的文件指针来维护流的各种操作的。
4.2 文件指针:
缓冲文件系统中,关键的概念是“文件类型指针”
,简称“文件指针”
。
每个被使用的文件都在内存中开辟了⼀个相应的文件信息区,用来存放文件的相关信息(如文件的名
字,文件状态及文件当前的位置等)。这些信息是保存在⼀个结构体变量中的。该结构体类型是由系
统声明的,取名FILE.
例如,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;
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建⼀个FILE结构的变量,并填充其中的信息,使用者不必关心细节。⼀般都是通过⼀个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建⼀个FILE*的指针变量:
FILE* pf;//pf为文件指针变量
定义pf是⼀个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是⼀个结构体变
量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够间接找到与
它关联的文件。
4.3 文件的打开和关闭:
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回⼀个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用 fopen
函数来打开文件, fclose
来关闭文件。
//打开⽂件
FILE * fopen ( const char * filename, const char * mode );
//关闭⽂件
int fclose ( FILE * stream );
fopen(打开文件)成功则返回指向文件的指针,失败则返回空指针NULL。
fclose(关闭文件)成功则返回0,失败则返回EOF。
因此,在打开文件前,需要判断文件指针是否为NULL(是否成功打开文件)
mode表示文件的打开模式,下面都是文件的打开模式:
文件使用方式 | 含义 | 如果指定文件不存在 |
---|---|---|
“r”(只读) | 为了输入数据,打开⼀个已经存在的文本文件 | 出错 |
“w”(只写) | 为了输出数据,打开⼀个文本文件 | 建立⼀个新的文件 |
“a”(追加) | 向文本文件尾添加数据 | 建立⼀个新的文件 |
“rb”(只读) | 为了输⼊数据,打开⼀个二进制文件 | 出错 |
“wb”(只写) | 为了输出数据,打开⼀个二进制文件 | 建立⼀个新的文件 |
“ab”(追加) | 向⼀个二进制文件尾添加数据 | 建立⼀个新的文件 |
“r+”(读写) | 为了读和写,打开⼀个文本文件 | 出错 |
“w+”(读写) | 为了读和写,建议⼀个新的文件 | 建立⼀个新的文件 |
“a+”(读写) | 打开⼀个文件,在文件尾进行读写 | 建立⼀个新的文件 |
“rb+”(读写) | 为了读和写打开⼀个二进制文件 | 出错 |
“wb+”(读写) | 为了读和写,新建⼀个新的二进制文件 | 建立⼀个新的文件 |
“ab+”(读写) | 打开⼀个二进制文件,在文件尾进行读和写 | 建立⼀个新的文件 |
实例代码:
/* fopen fclose example */
#include <stdio.h>
int main ()
{
FILE * pFile;
//打开⽂件
pFile = fopen ("myfile.txt","w");
//⽂件操作
if (pFile!=NULL)
{
fputs ("fopen example",pFile);
//关闭⽂件
fclose (pFile);
}
return 0;
}
5. 文件的顺序读写:
5.0 对输入输出的理解:
这里的输入,输出,读,写,都是针对内存而言的。
5.1 顺序读写函数介绍:
函数名 | 功能 | 适用于 |
---|---|---|
fgetc | 字符输入函数 | 所有输入流 |
fputc | 字符输出函数 | 所有输出流 |
fgets | 文本行输入函数 | 所有输入流 |
fputs | 文本行输出函数 | 所有输出流 |
fscanf | 格式化输入函数 | 所有输入流 |
fprintf | 格式化输出函数 | 所有输出流 |
fread | 二进制输入 | 文件 |
fwrite | 二进制输出 | 文件 |
上面说的适用于所有输入流一般指适用于标准输入流和其他输入流(如文件输入流);所有输出流一般指适用于标准输出流和其他输出流(如文件输出流)
下面来看几个顺序读写函数的具体功能及使用方法:
5.1.1 fgetc:
作用:
Read a character from a stream (fgetc, fgetwc) or stdin (_fgetchar, _fgetwchar).
从流(fgetc, fgetwc)或stdin (_fgetchar, _fgetwchar)中读取
一个字符
函数声明:
int fgetc( FILE *stream );//返回读取成功的字符,读取失败返回EOF
具体原理:
这两个函数分别从文件的当前位置读取一个字符;对于fgetc和fgetwc,这是与流相关联的文件。然后,该函数将相关的
文件指针
(如果定义了)加1,以指向下一个字符。如果流位于文件末尾,则设置流的文件结束指示符。
注意:使用此函数会改变文件指针的位置。
使用示例:
#include <stdlib.h>
void main( void )
{
FILE *stream;
char buffer[81];
int i, ch;
/* 打开文件 */
if( (stream = fopen( "fgetc.c", "r" )) == NULL )
exit( 0 );
/*顺序读取文件中80个字符并保存到buffer数组中 */
ch = fgetc( stream );
for( i=0; (i < 80 ) && ( feof( stream ) == 0 ); i++ )
{
buffer[i] = (char)ch;
ch = fgetc( stream );
}
/* 给字符串数组加上字符串结束标志,以便打印 */
buffer[i] = '\0';
printf( "%s\n", buffer );
fclose( stream );
}
5.1.2 fputc:
作用:
Writes a character to a stream (fputc, fputwc) or to stdout (_fputchar,_fputwchar).
将字符写入流(fputc, fputwc)或stdout (_fputchar, _fputwchar)
函数声明:
int fputc( int c, FILE *stream );//返回成功写入的字符,失败则返回EOF
具体原理:
这些函数中的每一个都将单个字符写到相关的文件位置指示器(如果定义了)所指示的位置,并酌情推进该指示器。在fputc和fputwc的情况下,文件与流相关联。如果文件不支持定位请求或以追加模式打开,则将该字符追加到流的末尾。
注意:该函数同样会改变文件指针的位置。再不重置文件指针位置时再次调用该函数,该函数会直接在上一次写入结束处继续写入。
使用示例:
int main()
{
FILE* pf = fopen("text.txt", "w");
int i = 1;
for (i = 0; i < 26; i++)
{
fputc(i + 'a', pf);
}
return 0;
}
运行结果如下:
5.1.3 fgets:
作用:
Get a string from a stream.
从流中获取字符串。
函数声明:
char *fgets( char *string, int n, FILE *stream );//返回一个指向字符串的指针,失败则返回NULL
具体原理:
fgets函数从输入流参数中读取一个字符串,并将其存储在string中。Fgets从当前流位置读取字符,直到并包括第一个换行字符,直到流的末尾,或者直到读取的字符数等于n - 1,以先读到的为准。存储在string中的结果附加一个空字符。换行符如果被读取,则包含在字符串中。
使用示例:
#include <stdio.h>
int main()
{
FILE* stream;
char line[100];
if ((stream = fopen("fgets.txt", "r")) != NULL)
{
if (fgets(line, 12, stream) == NULL)
printf("fgets error\n");
else
printf("%s", line);
fclose(stream);
}
}
运行结果:
5.1.4 fputs:
作用:
Write a string to a stream
将字符串写入流。
函数声明:
int fputs( const char *string, FILE *stream );//返回非负值,失败返回EOF
与fgets相比少了一个参数:最大读取字符数n。
具体原理:
该函数都将字符串复制到当前(文件指针)位置的输出流。Fputws根据stream是以文本模式还是二进制模式打开,分别将宽字符参数字符串作为多字节字符串或宽字符字符串复制到stream。两个函数都不复制终止null字符。
使用示例:
int main()
{
FILE* stream;
char line[100]="This is a test.";
if ((stream = fopen("fputs.txt", "w")) != NULL)
{
if (fputs(line, stream) == NULL)
printf("fgets error\n");
fclose(stream);
}
}
5.1.5 fprintf和fscanf:
作用与定义:
fprintf:
fscanf:
通过类别可以得出,如果这两个函数的第一个参数stream设置为stdout/stdin,那么这两个函数分别与printf,scanf等效。
5.1.5 sprintf和sscanf:
简要来说:
这两个函数比printf,scanf多了一个指向字符串的指针。
默认的输出输入来源是stdout/stdin。而这里的输入输出来源变成了字符串(可看作内存向内存输入输出)而已。
5.1.6小结:
sscanf/sprintf和fscanf/fprintf不过是比scanf/printf分别多了一个参数罢了。
fscanf/fprintf多了一个文件流
sscanf/sprintf多了一个字符指针
可以把这个不同的参数看作不同类型的"源"
和“目标”
。
从源里面拿数据,再转换为其他类型的数据(格式化成别的类型的数据)前提是数据类型合法。
sscanf
可以实现切割字符串等常见的字符串操作。
fscanf
可以让我们从文件中拿到想要的数据。
sprintf
可以实现将多个字符串合成一个字符串。
fprintf
可以让我们将数据。
注意:stdin和stdout本质也是文件流。
所以
1.sscanf/sprintf只能实现内存与内存之间的数据输入输出。
2.而fprintf/fscanf可以实现内存与文件,内存与屏幕,键盘(等外设)的数据的(标准)输入输出。
3.printf/scanf只能实现内存与屏幕,键盘的数据(标准输入输出)。
6. 文件的随机读写:
6.1 fseek:
根据文件指针的指定位置(origin)和偏移量(offset)来移动文件指针到某目标位置。
int fseek ( FILE * stream, long int offset, int origin );
origin表示起始位置(以哪个位置为标准)。
offset表示相对于origin的偏移量(从左向右)
origin的三个参数:
1.SEEK_CUR:文件指针现在的位置。
2.SEEK_END:文件末尾。
3.SEEK_SET:文件开头。
例子:
#include <stdio.h>
int main()
{
FILE* pFile;
pFile = fopen("example.txt", "wb");
fputs("This is an apple.", pFile);
fseek(pFile, 9, SEEK_SET);
fputs(" sam", pFile);
fclose(pFile);
return 0;
}
运行结果:
6.2 ftell:
返回文件指针相对于起始位置的偏移量,返回偏移量(长整形)。
long int ftell ( FILE * stream );
6.3 rewind:
让文件指针的位置回到文件的起始位置。
void rewind ( FILE * stream );
7. 文件读取结束的判定(feof):
牢记:在文件读取过程中,不能用feof函数的返回值直接来判断文件的读取是否结束。简单来说,它是用来判断中止原因的。
feof 的作用是:当文件读取结束的时候,判断是读取结束的原因是否是遇到文件尾结束。如果是因为遇到结尾而结束则返回非0值,如果是其他原因(掉电等)而造成的异常读取中止则返回0。
1.文本文件读取是否结束,判断返回值是否为 EOF ( fgetc )或者 NULL(fgets)
例如:
• fgetc 判断是否为 EOF .
• fgets 判断返回值是否为 NULL .
2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
例如:
• fread判断返回值是否小于实际要读的个数。
文本文件的例子:
#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);
}
二进制文件的例子:
include <stdio.h>
enum { SIZE = 5 };
int main(void)
{
double a[SIZE] = {1.,2.,3.,4.,5.};
FILE *fp = fopen("test.bin", "wb"); // 必须⽤⼆进制模式
fwrite(a, sizeof *a, SIZE, fp); // 写 double 的数组
fclose(fp);
double b[SIZE];
fp = fopen("test.bin","rb");
size_t ret_code = fread(b, sizeof *b, SIZE, fp); // 读 double 的数组
if(ret_code == SIZE) {
puts("Array read successfully, contents: ");
for(int n = 0; n < SIZE; ++n) printf("%f ", b[n]);
putchar('\n');
} else { // error handling
if (feof(fp))
printf("Error reading test.bin: unexpected end of file\n");
else if (ferror(fp)) {
perror("Error reading test.bin");
}
}
fclose(fp);
}
8. 文件缓冲区:
ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每⼀个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才⼀起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
#include <stdio.h>
#include <windows.h>
//VS2019 WIN11环境测试
int main()
{
FILE*pf = fopen("test.txt", "w");
fputs("abcdef", pf);//先将代码放在输出缓冲区
printf("睡眠10秒-已经写数据了,打开test.txt⽂件,发现⽂件没有内容\n");
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);//刷新缓冲区时,才将输出缓冲区的数据写到⽂件(磁盘)
//注:fflush 在⾼版本的VS上不能使⽤了
printf("再睡眠10秒-此时,再次打开test.txt⽂件,⽂件有内容了\n");
Sleep(10000);
fclose(pf);
//注:fclose在关闭⽂件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
这里可以得出⼀个结论:
因为有缓冲区的存在,C语言在操作文件的时候,需要及时刷新缓冲区或者在文件操作结束的时候及时关闭文件。
如果遗忘,可能会导致读写文件出错的问题。