C语言笔记(七):字符串、结构体和共用体

字符串char str []=“helloworld”;

在c语言里是有字符串类型的。但是实际上,在c里,字符串是将char数组里的内容进行拼接然后在后面加上这个转移字符。表示该char数组的是一个字符串。当然,字符串也是用的双引号。表示null,空的意思,在char里也就说有个空格的位置的字符数组是字符串。

简单的理解:字符串就是字符数组中的元素拼接然后末尾有个字符

字符串用%s来接收。即使string的首字母s。

字符串用%s来接收。即使string的首字母s。

字符串用%s来接收。即使string的首字母s。

如果c里没有字符串类型。干嘛用个%s来接收字符串的数据

定义一个字符串

不要求全部掌握,但是要知道最简单的定义字符串的方式以及认识这个是一个字符串

方式一

#include <stdio.h>
 
int main ()
{
   char greeting[6] = {'H', 'e', 'l', 'l', 'o', ''};
 
   printf("Greeting message: %s
", greeting );
 
   return 0;
}
  1. 声明并定义了一个char数组greeting。里面的元素hello。
  2. 打印的时候用%s接收,后面直接数组名字就可以了。因为有的原因,这里直接就识别为字符串,如果没有,则这个char数组就是一个char数组。如果还是用%s来接收一个char数组,则会乱码

方式二(简单)

最简单的。先说一下printf函数吧,比如要输出一个我爱你这3个字。代码是这样的

printf("我爱你
");
  • 是换行的转义字符
  • 在这里然后所有的字符串都是用的双引号

可以知道,我们也可以直接用双引号的方式来定义一个字符串。

#include <stdio.h>
 
int main ()
{
	// char love []   ="我爱你;
   char love []   ="我爱你";

   printf("Greeting message: %s
", love );
 
   return 0;
}

这样定义一个字符串也是可以。这样就和数组区分开了吧,但是注意,这里默认后面也是有个的,不写其实也是有的,所有的字符串后面默认都有一个当然,你写了也不会报错的

当然你也可以定义好字符串的长度,比如是char love [10] ="我爱你";

用方式一的时候,是在数组里放的是单引号要加逗号,也就是{'H', 'e', 'l', 'l', 'o', ''};这里要注意,这种是单引号,每个元素是字符,最后面必须添加‘’

如果是方式二也可以直接用{“”}的放方式例如: char love [10] = {"我爱你"};但是这个一般都是把{}去掉了的,这种情况是用的双引号,则是字符串,默认会添加的,所以可以不用自己添加

单引号是字符,双引号是字符串

单引号是字符,双引号是字符串

单引号是字符,双引号是字符串

常用的字符串函数

c里也有专门用来操作字符串的函数,这些函数在string.h库里,写法和stdio.h一样

序号 函数 & 目的
1 strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
2 strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
3 strlen(s1); 返回字符串 s1 的长度。
4 strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。
5 strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
6 strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

大多数string.h库里对字符串进行操作的函数都是str前缀的。所以和这个str记住就是了。当然这些函数不用强行记忆,知道就行了,用多了就知道了。

代码:

#include <stdio.h>
#include <string.h>
 
int main ()
{
   char str1[12] = "Hello";
   char str2[12] = "World";
   char str3[12];
   int  len ;
 
   /* 复制 str1 到 str3 */
   strcpy(str3, str1);
   printf("strcpy( str3, str1) :  %s
", str3 );
 
   /* 连接 str1 和 str2 */
   strcat( str1, str2);
   printf("strcat( str1, str2):   %s
", str1 );
 
   /* 连接后,str1 的总长度 */
   len = strlen(str1);
   printf("strlen(str1) :  %d
", len );
 
   return 0;
}

strlen 与 sizeof的区别

  • strlen 是函数,sizeof 是运算操作符,二者得到的结果类型为 size_t,即 unsigned int 类型。
  • sizeof 计算的是变量的大小,不受字符 影响;而 strlen 计算的是字符串的长度,以 作为长度判定依据。也就说在sizeof的时候也是占有一个未知的 。但是在strlen是不会占的,因为strlen在遇到第一个的时候就结束计算了。

所以说,在一个字符串中,可以有多个字符的

输出字符串

字符的方式输出

这里也简单介绍几种字符串输出的方式。因为字符串其实就是char数组,则是可以使用循环遍历输出的,这种遍历的方式是用来将字符串用字符的方式输出,也就是将字符串单个单个的拆成字符。当然两种字符串的写法都能用的,因为单引号的方式和双引号的方式都是表示的字符串

#include <stdio.h>
#include <string.h>
 
int main ()
{
   char str1[12] = "Hello";
   char str2[12] = "World";

   int i ;
 
   for(i=0;i<sizeof(str2);i++){
   
	   printf("
%c
",str2[i]);
   }
 
   return 0;
}

这里使用的for循环,字符串也是特殊的字符数组,所以下标也是从0开始的。而且这里用的sizeof()来计算的字符串的长度,所以是直接<这个长度就ok。然后依次打印相应索引的字符。

如果使用的是strlen()则是<=,至于为啥有个=,因为strln()=sizeof()-1

这里注意,因为定义的字符串数组长度是12但是实际上只用到了5个长度的字符串,因为单个字符占一个位置嘛。还剩下6个位置,则会打印6个?(问号),为啥不是(12-5=7)7个,因为/0是必须占用一个位置的,所以字符串的字符能用实际上是规定的长度-1也就是(12-1=11)个

直接打印字符串

如果是直接输出字符串。因为有时候需要用到。

这里也有两种方式,

  1. 一种就是直接%s接收,的方式

    #include <stdio.h>
    #include <string.h>
     
    int main ()
    {
       char str2[12] = "World";
    
       printf("%s
    ",str2);
     
       return 0;
    }
    
  2. 还有一种是直接打印输出。

    #include <stdio.h>
    #include <string.h>
     
    int main ()
    {
       char str2[12] = "World";
    
       printf(str2);
     
       return 0;
    }
    

    很明显这种是无法直接换行的,但是方便

结构体struct

其实就是数组的一种扩展类型,因为数组只能存储同一种数据类型。所有结构体就来了。结构体是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据这个可以说有点像对象。有各种属性,比如2个小狗,有年龄、性别、毛色等等属性。但是年龄和性别又不是同一种类型。所以如果要使用就得创建两个数组来存储两种不同类型的属性(变量)

这里注意,同一个结构体类型的变量,属性一致

这里注意,同一个结构体类型的变量,属性一致

这里注意,同一个结构体类型的变量,属性一致

比如都是人类,张三和李四都有眼睛鼻子耳朵;

比如下面方式一的dog01和dog02,都是一个类型的,所以他们的属性都一致。

定义结构体

一般结构体是定义在函数外面的

必须使用struct 关键字

语法 struct 结构体类型名字(结构体标签)

{

属性1;

属性2;

...

}结构体变量名;(不要忘记这个分号

是不是很像枚举啊枚举是用的enum关键字,而结构体是struct关键字

所以定义的几种方式也和枚举是类似的。在定义一个结构体的时候,首先要有struct关键字。然后可以只出现结构体标签和属性,也可以是出现属性和结构体变量名

  • 不能结构体标签和结构体变量名,不要属性例如struct student{};
  • 不能结构体标签和结构体变量名,不要属性例如struct student{};
  • 不能结构体标签和结构体变量名,不要属性例如struct student{};

简单的说 结构体标签、属性、结构体变量名两两出现

结构体可以定义在函数里,则及时局部结构体,也可以定义在外面作为全局变量使用。

方式一

定义的时候定义了结构体类型和结构体变量

这里的Dogs放在mian函数里的,也可以放在main函数外 ,但是这个不影响结构体类型,重要的而是结构体变量也就是dog01和dog02。如果dog01和dog02是在函数里,则是局部变量。在函数外面,则是全局,这里在变量的时候说过了。所以这里就不要纠结类型是啥全局和局部了。类型都是存储在内存的

看代码吧:

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}dog01,dog02;//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量
   return 0;
}

方式二

定义的时候只定义了结构体类型名字

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	};
    struct Dogs dog1;
    struct Dogs dog2;
    struct Dogs dog3,dog4;
   return 0;
}

可以知道,这里创建了4个结构体变量。这个和基本类型创建变量的时候类似。但是这里的结构体类型是自己定义的。所以需要加上struct关键字;

方式三

直接创建结构体变量

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	} dog1;
   return 0;
}

这里没后声明结构体类型也就是说,不晓得是啥类型的结构体,所以这种方式和下面也就是方式一是不一样的。虽然属性一样,但是类型不一样,这里是个坑

//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}dog01,dog02;//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量

方式四

使用typedef,这个typedef是啥,后面再说吧,你知道这个typedef能自定义类型就OK了。

#include <stdio.h>
int main ()
{

	typedef struct {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}Dogs;	//定义了小狗类型的结构体这里是定义在函数里面的
  	 Dogs dog1;
     Dogs dog2;
     Dogs dog3,dog4;
   return 0;
}

好滴,看看上面的代码,可以知道。typedef就是用来创建的一个struct类型的,类型名也就是之前我们放变量名的位置。所以说这种方式是直接自定义了一个类型。

且使用的时候和基本类型完全一致,直接类型名+变量名;

结构体不完全声明

不完全声明,有点难理解,但是这种情况下,是两个结构体互相包含的情况,比如有两个小狗类型,一个是哈士奇,一个是泰迪,虽然都是狗,但是是两种类型的狗。而且哈士奇有泰迪里的某些属性,比如说年龄。而泰迪里也有哈士奇里的某些属性,比如说毛发。

struct Taidi;
struct Hashiqi{

    struct Taidi *age;

};
struct Taidi{
 
    struct Hashiqi *maofa;
};

则这里就需要先对泰迪进行不完全声明。这个声明也就是和声明变量类似,这里是关键字加类型名

也就说结构图互相使用对方的属性,当然为啥要声明泰迪而不声明哈士奇。虽然都说两者之一都可以。但是我认为是哈士奇先用的泰迪的属性,泰迪是在哈士奇后面才会定义属性,所以需要先声明泰迪,告诉系统,我这里有个泰迪,然后哈士奇就可以找到泰迪的属性咯

结构体变量的初始化

如果一个结构体有3个属性,只初始化了一个属性,其他两个则是相应类型的默认值。如果都不初始化,则内存会随机分配,则是乱码,不能直接{}啥都不赋值。这个是代表的是语法体。必须要有内容

不能再结构体属性里直接初始化

不能再结构体属性里直接初始化

不能再结构体属性里直接初始化

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age=12;
		char  color [20]="白色";
	}dog01,dog02;//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量
  	printf("age:%d
 color:%s
",dog01.age,dog01.color);
    return 0;
}

顺序初始化(用这个)

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}dog01={12,"白色"},dog02={22,"黑色"};//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量
    struct Dogs dog03 = {23,"灰色"};
	printf("小狗1 age:%d	color:%s
",dog01.age,dog01.color);
	printf("小狗2 age:%d	color:%s
",dog02.age,dog02.color);
    printf("小狗3 age:%d	color:%s
",dog03.age,dog03.color);
   return 0;
}

  • 在结构体变量的位置,直接用{}按照结构体属性的位置进行初始化
  • 这种方式在定义结构体的时候直接给变量赋值也是可以的

这里get一下,属性的char定义的是一个数组,初始化的时候是给的一个字符串。则数组就变成字符串咯

乱序初始化

我在vc里识别不了.和:应该这个是c++里的语法,所以还是使用顺序的吧

#include <stdio.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	};
    //方式一
    struct Dogs dog01={
		.age=12,
		.color="白色",
	};
   	struct Dogs  dog02={
		.color="黑色",
		.age=22,
	};
    //方式二
    /*
        struct Dogs dog01={
		age:12,
		color:"白色",
	};
   	struct Dogs  dog02={
		color:"黑色",
		age:22,
	};
    
    */
	printf("小狗1 age:%d	color:%s
",dog01.age,dog01.color);
	printf("小狗2 age:%d	color:%s
",dog02.age,dog02.color);
   return 0;
}

访问结构体

之前打印的时候就已经使用了。直接结构体变量.属性。这里字符串也直接.数组名

使用strcpy来给字符串属性赋初值

#include <stdio.h>
#include <string.h>
int main ()
{
	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	};
    struct Dogs dog03 = {12};//这里按照顺序,只赋值了age
	strcpy(dog03.color,"超级黑色");//使用了strcpy。吧逗号后面的内容赋值给逗号前面的内容
    printf("小狗3 age:%d	color:%s
",dog03.age,dog03.color);//打印结构体属性
   return 0;
}

结构体作为函数参数

#include <stdio.h>
#include<string.h>

	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}dog01={12,"白色"},dog02={22,"黑色"};//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量


//声明打印函数
int PrintfStruct(struct Dogs test);


int main ()
{

	//给狗3赋值(初始化)
	//按照顺序,是给age赋值
    struct Dogs dog03 = {23};
	//用复制函数给color赋值
	strcpy(dog03.color,"超级黑色");
	
	//打印
	PrintfStruct(dog01);
	PrintfStruct(dog02);
	PrintfStruct(dog03);
   return 0;
}


//定义一个打印结构体属性的函数PrintfStruct()传入一个结构体
//这个test是形式参数,随便命名
int PrintfStruct(struct Dogs text){

	printf("小狗的属性 age:%d	color:%s
",text.age,text.color);

return 0;
}

指向结构的指针

声明一个名字是struct_pointer是Dogs类型的指针

struct Dogs *struct_pointer;

现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示

struct_pointer = &Dogs;

将Dogs的地址赋值给这个struct_pointer

为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符

这里还是用打印函数来做例子

这里打印函数里的参数是一个Dogs类型的指针,所以需要传一个地址,或者传一个指针也是可以的。

这里直接传入指针变量的地址

#include <stdio.h>
#include<string.h>

	//定义了小狗类型的结构体这里是定义在函数里面的
	struct Dogs {
		//结构体类型里的属性,则可以通过变量来对结构体属性进行赋值等操作
		int  age;
		char  color [20];
	}dog01={12,"白色"},dog02={22,"黑色"};//在定义结构体的时候就直接定义了结构体变量了。且变量名是dog01和dog02,这里有两个变量


//声明打印函数
int PrintfStruct(struct Dogs *dog);


int main ()
{

	//给狗3赋值(初始化)
	//按照顺序,是给age赋值
    struct Dogs dog03 = {23};
	//用复制函数给color赋值
	strcpy(dog03.color,"超级黑色");
    
	//声明三个指针,用来放dog123的地址
  struct Dogs *dog1=&dog01;
  struct Dogs *dog3=&dog03;
  struct Dogs *dog2=&dog02;
	//打印
	PrintfStruct(&dog01);
	PrintfStruct(&dog02);
	PrintfStruct(&dog03);
   return 0;
}


//定义一个打印结构体属性的函数PrintfStruct()传入一个结构体
//这个test是形式参数,随便命名
int PrintfStruct(struct Dogs *dog){

	printf("小狗的属性 age:%d	color:%s
",dog->age,dog->color);

return 0;
}

共用体union

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。共用体提供了一种使用相同的内存位置的有效方式。这里要知道共用体是在内存里用用的同一块内存空间就是了

定义共用体

这里使用的关键字就是union,方式与定义结构类似。union 语句定义了一个新的数据类型,带有多个成员

union Data
{
   int i;
   float f;
   char  str[20];
} data;
  1. Data:共用体类名
  2. data:共用体变量名
  3. {}里面的内容是共用体属性
  4. 末尾记得有个分号

和结构体定义和声明以及调用的方式一样。只是所有的属性都是存放在一个内存空间里的。当然这里有三种数据类型,则会选择最大的数据类型所占的内存空间来放所有的数据类型。

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   printf( "data占用的字节 : %d
", sizeof(data));
 
   return 0;
}

访问共用体成员

和结构的访问方式一样。都是通过打.调用

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
   data.i = 10;
   data.f = 220.5;
   strcpy( data.str, "C Programming");
 
   printf("data占用的内存空间:%d
",sizeof(data));

   printf( "data.i : %d
", data.i);
   printf( "data.f : %f
", data.f);
   printf( "data.str : %s
", data.str);
 
   return 0;
}

在这里,我们可以看到共用体的 if 成员的值有损坏,因为最后赋给变量的值占用了内存位置,这也是 str 成员能够完好输出的原因。这是在赋值的时候,i和f以及字符串是共同使用的一块内存空间,当然这里是使用的最大的空间则是str的空间,但是优先存储的是最后赋值的值,则是先放的字符串,然后再是i和f的,但是在存储i和f的时候由于空间不够,因为只能放20字节,所有就有内存丢失了

通过sizeof函数,也能看出data是占用的20个字节的内存空间

如果想要输出完整内容,则需要一个一个的存储。

#include <stdio.h>
#include <string.h>
 
union Data
{
   int i;
   float f;
   char  str[20];
};
 
int main( )
{
   union Data data;        
 
  printf("data占用的内存空间:%d
",sizeof(  data));

    
   data.i = 10;
	printf( "data.i : %d
", data.i);
	
   data.f = 220.5;
      printf("data占用的内存空间:%d
",sizeof(  data.f));

    //装了f后,在打印i
   printf( "data.f : %f
", data.f);
   printf( "data.i : %d
", data.i);



   strcpy( data.str, "C Programming");
   printf( "data.str : %s
", data.str);
   //装了字符串后,在打印i,f
   printf( "data.i : %d
", data.i);
   printf( "data.f : %f
", data.f);
   return 0;
}

这里为啥能打印完好的内容呢?首先要知道,这是一个共用体,则data的内存空间就是最大的变量的则是20字节,这个20字节是定死了的。

从上面的结果可以知道,当创建了f后,打印i还是乱码。虽然f占用4个字节,i占用4个字节,但是当他们在一个内存空间里存储的时候,就会让计算机不知道怎么换算了。因为二进制的01混淆了。所有结果就有错误,当然最后一个存储的数据是不会有错误的,只会让之前存储的数据错误

所以在存储字符串后,打印i,f还是会有错误,但是在存储了i然后马上打印,然后再存储f后马上打印,则不会出错,因为每次打印但是时候都是将该变量作为的最后一个存储的数据;

结构体与共用体的差别

  • 结构体变量所占内存长度是其中最大字段大小的整数倍
  • 共用体变量所占的内存长度等于最长的成员变量的长度。

结构体变量内存长度

所有的长度都是说的 字节

首先先看结构体变量中占用最大的字节,然后将这个最大的字节作为标准,也就说,结构体的内存是这个最大的字节的整数倍,如果其他变量加起来不够这个最大字节的整数倍,则进行自动添加到这个字节的整数倍就ok,看下面的例子

struct {   char a;   short b;   char c; }S1;
  1. 最大的是short,占用2个字节。好滴然后就将2作为一个标准
  2. 这里要知道char是占1个字节的。short是占用2个字节的。
  3. 所以short就不做添加内存的操作依旧保持2个字节
  4. 但是第一个char只有1个字节,所以需要添加1个,才能到2
  5. 第二个同理。所以就是2+2+2=6个字节
struct {  char  a;  char  b;  short c; }S2;
  1. 最大的是short,占用2个字节。好滴然后就将2作为一个标准
  2. 这里要知道char是占1个字节的。short是占用2个字节的。
  3. 所以short就不做添加内存的操作依旧保持2个字节。然后看其他的
  4. 这里的第一个char和第二个char都各自占用1个字节,所以他们凑一对,就是2个字节了
  5. 则就是2+2=4

如果是结构体里有其他结构体类型的数据呢

typedef struct A
{
    char a1;
    short int a2;
    int a3;
    double d;
};

A=16

typedef struct B
{
    long int b2;
    short int b1;
    A a;
};

上面的B中有A类型的数据。

  1. 不看A类型的。先看B类型的变量。(long int占用4个字节,shout int占用2个字节)
  2. 最大的是4,则用4作为标准
  3. long int刚好,所以不看。直接记做4
  4. shourt int 差2个,所以补上2,
  5. 则就是4+4=8
  6. 然后看A
  7. A里面最大的是double 也就是最大的是8,所以将8做为标准
  8. 直接忽略最大子double,记做8
  9. 然后看其他的,double前面的直接相加1+2+4=7还是不够,在加1
  10. 所以8+8=16
  11. 最终的就是8+16=24字节

总结

  1. 直接找属性里最大的字节,将该字节作为标准。然后看该字节前后的字节能否加起来等于该字节,如果小于,则添加相应的数凑成该字节数。

  2. 如果有多个该最大字节的。将他们抛开不看,记做最大的数,然后分别看前后,直接凑。

    #include <stdio.h>
    #include <string.h>
     
    int main( )
    {
    typedef struct A
    {
        char a1;
        short int a2;
        double c;
        int a3;
        double d;
    } a;
    
    printf("结构体A的占用字节数是:%d
    ",sizeof(a));
       return 0;
    }
    

    比如这里,有两个double所以直接先记下8+8=16.然后看double的前后,第一个double前面1+2=3不够所以要凑5=8,

    第二个double前面也不够,所以加4个则是4+4=8;所以最终是16+8+8=32

共用体的内存长度

最大属性的内存长度

原文地址:https://www.cnblogs.com/yuxiangqiezi/p/13128603.html