C语言- 语法基础

一、C语言介绍

​ 丹尼斯.里奇和肯.汤普逊在1971~1973年美国贝尔实验室,在开发UNIX操作系统时,在BCPL语言的基础上(new B语言),发明第一款高级编程语言,取BCPL第二个字母作为名字,所以叫C语言

​ BCPL->new B->C->UNIX->Minix->Linux

​ 它是为了开发操作系统而研发的一款编程语言,它特别擅长控制硬件,所以在服务器开发、驱动编程、单片机、嵌入式开发中使用较多

C语言的优点:

​ 1、语法简单、只有32个关键字、入门简单

​ 2、执行速度很快,可以媲美汇编语言的执行速度、适合用于执行实现算法、数据结构

C语言的缺点:

​ 1、需要对内存、操作系统有一定的了解,易学难精

​ 2、由于出现时间过早,个人计算机没有普及,在设计时没有为普通人做太多的考虑,有一些语法上的陷阱和缺陷

​ 3、自由源于自律

​ 4、没有大型的软件商业公司在背后支持,可用的软件库较少

二、第一个C程序

#include <stdio.h>
#include "stdio.h"
int main() {
    printf("Hello World!\n");
}  
如何编译执行代码:
gcc xxx.c # 编译代码,生成可执行文件,如果代码没有错误,会生成 a.out 可执行文件
./a.out # 执行程序
gcc xxx.c && ./a.out # 合并执行
详解Hello World:

1、程序员所编写的代码并不是标准C代码,它需要经过一段程序把它翻译成标准C代码,负责翻译的程序叫做预处理器,被翻译的代码叫做预处理指令所有预处理指令都是以#开头

2、#include 是一条预处理指令,它的功能是导入一个辅助文件到当前文件中

// 先在当前工作目录下查找是否有filename.h这个文件,找到则直接导入,如果找不到,则再去系统指定路径查找并导入,如果还找不到则报错
// 一般用于导入自定义的头文件
#include "filename.h"

// 直接去系统指定路径查找并导入,如果找不到则报错
// 一般用于导入标准库的头文件
#include <filename.h>

3、C语言标准委员会会给C语言以函数的形式提供了一些基础的功能,这些函数会被封装在libc.so库文件中,并且按照功能不同在库中分成了不同的头文件,例如:stdio.hstdlib.hstring.h

4、stdio.h是负责对输入、输出数据进行功能实现,当程序需要使用输入、输出数据时,需要导入该头文件

5、在C语言,通过函数function来管理代码的最小单位,一个函数就是一段具有某一项功能的代码块, main函数是C语言程序的默认的执行入口,入口必须有且只有一个,无论它定在任何位置都最先执行

6、函数名后面的内容是函数的调用者传递给该函数的一些数据

7、函数名前面是一种数据类型,它表示该函数的执行结果是什么类型的数据,int表示main函数的执行结果是整数

8、C语言是使用大括号开分隔代码区域,被大括号包含的代码都属于该函数

9、printf/scanf 属于标准库的函数,用于输出、输入数据,还可以用于调试代码

10、转义字符:在输出数据时,键盘上有一些字符无法直接表示,通过转义字符来表示这些特殊字符

转义字符 作用
\n 换行
\b 退格键\b \b可以模拟backspace的效果
\\ 显示一个\
%% 显示一个%
\r 回到行首
\t 制表符,相当于tab键
\a 铃响

11、return的两项功能:

  • 直接结束整个函数的执行,当执行到return语句时,哪怕后面还有代码也不会执行
  • 还可以返回一个数据给函数的调用者,main函数的调用者是操作系统

12、main函数的返回值表示该程序结束时的状态:

返回值 含义
正数 表示程序执行出现异常 (别人的错)
0 表示程序正常结束
负数 表示程序执行出现错误 (自己的错)​

echo $?可以查看最后一个程序的main函数的执行结果

13、C语言以分号作为一行代码的结束标志,如果代码过长可以换行

14、C语言的语法标准:

标准 特性和改进
C89 是C语言的第一套语法标准,特点是简洁、可移植且易于理解,被广泛应用于各种计算机平台。
C99 对C89的升级、扩展、增强,引入了一些新特性,如变长数组、复合字面量、单行注释等
C11 语法标准,全新的升级,它引入了一些新特性,如匿名结构体、泛型选择表达式、多线程支持等
C17 C17主要是对C11标准的修订和更新,旨在进一步改进语言的特性和可用性并引入了一些新特性,如初始化宏、属性和线程局部存储等

通过 编译参数可以-std=gnu89\99\11\17更改编译的语法标准

三、编译器和gcc

1、什么是编译器

它是一个负责编译代码的程序,它负责把人能看得懂的代码(文本文件)翻译成计算机能看懂的二进制指令,它由预处理器、编译器、汇编器、链接器组成,统称编译器。
gcc是由GNU组织为了编译Linux内核而开发的一款C语言编译器,嵌入式开发使用的是 arm-linux-gcc(交叉编译器)

2、gcc编译器把C代码变成可执行程序的过程

  1. 把程序员所编写的代码进行预处理
gcc -E filename.c  # 把预处理的结果直接显示到屏幕上
gcc -E filename.c -o filename.i # 会生成以.i结尾的预处理文件
  1. 把预处理的结果翻译成汇编代码
gcc -S filename.i # 会生成以 .s 结尾的汇编文件
  1. 把汇编代码翻译成二进制指令
gcc -c filename.s # 会生成以 .o 结尾的目标文件
  1. 把若干个目标文件、库文件合并成最终的可执行文件(默认名字a.out)
# 合并链接若干个目标文件,生成a.out可执行文件
gcc aa.o bb.o cc.o ... 
# 合并链接若干个目标文件,生成名为filename可执行文件    
gcc aa.o bb.o cc.o ... -o filename 

注意gcc filename.c就包含了以上四个步骤。

3、gcc、arm-linux-gcc编译器常用的参数(了解):

参数 作用
-E 预处理
-S 生成汇编文件
-c 生成目标文件\只编译不链接
-o 设置编译结果的名字
-I 设置要导入的头文件的路径<> ""
-l 设置要链接的库名,例如:使用sqrtpow等数学函数时就需要链接数学库-lm
-L 设置要链接的库的路径,一般用于连接第三方库,或自定义的库
-D 在编译时定义宏
-g 编译时添加调试信息,这样编译出的程序可以用gdb调试。
-Wall 显示所有警告,编译器会以更严格的标准检查代码
-Werror 把警告当错误处理
-std 指定编译器要遵循的语法标准,c89c99c11c17

4、C语言的文件类型(了解):

文件类型 说明
.h 头文件,里面是一些对.c文件的说明
.c 源文件,里面是一些功能型代码
.i 预处理文件
.s 汇编文件
.o 目标文件
.gch 头文件的编译结果,用于检查自定义的头文件是否有语法错误,建议立即删除gcc -c xxx.h
.a 静态库文件,相当Windows系统下的.lib文件
.so 动态库文件,相当Windows系统下的.dll文件

四、C语言中基础的数据类型

1、C语言为什么要把数据分成不同的类型:

数据存储在计算机中需要耗费存储空间(内存、硬盘),在编程语言中把数据按照实际需要:范围、特点划分成不同的种类,解决什么问题就使用合适的类型,这样可以节约存储空间、提高运算速度,因此这是程序员的基本功,对于嵌入式开发这点尤为重要。

C语言中数据类型分为两大类:自建类(程序员自己设计的类型:结构、联合、类C++)、内建类(C语言自带的类型)

2、存储空间的单位:

单位 大小
Bit 比特\比特位\位,一个bit存储一个0\1,计算机中的存储数据的最小单位
Byte 1字节,存储8个比特,计算机中存储数据的基本单位
Kb 1024字节
Mb 1024Kb
Gb 1024Mb
Tb 1024Gb
Pb 1024Tb

3、基础数据类型

  • 整数 signed(默认/缺省)/unsigned 定点数
    • 短整型 short 2byte
    • 整型 int 4byte
    • 长整型 long 4/8byte
    • 长长整型 long long 8byte
    • 字符 char 1byte
  • 浮点数
    • 单精度浮点类型 float 4byte
    • 双精度浮点类型 double 8byte
    • 高精度浮点类型 long double 12/16byte
类型 字节大小 取值范围 取值范围
[signed] char 1byte \([-128,127]\) \([-2^7, 2^7 -1]\)
unsigned char 1byte \([0,255]\) $[0,2^8 -1] $
[signed] short [int] 2byte \([-32768,32767]\) \([-2^{15},2^{15}-1]\)
unsigned short [int] 2byte \([0,65535]\) \([0, 2^{16}-1]\)
[signed] int 4byte \([-2^{31}, 2^{31}-1]\)
unsigned int 4byte \([0, 2^{32}-1]\)
[signed] long [int] 4/8byte \([-2^{31}, 2^{31}-1]/ [-2^{63}, 2^{63}-1]\)
unsigned long [int] 4/8byte \([0, 2^{32}-1]/[0,2^{64}-1]\)
[signed] long long [int] 8byte \([-2^{63}, 2^{63}-1]\)
unsigned long long [int] 8byte \([0, 2^{64}-1]\)
float 4byte +-3.4*1038 +- 2128 - 2104
double 8byte +-1.79*10308 +- 21024 - 2971
  • 计算机存储数据

    • 二进制 整数与二进制转换

      • 整数转二进制 除2取余
      • 小数转二进制 乘2取整
    • 原码/反码/补码/移码 原码/反码/补码

      • 正数 原码 == 反码 == 补码

      • 负数 反码 = 其原码符号位不变,其它位按位取反

        ​ 补码 = 其原码符号位不变,其它位按拉取反+1

    • 整数

      • 计算机以补码形式存储整数
    • 浮点数

      • 计算机以IEEE754标准存储小数(float/double/long double)
  • 浮点数存储

    • IEEE754标准

      value = sign x exponent x fraction
                     (exponent)max = 127
                     (fraction)max = .111111111111111111111(23个1) 
                     1.111111111111111111111(23) x 2^127
                     = (1+1/2+1/4+1/8+  1/2^23) x 2^127
                     = (2-1/2^23) x 2^127
                     = 2^128 - 2^104
      
      3.25   按正常的转换为二进制
          0011.01   ==>   1.101 x 2的1次方
          xxxx.xxxx ==>   1.xxxxxxx  * 2^n次方
          存储:
          	符号位:   1bit     0正/1负
          	n次方:    指数偏移数    1 + 2^(exponet的二进制位-1)-1
          	         指数   以   移码形式存储
          	1.xxxxx:  小数位          小数点前固定1,只存储尾数
          [0]      [0000 0001 + 2^(8-1)-1]  [101]
                   [0000 0001 + 0111 1111]
                   [1000 0000]              [101  (20个0)]
          sign    exponent                   fraction
          0100 0000 0101 0000 0000 0000 0000 0000
          0100 0000 0101 0000 0000 0000 0000 0000
      
      • float == 4byte === 32bit
        • sign 1bit
        • exponent 8bit
        • fraction 23bit
      • double == 8byte ==== 64bit
        • sign 1bit
        • exponent 11bit
        • fraction 52bit
    • 定点数与浮点数

      • 定点 小数点固定,在末尾 整数
      • 浮点 小数点位置不固定,是浮动 小数

五、变量

什么是变量:

​ 在程序运行过程中可以变化的数据,它就是存储数据的容器,需要先定义才能使用

定义变量:

数据类型 <变量名>; 

1、变量所占用的内存字节数、存储数据的范围、使用的规则都由变量的数据类型决定。并且确定后无法改变

2、变量名也叫做标识符,变量的定义本质上就是操作系统把一个标识符与内存之间建立一种映射关系,操作系统不会对映射好的内存进行初始化清理,所以变量的默认值是不确定,所以我们要对一些有特殊用途的变量必须进行初始化,例如:求和、计数、平均值、累加。

3、定义好的变量,出了它所在的大括号就不能再使用了。

变量名的取名规则:

1、必须由数字、字母、下划线组成

2、必须不不能以数字开头

3、必须不能与C语言32关键字重名

​ 合法:printf scanf num_1 true bool

​ 非法:32_num *num

4、见名知意,了解类型、功能、作用范围、归属模块

变量的使用:

num = 10;	// 被赋值,存储数据、修改数据,当成容器使用
num * 3 / 4;	// 参与运算,此时变量名代表它存储的数据

变量的输出:

#include <stdio.h>
int printf(const char *format, ...);
功能:它C标准库函数,把若干个数据输出显示到屏幕终端上
format:"提示信息+变量的类型信息+转义字符"
...:	可变长参数,若干个变量名,变量名之间使用逗号隔开   
返回值:表示printf输出到终端上的字符个数 一般不会用

在C语言中使用占位符来表示变量的类型:
	%hhd %hd %d %ld %lld	有符号整型的占位符
	%hhu %hu %u %lu %llu	无符号整型的占位符
	%f  %lf  %LF			浮点型的占位符
	%c	字符型的占位符
	布尔类型用于逻辑运算的,不参与输入、输出,没有专属的占位符,如果想要强行输入输出、当成整数处理即可(0\1)

变量的初始化

int num;
num = 10;		//	赋值操作
int sum = 0;	//	初始化
sum = 0;		//	赋值

变量的输入:

int scanf(const char *format, ...);	
功能:在终端读取数据值并赋值给变量
format:"只放占位符
...:	变长参数,提供变量的地址 &变量名 获取该变量的地址
返回值:成功读取到的变量个数,一般不使用

六、常量(了解)

​ 常量就是程序运行过程中不能改变的量,C语言中常量有:字面值常量、宏常量、枚举常量。

字面值常量

100     int
100l	long
100ll	long long
100u	unsigned int
100lu	unsigned long
100llu	unsigned long long

3.14	double
3.14f	float
3.14F   long double

'b' 	char

注意:使用适当的后缀可以确保数据类型的匹配,其次提高代码可读性,如果没有正确的后缀,可能导致不正确的结果或者类型转换的问题

七、格式化输出

%nd		最少显示n个字符宽度,不够则补空格,右对齐
%-nd	最少显示n个字符宽度,不够则补空格,左对齐
%0nd	最少显示n个字符宽度,不够则补0,右对齐
%n.mf	最少显示n个字符宽度(包括小数点),小数点后显示m个字符宽度,不够则补空格,右对齐
%g		不显示小数点后多余的0

八、运算符

自变运算符

前自变:++num/--num
	变量的值立即加1或者减1
后自变:num++/num--
    变量的值也会加1或减1,但是在下一行代码才生效

注意:不要在一行代码中过多地使用自变运算符,因为不同的编译器对它们的解释规则不同,而且有时候会在合适的地方把后自变优化成前自变.只能给变量使用自变运算符

算术运算符

+ - * 
    /	除		进行除法运算,获取商
    %	求余		进行除法运算,获取余数
注意:
	整数/整数	计算结果没有小数部分	例如5/3 1  3/5 0
    /和%都是除法运算,除数不能为0,运行时会出现 浮点数例外 (核心已转储) 的报错信息,并且程序立即停止运行
    求余的运算对象不能出现浮点数 

关系运算符

>
>=
<
<=
==	相等
!=	不相等

注意1:它们的运算结果是逻辑值,C语言中的逻辑值用0(假)和1(真)来模拟的,并且计算出来的结果还可以继续参与数学运算
注意2:与数学规则不同 ,10 < num < 100 在C语言中会先计算左边小于号,得到逻辑值1\0,该结果继续与100进行比较,一定满足小于100,所以在C中是恒为真的。

注意3:使用 == 运算符时,容易漏写一个= ,变成赋值,所以一般把常量放在==的左边,如果漏写,编译器会报错

逻辑运算符

会把运算对象先转换成逻辑值再运算,值为0时逻辑为假,值非0时逻辑为真
A && B	逻辑与运算符 
	一假即假
A || B	逻辑或运算符 
	一真即真
!A	逻辑非运算符	
	求反

注意! 运算符的优先级要比&& ||

&& || 短路特性:

​ 当左边的运算结果已经可以确定整个逻辑运算表达式的结果时,右边的不再进行运行
​ 适当地使用该特性可以精简if单分支结构,能看懂即可,不要太过分,不要过分地影响代码可读性

三目运算符

[A] ? [B] : [C]
有三个运算对象,先把A转换成逻辑值,为真则执行B,为假则执行C,相当于精简版的if else结构

注意:与else if不同的是三目运算符必须有运算结果,所以里面不能出现流程控制语句:return 0 \ break \ continue

赋值运算符

=		注意:赋值的值就是整个赋值运算符的运算结果
a += b;		a = a+b;
a -= b;	a *= b; a/=b; a%=b;

sizeof字节运算符:

sizeof不是函数,是C语言32个关键字之一,能计算出数据在内存中的所需要的字节数,如果运算对象不是一个表达式时,可以不使用小括号 sizeof num

​ 并且sizeof括号内的表达式没有执行,只是猜测里面字节数最大为结果

sizeof(10 > 100 ? 3.13 : 40)	//	结果是8 并没有执行三目运算符

位运算符

& | ~ ^ >> << 都是针对数据的二进制补码进行运算,后序再详细讲解

九、类型转换

前提:只有相同类型的数据才能在一起进行运算,因为不同类型的数据,字节数不同、格式、运算规则不同,必须把不同类型的数据转换成同一类型才能运算。

自动类型转换(隐式类型转换):

​ 不同类型的数据组成的表达式,编译器会把先它们转换成相同的类型再计算,这叫作自动类型类型或隐式类型转换,它们的转换规则是以不丢失数据为前提:在C语言中,当不同的基本类型组成表达式时,会发生自动类型转换。这些转换规则通常是为了使表达式能够正确计算。以下是一些基本类型在表达式中自动类型转换的规则:

  1. 整数提升:在表达式中,所有的charshort类型的值都会被提升为int类型。

  2. 算术转换:算术转换发生在不同数值类型之间。具体来说,当一个操作数是浮点数(floatdouble)而另一个操作数是整数(intcharshort等)时,整数会被转换为浮点数。这种转换确保了浮点数和整数之间进行运算时的精度。

  3. 无符号整数和有符号整数的转换:当无符号整数和有符号整数进行运算时,有符号整数的类型会被提升为与其范围相同的无符号整数类型。

  4. 赋值运算符(=):在赋值运算符中,右边的表达式会被转换为左边变量的类型。
    ​ 这些规则是为了使C语言在处理不同类型的数据时能够正确地计算结果。然而,需要注意的是,过多的类型转换可能会导致代码难以理解和维护,因此在编写代码时需要小心处理这些转换。

强制类型转换(显式类型转换):

​ 在C语言中,强制类型转换(也称为显式类型转换)用于将一个表达式或变量的值强制转换为指定的类型。强制类型转换的语法如下:

(type) expression

其中,type 是目标类型,expression 是要转换的表达式或变量名。

请注意以下几点关于强制类型转换的注意事项

  1. 强制类型转换可以将表达式或变量的值转换为目标类型,无论其原始类型是什么。这可能导致精度损失或数据截断(字节多的向字节数少进行转换 )。
  2. 强制类型转换只改变值的解释方式,而不改变任何位的内容。例如,将一个浮点数转换为整数时,小数部分会被丢弃。
  3. 当进行强制类型转换时,应确保转换操作是合法和安全的。例如,在将浮点数转换为整数时,如果浮点数超出了整数类型的表示范围,则结果可能是不确定的。
  4. 强制类型转换可以应用于基本类型、指针类型和枚举类型。
  5. 需要谨慎使用强制类型转换,确保转换操作的安全性和合理性。不正确的类型转换可能会导致程序运行时错误和不确定的行为。应仔细考虑使用强制类型转换的场景,并尽量避免过度依赖强制类型转换来解决问题,而是考虑是否有更好的设计和类型匹配的方案。

十、if语句

​ 代码的默认执行流程是从上到下,逐步、逐条执行的,if语句可以根据判断条件选择让代码是否执行,改变了代码 的默认执行流程,所以这种语句也叫流程控制语句。

if(条件) { // 单分支  
	// 当条件为真时,执行此处代码,如果此处代码只有一行,大括号可以省略,但在商业项目中建议不要省略,因为这样会影响代码的可扩展性
}

if(条件) {	// 双分支
	// 当条件为真时,执行此处代码
} else {
	// 当条件为假时,执行此处代码
}

if(条件1) { // 多分支
	// 当条件1为真时,执行此处代码
} else if(条件2) { // 可以有多个else if
    // 当条件2为真时,执行此处代码
} ...{

} else {
	// 当条件1、条件2都为假时,执行此处代码
}

注意:如果ifelse的代码块只有一行代码,大括号可以省略。

尽管C语言允许在if语句中省略大括号,但这样做可能导致以下问题:

  1. 可读性差:如果省略大括号,只有紧随其后的一行代码会在if条件为真时执行。这样容易导致代码混淆,特别是当if语句中有多行代码时,很难一目了然地知道哪些代码受到if条件的控制。
  2. 容易出错:因为只有紧随其后的一行代码受到if条件的控制,如果在if语句后面添加其他代码而没有添加大括号,则新添加的代码无论if条件真还是假都会执行。这可能会导致逻辑错误和意外行为。
  3. 可维护性低:如果要在if条件为真时添加更多代码,需要手动添加大括号。在多个if语句嵌套时,容易忘记添加大括号或添加错误的大括号,导致逻辑混乱和错误。

因此,为了保证程序的可读性、可靠性和可维护性,推荐始终使用大括号明确指定if语句的代码块。即使if条件只控制一行代码,也应该用大括号将该行代码包裹起来,以防止潜在的错误和增加代码的可读性。

十一、switch开关语句

switch (数据) {
    case v1:	语句1;	break;
    case v2:	语句2;	break;
    case v3:	语句3;	break;
	...
    default:	语句4;
}
  1. switch小括号中的数据可以是变量、表达式、常量,但是结果一定是整型;case后面的数据也必须是整型常量,不能是变量
  2. case后面的值与数据相等时会打开开关,case后面的语句会执行,如果开关没有通过break关闭,那会一直往下执行
  3. default无论放在switch中哪个位置,当所有的case开关都不满足时,会最后执行dafault的内容
    4. switchif else比较只是代码较为简洁,switch能解决的问题,if else一样可以解决,所以在实际开发中程序员一般只使用if else就足够了
gnu编译器的专用语法:
switch (数据) {
    case n1 ... n2:	语句; break;
}
表示 [n1, n2]之间都满足打开开关

十二、for循环语句

​ 通过反复执行一段代码,达到解决问题的目的,被反复执行的代码称为循环语句

for是一种非常灵活的循环,一般使用一个变量来引导它的执行,该变量称为循环变量,早期使用index名字作为循环变量名,后面逐渐演变成i,如果有多个循环嵌套时,可以使用i j k l

for ([1]; [2]; [3]) {
    [4]; 
}

模块1:为for循环做一些准备工作:定义循环变量、给循环变量赋初值,但是在for中定义的变量,一旦出了循环就无法使用
模块2:判断循环条件是否成立,如果条件成立执行模块4,如果条件不成立则结束for循环,如果没有语句,则默认条件成立
模块4:被反复执行的代码,称为循环体语句
模块3:改变循环变量的值,让循环变量自加或自减,i++i--,防止出现死循环
注意:C89语法标准下不允许在模块1中定义变量,C99之后允许定义。

十三、while循环语句

while (循环条件) {
   	//	循环语句
}

for (;循环条件;) {
    //	与上面的while效果一致
}

执行流程:先检查循环条件,条件为真执行循环体,条件为假,直接结束while循环。
for循环的精简版本效果与while相似,当明确知道循环次数的问题适合使用for循环解决,当不明确知道循环次数的问题,适合使用while循环解决。

十四、do-while循环语句

do {
   //	循环语句 
} while (循环条件);	//	分号不能少

执行流程:先执行循环语句,再判断循环条件,条件为真执行循环体,条件为假,直接结束循环,无论条件真或假,循环语句必定最少执行一次
注意:适合先干活、再判断的场景:输入密码判断,在do-while的大括号中定义变量,在小括号中不能使用

十五、循环嵌套

循环语句中包含了循环语句,特点:外层循环执行1次,内层循环执行n次

for (int i = 0; i < 10; ++i) {
    for (int j = 0; j < 10; ++j) {
    }
    printf("\n");
}

十六、跳转语句

break跳转语句

用法1:在switch语句中可以关闭开关
用法2:在循环语句中,可以跳出所在的一层循环,是一种提前结束循环的一种方式,提高循环效率

continue跳转语句

只能在循环语句中,停止本次循环,直接进入下一次循环,根据条件改善循环的执行

return跳转语句

return可以提前结束函数,并返回一个结果给调用者

goto跳转语句

标签名:
	...
    goto 标签名;

goto可以在函数内任意跳转,但是由于它过于灵活自由,可能会破坏已经设计好的分支、循环语句,因此一般都禁止使用goto,新的编程语句中已经取消了该关键字

goto非常适合驱动程序中处理异常、释放资源,所以在硬件编程中适合使用

下一篇:数组、函数

posted @ 2024-05-05 14:34  sleeeeeping  阅读(105)  评论(0)    收藏  举报