• C语言博客作业05-指针


    这个作业属于哪个班级 C语言--网络2012
    这个作业的地址 C博客作业05--指针
    这个作业的目标 学习指针相关内容
    姓名 朱芳芳

    指针目录

    0.展示PTA总分(0----2)

    1.本章学习总结(3分)

    整理指针主要知识点,必须包含内容有:

    1.1 指针定义、指针相关运算、指针做函数参数。

    指针变量的定义
    类型名 * 指针变量名
    执政变量所指向的变量类型 指针声明符
    int *ptr;
    p是整型指针,指向整型变量
    char *cPtr;
    cp 是字符型指针,指向字符型变量

    指针的基本运算
    指针的值,是某个变量的地址。
    int *p,a=3;
    p=&a; & 取地址运算符
    把a的地址赋给p,即p指向a,指针变量的类型为所指向的变量类型

    • 间接访问运算符,访问指针所指向的变量
      printf("%d",*p);输出p所指向的变量a的变量的值

    指针取地址运算和间接访问运算
    p=&a; 指针的赋值
    p=10; 指针的内容的访问运算
    p直接对内存单元操作,改变变量数据;
    tips:
    1)
    &表示取地址,
    表示取内容
    2)
    &
    p与&a相同,是地址,&a与a相同,是变量
    3)
    p)++ 等价于a++ 将p所指向的变量值加1
    p++?等价于(p++),先取*p,然后再自加,此时p不再指向a
    指针变量的初始化
    1)
    指针变量先定义,赋值必须是地址
    int a;
    int *p1;
    p1=&a;
    2)
    在定义指针变量时,可以同时对它赋初值
    int a;
    int *p1=&a; *p1=&a是错误的!!
    int *p2=p1;

    3)
    不能用数值作为指针变量的初值,但可以将一个指针变量初始化为一个空指针
    int p=10000是错误的!!!
    p=0;
    p=NULL;
    p=(int
    )1732; (int*)是将数字强转为指针类型

    指针的赋值运算
    int a=3,p1,p2;
    printf("%d ",*p1);?
    p1=&a;
    p2=p1;
    相同类型的指针才能互相赋值,没有指向的指针是危险的,会出现所谓的段错误!

    指针作为函数参数
    形参:指针变量 int *p
    实参:&a,某个指针
    eg:指针作为函数参数模拟角色互换

    #include<stdio.h>
    void swap(int *px,int *py);
    int main(void)
    {
    	int a = 1, b = 2;
    	int* pa = &a, * pb = &b;
    
    	swap(pa, pb);
    	printf("%d,%d",a,b);
    
    
    	return 0;
    }
    void swap(int* px, int* py)
    {
    	int t;
    	t = *px;
    	*px = *py;
    	*py = t;
    }
    

    要通过函数调用来改变主函数中某一个变量的值:
    1)主调函数中,将该变量的地址或者指向该变量的指针作为实参(实参是数组名)
    2)被调函数中,用指针类型形参接受该变量的地址(形参是指针变量,可以写成数组形式)
    3)在被调函数中,改变形参所指向变量的值
    3)数值型数组传数组地址,数组长度

    传地址的优点?
    数据量少,直接对地址操作,效果更好,可以改变多个变量的值,比return返回更实用

    #include<stdio.h>
    void monthDay(int year, int yearday, int* monthPtr, int* dayPtr);
    int main(void)//年,天数,月份指针,天数指针
    {
    	int day=0, month=0, year=0, yearday=0;//初始化日期为零
    	scanf("%d %d",&year,&yearday);//输入年和天数
    	monthDay(year, yearday, &month, &day);//转换为月份日期的函数调用,返回为地址
    	printf("%d-%d-%d",year,month,day);
    
    	return 0;
    }
    void monthDay(int year, int yearday, int* monthPtr, int* dayPtr)
    {
    	int k, leap;//k为具体天数
    	int tab[2][13] =//初始化二维数组
    	{
    		{0,31,28,31,30,31,30,31,31,30,31,30,31},{0,31,29,31,30,31,30,31,31,30,31,30,31},
    	};
    	/*建立闰年判别条件*/
    	leap = (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
    	for (k = 1; yearday > tab[leap][k]; k++)
    	{
    		yearday = yearday - tab[leap][k];
    		*monthPtr = k;
    		*dayPtr = yearday;//逐渐减去每一个月的时间
    	}
    
    
    
    }
    

    数组和地址间的关系
    int a[100];
    数组名代表一个地址,他的值是数组首元素的地址,(基地址)a+i是数组a的及地址的第i个偏移量
    指针+1,实际下移一个数据类型存储单元

    注意:
    移动的写法,a++!=*a++

    注意运算符号的优先级

    指针和数组的关系
    任何由数组下标来实现的操作都能用指针来完成
    例如:

    &a[i] == a+i;
    p+i == &p[i];
    a[i] == *(a+i);
    *(p+i) == p[i];

    移动指针:p+1
    地址加法:移动下一个数据单元
    用指针完成对数组的操作

    int a[100],*p;
    p=a;
    int sum=0;
    for(p=a;p<a+n;p++)
    {
    sum=sum+*p;
    }
    

    指针做循环变量,务必了解初始和结束地址

    遍历数组的方法:
    下标法

    for(int i=0;i<n;i++)
    {
    a[i];
    }
    

    指针法

    for(p=a;p<a+n;p++)
    {
    *p;
    }
    

    使用指针计算数组元素个数和数组元素的存储单元数
    p,q;
    p=&a[0];
    q=p+1;
    printf("%d ",q-p);//指针p和 q之间元素的个数
    printf("% ",(int)q-(int)p);//指针p和q之间的字节数,(int)q代表地址值

    指针的算数运算和比较运算
    double p,q;
    q-p:两个相同类型的指针相减,表示他们之间的存储单元的数目
    p+1/q-1:至此昂下一个存储单元,指向上一个存储单元
    p<q:两个相同类型的指针关系可以用关系运算符比较大小

    1.2 字符指针

    包括指针如何指向字符串、字符串相关函数及函数代码原型的理解、字符串相关函数用法(扩展课堂未介绍内容)
    字符数组和字符指针的重要区别

    如果要改变数组所代表的字符串,只能改变数组元素的内容
    如果要改变指针所代表的字符串,通常直接改变指针的值,让他直接指向新的字符串

    char sa[]="this is a string";
    const charsp="this is a string";
    char
    sp="this is a string";错误❌
    sp="hello";错误❌
    sa="hello";错误❌
    数组名是常量,不能对它赋值
    sp是指针,只能指向,不能直接赋值

    **关于const **
    const是constant的缩写,意思是恒定不变的!
    const定义的是变量,但又相当于常量
    不允许给它重新赋值,即使是相同的值也不可以
    只读变量,必须在定义的时候就给他赋初值
    const char*sp="this is a string";错误❌
    sp='a';
    char sa[] ="this is a string";正确✔
    char
    sp=sa;

    字符串的输出
    char sa[]="array";
    const char*sp="point";
    printf("%s",sa); array 数组
    printf("%s",sp); point 指针
    printf("%s","string"); string字符串
    printf("%s",sa+2); ray 对数组进行操作
    printf("%s",sp+3); int对指针进行操作
    printf("%s","string"+1); tring 对字符串进行操作

    即 数组名sa,指针sp和字符串string 的值都是地址
    printf("",地址);
    //%s:输出从指定地址开始,‘’结束的字符串

    字符指针,先赋值,后引用
    定义字符指针后,没有对他赋值,指针的值不确定,
    chars;
    scanf("%s",s);错误❌ 不要引用未赋值的指针
    char
    s ,str[20];正确 ✔
    s=str;
    scanf("%s",s);
    定义指针是,现将他的初值置为空
    char*s=NULL;

    1)指针如何指向字符串

    C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中

    #include <stdio.h>
    #include <string.h>
    
    int main(){
        char str[] = "http://c.china.net";
        int len = strlen(str), i;
        //直接输出字符串
        printf("%s
    ", str);//使用%s
        //每次输出一个字符
        for(i=0; i<len; i++){
            printf("%c", str[i]);//使用%c
        }
        return 0;
    }
    

    字符数组当然是数组,利用指针对字符数组进行操作。

    #include <stdio.h>
    #include <string.h>
    
    int main(){
        char str[] = "http://c.biancheng.net";
        char *pstr = str;
        int len = strlen(str), i;
    
        //使用*(pstr+i)
        for(i=0; i<len; i++){
            printf("%c", *(pstr+i));
        }
        printf("
    ");
        //使用pstr[i]
        for(i=0; i<len; i++){
            printf("%c", pstr[i]);
        }
        printf("
    ");
        //使用*(str+i)
        for(i=0; i<len; i++){
            printf("%c", *(str+i));
        }
        printf("
    ");
    
        return 0;
    }
    
    #include <stdio.h>
     int main(){
         char *str = "Hello World!";
         str = "I love C!";  //正确
         str[3] = 'P';  //错误
     
         return 0;
     }
    

    这段代码能够正常编译和链接,但是在运行时会出现段错误(Segment Fault)或者写入错误。

    第四行代码是正确的,可以更改指针变量本身的指向;第5行代码是错误的,不能修改字符串中的字符

    课外拓展
    ** 字符数组和使用一个指针指向字符串是非常相似。区别是什么?

      字符数组和指针字符串的区别是内存中的存储区域不一样,字符数组存储在全局数据区或栈区,指针字符串存储在常量区。

    全局区和栈区的字符串(包括其它数据)有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。

      内存权限不同导致一个明显的结果就是,字符数组在定义后可以读取和修改每个字符,而对于指针字符串来说,一旦被定义

    后就只能读取而不能修改,任何对它的赋值都是错误的。

    2)字符串相关函数及函数代码原型的理解
    1.字符串的输入输出
    输入: scanf()/可以接受空格/或者 fgets()/不能接受空格并且以''结束/
    输出:printf()或者 puts()
    stdio.h
    2.字符串的复制,连接,比较,求字符串长度
    1、求长度

    函数:strlen()

    格式:strlen(字符串/字符串变量)
    函数原型:

    //求字符串长度函数strlen
    #include <stdio.h>
    int str_strlen(char *Str)
    {
     int l=0;
     while(*Str!='')
     {
      *Str++;
      l++;
     }
     return l;
    }
    

    注意:计算字符串的有效长度,不包括结束标志'';

    2、复制

    函数:strcpy()

    strcpy(字符串1(目标),字符串2(源))--将字符串2的内容复制给字符串1

    注意:字符数组1的长度要能容纳字符串1+2的内容,

    函数原型:

    拷贝函数strcpy
    #include <stdio.h>
    char *str_strcpy(char *strDest,char *strSour)
    {
    
     while(*strSour!='')
     {
      *strDest=*strSour;
      *strDest++;
      *strSour++;
     }
     *strDest='';//直到最后一个给予结束标志符
     return strDest;
    }
    
    注意:使用strDest,strSrc这样增强可读性的名字
    对于传入参数的strDest,strSrc
    进行检查,禁止空指针传入
    使用const 来约束strSrc,提高程序的健壮性,如果函数体内的语
    句试图改动strSrc的内容。编译器将指出错误
    
    
    

    3、比较

    函数:strcmp()

    格式:strcmp(字符串1,字符串2)

    若字符串1与字符串2相等返回--0 字符串1大于2 返回--1 小于返回--- -1
    注意:strcmp函数实际上是对字符的ASCII码进行比较 , 其中str1和str2可以是字符串常量或者字符串变量,返回值为整形,所以区分大小写

    函数原型:

    //字符串比较函数strcmp,。
    #include <stdio.h>
    int str_strcmp(char *str1,char *str2)
    {
     while((*str1==*str2)&&(*str1!=''))
     {
      *str1++;
      *str2++;
     }
     if(*str1==''&&*str2=='')//结束位置相同,说明长度一样长,否则不一样长
      return 1;
     else
      return -1;
    }
    
    

    4、合并

    函数:strcat()

    格式:strcat(字符串1,字符串2)--将字符串2的内容合并到字符串1

    注意:字符数组1的长度要能容纳1+2的内容,且 str1=str1+str2是非法的·!❌

    函数原型:

    char *str_strcat(char *strDest,const char *strSour)
    {
     while(*strDest!='')//先将指针指向结尾的位置
      strDest++;//移动到字符串末尾
     while(*strSour!='')
     {
      *strDest=*strSour;//从字符串2的起始位置开始赋值到字符串1的结尾位置,再分别移动
      strDest++;
      strSour++;
     }
     *strDest='';//给予结束标志
     return strDest;
    }
    
    或
    char*strcat(char*str1,const char*str2)
    {
    char *tempstr;
    tempstr=str1;
    if(!str1||!str2)//防止空指针传入,否则程序崩溃
    {
    return NULL;
    }
    while(*str1)
    {
    str1++;
    }
    while(*str2)
    {
    *str1=*str2;
    str1++,str2++;
    }
    *str1='';
    return tempstr;//连接后字符串的首地址
    //函数类型为指针,则返回地址,首地址不要改变
    
    
    
    
    

    注意:将字符串str2连接在str1后,str1最后的结束字符NULL会被覆盖掉,并且连接后的字符串的尾部
    会再增加一个NULL.注意:str1和str2所指的内存空间不能重叠,且str1要有足够的空间来容纳要复制
    的字符串。返回是str1字符串的首地址。

    字符串复制和合并函数存在的问题?
    源字符串要足够大,否则会溢出导致系统崩溃
    while((strDest++=strSrc++)!='');
    如何解决?
    strncpy函数
    charstrncpy(chardest,const char*stc ,size_t n)
    把src所指向的字符串复制到dest,最多复制n个字符,当长度小于n时,剩余部分将用控字节填充
    strncat()函数

     char*strncat(char*dest,const char*stc ,size_t n)
    

    把src所指向的字符串追加到dest所指向的字符串的结尾,追加最多n个字符

    字符串比较函数用来比较字符串的大小
    if(str1>str2){}比较字符串首元素的地址 错误❌
    if (strcmp(str1,str2)>0) 比较字符串内容 {}正确✔

    3)字符串相关函数用法

    1.3 指针做函数返回值

    指针作为函数的返回值,函数的返回值类型需要定义为指针类型
    一般定义格式为
    数据类型*函数名称(形式参数列表)

    注意事项:
    一定要保证返回的指针是有效指针,一个常犯的错误是,返回局部变量的指针

    1.4 动态内存分配

    为什么要动态内存分配
    堆区申请的空间,想要多少就申请多少
    数组要指定数组长度,空间浪费
    栈区空间有限
    一般情况下,运行中的很多存储要求在写程序时无法确定,故需要动态内存分配
    使用动态内存分配能有效使用内存
    全局变量,静态局部变量,自动变量由编译器系统分配

    课外拓展 堆区和栈区区别

    一、区别
    注:首先堆和栈可以分为两种,一种是数据结构,另一种是和内存的分配有关,这两种虽然都有栈和堆,但是两者关系并不大,

    1、栈、堆是数据结构里面的叫法,注意:有时候有人喜欢这样说 "堆栈" 其实说的就是栈而不是堆。  

    2、堆区、栈区则是内存模型的叫法。

    二、内存中的栈区和堆区
    我们知道底层是C
    而C语言的内存模型分为5个区:栈区、堆区、静态区、常量区、代码区。每个区存储的内容如下:

    1、栈区:存放函数的参数值、局部变量等,由编译器自动分配和释放,通常在函数执行完后就释放了,
    其操作方式类似于数据结构中的栈。栈内存分配运算内置于CPU的指令集,效率很高,但是分配的内存量
    有限,局部变量申请大小是有限制的,由栈的剩余空间决定,栈的大小为2m,也有的说是1m,总之,是一个
    编译时就会确定的常数,如果申请的空间超过了剩余空间,将会出现问题。

    2、堆区:就是通过new、malloc、realloc分配的内存块,编译器不会负责它们的释放工作,需要用程序
    区释放。分配方式类似于数据结构中的链表。“内存泄漏”通常说的就是堆区。

    3、静态区:全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始
    化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后,由系统释放。

    4、常量区:常量存储在这里,不允许修改。eg:const

    5、代码区:顾名思义,存放代码。

    动态内存分配相关函数及用法
    使用时申请:malloc,calloc
    用完就释放:free

    void*calloc (unsigned n,unsigned size)
    在内存的动态存储区中分配连续n个连续空间,每一存储空间的长度为size,并且分配后还把存储块里全部初始化为0
    若申请成功则返回地址,若不成功,则返回NULL;
    void free (void *ptr)
    释放有动态存储分配函数申请到的整块内存空间,ptr为指向要释放空间的首地址。
    free(p);

    void malloc(unsigned size)
    在内存的动态存储区中分配连续的空间,其长度为size
    eg:
    p=(int )malloc(nsizeof(int))
    若申请成功,则返回一个指向所分配内存空间的其实地址的指针,不成功则返回NULL;
    malloc对所分配的存储区域不做任何事情
    和free结合使用

    举例为多个字符串做动态内存要如何分配

    1.5 指针数组及其应用

    多个字符串用二维数组表示和用指针数组表示区别?

    1.6 二级指针

    1.7 行指针、列指针

    行指针:二级指针
    1.形如 int(*p)[n]
    指向有n个元素的一维数组

    p+i=a+i;
    (p+i)=(a+i)=a[i]
    2.二维数组与指针

    int a[3][4];
    int (p)[4];
    p=a;
    (
    p)[0]=a[0][0];
    (*(p+1))[0]=a[1][0];
    ((p+i)+j)=a[i][j];
    列指针:一级指针
    int a[3][4];
    int * p;
    p=a;

    • (p+1)=a[0][1];
      //移向下一个元素
      注意区分行指针与列指针
      行指针:p首先指向第0行,然后p+i定位到第i行,然后p+i进行解引用(*(p+i))把行地址转化为列地址,在得到第i行第0列地址后在加j得到第i行第j列地址,在进行解引用得到a[i][j]
      列指针:p直接指向了第0行第0列,找出a[i][j]相对于a[0][0]的偏移量,i * n+j

    2.PTA实验作业(7分)

    2.1 7-3 字符串的冒泡排序 (2分)

    我们已经知道了将N个整数按从小到大排序的冒泡排序法。本题要求将此方法用于字符串序列,并对任意给定的K(<N),输出扫描完第K遍后的中间结果序列。

    输入格式:
    输入在第1行中给出N和K(1≤K<N≤100),此后N行,每行包含一个长度不超过10的、仅由小写英文字母组成的非空字符串。

    输出格式:
    输出冒泡排序法扫描完第K遍后的中间结果序列,每行包含一个字符串。

    2.1.1 伪代码

    数据表达:
    字符串存储:使用二维字符数组
    输入二维数组:
    外层循环for:扫描k遍
    {
    内层循环for:冒泡n-i-1次
    {
    if(后一个字符串的长度比前一个字符串的长度要长)
    {
    交换两个字符串的位置;
    }
    }end for;
    }end for:

    2.1.2 代码截图

    2.1.3

    //main.c
    //author
    //连续输入是个字符,以回车结束
    #include "stdafx.h"
    //输入10个数字
    #define N 10
    char min(char a, char b);
    char max(char a, char b);
    int main()
    {
        //int a[N] = { 10,9,8,7,6,5,4,3,2,1 };
        int flag = N;
        //指针的方法
        char a[N] = { 0 };
        char *p = a;
        for (int i = 0; i < N; i++)
        {
            scanf_s("%c", p);
            p++;
        }
        p = &a[0];
        for (int i = 0; i < N; i++)
        {
            printf("输入的数为%c
    ", *p++);
        }
        p = a;
        while (1)
        {
            for (int i = 0; i < flag - 1; i++)
            {
                char tmp1 = *(p + i);
                char tmp2 = *(p + i + 1);
                *(p + i) = min(tmp1, tmp2);
                *(p + i + 1) = max(tmp1, tmp2);
            }
            if (flag == 2)break;
            flag--;
        }
        p = a;
        for (int i = 0; i < N; i++)
        {
            printf("%c ", *(p + i));
        }
    

    2.2 6-9 合并两个有序数组(2分)

    选择合并2个有序数组这题介绍做法。
    要求实现一个函数merge,将元素个数为m的升序数组a和长度为n的升序数组b合并到数组a,合并后的数组仍然按升序排列。假设数组a的长度足够大。

    2.2.1 伪代码

    合并两个有序数组有三种做法:
    1:创建一个新数组c,把有序数组a和有序数组b中的元素放到新创建的数组c中,然后利用冒泡排序把数组c中的元素进行有序排序。
    2:创建一个新的数组c,此数组的大小大于或等于已知两个数组之和。通过比较两个数组中的元素,谁小就把谁放到空数组中,知道其中一个数组为空,最后把剩下的数组全部放到新创建的始组中。
    3:有两个有序数组a和b,其中数组a的末尾有足够的空间容纳数组b,将数组b容纳到数组a中
    此题为第三种做法
    考虑到a数组很大,可以直接在a数组上进行合并,但是要讲究效率。如果单纯从前往后合并,那么效率会非常低,因为a数组后面的数字需要不停的移动。换一种思路,采用从后往前合并,首先计算出总长度,设置一个指针从a数组最后往前移动

    具体伪代码实现如下:

    w计算合并后a数组的长度;
    hile(b数组的长度递减大于零)
    {
    每一轮都要判断a的长度是否满足大于零并且递减的指针内容位置a和b谁比较大?a大就将a的该位置的值传入新数组:否则就是b;

    }

    void merge(int* a, int m, int* b, int n)
    {/* 合并a和b到a */
    int size = m-- + --n;
        while (n >= 0) //b数组的长度递减大于零
        {
            *(a+(size--)) = m >= 0 && *(a+m) >*(b+n) ? *(a+(m--)) :*(b+(n--));//每一轮都要判断a的长度
        }
    }
    

    2.2.2 代码截图

    2.2.3


    2.3 7-4 说反话-加强版(3分)

    void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n) {
     int i;
     int j = 0;
     for (i = m; i < m + n; i++) {
      nums1[i] = nums2[j];
      j++;
      }
     for (i = 0; i < m + n - 1; i++) {
      for (j = 0; j < m + n - 1 - i; j++) {
       if (nums1[j] > nums1[j + 1]) {
        int tmp = nums1[j];
        nums1[j] = nums1[j + 1];
        nums1[j + 1] = tmp;
       }
      }
     }
    }
    

    本题做法使用数组下标或者使用指针都可,亮点是使用了三目运算符和自增自减运算符,以及使用数组重构,使代码更加简洁

    给定一句英语,要求你编写程序,将句中所有单词的顺序颠倒输出。

    输入格式:
    测试输入包含一个测试用例,在一行内给出总长度不超过500 000的字符串。字符串由若干单词和若干空格组成,其中单词是由英文字母(大小写有区分)组成的字符串,单词之间用若干个空格分开。

    输出格式:
    每个测试用例的输出占一行,输出倒序后的句子,并且保证单词间只有1个空格。

    2.3.1 伪代码

    字符串存储(使用一维字符数组;
    使用初始位置指针和结束位置指针来控制逆序;
    while(没有到结束标志)
    {
    将结束位置指针指向字符数组末尾
    }end while;

    while(结束位置指针不等于初始位置指针)
    {
    逆向遍历字符数组;
    利用指针所指向的内容是否为空格来判断单词;
    输出单词,使用flag来控制输出格式;
    }

    2.3.2 代码截图

    2.3.3 请说明和超星视频做法区别,各自优缺点。

    借鉴超星的做法,
    超星优点:
    定义指针指向字符串,更能动态了解当前字符位置
    while(endPtr&&endPtr!=' ')
    {
    endPtr++;
    }
    逆向扫描字符串
    while(p!=beginPtr)
    {
    p--;
    }
    怎么找字符串单词,即当前字符的前一个字符为空格
    if(p!=' '&&(p-1)==' ')
    {}

    keep it up
  • 相关阅读:
    led呼吸灯
    定时器中断
    npgsql
    中断
    PAT (Advanced Level) 1128~1131:1128N皇后 1129 模拟推荐系统(set<Node>优化) 1130 中缀表达式
    PAT (Advanced Level) 1132~1135:1132 模拟 1133模拟(易超时!) 1134图 1135红黑树
    PAT (Advanced Level) 1136~1139:1136模拟 1137模拟 1138 前序中序求后序 1139模拟
    PAT (Advanced Level) 1140~1143:1140模拟 1141模拟 1142暴力 1143 BST+LCA
    PAT (Advanced Level) 1144~1147:1145Hash二次探查 1146拓扑排序 1147堆
    python实现http接口测试
  • 原文地址:https://www.cnblogs.com/Z1188G/p/14199449.html
Copyright © 2020-2023  润新知