目录
一、什么是文件?
1.1 - 文件和流的基本概念
1.2 - 文件的分类
1.3 - 文件名
二、缓冲文件系统和非缓冲文件系统
三、文件指针类型
四、文件的打开和关闭
4.1 - fopen
4.2 - fclose
五、文件的顺序读写
5.1 - 字符输出函数 fputc
5.2 - 字符输入函数 fgetc
5.3 - 文本行输出函数 fputs
5.4 - 文本行输入函数 fgets
5.5 - 格式化输出函数 fprintf
5.6 - 格式化输入函数 fscanf
5.7 - 二进制输出函数 fwrite
5.8 - 二进制输入函数 fread
5.9 - 总结
5.10 - 对比一组函数
六、文件的随机读写
6.1 - fseek
6.2 - ftell
6.3 - rewind
七、文件读取结束的判定
7.1 - feof
7.2 - ferror
7.3 - 总结
一、什么是文件?
1.1 - 文件和流的基本概念
文件(File)和流(stream)是既有区别又有联系的两个概念。
文件是一些具有永久存储及特定顺序的字节组成的一个有序的、具有名称的集合。
流提供了一种向后备存储写入字节和从后备存储读取字节的方式,后备存储可以为多种存储媒介之一。正如除磁盘之外还存在多种后备存储一样,除文件流之外也存在多种流,例如,还存在网络流、内存流和磁带流等。
1.2 - 文件的分类
从不同的角度可对文件作不同的分类。
-
从文件的功能角度来分类,文件可分为程序文件和数据文件。
-
程序文件包括源程序文件,目标文件(windows 环境后缀为 .obj),可执行文件(windows 环境后缀为 .exe)。
-
文件的内容不一定是程序,而是程序运行时读写的数据,比如程序运行需要从中读取数据的文件,或者输出内容的文件。
-
-
根据数据的组织形式,文件可分为文本文件和二进制文件。
-
二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,即存放的是数据的原形式。
-
文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,即存放的是数据的终端形式。
计算机的存储在物理上是二进制的,所以文本文件与二进制文件的区别并不是物理上的,而是逻辑上的。这两者只是在编码层次上有差异。简单来说,文本文件是基于字符编码的文件,常见的编码有 ASCII 编码,UNICODE 编码等;二进制文件则是基于值编码的文件。
记事本支持文本文件而不支持二进制文件,所以用记事本打开文件文件则一切正常,如果打开的是二进制文件就会出现乱码。但也有不乱码的地方,那些地方都是字符编码的,因为字符数据本身在内存中就经过了编码,所以无论是二进制还是文本形式都是一样的,而对于 int、double 等类型所对应的值则都是乱码的。
-
1.3 - 文件名
一个文件需要有唯一的文件标识(即文件名),以便用户识别和引用。
文件名包含 3 个部分:
-
文件路径:
-
绝对路径,也称为完整路径,是指向文件系统中某个固定位置的路径,不会因当前的工作目录而产生变化。为了做到这点,它必须包含根目录。
在计算机的文件系统中,根目录指文件系统的最上一级目录,它是相对子目录来说的,它如同一棵大树的"根"一般,所有的树杈都以它为起点,故被命名为根目录。
以微软公司开发的 Windows 操作系统为例:打开这台电脑(我的电脑、计算机),双击 C 盘就进入 C 盘的根目录,双击 D 盘就进入 D 盘的根目录。
Unix 完全抽象了树层次结构的本质,在 Unix 和类 Unix 系统中,根目录用
/
符号表示。虽然根目录通常称为/
,但目录条目本身没有名称,它的名称是初始目录分隔符(/)之前的"空"部分。所有文件系统条目(包括已挂载的文件系统)都是此根的"分支"。 -
相对路径则是以指定的工作目录作为基点,避开提供完整的绝对路径。文件名称就可以被视为以指定工作目录为基点的一个相对路径(虽然一般不将其称之为路径)。
路径中的 "./" 代表目前所在的目录,"../" 代表上一级所在的目录,"../../" 则代表上上级所在的目录。
-
-
文件名主干。
-
文件后缀:
文件后缀名也叫文件扩展名,后缀名可以用来帮助用户了解文件是应该使用哪种软件打开文件。
二、缓冲文件系统和非缓冲文件系统
C 语言所使用的磁盘文件系统有两大类:一类称为缓冲文件系统,又称为标准文件系统;另一类称为非缓冲文件系统。
-
缓冲文件系统的特点:
系统自动地在内存区为每一个正在使用的文件开辟一个缓冲区,从磁盘向内存读入数据时,则一次从磁盘文件将一些数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送给接受变量;向磁盘文件输出数据时,先将数据送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。
用缓冲区可以一次读入一批数据,或输出一批数据,而不是执行一次输入或输出函数就去访问一次磁盘,这样做的目的是减少对磁盘的实际读写次数,因为每一次读写都要移动磁头并寻找磁道扇区。缓冲区的大小由各个具体的 C 版本确定,一般为 512 byte,即 0.5 kb。
-
非缓冲文件系统的特点:
非缓冲文件系统不由系统自动设置缓冲区,而由用户自己根据需要设置。在传统的 Unix 系统下,用缓冲文件系统来处理文本文件,用非缓冲系统处理二进制文件。
1983 年 ANSI C 标准决定不采用非缓冲文件系统,而只采用缓冲文件系统。即用缓冲文件系统处理文本文件,也用它来处理二进制文件,也就是将缓冲文件系统扩充为可以处理二进制文件。
// VS 2019 WIN 10 环境测试
#include <stdio.h>
#include <windows.h>
int main()
{
FILE* fp = fopen("test.txt", "w");
fputs("hello world!", fp);
printf("睡眠 10 秒 - 此时打开 test.txt 文件,会发现文件中没有内容\n");
Sleep(10000);
fflush(fp);// 刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
// 注意:fflush 在高版本的 VS 上不能使用了
printf("刷新缓冲区\n");
printf("再睡眠 10 秒 - 此时再次打开 test.txt 文件,文件中就有内容了\n");
Sleep(10000);
fclose(fp); // 注:fclose 在关闭文件的时候,也会刷新缓冲区
fp = NULL;
return 0;
}
文件操作相关的函数会在后面进行详解,我们只需要知道,根据这个程序,可以得出一个结论:因为有缓冲区的存在,在进行文件操作时,需要刷新缓冲区或者在文件操作结束时关闭文件,否则可能导致文件读写的问题。
三、文件指针类型
缓冲文件系统中,关键的概念是"文件指针类型",简称"文件指针"。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字、文件状态以及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是由系统声明的,取名为 FILE。
例如,VS 2013 编译环境提供的 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 *fp;
四、文件的打开和关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
ANSIC 规定使用 fopen
函数来打开文件,fclose
函数来关闭文件。
4.1 - fopen
FILE* fopen(const char* filename, const char* mode);
参数:
-
filename:要打开的文件的文件名。
-
mode:打开文件的方式。
文件打开方式 含义 "r" read:打开文件进行输入操作(input operations),该文件必须存在 "w" write:为输出操作(output operations)创建一个空文件,如果已存在同名文件,则会丢弃其内容,并将该文件视为新的空文件 "a" append:打开文件在文件末尾进行输出操作(output operations),输出操作始终将数据写入(write)文件末尾,对其进行扩展。重新定位操作(repositioning operations)( fseek
、fsetpos
、rewind
)将被忽略。如果文件不存在,则创建一个新文件"r+" read/ update:打开文件进行输入输出操作,该文件必须存在 "w+" write/ update:为输入输出操作创建一个空文件,如果已存在同名文件,则会丢弃其内容,并将该文件视为新的空文件 "a+" append/ update:打开一个文件进行输入输出操作,所有输出操作都在文件末尾写入数据。重新定位操作( fseek
、fsetpos
、rewind
)会影响下一个输入操作,但输出操作会将位置移回文件末尾。如果文件不存在,则创建该文件。使用上面的打开方式,文件将作为文本文件打开。
为了将文件作为二进制文件打开,mode 中必须包含 b 字符。这个额外的 b 字符要么追加到末尾,从而形成复合的 mode:"rb"、"wb"、"ab"、"r+b"、"w+b"、"a+b",要么插入到字母和 + 号之间形成混合的 mode:"rb+"、"wb+"、"ab+"。
返回值:
如果文件打开成功,fopen
函数将返回一个 FILE 类型的指针,如果打开失败,则返回一个 NULL 指针,因此需要对 fopen
函数的返回值做检查。
4.2 - fclose
int fclose(FILE* stream);
如果流(stream)成功关闭,则返回 0,失败则返回 EOF(本质上是 -1)。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("./test.txt", "r");
if (NULL == fp)
{
perror("fopen");
return 1;
}
fclose(fp);
fp = NULL;
return 0;
}
如果当前目录下并不存在 test.txt 文件,文件打开失败,终端上将会显示 "fopen: No such file or directory"。
五、文件的顺序读写
5.1 - 字符输出函数 fputc
int fputc(int character, FILE* stream);
函数说明:将字符写入流中(Write character to stream)。
Writes a
character
to thestream
and advances the position indicator.
返回值:写入成功,返回写入字符的 ASCII 值;写入失败,则返回 EOF 并设置错误指示器 error indicator(ferror
)。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_char.txt", "w");
if (NULL == fp)
{
perror("fopen");
return 1;
}
for (int ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, fp); // 将小写字母 a ~ z 输出到文件当中
}
fclose(fp);
fp = NULL;
return 0;
}
5.2 - 字符输入函数 fgetc
int fgetc(FILE* stream);
函数说明:从流中读取字符(Get character from stream)。
Returns the character currently pointed by the internal file position indicator of the specified
stream
. The internal file position indicator is then advanced to the next character.
返回值:读取成功,返回读取字符的 ASCII 值;如果遇到文件末尾,则返回 EOF 并设置文件末尾指示器 end-of-file indicator(feof
);读取失败,则返回 EOF 并设置错误指示器。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_char.txt", "r");
if (NULL == fp)
{
perror("fopen");
return 1;
}
int ch = 0;
int cnt = 0;
do
{
ch = fgetc(fp);
if (ch >= 'a' && ch <= 'z')
{
cnt++;
}
} while (ch != EOF);
printf("The file contains %d lowercase(a ~ z).\n", cnt); // 26
fclose(fp);
fp = NULL;
return 0;
}
5.3 - 文本行输出函数 fputs
int fputs(const char* str, FILE* stream);
函数说明:将字符串写入流中(Writes string to stream)。
The function begins copying from the address specified(
str
)until it reaches the terminating null character('\0'
). This terminating null-character is not copied to the stream.
返回值:写入成功,返回一个非负数;写入失败,则返回 EOF 并设置错误指示器。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_str.txt", "w");
if (NULL == fp)
{
perror("fopen");
return 1;
}
fputs("你好,世界!\n", fp);
fputs("Hello World!\n", fp);
fclose(fp);
fp = NULL;
return 0;
}
5.4 - 文本行输入函数 fgets
char* fgets(char* str, int num, FILE* stream);
函数说明:从流中读取字符串(Get string from stream)。
-
从流中读取字符并将其存储到
str
指向内存空间中,直到读取了num - 1
个字符或者遇到换行或者达到文件末尾。 -
换行符('\n')会使
fgets
函数停止读取字符,但它被视为有效字符并存储到str
指向的内存空间中。 -
str
指向的内存空间最后会自动添加一个字符串结束符 '\0'。
返回值:读取成功,返回字符串首字符的地址,即 str
(如果在尝试读取字符时遇到了文件末尾,则设置文件末尾指示器;如果在读取任何字符之间发生这种情况,则返回 NULL,str
的内容保持不变);读取失败,则返回 NULL 并设置错误指示器(但 str
指向的内容可能已更改)。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_str.txt", "r");
if (NULL == fp)
{
perror("fopen");
return 1;
}
char buf[20] = { 0 };
printf("%s", fgets(buf, 20, fp));
printf("%s", fgets(buf, 20, fp));
fclose(fp);
fp = NULL;
return 0;
}
5.5 - 格式化输出函数 fprintf
int fprintf(FILE* stream, const char* format, ...);
函数说明:将格式化数据写入流中(Write formatted data to stream)。fprintf
函数和 printf
函数的功能相同,只是后者是将数据写到屏幕上。
返回值:写入成功,返回写入的字符总数;写入失败,则返回一个负数并设置错误指示器。
例如:
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
FILE* fp = fopen("test_for_format.txt", "w");
if (NULL == fp)
{
perror("fopen");
return 1;
}
struct Stu s = { "zhangsan", 18 };
fprintf(fp, "%s %d\n", s.name, s.age);
fclose(fp);
fp = NULL;
return 0;
}
5.6 - 格式化输入函数 fscanf
int fscanf(FILE* stream, const char* format, ...);
函数说明:从流中读取格式化数据(Read formatted data from stream)。
返回值:
-
On success, the function returns the number of items of the argument list successfully filled. This count can match the expected number of items or be less (even zero) due to a matching failure, a reading error, or the reach of the end-of-file.
If a reading error happens or the end-of-file is reached while reading, the proper indicator is set (feof or ferror). And, if either happens before any data could be successfully read, EOF is returned.
例如:
int main()
{
FILE* fp = fopen("test_for_format.txt", "r");
if (NULL == fp)
{
perror("fopen");
return 1;
}
struct Stu s = { 0 };
fscanf(fp, "%s %d", s.name, &s.age);
fprintf(stdout, "%s %d", s.name, s.age);
fclose(fp);
fp = NULL;
return 0;
}
5.7 - 二进制输出函数 fwrite
size_t fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
函数说明:将 count
个元素的数组(每个元素的大小为 size
,单位是字节)从 ptr
指向的内存块中写入到流中。
返回值:返回成功写入的元素总数;如果该数字和 count
不同,则表明出现写入错误,在这种情况下,设置错误指示器;如果 size
或 count
为 0,函数则返回 0,错误指示器不变。
例如:
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
FILE* fp = fopen("test_for_binary.txt", "wb");
if (NULL == fp)
{
perror("fopen");
return 1;
}
struct Stu s = { "zhangsan", 18 };
fwrite(&s, sizeof(struct Stu), 1, fp);
fclose(fp);
fp = NULL;
return 0;
}
5.8 - 二进制输入函数 fread
size_t fread(void* ptr, size_t size, size_t count, FILE* stream);
函数说明:从流中读取 count
个元素的数组(每个元素的大小是 size
,单位是字节),并将它们存储到 ptr
指向的内存块中。
返回值:返回成功读取的元素总数;如果该数字和 count
不同,则表明出现读取错误或遇到文件末尾,在这两种情况下,会设置正确的指示器,可以分别用 ferror
和 feof
进行检查;如果 size
或 count
为 0,函数则返回 0,并且流的状态和 ptr
指向的内容保持不变。
例如:
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
FILE* fp = fopen("test_for_binary.txt", "rb");
if (NULL == fp)
{
perror("fopen");
return 1;
}
struct Stu s = { 0 };
fread(&s, sizeof(struct Stu), 1, fp);
printf("%s %d\n", s.name, s.age); // zhangsan 18
fclose(fp);
fp = NULL;
return 0;
}
5.9 - 总结
功能 | 函数名 | 适用于 |
---|---|---|
字符输出函数 | fputc | 所有输出流 |
字符输入函数 | fgetc | 所有输入流 |
文本行输出函数 | fputs | 所有输出流 |
文本行输入函数 | fgets | 所有输入流 |
格式化输出函数 | fprintf | 所有输出流 |
格式化输入函数 | fscanf | 所有输入流 |
二进制输出函数 | fwrite | 文件 |
二进制输入函数 | fread | 文件 |
5.10 - 对比一组函数
printf/ fprintf/ sprintf
scanf/ fscanf/ sscanf
sprintf:
int sprintf(char* str, const char* format, ...);
函数说明:将格式化数据写入到字符串中(Write formatted data to string)。
sscanf:
int sscanf(const char* s, const char* format, ...);
函数说明:从字符串中读取格式化数据(Read formatted data from string)。
例如:
#include <stdio.h>
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s1 = { "zhangsan", 18 };
char buf[20] = { 0 };
sprintf(buf, "%s %d", s1.name, s1.age); // 将格式化数据写入到字符串中
printf("%s\n", buf); // zhangsan 18
struct Stu s2 = { 0 };
sscanf(buf, "%s %d", s2.name, &s2.age); // 从字符串中读取格式化数据
printf("%s %d\n", s2.name, s2.age); // zhangsan 18
return 0;
}
六、文件的随机读写
6.1 - fseek
int fseek(FILE* stream, long int offset, int origin);
函数说明:根据 origin
和 offset
移动文件指针。
参数:
-
stream:Pointer to a FILE object that identifies the stream.
-
offset:偏移量。
-
origin:用作
offset
参考的位置,它可以是以下值:Constant Reference position SEEK_SET Beginning of file SEEK_CUR Current position of the file pointer SEEK_END End of file *
返回值:成功返回 0,否则返回一个非零数。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_fseek.txt", "wb");
if (NULL == fp)
{
perror("fopen");
return 1;
}
fputs("this is an apple.", fp);
fseek(fp, 9, SEEK_SET);
fputs(" sam", fp);
fclose(fp);
fp = NULL;
return 0;
}
成功执行此代码后,test_for_fseek.txt 文件中包含:This is a sample.
6.2 - ftell
long int ftell(FILE* stream);
函数说明:返回文件指针相对于起始位置的偏移量。
返回值:成功返回位移量,否则返回 -1。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_ftell.txt", "rb");
if (NULL == fp)
{
perror("fopen");
return 1;
}
fseek(fp, 0, SEEK_END); // non-portable
long size = ftell(fp);
fclose(fp);
fp = NULL;
printf("Size of test_for_ftell.txt: %ld bytes.\n", size);
return 0;
}
该程序打印出 test_for_ftell.txt 文件的大小(单位是字节)。
6.3 - rewind
void rewind(FILE* stream);
函数说明:让文件指针回到文件的起始位置。
返回值:无。
例如:
#include <stdio.h>
int main()
{
FILE* fp = fopen("test_for_rewind.txt", "w+");
if (NULL == fp)
{
perror("fopen");
return 1;
}
for (int n = 'A'; n <= 'Z'; n++)
{
fputc(n, fp);
}
rewind(fp);
char buf[27] = { 0 };
fread(buf, 1, 26, fp);
puts(buf); // ABCDEFGHIJKLMNOPQRSTUVWXYZ
fclose(fp);
fp = NULL;
return 0;
}
七、文件读取结束的判定
7.1 - feof
int feof(FILE* stream);
函数说明:检查文件末尾指示器(Check end-of-file indicator)。
返回值:如果设置了与流关联的文件末尾指示器,则返回非零值,否则返回零。
7.2 - ferror
int ferror(FILE* stream);
函数说明:检查错误指示器(Check error indicator)。
返回值:如果设置了与流关联的错误指示器,则返回非零值,否则返回零。
7.3 - 总结
牢记:在文件读取过程中,不能用 feof
函数的返回值直接判断文件是否读取结束,而是应用于当文件读取结束时,判断是因为读取失败结束,还是因为遇到文件末尾结束。
-
文本文件读取是否结束,判断返回值是否为 EOF(
fgetc
),或者 NULL(fgets
)。#include <stdio.h> #include <stdlib.h> int main() { FILE* fp = fopen("test_for_char.txt", "r"); if (NULL == fp) { perror("File Opening failed!"); return EXIT_FAILURE; } int ch = 0; // 注意:int,而非 char,因为要处理 EOF while ((ch = fgetc(fp)) != EOF) { putchar(ch); } putchar('\n'); 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; }
-
二进制文件读取是否结束,判断返回值是否小于实际要读取的个数。
#include <stdio.h> int main(void) { double a[5] = { 1.0, 2.0, 3.0, 4.0, 5.0 }; FILE* fp = fopen("test.bin", "wb"); fwrite(a, sizeof(double), 5, fp); fclose(fp); double b[5]; fp = fopen("test.bin", "rb"); size_t ret_code = fread(b, sizeof(double), 5, fp); if (ret_code == 5) { puts("Array read successfully, contents: "); for (int n = 0; n < 5; ++n) printf("%f ", b[n]); putchar('\n'); } else { if (feof(fp)) printf("Error reading test.bin: unexpected end of file\n"); else if (ferror(fp)) perror("Error reading test.bin"); } fclose(fp); }