
🚀write in front🚀
📜所属专栏:> c语言学习
🛰️博客主页:睿睿的博客主页
🛰️代码仓库:🎉VS2022_C语言仓库
🎡您的点赞、关注、收藏、评论,是对我最大的激励和支持!!!
关注我,关注我,关注我,你们将会看到更多的优质内容!!

在前面的通讯录学习中,我们发现,当程序结束以后,内存空间就会被释放并回收,我们所存的通讯录人的消息就全部消失了。如此,我们无法将数据真正保留下来。
所以,我们的目的便是希望将数据保留在本地,只有当我们进行删除操作时,才将对应的数据删除掉,即尝试实现数据的持久化。而我们实现数据持久化的方式一般有两种:使用 数据库 或将数据以 文件操作 存放至硬盘中。今天我们就来学习一下文件操作相关知识。
我们通常所说的文件,一般是指存放在我们计算机本地硬盘上的文件。但是在我们的程序设计中,则指的是 程序文件 与 数据文件 两种文件(根据文件功能分类)。
包括源程序文件(后缀为.c),目标文件(windows环境后缀为.obj),可执行程序(windows环境后缀为.exe)。
本章讨论的是数据文件。
在以前各章所处理数据的输入输出都是以终端为对象的,即从终端加粗样式的键盘输入数据,运行结果显示到显示器上。而今天我们的目的则是将数据信息输入至我们的本地磁盘上,而当我们想要对数据进行操作时,再从本地硬盘进行读取。
一个文件要有一个唯一的文件标识,以便用户识别和引用。绝对路径的文件名包含3部分:文件路径+文件名主干+文件后缀
例如: c:\code\test.txt
为了方便起见,文件标识常被称为文件名。
如果打开所在文件路径的文件,则不用前面的文件路径
例如: test.txt
如果要打开所在文件的上一层文件,则在前面+一个 . ./ ;
例如: . ./test.txt
同理,如果是上上一层文件,则+ . ./. ./

我们在使用或操作我们的文件之前,首先需要在我们的程序中及将其打开,于是我们就来研究一下文件的打开和关闭方式。
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个文件在被使用时,都会在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.

例如,VS2022编译环境提供的 stdio.h 头文件中有以下的文件类型申明

此时结构体内的指针指向保存文件信息的成员。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息 ,使用者不必关心细节。
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。
下面我们可以创建一个FILE*的指针变量:
FILE* p;
//定义一个文件指针p
像这样,我们就能创建出一个文件指针,而接下来就可以使这个文件指针 p 指向某个文件信息区(FILE 类型的结构体变量),并通过该文件信息区中所保存的信息来访问本地硬盘内的文件了。这样,我们通过使用文件指针就可以找到与其相关联的文件了。
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen函数来打开文件,fclose来关闭文件.
fopen 函数的使用方式:
FILE* p = fopen(const char* filename, const char* mod);
其中“ const char* filename ”指文件名(是字符串,文件名即文件标识符**)。
其中“ const char* mod ”指文件打开模式(也是字符串,后面会为大家列出)。
返回值:若成功,返回一个文件指针,若文件打开失败,则返回一个空指针
fclose 函数的使用方式为:
fopen(FILE* strname);
int main()
{
//D:\\code\\test.txt - 绝对路径
//
FILE* pf = fopen("test2.txt", "w");
if (pf == NULL)
{
perror("fopen");
return 1;
}
else
{
printf("打开文件成功\n");
}
//读文件
//....
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
| 文件使用方式 | 含义 | 如果指定文件不存在 |
|---|---|---|
| “r”(只读) | 为了输入数据,打开一个已经存在的文本文件 | 出错 |
| “w”(只写) | 为了输出数据,打开一个文本文件(会清除文件里的消息) | 建立一个新的文件 |
| “a”(追加) | 向文本文件尾添加数据 | 建立一个新的文件 |
| “rb”(只读) | 为了输入数据,打开一个二进制文件 | 出错 |
| “wb”(只写) | 为了输出数据,打开一个二进制文件 | 建立一个新的文件 |
| “ab”(追加) | 向一个二进制文件尾添加数据 | 出错 |
| “r+”(读写) | 为了读和写,打开一个文本文件 | 出错 |
| “w+”(读写) | 为了读和写,建议一个新的文件 | 建立一个新的文件 |
| “a+”(读写) | 打开一个文件,在文件尾进行读写 | 建立一个新的文件 |
| “rb+”(读写) | 为了读和写打开一个二进制文件 | 出错 |
| “wb+”(读写) | 为了读和写,新建一个新的二进制文件 | 建立一个新的文件 |
| “ab+”(读写) | 打开一个二进制文件,在文件尾进行读和写 | 建立一个新的文件 |
| 功能 | 函数名 | 适用于 |
|---|---|---|
| 字符输入函数 | fgetc | 所有输入流 |
| 字符输出函数 | fputc | 所有输出流 |
| 文本行输入函数 | fgets | 所有输入流 |
| 文本行输出函数 | fputs | 所有输出流 |
| 格式化输入函数 | fscanf | 所有输入流 |
| 格式化输出函数 | fprintf | 所有输出流 |
| 二进制输入 | fread | 文件 |
| 二进制输出 | fwrite | 文件 |

int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
/*fputc('a', pf);
fputc('b', pf);
fputc('c', pf);
fputc('d', pf);*/
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++)
{
fputc(ch, pf);
}
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:

这就是字符的写入,大家会在该项目找到这个文件。

我们在上面代码的基础下展示文件的读取。
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
int ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
ch = fgetc(pf);
printf("%c\n", ch);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:

注意:
int main()
{
FILE* pf = fopen("test.txt", "w"); //以只写的方式打开文件
if (pf == NULL)
{
perror("fopen");
return 0;
}
char ch = 0;
for (ch = 'a'; ch <= 'z'; ch++) //向文件写入26个小写英文字母
{
fputc(ch, pf);
}
fclose(pf);
pf = NULL;
pf= fopen("test.txt", "r"); //以只读的方式打开文件
if (pf== NULL)
{
perror("fopen");
return 0;
}
while (ch != EOF) //判断是否读取到文件末尾
{
ch = fgetc(pf); //从文件中读取字符
printf("%c", ch); //打印读取字符
}
fclose(pf);
pf = NULL;
return 0;
}
代码结果:


int main()
{
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
//测试写一行数据
fputs("hello world\n", pf);
fputs("hello bit\n", pf);
return 0;
}
运行结果:


特殊情况:
int main()
{
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
//测试一行数据
char buf[20] = {0};
fgets(buf, 20, pf);
printf("%s", buf);
fgets(buf, 20, pf);
printf("%s", buf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码结果:


其实该函数和printf函数一模一样,只是前面多了一个指向文件的指针! 这里的意思其实是将后面的数据以不同的格式写入指针指向的文件信息中。
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { "zhangsan", 20, 95.5 };
FILE* pf = fopen("test.txt", "w");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//格式化的写入文件
fprintf(pf, "%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:


一样的,都是多了一个文件指针而已,其他都是一模一样的!这里的意思是将文件指向的文件消息以不同格式读出并且保存在不同类型的变量中。
int main()
{
struct S s = { 0 };
FILE* pf = fopen("test.txt", "r");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//格式化的读取文件
fscanf(pf, "%s %d %f", s.name, &(s.age), &(s.score));
//打印看数据
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
运行结果:


int main()
{
struct S s = { "张三", 20, 98.5};
FILE* pf = fopen("test.txt", "wb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//写文件
fwrite(&s, sizeof(struct S), 1, pf);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}

文本形式是这样的:

但是我们转出2进制看就是这样的:



int main()
{
struct S s = { 0};
FILE* pf = fopen("test.txt", "rb");
if (NULL == pf)
{
perror("fopen");
return 1;
}
//读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f\n", s.name, s.age, s.score);
//关闭文件
fclose(pf);
pf = NULL;
return 0;
}
代码结果:


小伙伴对于上面所提到的输入流输出流肯定非常迷惑。可以看到流其实流是一个极其抽象的概念,我们可以把它理解成C程序与外部设备进行交流的一个媒介,文件是一个流,我们的键盘,屏幕也是一个流,并且他们有各自专门的文件指针的名称。
其实我们也可以用上面的fprintf,fscanf来实现printf和scanf
struct S
{
char name[20];
int age;
float score;
};
int main()
{
struct S s = { 0 };
fscanf(stdin, "%s %d %f", s.name, &(s.age), &(s.score));
fprintf(stdout, "%s %d %f\n", s.name, s.age, s.score);
//int ch = fgetc(stdin);
//fputc(ch, stdout);
return 0;
}
在这里,为了方便辨析是输入还是输出。我们可以把自己当作内存!以内存为主体来判断是输出还是输入。比如键盘上打字对于内存来说是输入/读取,在屏幕上打印是输出/写入。写文件是将内存的东西放入文件是输出/写入,从文件获取信息是输入/读取。

相信大家看到前面的s就知道肯定是和字符串有关,其实这两个函数就是将格式化数据和字符串数据相互转化罢了,看一个栗子🌰就够了!
int main()
{
struct S s = {"zhangsan", 20, 98.5};
char buf[100] = { 0 };
//转化成字符串形式
sprintf(buf, "%s %d %f", s.name, s.age, s.score);
//按照字符串打印的
printf("%s\n", buf);
struct S tmp = { 0 };
//转化为格式化数据
sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.score));
//打印结构体数据
printf("%s %d %f\n", tmp.name, tmp.age, tmp.score);
return 0;
}
今天介绍了文件的一些基本知识,对于随机读取,缓冲区的相关知识我将会在下一篇文章讲解!
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!
专栏订阅:
每日一题
c语言学习
算法
智力题
更新不易,辛苦各位小伙伴们动动小手,👍三连走一走💕💕 ~ ~ ~ 你们真的对我很重要!最后,本文仍有许多不足之处,欢迎各位认真读完文章的小伙伴们随时私信交流、批评指正!

我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时
我的目标是转换表单输入,例如“100兆字节”或“1GB”,并将其转换为我可以存储在数据库中的文件大小(以千字节为单位)。目前,我有这个:defquota_convert@regex=/([0-9]+)(.*)s/@sizes=%w{kilobytemegabytegigabyte}m=self.quota.match(@regex)if@sizes.include?m[2]eval("self.quota=#{m[1]}.#{m[2]}")endend这有效,但前提是输入是倍数(“gigabytes”,而不是“gigabyte”)并且由于使用了eval看起来疯狂不安全。所以,功能正常,
Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题
对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl
我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚
使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta
好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信
我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A
我注意到像bundler这样的项目在每个specfile中执行requirespec_helper我还注意到rspec使用选项--require,它允许您在引导rspec时要求一个文件。您还可以将其添加到.rspec文件中,因此只要您运行不带参数的rspec就会添加它。使用上述方法有什么缺点可以解释为什么像bundler这样的项目选择在每个规范文件中都需要spec_helper吗? 最佳答案 我不在Bundler上工作,所以我不能直接谈论他们的做法。并非所有项目都checkin.rspec文件。原因是这个文件,通常按照当前的惯例,只