当前位置: 首页 > news >正文

【Linux】基础IO之文件操作(文件fd)——针对被打开的文件

系列文章目录


文章目录

  • 系列文章目录
  • 前言
    • 浅谈文件的共识
  • 一、 回忆c语言对文件操作的接口
    • 1.fopen接口和cwd路径
    • 2.fwrite接口和"w","a"方法
    • 3.fprintf接口和三个默认打开的输入输出流(文件)
  • 二、过渡到系统,认识文件调用
    • 2.1看一看文件的系统调用接口——open
    • 2.2 write系统接口
  • 访问文件的本质
  • 总结


前言

浅谈文件的共识

  • 1.文件 = 内容 + 属性

  • 2.文件分为打开的文件和未打开的文件

    • 1)打开的文件:进程打开的。本质上是研究进程和文件的关系。
    • 文件被打开,就必须先加载到内存中。
    • 一个进程可以打开多个文件,操作系统要对这些文件进行管理,就要先描述,再组织。在内核中,操作系统要管理好这些文件,就必须有这个文件的对象,包含很多的文件属性。
  • 2)未打开的文件有很多,操作系统要将这些文件存储好,本质上就是对这些文件进行增删查改的操作!

    • 未打开的文件,在磁盘上放着。

本文章目标:针对被打开的文件,进行各种深入剖析。

一、 回忆c语言对文件操作的接口

1.fopen接口和cwd路径

先执行一下代码:

  1 #include<stdio.h>2 #include<unistd.h>3 4 int main()5 {6     FILE* fp = fopen("log.txt","w");7     if(fp == NULL)8     {9         perror("fopen");10         return 1;11     }12     printf("pid: %d\n",getpid());13     fclose(fp);14     sleep(1000);                                                15     return 0;16 }

该程序运行起来后,以"w"的方式打开log.txt文件,如果该文件不存在,则会创建一个文件。

运行起来时可以看到该进程的pid已经被打印出来。
ll查看能看到的确存在一个log.txt的文件。
在这里插入图片描述
那为什么是在当前目录下创建log.txt文件呢???
这是因为一个叫做cwd的东西的存在。

在根目录下的proc目录下,有该进程的当前路径。
即通过ls /proc/进程pid -l 可以看到,该运行中的进程的cwd路径!

在这里插入图片描述并且该cwd路径就是可执行程序所在的路径!

cwd:current work directory——当前工作目录!

所以,fopen以写的方式打开文件,如果文件不存在,就会在该进程的cwd路径下创建一个log.txt的文件!

由此可以得出,如果我们自己把该进程的cwd路径改了,那么它就会在更改后的cwd路径下创建log.txt文件!

怎么改?

用一个接口:chdir()即可更改当前的cwd路径。

chdir("/home/dzt/learning");   

在上面代码的基础上,在main函数开头就增加这一句代码后。

运行起来通过查找cwd路径发现,cwd被修改了!
在这里插入图片描述

且在/home/dzt/learning路径下发现:
在这里插入图片描述
真就被创建了一个log.txt文件

且在进程对应的工作目录中,不再有log.txt文件。
在这里插入图片描述

注意:1.chdir也受权限的约束,作为普通用户,不能将路径修改到/home/dzt路径下!
2.如果fopen打开的文件带绝对路径,那就按绝对路径来,如果是相对路径,就按该进程的cwd来!

总结:这个小节讲了复习了fopen函数,并且引入了cwd当前工作目录这个概念!

2.fwrite接口和"w","a"方法

在这里插入图片描述
fwrite的使用方法是:将ptr这个字符串,以size大小,nmemb个长度写入stream文件指针指向的文件中。

在这里插入图片描述
w方法的特点是:如果该文件不存在,会创建一个文件。如果该文件存在,会先将该文件清空,再打开!

注意这里的一个细节:

6     const char* message = "Hello Linux\n";                         
17    fwrite(message,strlen(message),1,fp);

执行该函数fwrite时,是否需要strlen(message)+1

答案是不需要的,+1是为了将字符串后面的’\0’也写入文件中,可是:

字符串以’\0’结尾是c语言的规定,关文件操作什么事?!

所以并不需要+1。

而a方法的作用是,直接在文件的末尾追加字符串。

由此可知,Linux中的 “>” 和 ">>"两个符号的区别一定是一个以"w"方式打开,一个以"a’方式打开的区别!!


3.fprintf接口和三个默认打开的输入输出流(文件)

fprintf接口比我们常见的printf函数多了一个字符f,默认情况下,printf就是向显示器打印数据。

而Linux下一切皆文件,所以显示器也是一个文件。

而fprintf接口,就是向指定的文件中输入数据。

fprintf(stdout,"%s %d\n",message,123);      

在这里插入图片描述

而我们在运行该程序时,会发现显示器中出现了这些信息,这就是被打印到了显示器文件中,而不是打印到其他文件中。

在这里插入图片描述
而这三个标准输入输出流,就是对应的:

键盘文件——stdin
显示器文件——stdout
显示器文件——stderr

一旦c程序运行起来,就会默认打开这三个文件。

二、过渡到系统,认识文件调用

文件其实是在磁盘上的,磁盘是外部设备,访问磁盘文件的本质,其实是访问硬件!

2.1看一看文件的系统调用接口——open

使用man 2 手册进行查找open接口的功能
man 2 open

该函数的功能是:打开/创建一个文件或设备。

在这里插入图片描述

这里多嘴一句:c语言中的fopen函数,实现也是将这个open函数进行封装得来的。

pathname是文件路径,如果传的是相对路径,就是按进程所在的cwd路径为主。
flags是一个标志位:
在这里插入图片描述
这里的标志位有三个:
O_RDONLY:表示只读操作
O_WRONLY:表示只写操作
O_RDWR:表示可读可写

为了更好地进行后面的传参,下面来讲一个比特位传参的方式

看下面的代码:

  1 #include<stdio.h>2 3 #define ONE (1<<0)4 #define TWO (1<<1)5 #define THREE (1<<2)6 #define FOUR (1<<3)7 8 void show(int flags)9 {10     if(flags&ONE)   printf("hello function 1\n");11     if(flags&TWO)   printf("hello function 2\n");12     if(flags&THREE)   printf("hello function 3\n");13     if(flags&FOUR)   printf("hello function 4\n");14 15 }16 17 int main()18 {19     show(ONE);20     printf("\n");21     show(TWO);22     printf("\n");23     show(ONE|THREE);24     printf("\n");25     show(ONE|TWO|THREE|FOUR);                                                                                                               26     printf("\n");27 28     return 0;29 }

上图所示的代码定义了几个宏,分别表示(1<<n位)
传参时如果穿过来的flag是ONE,则会打印function1,
如果传的是ONE|TWO|THREE,则传过去的flag的二进制为:111
此时就能够匹配三个if语句,就会打印出三个function。

通过这个例子就可以理解了,open函数中的flags作为一个标志位,未来会传很多比特位为1的宏,如果传多个,就能达到不一样的效果!

下面看这个例子:

1 #include<stdio.h>2 #include<unistd.h>3 #include<string.h>4 #include<sys/types.h>5 #include<sys/stat.h>6 #include<fcntl.h>7 8 int main()9 {10     // pathname, flags, modes11     int fd = open("log.txt",O_WRONLY); //采用八进制,默认权限位66612 13     if(fd < 0)14     {                                                                                                                                       15         printf("open file error\n");16         return 1;17     }18 19     return 0;20 }

接下来的操作上打开一个文件,因为传的是相对路径,如果按照c语言的fopen函数,如果该文件不存在,那它就会在该进程的cwd路径下创建log.txt文件。

运行后会发现:居然打开失败了!?

在这里插入图片描述

因为系统的open函数的O_WRONLY是只读的,并没有创建文件的功能!
要想解决这个问题:只需要

int fd = open("log.txt",O_WRONLY|O_CREAT);

增加一个比特位传参即可!
在这里插入图片描述
此时就创建出了一个log.txt文件!

注意:为什么log.txt的权限那么奇怪呢?还是一些随机的权限???

因为open函数中,第三个函数 mode是权限,我们没有传,就默认是随机的!

int fd = open("log.txt",O_WRONLY|O_CREAT, 0666); //采用八进制,默认权限位666      

再传参之后,重新试试,结果如下:

在这里插入图片描述
此时就相对正确了,可是,666权限对应的权限位应该是-rw-rw-rw-
明显不同,这是因为权限掩码的存在,默认的umask是2,根据权限掩码和权限的计算规则:

最终权限 = 起始权限 &~umask)

最终权限就是664–>-rw-rw-r--

如果非要将权限设置成666,就更改权限掩码:

umask(0); 

在全局中有一个umask(2),在该进程中也有一个umask(0),所以该文件创建之后其实是听进程中的umask(0)的,因为**就近原则,局部优先,**进程的umask会影响整个进程,但不会影响全局的。
在这里插入图片描述

此时log.txt的权限就非常正确了


总结:这个小节讲了open函数的三个参数:

pathname , flags , mode

2.2 write系统接口

在这里插入图片描述
write接口是向文件描述符对应的文件中写入。

文件描述符:file descrpitor(fd),也就是open函数的返回值,这个文件描述符就是一个文件的标识。

 8 int main()9 {10     umask(0);11     // pathname, flags, modes12     int fd = open("log.txt",O_WRONLY|O_CREAT, 0666); //采用八进制,默认权限位66613 14     if(fd < 0)15     {16         printf("open file error\n");17         return 1;18     }                                                                                                                                     19 20     const char* mesg = "Hello Linux";21     ssize_t id = write(fd,mesg,strlen(mesg));22 23     return 0;24 }

此时,向fd文件描述符对应的文件中写入Hello Linux;
结果显而易见,就不展示了,但是当我们将字符串修改成"aaa"时,结果如下:

20     const char* mesg = "Hello Linux";

在这里插入图片描述

这个结果跟fwrite函数结果完全不同,fwrite函数是每次打开文件都会清空内容再写入。

所以,只需要小小地操作:

12     int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666); //采用八进制,默认权限位666

在这里插入图片描述

O_TRUNC就是truncate的简写。

通过O_WRONLY|O_CREAT|O_TRUNC选项,就实现了如果文件不存在就创建,如果文件存在就打开并先清空的逻辑!

所以,O_APPEND,就是追加的逻辑!

访问文件的本质

由此可知,c语言,c++,java等任何其他语言,对文件的操作接口的底层一定是对这些open函数,write函数的封装!!!

在这里插入图片描述

可是还有一个问题:open系统调用的返回值是int fd,而fopen函数的返回值是FILE* fp

这两者有什么关系?

每次创建一个进程时,都会在内存中创建一个描述该进程的task_struct对象,包含进程中的各种信息,其中就有一个叫做struct file_struct* files的指针,该指针指向一个struct files struct数组,且该数组中的所有成员类型都是struct file*的指针。

为什么要这样设计呢?

来看右边:

每次打开一个文件时,都会创建一个描述该文件的struct file文件对象,该对象存储文件的各种信息。而该文件对象的地址就恰好被进程中的一个指针数组存储着!!!

在这里插入图片描述

所以,为什么open函数的返回值为int fd这个文件描述符,其实就是进程中维护的指针数组存储该文件对象的下标!!

如果该文件对象的被存储在指针数组的2号下标处,打开文件成功后就返回2!(fd)

当我们尝试着打印该文件的fd时,发现结果是3!

在这里插入图片描述

这恰好证明了, 该文件的文件描述符一定是放在进程管理的文件对象指针数组的3号下标处!!!

可是,为什么是3呢?

因为前面说过,一个进程创建后,会默认打开三个输入输出流(文件)
这三个输入输出流分别是:

stdin stdout stderr

分别对应的下标是:

0 1 2

 10 int main()11 {12     char buffer[1024];13     ssize_t sz = read(0,buffer,sizeof(buffer));14     //sz返回读取到的个数15     if(sz < 0)16     {17         perror("read fail");18         return 1;19     }20     buffer[sz] = '\0'; // read是按字节读取,如果想把它识别成字符串,就得主动加'\0'                                                          21     printf("%s\n",buffer);22 }

如上就是从0号文件中读取数据,放入到buffer数组中。

在这里插入图片描述

运行起来后会发现,结果就是等待输入,等待键盘文件的输入。

read系统接口的注意事项:返回值是返回成功读取到的字符的个数。如果想将读取到的若干字符识别成字符串,需要主动添加’\0’。


下面再看一组测试代码:

  8 int main(){9     close(1);10 12     const char* msg = "Hello Linux\n";13     write(1,msg,strlen(msg));14     write(2,msg,strlen(msg));                                                                                                               15                                       16 }     

首先close 1号文件后,运行结果只打印了一行msg代码。

前面说过,1号文件是stdout,对应的是显示器文件,2号文件是stderr,对应的也是显示器文件。它们本质上没有区别,那为什么关闭了1号文件,也就是关闭了显示器文件后,通过2号文件仍然能向显示器中打印呢?

1号文件和2号文件虽然都是显示器文件,但是他们对应的struct file*指针不同,也就是说,有两个指针指向显示器文件。
关闭1号文件的本质是,让1号文件对应的指针置空,同时让显示器文件对应的引用计数减减。这个就是close函数的本质操作。

综上:

C语言中将fd(文件描述符)封装成了FILE的结构体,不止是c语言,在任何其他语言中,只要是文件操作的结构体,就一定封装了fd(文件描述符)


总结

这篇文章讲述了关于文件的基础理解。
针对的是被打开的文件。

相关文章:

【Linux】基础IO之文件操作(文件fd)——针对被打开的文件

系列文章目录 文章目录 系列文章目录前言浅谈文件的共识 一、 回忆c语言对文件操作的接口1.fopen接口和cwd路径2.fwrite接口和"w"&#xff0c;"a"方法3.fprintf接口和三个默认打开的输入输出流&#xff08;文件&#xff09; 二、过渡到系统&#xff0c;认识…...

什么是超算数据中心

超算数据中心是基于超级计算机或者是大规模的计算集群的数据中心&#xff0c;它具备高性能、高可靠性、高可用性和高扩展性这些特点&#xff0c;能够提供大规模计算、存储和网络服务的功能&#xff0c;在人工智能、科学计算、数据分析等等领域应用比较广泛。 超算数据中心有以下…...

阿里云服务器省钱购买和使用方法(图文详解)

阿里云服务器使用教程包括云服务器购买、云服务器配置选择、云服务器开通端口号、搭建网站所需Web环境、安装网站程序、域名解析到云服务器公网IP地址&#xff0c;最后网站上线全流程&#xff0c;新手站长xinshouzhanzhang.com分享阿里云服务器详细使用教程&#xff1a; 一&am…...

Apache Flink 1.12.0 on Yarn(3.1.1) 所遇到的問題

Apache Flink 1.12.0 on Yarn(3.1.1) 所遇到的問題 新搭建的FLINK集群出现的问题汇总 1.新搭建的Flink集群和Hadoop集群无法正常启动Flink任务 查看这个提交任务的日志无法发现有用的错误信息。 进一步查看yarn日志&#xff1a; 发现只有JobManager的错误日志出现了如下的…...

pandas - 数据分组统计

1.分组统计groupby()函数 对数据进行分组统计&#xff0c;主要适用DataFrame对象的groupby()函数。其功能如下。 &#xff08;1&#xff09;根据特定条件&#xff0c;将数据拆分成组 &#xff08;2&#xff09;每个组都可以独立应用函数&#xff08;如求和函数sum()&#xff0…...

Git简介和安装

一&#xff0c;Git简介 Git 是一个分布式版本控制工具&#xff0c;通常用来对软件开发过程中的源代码文件进行管理。通过Git 仓库来存储和管理这些文件&#xff0c;Git 仓库分为两种&#xff1a; 本地仓库&#xff1a;开发人员自己电脑上的 Git 仓库 远程仓库&#xff1a;远程…...

思维模型 布里丹毛驴效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。犹豫不决是病&#xff0c;得治&#xff5e; 1 布里丹毛驴效应的应用 1.1 犹豫不决的产品“施乐 914” 20 世纪 60 年代&#xff0c;美国一家名为施乐&#xff08;Xerox&#xff09;的公司…...

预处理、编译、汇编、链接

1.预处理 宏替换去注释引入头文件 #之后的语句都是预处理语句&#xff0c; #include<iostream> 将该文件的内容拷贝到现有文件中&#xff0c; 2.编译 3.汇编 4.链接 gcc 基于C/C的编译器 补充说明 gcc命令 使用GNU推出的基于C/C的编译器&#xff0c;是开放源代…...

面试问题?

1.面向对象的特征&#xff1f; 2.开放闭合 3.java中的泛型可以用基本类型吗&#xff1f; 4.重载和重写的区别&#xff1f; 5.string、stringbuffer、stringbuilder? 6.单例模式的实现方式有哪几种&#xff1f; 7.volicate除了保证 8.sy是重量级锁还是轻量级锁&#xff…...

pytorch 笔记:PAD_PACKED_SEQUENCE 和PACK_PADDED_SEQUENCE

1 PACK_PADDED_SEQUENCE 1.0 功能 将填充的序列打包成一个更加紧凑的形式这样RNN、LSTM和GRU等模型可以更高效地处理它们&#xff0c;因为它们可以跳过不必要的计算 1.2 基本使用方法 torch.nn.utils.rnn.pack_padded_sequence(input, lengths, batch_firstFalse, enforce_…...

Ubuntu 创建用户

在ubuntu系统中创建用户&#xff0c;是最基本的操作。与centos7相比&#xff0c;有较大不同。 我们通过案例介绍&#xff0c;讨论用户的创建。 我们知道&#xff0c;在linux中&#xff0c;有三类用户&#xff1a;超级管理员 root 具有完全权限&#xff1b;系统用户 bin sys a…...

华为政企路由器产品集

产品类型产品型号产品说明 maintainProductA821 E_2*10GE/GE/FE(o)8*GE/FE(o)8*GE/FE(e),1*交流电源华为企业云端NetEngine A800 E综合业务一体化接入路由器是华为公司面向云时代推出的一款产品&#xff0c;用于企业快速接入网络&#xff0c;具备易部署、易运维、高性能、高…...

性能测试知多少---了解前端性能

我的上一篇博文中讲到了响应时间&#xff0c;我们在做性能测试时&#xff0c;能过工具可以屏蔽客户端呈现时间&#xff0c;通过局域网的高宽带可以忽略数据传输速度的障碍。这并不是说他们不会对系统造成性能影响。相反&#xff0c;从用户的感受来看&#xff0c;虽然传输速度受…...

Docker-compose容器群集编排管理工具

目录 Docker-compose 1、Docker-compose 的三大概念 2、YAML文件格式及编写注意事项 1&#xff09;使用 YAML 时需要注意下面事项 2&#xff09;ymal文件格式 3&#xff09;json格式 3、Docker Compose配置常用字段 4、Docker-compose的四种重启策略 5、Docker Compose…...

Python 深度学习导入的一些包的说明

Python 深度学习导入的一些包的说明 这段代码导入了一些Python库和模块&#xff0c;并定义了一些数据转换操作。 from future import print_function, division&#xff1a;这是一个Python 2和Python 3兼容性的导入语句。它确保在Python 2中使用Python 3的print函数和除法运算符…...

劲升逻辑与安必快、鹏海运于进博会签署合作协议,助力大湾区外贸高质量发展

新中经贸与投资论坛签约现场 中国上海&#xff0c;2023 年 11 月 6 日——第六届进博会期间&#xff0c;由新加坡工商联合总会主办的新中经贸与投资论坛在上海同期举行。跨境贸易数字化领域的领导者劲升逻辑与安必快科技&#xff08;深圳&#xff09;有限公司&#xff08;简称…...

hivesql,sql 函数总结:

1、NVL函数与Coalesce差异 -- select nvl(null,8); -- 结果是 8 -- select nvl(,7); -- 结果是"" -- select coalesce(null,null,9); -- 结果是 9 -- select coalesce("",null,9); -- 结果是 "" 1.2、 NVL函数与Coalesce差异 …...

前端js实现井字游戏和版本号对比js逻辑【适用于vue和react】

// 实现 compareVersion 方法&#xff0c;用于比较两个版本号&#xff08;version1、version2&#xff09; * 如果version1 > version2&#xff0c;返回1&#xff1b; * 如果version1 < version2&#xff0c;返回-1&#xff1b; * 其他情况&#xff0c;返回0。 * 版本号规…...

unity 通过Andriod arr 访问 手机自带的浏览器

unity 通过Andriod arr 访问 手机自带的浏览器 using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.UI;public class OpenURL : MonoBehaviour {public Button button;string url "http://192.168.1.…...

MySQL -- 索引

MySQL – 索引 文章目录 MySQL -- 索引一、索引简介1.简介2.索引效率的案例 二、认识磁盘1.磁盘2.结论3.磁盘随机访问(Random Access)与连续访问(Sequential Access) 三、MySQL 与磁盘交互基本单位1.基本单位2.MySQL中的数据管理 五、索引的理解1.索引案例2.单页mysql page3.管…...

23ccpc(最长上升子序列题解)

你原本有一个 1 到 n 的排列但是不慎地你遗忘了它但是你记得以 第i个位置 结尾的最长上升子序 列的长度数组 an 现在希望你能够构造一个符合条件的排列 p 如果不存在符合上述条件的排列 p 则输出 −1。 这里定义以 第i位置 结尾的最长上升子序列的长度为符合…...

BUUCTF easycap 1

BUUCTF:https://buuoj.cn/challenges 题目描述&#xff1a; 下载附件&#xff0c;解压得到一个.pcap文件。 密文&#xff1a; 解题思路&#xff1a; 1、这道题和它的名字一样&#xff0c;真的很easy。双击easycap.pcap文件&#xff0c;打开Wireshark。在Wireshark中&#xf…...

[LeetCode]-160. 相交链表-141. 环形链表-142.环形链表II-138.随机链表的复制

目录 160.相交链表 题目 思路 代码 141.环形链表 题目 思路 代码 142.环形链表II 题目 思路 代码 160.相交链表 160. 相交链表 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/intersection-of-two-linked-lists/description/ 题目 给你两个…...

聊一聊关于手机Charge IC的电流流向

关于手机Charge&#xff0c;小白在以前的文章很少讲&#xff0c;一是这部分东西太多&#xff0c;过于复杂。二是总感觉写起来欠缺点什么。但后来想一想&#xff0c;本是抱着互相学习来写文章的心理态度&#xff0c;还是决定尝试写一些。 关于今天要讲的关于手机Charge的内容&a…...

【k8s】pod调度——亲和,反亲和,污点,容忍

官方网址&#xff1a;https://kubernetes.io/zh/docs/concepts/scheduling-eviction/assign-pod-node/ 一、亲和性 &#xff08;1&#xff09;节点亲和性 pod.spec.nodeAffinity ●preferredDuringSchedulingIgnoredDuringExecution&#xff1a;软策略 p开头 ●requiredDuri…...

分享者 - 携程旅游创作者搬砖项目图文教程

大家好&#xff01;携程这个出行旅游平台相信大家都不陌生吧。 每天都有大量的旅客在里面浏览攻略&#xff0c;寻找灵感和旅游建议。 那么&#xff0c;我们的项目就是把一些优质的小红书平台上的旅游攻略或作品&#xff0c;经过处理后搬运到携程平台上发布。 这个项目如何操作呢…...

vite配置.env环境变量文件,开发环境,测试环境,预发布环境,生产环境

在vue2&#xff0c;用的vue-cli脚手架搭建项目&#xff0c;cli用的是webpack 当你yarn dev时&#xff0c;命令会启动package.json中的dev键名的值&#xff0c;也就是后面的一行命令 这时浏览器会去识别你是开发环境还是生产环境&#xff0c;其实windows是不能直接识别你是开发…...

0003Java安卓程序设计-springboot基于Android的学习生活交流APP

文章目录 **摘** **要**目 录系统设计开发环境 编程技术交流、源码分享、模板分享、网课教程 &#x1f427;裙&#xff1a;776871563 摘 要 网络的广泛应用给生活带来了十分的便利。所以把学习生活交流管理与现在网络相结合&#xff0c;利用java技术建设学习生活交流APP&…...

Java8 时间字符串校验是否为对应的日期格式

时间字符串格式校验 严格模式下校验日期字符串 public static boolean isDateStrict(String dateStr, String pattern) {try {DateTimeFormatter formatter new DateTimeFormatterBuilder().appendPattern("yyyyMMdd").parseDefaulting(ChronoField.ERA, 1).toFor…...

2023.11.6联赛总结

T 1 T1 T1让你构造出一个不超过 40 ∗ 40 40*40 40∗40的矩阵&#xff0c;满足连续的 r y x ryx ryx有 n n n个。 一开始我想着直接放 r y x ryx ryx&#xff0c;这个做法有 80 80 80分&#xff0c;但是打挂了&#xff0c;再调了将近1个小时后&#xff0c;选择先跳过&#xff…...