📝个人主页:@Sherry的成长之路
🏠学习社区:Sherry的成长之路(个人社区)
📖专栏链接:C语言进阶
🎯长路漫漫浩浩,万事皆有期待
文章目录
- 1.文件操作
- 1.1 概述:
- 1.1.1 为什么使用文件:
- 1.1.2 什么是文件:
- ①.程序文件:
- ②.数据文件:
- ③.文件名:
- 2. 文件顺序读写:
- 2.1 文件的打开和关闭:
- ①.文件指针:
- ②.文件的打开与关闭:
- ③.文件打开模式:
- 2.2.文件的顺序读写:
- 3. 文件随机读写:
- 3.1 fseek 函数:
- 3.2 ftell 函数:
- 3.3 rewind 函数:
- 4. 文本文件与二进制文件:
- 5. 文件读取结束的判定:
- 5.1 错误使用 feof 函数:
- 5.2 判断文件读取结束:
- 6. 文件缓冲区:
- 7.总结:
1.文件操作
1.1 概述:
我们进行的所有操作都是在计算机内存中进行的,并且我们所操作的数据也是存放在计算机内存中的,一旦我们退出了程序,内存空间就会被释放并回收,而这个过程中我们所操作的所有的数据也将不复存在。如此,我们无法将数据真正保留下来,只能在每次运行程序时重新进行输入,重新录入信息,使用起来极为不便。
1.1.1 为什么使用文件:
我们希望将数据保留在本地,只有当我们进行删除操作时,才将对应的数据删除掉,即尝试实现数据的持久化。而我们实现数据持久化的方式一般有两种:使用数据库
或将数据存放至本地磁盘
中。
目前还没有接触到数据库的知识,于是我们便通过学习文件操作,来将我们的数据存放至我们计算机的本地硬盘中,从而实现数据的持久化。
1.1.2 什么是文件:
我们通常所说的文件,一般是指存放在我们计算机本地硬盘上的文件。但是在我们的程序设计中(根据文件功能
分类),指的是程序文件与数据文件两种文件。
①.程序文件:
程序文件主要包括源程序文件、目标文件和可执行程序文件。
源程序文件(后缀为 .c)
目标文件( Windows 环境下后缀为 .obj)
可执行程序文件( Windows 环境下后缀为 .exe)
②.数据文件:
数据文件的内容不一定是程序,而是程序运行过程中所进行读写的数据,比如程序运行中需要从中读取的数据,或者程序运行完毕所输出的文件。
今天所讨论的,正是数据文件的相关操作
在前面所有内容的学习中,我们所有的输入输出,其操作对象都是终端,均为从键盘读取输入内容,并将处理结果输出致我们的计算据显示器上进行反馈。而今天我们的目的则是将数据信息输入至我们的本地磁盘上,当我们想要对数据进行操作时,便从本地硬盘进行读取。
③.文件名:
文件和我们人类有自己的名字一样,也需要有一个文件标识符,文件名的存在就是为了便于我们进行识别和引用。
而一个文件的文件名由三部分组成:文件路径 + 文件名主干 + 文件后缀
以文件名“ c:\code\test.txt ”为例:
文件路径为“ c:\code\ ”,表示文件存放在硬盘 C 盘下的 code 文件夹内。
文件名主干为“ test ”,表示该文件的文件名为 test。
文件后缀为“ .txt ”,表示该文件的文件类型为文本文件。
2. 文件顺序读写:
2.1 文件的打开和关闭:
我们在使用或操作我们的文件之前,首先需要在我们的程序中及将其打开
①.文件指针:
首先需要了解一下文件指针这个概念,这是因为不光是文件的打开与关闭,包括后面我们在对我们的文件进行操作时,也都是通过文件指针实现的。
在缓冲文件系统中,最关键的一个概念就是“ 文件类型指针 ”,即我们通常所说的“ 文件指针 ”。并且我们要知道,我们使用的每一个文件都在内存中开辟了相应的文件信息区,用于存放该文件的相关信息,并且这些信息都保存在一个结构体变量中。
并且这样的结构体类型是有系统声明的,取名为 FILE。
例如在 Visual Studio
的头文件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* p;
//定义一个文件指针p
像这样,我们就能创建出一个文件指针,而接下来就可以使这个文件指针 p 指向某个文件信息区(FILE 类型的结构体变量),并通过该文件信息区中所保存的信息来访问本地硬盘内的文件了。通过使用文件指针就可以找到与其相关联的文件了。
②.文件的打开与关闭:
应在读写文件之前打开文件,并在文件读写结束后关闭文件。同时ANSI C
规定,使用 fopen 函数(file open)来打开文件,用 fclose 函数(file open)来关闭文件。
什么是ANSI C、ISO C、C89、C90标准?
随着C语言使用得越来越广泛,出现了许多新问题,人们日益强烈地要求对C语言进行标准化。1983年,美国国家标准协会(ANSI)组成了一个委员会,X3J11,为了创立C的一套标准。经过漫长而艰苦的过程,该标准于1989年完成,这个版本的语言经常被称作ANSI C,或有时称为C89(为了区别C99)在1990年,ANSI C标准(带有一些小改动)被美国国家标准协会(ANSI)采纳为ISO/IEC 9899:1990。这个版本有时候称为C90或者ISO C。综上,ANSI C、ISO C、C89、C90其实是同一种标准。
open 函数的使用方式为:
FILE* p = fopen(const char* filename, const char* mod);
1.其中“ const char* filename ”指文件名(是字符串,文件名即文件标识符)。
2.其中“ const char* mod ”指文件打开模式(也是字符串,后面会列出)
int main()
{
//打开文件:
FILE* p = fopen("test.txt", "r");
//以"r",即只读模式打开文件c:\code\test.txt
//默认路径为.c文件同目录下
if (p == NULL)
//判断文件打开是否成功
{
perror("FILEOPEN");
//打开失败打印错误原因并退出
return 1;
}
printf("success\n");
return 0;
}
fclose 函数的使用方式为:
fopen(FILE* strname);
“ FILE* strname ”指的是指向期望关闭文件的文件指针。
因为我们在打开文件时没有特意注明路径,则默认路径为 .c 文件的同目录下。为了验证我们代码的正确性,我们在该目录下创建“ test.c ”文件用于代码测试:
int main()
{
//打开文件:
FILE* p = fopen("test.txt", "r");
//以"r",即只读模式打开文件c:\code\test.txt
if (p == NULL)
//判断文件打开是否成功
{
perror("FILEOPEN");
//打开失败打印错误原因并退出
return 1;
}
printf("open success\n");
//验证文件是否成功打开
fclose(p);
p = NULL;
if (p == NULL)
{
printf("close success\n");
//验证文件是否成功关闭
}
return 0;
}
③.文件打开模式:
2.2.文件的顺序读写:
这其中较为常用的就是 fputc 函数与 fgetc 函数,我们一般就通过这两个函数来实现对文件内容的顺序读写。
这两个函数的使用方式为:
fputc(const char charname, FILE* strname);
fget(FILE* strname);
首先文本文件 test.txt 中的内容为空,没有内容
接下来我们使用“ 写 ”模式打开该文件,并在判断非空后使用 fputc 函数来进行顺序写入:
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILEOPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fclose(p);
p = NULL;
return 0;
}
我们等待程序编译运行并完成数据写入后关闭程序,这时我们来到本地文件中查看硬盘中本地文件的数据写入情况:
我们看到本地文件中的内容已经实现了数据的顺序写入。
完成后我们再用“ 读 ”模式打开该文件,并在判断非空后使用 fgetc 函数来顺序读取该文件中的内容:
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
int ch = 0;
while ((ch = fgetc(p)) != EOF)
{
printf("%c ", ch);
//顺序读取文件指针pp指向文件内的信息并打印
}
fclose(p);
p = NULL;
return 0;
}
将程序编译运行起来查看我们 fgetc 函数的读取结果:
并且我们也可以使用 fputs 函数(区别于 fputc 函数)来实现字符串的顺序写入:
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
fputs("The test TXT\n", p);
//fputc 为写入字符,fouts 为写入字符串
//只写入字符串内容,不会自动换行,想要换行需手动添加换行转义字符\n
//并且在写入时,会覆盖原本的内容数据
fputs("The test TXT", p);
fclose(p);
p = NULL;
return 0;
}
或使用 fgets 函数(区别于 fgetc 函数)来实现字符串的顺序读取:
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char arr[256] = { 0 };
//定义字符数组用于存放读取到的字符串
fgets(arr, 256, p);
//从文件指针p指向文件处,读取最多256个字符,并将数据读取至字符数组arr中
//该函数为按行读取,读取至换行转义符\n处主动停止并换行
printf("%s", arr);
//想要读取两行就需要使用两次fgets函数
fgets(arr, 256, p);
printf("%s", arr);
fclose(p);
p = NULL;
return 0;
}
最终目的,就是要结合文件操作将数据保存至本地硬盘中,从而实现优化,于是我们可以使用 fprintf 函数实现将结构体变量的内容保存至本地硬盘之中:
typedef struct Contact
{
char name[20];
char sex[5];
int age;
char tele[11];
}con;
int main()
{
FILE* p = fopen("test.txt", "w");
//文件打开模式为“写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
con c1 = { "Sherry","女",18,"3478290" };
fprintf(p, "%s %s %d %s\n", c1.name, c1.sex, c1.age, c1.tele);
//按照"%s %s %d %s\n"的格式将数据c1.name, c1.sex, c1.age, c1.tele写入至p所指向的文件内
fclose(p);
p = NULL;
return 0;
}
程序编译运行结束后关闭,这时我们再去本地文件中查看会发现,数据已经成功的保存至本地硬盘中了:
并且我们也可以通过 fscanf 函数从本地硬盘文件中读取数据:
typedef struct Contact
{
char name[20];
char sex[5];
int age;
char tele[11];
}con;
int main()
{
FILE* p = fopen("test.txt", "r");
//文件打开模式为“读”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
con c1 = { 0 };
fscanf(p, "%s %s %d %s", c1.name, c1.sex, &(c1.age), c1.tele);
//按照"%s %s %d %s"的格式,从p所指向的文件中将数据读取至c1.name, c1.sex, &(c1.age), c1.tele中
printf("%s %s %d %s\n", c1.name, c1.sex, c1.age, c1.tele);
fclose(p);
p = NULL;
return 0;
}
3. 文件随机读写:
我们很多时候并不是要进行顺序读写,而是进行随机读写(伪随机,指不按照顺序依次进行读写)。为了实现这样的操作,我们就需要使用 fseek
、ftell
和 rewind
三个函数来帮助我们对这样的操作进行实现。
3.1 fseek 函数:
fseek 函数的作用为,根据文件指针的位置和偏移量来定位文件指针。
int fseek(FILE* strname, long int offset, int origin);
1.“ offset
”为相对于指针位置的指针偏移量。
2.“ origin
”为指针位置,其参数有三种:“ SEEK_CUR
”表示文件指针当前位置;“ SEEK_END
”表示文件末尾的位置;“ SEEK_SET
”表示文件开始位置。
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, 10, SEEK_SET);
//使用fseek函数将文件指针从文件开始处(参数SEEK_SET表示文件起始位置)指向偏移量为10处
//偏移量为正表示向后偏移,为负表示向前偏移
char output;
output = fgetc(p);
//接下来进行读取时,继续向后读取一个字符,即字符k
printf("%c\n", output);
fclose(p);
p = NULL;
return 0;
}
3.2 ftell 函数:
long int ftell(FILE* strname);
ftell 函数的作用为,返回文件指针相对于文件起始位置的偏移量。
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, -5, SEEK_END);
//使用fseek函数将文件指针从文件结尾处(参数SEEK_SET表示文件起始位置)指向偏移量为-5处
//偏移量为正表示向后偏移,为负表示向前偏移
long back = ftell(p);
//定义整型变量用于接受并记录指针相对于起始位置的偏移量
printf("指针相对于起始位置的偏移量为:%ld\n", back);
fclose(p);
p = NULL;
return 0;
}
3.3 rewind 函数:
void rewind(FILE* strname);
rewind 函数的作用为,使文件指针位置返回文件的起始位置
int main()
{
FILE* p = fopen("test.txt", "r+");
//文件打开模式为“读写”
if (p == NULL)
{
perror("FILE_OPEN");
return 1;
}
char ch = 'a';
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, p);
//使用 fputc 函数顺序写入小写字符a~z
}
fseek(p, -5, SEEK_END);
rewind(p);
//使文件指针回归文件起始位置
printf("%c\n", fgetc(p));
//打印验证指针当前位置
fclose(p);
p = NULL;
return 0;
}
4. 文本文件与二进制文件:
根据数据的组织形式,我们将数据文件称为文本文件或二进制文件
二进制文件:在我们的计算机内存中,各种数据都是以二进制码
的形式进行存储的,以 二进制码形式进行存储的文件
文本文件:以 ASCII
字符形式进行存储的文件
如果我们想要在外存上以 ASCII 码的形式存储数据,就需要在存储前将数据进行转换
。
数据在内存中数据到底是如何让进行存储的呢?
实际上,字符在内存中的存储一律是以 ASCII 码的形式进行存储的,而数值型数据既可以用ASCII 码存储,也可以用二进制形式进行存储。
例如十进制数字 10000 在进行存储时,就可以有两种存储形式:
1.二进制形式:
00000000 00000000 00100111 00010000
2.ASCII码形式:
00110001 00110000 00110000 00110000 000110000
1 0 0 0 0
int main()
{
int a = 10000;
FILE* p = fopen("test.txt", "wb");
//“wb”表示以只写模式打开二进制文件
if (p == NULL)
{
perror("FileOpen");
return 0;
}
fwrite(&a, 4, 1, p);
//将变量a中的数据,每四个字节存储一次,写入文件指针p所指向的文件
fclose(p);
p = NULL;
return 0;
}
在上面这段代码运行成功后,我们已经成功的将变量 a 中的数据写入到了本地磁盘对应的 txt 文件中了,可是我们发现,当我们尝试打开本地文件查看存储的数据时,里面看起来并不是我们想要的结果:
是我们的程序哪里出错了吗?其实并不是,而是因为我们使用了" wb "方式,即以二进制形式进行了写入,所以我们在尝试直接查看时显示的是二进制符号。
所以我们就需要换一种方式才能对我们的文件内容进行查看。首先选中我们的“ 源文件 ”栏,右击选择“ 添加 ” -> “ 添加现有项 ”,将写入时创建的 txt 文件添加进来:
然后选中 txt 文件,右击选择“ 打开方式 ”选项:
接着在选项框中下拉选择“ 二进制编辑器 ”:
这时我们就看到了文件内的实际存储情况了:
它显示我们的文件中的实际存储数据为 10 27 00 00,这又是怎么来的呢?这是因为,我们在存储时根据语句:
fwrite(&a, 4, 1, p);
根据这条语句,我们在进行数据写入时,是四个字节一组进行写入的,于是根据其二进制码可以得到它的存储为:
每四个字节进行划分:
0000 0000 0000 0000 0010 0111 0001 0000
于是得到:
00 00 27 10
又因为在之前我们就验证过我的计算机采用的是小端存储模式
,于是在进行压栈时会将数据进行倒置存储,于是就有:
10 27 00 00
5. 文件读取结束的判定:
5.1 错误使用 feof 函数:
- 在文件的读取过程中,不能通过 feof 函数的返回值来判定文件是否读取结束。
- 该函数的作用为,在已经确定文件读取结束的情况下,用于判定文件读取结束的原因。
5.2 判断文件读取结束:
如何正确判断文件是否读取结束?
文本文件判断文件读取结束方法:
①. 使用fgetc
函数判断是否为EOF
。
②. 使用fgets
函数判断返回值是否为NULL
。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int c;
//注意:int,非char,要求处理EOF
FILE* fp = fopen("test.txt", "r");
if (!fp)
{
perror("File opening failed");
return 1;
}
// 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);
fp = NULL;
return 0;
}
二进制文件判断文件读取结束方法:
使用fread
函数判断返回值是否小于实际要读取的数据个数。
#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);
fp = NULL;
return 0;
}
6. 文件缓冲区:
文件缓冲区是用以暂时存放读写期间的文件数据而在内存区预留的一定空间。通过磁盘缓存来实现,磁盘缓存不是一种实际存在的存储介质
,它依托于固定磁盘,提供对主存储器存储空间的扩充,即利用主存中的存储空间, 来暂存
从磁盘中读出 (或写入)的信息。
例如在国际 ANSI C 标准
中,就是采用“ 缓冲文件系统 ”来对数据文件进行处理的。缓冲文件系统会自动地在我们的内存空间中为程序中的每个正在使用的文件开辟一块“ 文件缓冲区 ”。
计算机内存中向本地磁盘中输出数据:
将会先送至缓冲区->将缓冲区全部装满后->一并送达磁盘。
从磁盘向计算机读入数据:
从磁盘文件中读取数据->并将其输入至缓冲区->并在充满缓冲区后再逐个地将数据送达程序数据区
而至于缓冲区的大小,则是由 C 编译系统决定的
验证缓冲区:
#include <stdio.h>
#include <Windows.h>
//VS2022 WIN10环境测试
int main()
{
FILE* pf = fopen("test.txt", "w");
fputs("abcdef", pf);
//先将代码放在输出缓冲区
printf("睡眠10秒\n");
//已经写数据了,但是打开test.txt文件,发现文件没有内容
Sleep(10000);
printf("刷新缓冲区\n");
fflush(pf);
//刷新缓冲区时,才将输出缓冲区的数据写到文件(磁盘)
//注:fflush 在高版本的VS上不能使用了
printf("再睡眠10秒\n");
//此时,再次打开test.txt文件,文件有内容了
Sleep(10000);
fclose(pf);
//注:fclose在关闭文件的时候,也会刷新缓冲区
pf = NULL;
return 0;
}
因为缓冲区的存在,数据只有在填满缓冲区后才会进行真正的写入或读取,所以在 C 语言程序代码的编写过程中,在对文件进行操作时,需要刷新缓冲区并在文件操作结束时关闭文件,否则就可能导致文件的读写操作出现问题。
7.总结:
今天我们了解了文件操作与管理的相关知识,学习了文件的顺序读写、随机读写、文件读取结束的判定以及文件缓冲区的相关介绍,并且对文本文件和二进制文件有了一定的了解和区分,通过文件操作,可以实现对本地文件的修改与维护,希望我的文章和讲解能对大家的学习提供一些帮助。
当然,本文仍有许多不足之处,欢迎各位小伙伴们随时私信交流、批评指正!我们下期见~