• 【C语言】文件操作


    对文件的理解

    什么是文件

    文件就是电脑C盘里存放东西就叫文件,从功能上我们会把文件分为二种:

    • 程序文件
      程序文件分为三种:源程序文件(后缀为.c)
      目标文件(windows环境后缀为.obj)
      可执行程序(windows环境 后缀为.exe)
    • 数据文件
      数据文件就是我们在运行程序所产生的内容或者是读写的数据

    而文件的组成又分为三部分:

    • 文件路径
      文件路径是用来表示文件所在的位置
      例如:C:\ Windows \ addins
      上面表示的是:addins 文件在的位置在C盘Windows 文件中
    • 文件名
      文件名就是字面上的意思,文件的名字,用来分别文件里存放的是什么文件或软件
      文件里还可以存放很多的子文件
    • 文件的后缀
      文件的后缀代表的是文件的功能

    文件的类型指针

    如果想打开一个文件,就会用到文件的类型指针,又叫文件指针(FILE*

    • 文件指针(FILE*
      每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息
      (如文件的名字,文件状态及文件当前的位置等)
      这些信息是保存在一个结构体变量中,该结构体类型是有系统声明的
    • 文件指针(FILE*)的类型申明:
    struct _iobuf {
    char *_ptr;
    int _cnt;
    char *_base;
    int _flag;
    int _file;
    int _charbuf;
    int _bufsiz;
    char *_tmpfname;
    };
    typedef struct _iobuf FILE;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 这是系统里(FILE*)的类型申明,不同的编译器的FILE类型包含的内容不完全相同,但是大同小异
      正常都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。

    文件指针(FILE*)的使用

    正常是 FILE* + 变量名,这样就叫文件指针变量

    FILE* pd;
    
    • 1

    任何一个C程序,运行起来就会默认打开3个流

    1、FILE* stdin(标准输入流) ,从键盘上输入数据对应的是 printf

    2、FILE* stdout(标准输出流),打印数据到屏幕上对应的是 scanf

    3、FILE* stderr(标准错误流)打引错误信息到显示器

    流可以理解为输入/输出缓冲区

    文件的使用函数

    文件的打开(fopen)和关闭(fclose)

    文件打开(fopen)函数参数

    FILE * fopen ( const char * filename, const char * mode );
    
    • 1
    • 第一个参数filename :是要打开文件的文件名
    • 第二个参数mode :是文件要用什么方式进行打开

    文件的打开方式:

    文件的打开方式描述如果文件不存在
    “r” (只读)打开一个已经存在的文本文件,读取数据出错
    “w” (只写)打开一个文本文件,输出数据建立一个新的文件
    “a”(追加)向文本文件尾添加数据建立一个新的文件
    “rb”(读取)输入数据,打开一个二进制文件出错
    “wb”(只写)为了输出数据,打开一个二进制文件建立一个新的文件
    “ab”(追加)向一个二进制文件尾添加数据出错
    “r+”(读写)为了读和写,打开一个文本文件出错
    “w+”(读写)为了读和写,新建一个新的文件建立一个新的文件
    “a+”(读写)打开一个文件,在文件尾进行读写建立一个新的文件
    “rb+”(读写)为了读和写打开一个二进制文件出错
    “wb+”(读写)为了读和写,新建一个新的二进制文件建立一个新的文件
    “ab+”(读写)打开一个二进制文件,在文件尾进行读和写建立一个新的文件

    文件关闭(fclose)函数的参数

    int fclose ( FILE * stream );
    
    • 1
    • 参数stream:是文件指针,文件使用完后一定要fclose关闭,并把文件指针置空。(和 free 使用方法一样)
      如果不关闭文件会导致数据的丢失

    文件的打开(fopen)和关闭(fclose)的使用

    int main()
    {
    	FILE* pd = fopen("maun.txt", "r");//打开文件
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写文件
    	........
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    文件的顺序读写

    文件的顺序读写就是按照一定的顺序进行输入或者输出

    功能函数名适用于
    字符输入函数fgetc所有输入流
    字符输出函数fputc所有输入流
    文本行输入函数fgets所有输入流
    文本行输出函数fputs所有输入流
    格式化输入函数fscanf所有输入流
    格式化输出函数fprintf所有输入流
    二进制输入fread文件
    二进制输出fwrite文件

    使用fgetc输入

    fputc功能:只写一个字符
    fputc 输出函数的参数

    int fputs ( const char * character, FILE * stream );
    
    • 1
    • 第一个参数:character:要写入的字符
      第二个参数:stream:指向输出流 FILE 对象的指针

    fputc 输出函数的使用:

    int main()
    {
    	FILE* pd = fopen("moun.txt", "w");// w 是只写
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写文件
    	//循环的方式进行写
    	for (char i = 'a'; i <= 'z'; i++)
    	{
    		fputc(i , pd);
    	}
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    结果:

    在这里插入图片描述

    使用fputc输出

    fgetc 输出函数的参数

    int fgetc ( FILE * stream );
    
    • 1

    fgetc 输入函数的使用:
    fgetc功能:只读一个字符

    int main()
    {
    	FILE* pd = fopen("moun.txt", "r");// r 是只读一个字符
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//读文件
    	char i = fgetc(pd);
    	printf("%c\n", i);
    	//读第二次
    	i = fgetc(pd);
    	printf("%c\n", i);
    	
    或者用循环的方式进行打印
        //char ch = 0;
    	//while ((ch = fgetc(pd)) != EOF)
    	//{
    	//	printf("%c ", ch);
    	//}
    
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    结果:
    在这里插入图片描述

    写一行数据(fputs)

    fgets:从文件中读取一行字符到内存中

    fputs 函数的参数:

    int fputs ( const char * str, FILE * stream );
    
    • 1

    fputs 函数的使用:

    int main()
    {
    	FILE* pd = fopen("moun.txt", "w");// w 是只写
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写一行数据
    	fputs("Hello China!", pd);
    
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    结果:
    在这里插入图片描述

    • 写文件时,如果文件里有内容,它会把文件里的所有内容销毁重新写进入数据
    • 如果不想销毁文件的内容,那应该用 “a”的形式,“a” 代表追加数据
      在这里插入图片描述

    fgets 读一行数据

    fputs功能:从内存中输出一行字符到文件中
    fgets 函数的参数:

    char * fgets ( char * str, int num, FILE * stream );
    
    • 1
    • 第一个参数str:读到的字符串放到str指向的空间里去
    • 第二个参数num:读取num-1个字符,最后一个补上\0
    • 第二个参数stream:指向输入流 FILE 对象的指针
    int main()
    {
    	FILE* pd = fopen("moun.txt", "r");// r是只读
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//读一行数据
    	char arr[10];
    	fgets(arr, 5, pd);
    	//打印
    	printf("%s\n", arr);
    
    
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    结果:
    在这里插入图片描述

    • 如果读取成功返回的str 的地址错误返回的是 NULL

    fscanf 输出函数

    fscanf功能:把文件中的数据格式化的读取到内存中
    fscanf 函数的参数:

    int fprintf ( FILE * stream, const char * format, ... );
    
    • 1
    • 第一个参数stream:指向输出流 FILE 对象的变量指针
    • 第一个参数format:就是和 printf 的输出格式一样

    fscanf 函数的使用:

    struct S
    {
    	char name[20];
    	int age;
    	float scores;
    };
    int main()
    {
    	struct S s = { "zhangsan",23,49.5f };
    	FILE* pf = fopen("moun.txt", "w");
    	if (pf == NULL)
    	{
    		perror("fopen:");
    	    return 1;
    	}
    	fprintf(pf, "%s %d %f", s.name, s.age, s.scores);//打印到txt文件
    	// 下面的方法在前面 流 中有写
    	// stdout 是打印到屏幕上   流 的函数和 printf 一样
    	fprintf(stdout, "%s %d %f", s.name, s.tele, s.scores);
    
    	fclose(pf);
    	pf = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    结果:

    在这里插入图片描述

    • fprintf 是把内容输出到文件上
      printf 是把内容输出到屏幕上

    fscanf 输入函数

    fscanf 输入函数的参数:

    int fscanf ( FILE * stream, const char * format, ... );
    
    • 1
    • 第一个参数stream:指向输入流 FILE 对象的指针
    • 第二个参数:使用方法和scanf一样

    fscanf 输入函数的使用:

    struct S
    {
    	char name[20];
    	int age;
    	float scores;
    };
    
    int main()
    {
    	struct S s = { 0 };
    	FILE* pd = fopen("moun.txt", "r");
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//写文件
    	//从 pd 的文件读数据到结构体 s 里
    	fscanf(pd,"%s %d %f", s.name, &(s.age), &(s.scores));
    	//打印到屏幕上
    	printf("%s %d %f\n", s.name, s.age, s.scores);
    	或者:
    	//fprintf(stdout,"%s %d %f\n", s.name, s.age, s.scores);
    
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结果:
    在这里插入图片描述

    • 文件的信息是之前使用 fprintrf 函数写到文件里的,叫写文件
      fscanf 是从文件里读数据到内存中,叫读数据
    • 上面的代码是通过 fscanf 函数把文件上的数据读到 struct S s
    • 第一个是文件指针,剩下的是格式化输入

    fwite 二进制输出

    fwite 函数的参数:

    size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
    
    • 1

    fwite 二进制输出的使用:
    在这里插入图片描述

    • wb 是以二进制的方式写
    • &s 的里读 1sizeof( struct S) 的大小的数据到 pd 文件里(二进制的方式)

    fread 二进制输入

    fread 函数的参数:

    size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );
    
    • 1

    fread 二进制输入的使用:

    
    struct S
    {
    	char name[20];
    	int age;
    	float scores;
    };
    int main()
    {
    	struct S s = { "zhengsan",23,49.5f };
    	//以二进制的方式读到内存中
    	// rb 是以二进制的方式进行读取
    	FILE* pd = fopen("moun.txt", "rb");
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	// 二进制的方式读
    	fread(&s, sizeof(struct S), 1, pd);
    	//打印
    	printf("%s %d %f", s.name, s.age, s.scores);
    
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    结果:

    在这里插入图片描述

    sscanf 与 sprintf

    sprintf 函数的参数:

    int sprintf ( char * str, const char * format, ... );
    
    • 1

    sscanf 函数的参数:

    int sscanf ( const char * s, const char * format, ...);
    
    • 1

    sprintf sscanf 函数数的使用:

    struct S
    {
    	char name[20];
    	int age;
    	float scores;
    };
    int main()
    {
    	struct S s = { "zhengsan",23,49.5f };
    	struct S tmp = { 0 };
    	char buf[100] = { 0 };
    	//把s中的格式化数据转换成字符串放到 buf 中
    	sprintf(buf, "%s %d %f", s.name, s.age, s.scores);
    	//打印
    	printf("字符串:%s\n", buf);
    	//从字符串buf中获取一个格式化的数据到 tmp 中
    	sscanf(buf, "%s %d %f", tmp.name, &(tmp.age), &(tmp.scores));
    	printf("格式化:%s %d %f\n", tmp.name, tmp.age, tmp.scores);
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    结果:

    在这里插入图片描述

    • 格式化输出就是%d %f这些就是按照格式化的输出
      - sprintf 是把一个格式化的数据转换成字符串
    • sscanf 一个字符串中转化为一个格式化的数据
    • 这二个函数是针对字符串的,不能使用到其他地方

    输入和输出函数的对比

    • scanf:是针对标准输入的格式化输入语句
      printf:是针对标准输出的格式化输出语句

    • fscanf:是针对所有输入流的格式化输入语句
      fprintf:是针对所有输出流的格式化输出语句

    • sscanf:从一个字符串中转化为一个格式化的数据
      sprintf:是把一个格式化的数据转换成字符串

    文件的随机读写

    前面的文件函数都是按照顺序进行读写的,那么是可以从中间或者从末尾进行读写呢
    下面的函数就可以实现

    fseek (指定文件指针的位置)

    fseek 函数的参数

    int fseek( FILE *stream, long offset, int origin );
    
    • 1
    • 第一个参数stream :对应文件指针
    • 第二个参数offset:相对于origin参数的偏移量
    • 第三个参数origin:从那个位置开始进行
      origin 有三种可能取值:
    数值位置
    SEEK_SET文件开头
    SEEK_CUR文件指针的当前所处的位置
    SEEK_END文件末尾

    fseek 函数的使用:

    int main()
    {
    	//打文件
    	FILE* pd = fopen("text.txt", "r");
    	if (pd == NULL)
    	{
    		perror("fopen");
    		return 1;
    	}
    	//读文件  定位文件的位置
    	fseek(pd, 2, SEEK_SET);//跳过 2 个字符
    	int ch = fgetc(pd);
    	printf("%c\n", ch);
    
    	//从当前的位置在跳过 2 个字符
    	fseek(pd, 2, SEEK_CUR);
    	ch = fgetc(pd);
    	printf("%c\n", ch);
    	//从末尾的位置向前跳 2 个字符
    	fseek(pd, -2, SEEK_END);
    	ch = fgetc(pd);
    	printf("%c\n", ch);
    
    	//关闭文件
    	fclose(pd);
    	pd = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    结果:

    在这里插入图片描述

    • 注意:上面每次文件读取完毕后,文件指针++ 一次

    ftell(求文件指针与起始位置的偏移量)

    ftell 函数的参数

    long int ftell ( FILE * stream );
    
    • 1
    • 第一个参数:对应文件指针
    • 返回参数:是返回相对于起始位置的偏移量,出错时返回-1

    结果:
    在这里插入图片描述

    rewind(让文件指针回到起始位置)

    rewind 函数的使用:

    void rewind ( FILE * stream );
    
    • 1
    • 参数stream :对应文件指针

    结果:

    在这里插入图片描述

    文本文件和二进制文件的区别

    • 二进制文本:
      数据在内存中以二进制的形式存储,如果不加转换的输出到文件里,就叫二进制文件

    • 文本文件:
      如果要求在文件上以ASCII码的形式存储,则需要在存储前转换
      以ASCII字符的形式存储的文件就是文本文件

    • 一个数据在内存中是怎么存储的呢?
      如果是字符,一律以ASCII形式存储
      数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储

    例如:
    如果有整数10000,如果以ASCII码的形式输出到磁盘,则磁盘中占用5个字节(每个字符一个字节)
    而二进制形式输出,则在磁盘上只占4个字节

    下面就是ASCII形式存储 和 二进制形式存储的区别:
    在这里插入图片描述

    下面转化成代码的形式:

    int main()
    {
    	int a = 10000;
    	FILE* pf = fopen("test.txt", "wb");
    	fwrite(&a, 4, 1, pf);//二进制的形式写到文件中
    	fclose(pf);
    	pf = NULL;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 上面代码是把 a 的值 10000 以二进制的方式存储到文件中
      文件里的数据:
      在这里插入图片描述
      文件里的数据是以二进制的方式存储的,我们看不懂,但是可以用编译器自带的二进制编译器查看

    在这里插入图片描述
    在这里插入图片描述

    文件读取结束的判定

    文件是怎么判定文件的结束的呢,是什么情况下才是结束的?

    被错误使用的 feof

    feof 是不能用来判断文件是否结束
    feof 是当文件结束后,用来判断是什么原因而结束的,是读取失败还是遇到了文件末尾

    • 文本文件读取是否结束有二种:
      判断返回值是否为 EOF ( fgetc )
      或者用( fgets)返回值是否为 NULL

      例如:
      fgetc 判断是否为 EOF .
      fgets 判断返回值是否为 NULL

    二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
    例如:
    fread判断返回值是否小于实际要读的个数
    如果要读的字符个数是 6 ,真实读到的是 3 个字符,那说明后面没有字符了,就判定结束
    如果要读的字符个数是 6 ,真实读到的是 6 个字符,那说明后面还有字符

    例如:
    文本文件的判断

    #include 
    #include 
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    二进制文件的判断:

    #include 
    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);
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    文件缓冲

    • ANSIC 标准采用“缓冲文件系统”处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”
    • 从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上
    • 如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)
    • 缓冲区的大小根据C编译系统决定的

    在这里插入图片描述

    用于证明缓冲区存在的代码:

    #include 
    #include 
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    结果:

    在这里插入图片描述

    在这里插入图片描述

    因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。

  • 相关阅读:
    CentOS7 建立静态地址网桥br0
    算法设计与分析复习--分支界限法
    【python数据分析刷题】-N08.排序&函数
    QT scrollArea控件背景透明,但不影响子控件的方法
    java之二分查找算法
    ​力扣解法汇总754. 到达终点数字
    JavaScript面向对象
    使用OpenCV3处理图像
    浏览器输入url到页面展示过程
    概率的本质是什么
  • 原文地址:https://blog.csdn.net/m0_66483195/article/details/125924389