C语言探索之旅 | 第二部分第六课:创建你自己的变量类型

2020-06-28  本文已影响0人  哪有岁月静好

上一课是 [C语言探索之旅 | 第二部分第五课:预处理(https://www.jianshu.com/p/5fad915696de)
应该是比较轻松的。

这一课将会非常令人激动也很有意思,不过有些难度。

众所周知,C语言是面向过程的编程语言,与 Java,C++,等面向对象的编程语言有所不同。

在面向对象的编程语言中,有 (class)的概念。

C语言是没有类这种“类型”的,但是 C语言就不能“模拟”面向对象编程了吗?

不,只要你设计得好,C语言也可以模拟面向对象编程。

这一课我们要学习的 struct(结构体)的知识就可以使你有能力用 C语言实现“面向对象”。

前面我们学习了指针,数组,字符串和预处理,掌握这些知识你的 C语言水平已经还不错啦。但是我们岂能就此止步,必须 Bigger than bigger~

除了使用 C语言已经定义的变量类型,我们还可以做一些更厉害的事情: 创建你自己的变量类型

我们可以将其称为“自定义的变量类型”,我们来看三种:struct,union 和 enum。

因为当你需要编写比较复杂的程序时,你会发现创建自定义的变量类型是很重要的。

幸好,这学起来其实也不是特别难。但是大家需要专心学习这一课,因为从下一课开始,我们会一直用到 struct 了。

2. 定义一个 struct

什么是 struct 呢?

struct 是 structure(表示“结构”)的缩写,所以 struct 的专业术语是“结构体”。

定义:struct 就是一系列变量的集合,但是这些变量可以是 不同 类型的。

这个定义是不是唤起了大家对我们的老朋友 数组 的怀念啊?数组里面的每个成员都必须是 同一个 类型的,相比之下 struct 更灵活。

一般来说,我们习惯把 struct 定义在 .h 头文件中,也就是和预处理命令以及函数原型那群“家伙”在一起。

下面就给出一个 struct 的例子:

struct 你的struct的名字
{
    char variable1;
    short variable2;
    int otherVariable;
    double numberDecimal;
};
复制代码

可以看到:struct 的定义以关键字 struct 开始,后面接你自定义的 struct 的名称(比如 Dog,Cat,Person,等)。

一般来说,在我的代码里,我的 struct 的命名也是遵照变量的命名规则,唯有一点不一样,就是 struct 的名称我会将首字母大写,例如:SchoolName。

但是我的普通变量一般都是首字母小写,例如:studentNumber。

这样做只是个人习惯,便于在代码里区分普通的变量和自定义的变量,之后会学到的 enum 和 union,我也是习惯将其名称的首字母大写。

在 struct 的名字之后,我们需要写上一对大括号,在这对大括号里面写入你的 struct 要包含的各种类型的变量。

通常说来,struct 的大括号内至少得定义两个变量吧。如果只有一个变量,那定义这个结构体也没什么意义。

注意:不要忘了,在大括号后面还要加上一个分号( ; ), 因为毕竟这个 struct 是一个变量,变量的定义最后都要加分号的。

如你所见,创建一个自定义的变量也不复杂么。其实结构体就是各种基本类型变量的集合,是一个“大杂烩”。

当然以后的课程中我们还会看到:结构体的嵌套定义(结构体里包含另一个结构体)。

结构体的例子

假设我们需要自定义一个结构体,它储存屏幕上的一个点的坐标。

下面就给出 2D(D 是英语 dimension 的首字母,表示维度)世界的坐标系的大致印象:

image

当我们在 2D 世界中做研究时,我们有两个轴:

只要数学还没有还给小学体育老师,应该都知道 x 和 y 轴的。

现在,你可以写出一个名叫 Coordinate(表示“坐标”)的 struct 的定义了吗?我看好你!

可以自己先写,然后对一下我们给出的参考答案:

struct Coordinate
{
    int x;  // 横坐标
    int y;  // 纵坐标
};
复制代码

很简单,不是吗?我们的 Coordinate 这个 struct 包含了两个变量:x 和 y,都是 int 类型,分别表示横坐标值和纵坐标值。

当然了,如果你愿意,也可以创建一个表示 3D(三维)空间的点的 struct,只需要在刚才的 Coordinate 这个结构体的基础上加上 z 轴。

结构体里面的数组

结构体里面也可以存放数组。

例如,可以构造一个名叫 Person(表示“人”)的结构体,如下所示:

struct Person
{
    char firstName[100];  // 名
    char lastName[100];   // 姓
    char address[1000];   // 地址

    int age;  // 年龄
    int boy;  // 性别,布尔值 : 1 = boy(表示“男孩”), 0 = girl(表示“女孩”)
};
复制代码

可以看到,这个结构体变量包含五个基本的变量。

这个结构体可以用于构建一个通讯录程序。当然,你完全可以在这个结构体里再添加其他变量,使其更完善。

只要内存够,一般来说在一个结构体里没有变量的数目限制。

3. 结构体的使用

现在,我们的结构体已经定义在 .h 头文件里了,那么我们就可以在 include(“包含”)此头文件的文件中使用这些结构体了。

以下展示如何创建一个类型为 Coordinate(我们之前已经定义了这个结构体,表示二维空间的坐标)的变量:

#include "coordinate.h"  // 假设包含结构体定义的头文件叫 coordinate.h

int main(int argc, char *argv[]) {
    struct Coordinate point;  // 创建一个 Coordinate 类型的变量,名字是 point

    return 0;
}
复制代码

如上,我们创建了一个 Coordinate 类型的变量,名字是 point(表示“点”)。

这个变量自动拥有两个子变量:x 和 y,都是 int 类型,分别表示此二维坐标的横坐标值和纵坐标值。

你也许要问:“创建结构体变量开头的那个关键字 struct 是必须的吗?”

是的,是必须的。

struct 关键字使电脑能够区分基础变量类型(例如 int)和自定义变量类型(例如 Coordinate)。

然而,每次加 struct 关键字也有点麻烦。所以聪(懒)明(惰)伶(成)俐(性)的 C语言开发者设计了 typedef 关键字。

当然了,人类的大多数发明都是为了“懒惰”的缘故,能提高效率谁不愿意啊?

typedef 关键字

typedef 是 C语言的一个关键字,是 type(表示“类型”)和 define(表示“定义”)的缩合,顾名思义是表示“类型定义”。

听到“类型定义”,好像很难理解。但其实 typedef 的作用并没有它的含义那么“高深莫测”。

重新回到刚才定义 Coordinate 这个结构体的 .h 头文件中。我们来加一条由 typedef 开头的命令,目的是为 Coordinate 结构体创建一个别名。

什么是别名(alias)呢?

就比如有一个人,真实姓名叫王小明,别名可以是小明,明明,等,但都代表那个人。

有点类似 C++ 语言的引用的机制。

所以对别名的操作就是对原先对象的操作。

比如小时候你上课不乖,老师点名的时候,点到你的小名或者你的真实名字,都是叫的你,你就得去罚站。

我们就在 Coordinate 结构体的定义之前加这句命令吧,一般习惯加在后面的,但是加在前面也可以:

typedef struct Coordinate Coordinate;

struct Coordinate
{
    int x;
    int y;
};
复制代码

可以看到,我们新加了一行命令:

typedef struct Coordinate Coordinate;
复制代码

为了更好地理解这句命令的作用,我们把它拆为三部分来看:

typedef
struct Coordinate
Coordinate

所以,上面这句命令的含义就是“从今以后, Coordinate 就相当于 struct Coordinate 了 ”。

这样做以后,我们就可以不用每次在创建一个新的 Coordinate 结构体的变量时都加上 struct 关键字了。

所以,我们的 .c 文件中就可以改写为:

int main(int argc, char *argv[]) {
    Coordinate point;  // 因为用了 typedef,电脑就清楚地知道此处的 Coordinate 其实就是 struct Coordinate

    return 0;
}
复制代码

当然,别名不一定要叫 Coordinate,也可以叫作 Coor,也许更不容易混淆。例如:

typedef struct Coordinate Coor;

struct Coordinate
{
    int x;
    int y;
};

Coor coor;  // 创建一个结构体变量
复制代码

建议大家在平时定义了 struct 类型后,也加一句 typdedef 命令,这样在代码里就不用每次新建一个此类型的变量时都要在开头写 struct 关键字了。

很多程序员都会这么做。 因为一个好的程序员是懂得如何“偷懒”的程序员,这和一个懒惰的程序员是有区别的。 我们要使代码"write less,do more"(用尽量少的代码做更多的事)。

当然,上面的代码块可以简写为:

typedef struct struct的名字 {
  // struct 的内容
} 别名;
复制代码

所以上面 Coordinate 的代码块可以简写为:

typedef struct Coordinate
{
    int x;
    int y;
} Coordinate;
复制代码

注意:之后我们的示例代码,有时会出现例如

Person player1;
复制代码

这样的形式,那就是假定我们之前已经用了 typedef 了:

typedef struct Person Person;
复制代码

这样就可以省略开头的 struct 关键字,不需要再写成:

struct Person player1;
复制代码

修改 struct 的成员变量

既然我们的 point 变量(是 Coordinate 类型的,希望大家还没晕)已经创建好了,那我们就可以修改它的成员的值了。

我们如何访问 point 的两个成员 x 和 y 呢?如下所示:

int main(int argc, char *argv[]) {
    Coordinate point;

    point.x = 10;
    point.y = 20;

    return 0;
}
复制代码

这样,我们就顺利地修改了 point 的两个成员的值,使其 x 坐标为 10,y 坐标为 20。

因此我们的点就位于坐标系的(10, 20)处了。

所以,为了能访问到结构体的某个成员,我们可以这样做:

结构体实例名称.成员名
复制代码

中间的点( . )表示“从属”关系。

如果有面向对象编程基础的朋友,就会觉得:这与“类和对象”也太像了吧。

是的,其实我们可以用 struct 来“模拟”类。

如果我们用之前创建的 Person 这个结构体来举例的话:

int main(int argc, char *argv[]) {
    Person user;  // user 表示“用户”

    printf("你姓什么 ? ");
    scanf("%s", user.lastName);

    printf("你名叫什么 ? ");
    scanf("%s", user.firstName);

    printf("原来你的名字是 %s%s,失敬失敬\n", user.lastName, user.firstName);

    return 0;
}
复制代码

运行输出:

你姓什么?王
你名叫什么?小明
原来你的名字是 王小明,失敬失敬
复制代码

我们把 user.lastName 传给 scanf,使得用户输入的值直接修改 user 的 lastName 成员;我们对 user.firstName 也是如此。

当然我们也可以再添加对 address,age,boy 的赋值。

当然了,你也许会说:“我不知道结构体的使用,我用两个单独的字符串变量 lastName 和 firstName 不是也可以做到和上述程序相同的事么?”

是的,但是用结构体的好处就是我们可以创建此结构体的变量,将很多相关联的数据封装在一起,成为一个整体,而不是零散地定义。

比如定义了 Person 这个结构体之后,凡是用 Person 来创建的变量,里面都自动包含了 lastName,firstName,address,age 和 boy 这五个变量,非常方便。

比如我们可以这样创建:

Person player1, player2;  // 之前已经用 typedef( typedef struct Person Person; )
复制代码

在 player1 和 player2 中都包含 lastName,firstName,address,age 和 boy 这五个变量。

我们也可以更“偷懒”一些:创建结构体数组。例如:

Person players[2];
复制代码

这样,我们就可以很方便的访问 players[1] 当中的变量了,例如:

players[1].lastName = "xiaoming";
复制代码

用结构体数组的好处是可以方便地使用循环,等等。

自测小练习

创建一个名叫 CoderHub(「程序员联盟」公众号)的结构体,在定义里放入你想创建的变量。然后创建此结构体的一个数组,用循环的方式给变量赋值,再用循环的方式打印出其中变量的信息。

结构体的初始化

之前的课程里,我们建议对于基本变量,数组和指针,最好在创建的时候对其初始化。结构体也不例外。

初始化有一个很大的好处,就是避免此变量里存放“任意数据”。

事实上,一个变量在创建时,如果没有初始化,那么它会取当时在内存那个位置所存的值,所以这个值的随机性是很大的。

我们来回忆一下,不同变量的初始化应该怎么做:

#define NULL 0
#define NULL 0L
#define NULL ((void *) 0)
复制代码

但是我们只要每次用 NULL 就好了,为了清楚表明这是指针变量,而不是一般变量。

那么对于我们的“朋友” 结构体 ,我们怎么初始化呢?

其实结构体的初始化也很简单,与数组的初始化很类似。我们可以像下面这样定义:

Coordinate point = {0, 0};
复制代码

这样,我们就依照顺序将 point.x 和 point.y 都初始化为 0 了。

对于像 Person 这样的结构体,里面的变量类型有 char 型数组和 int,那么我们可以将 char 型数组初始化为 "" (双引号中间为空)。

我们可以像这样初始化一个字符串,在 C语言探索之旅 | 第二部分第四课:字符串 那一课忘记提了。不过,我想现在提还不算晚吧。

所以我们就可以这样来初始化我们的 Person 结构体变量:

Person player = {"", "", "", 0, 0};
复制代码

然而,我们也可以这样来初始化一个结构体变量:创建一个函数,比如叫 initializeStruct,可以为每一个传递给它的结构体做初始化,这样就方便很多,特别是当结构体中的变量很多时。

之前指针那一章我们也已经学了,如果我们对函数传递普通变量,那么因为 C语言的函数参数传递方式是值传递,所以它会对传给它的函数参数做一份拷贝,这样函数里面修改的其实是那一份拷贝,真正的实参并没有被改变。

为了让实参实实在在被修改,我们需要用到指针,也就是传递此变量的地址。

对于结构体,也需要这样。因此,接下来我们就来学习如何使用结构体指针。开始难起来咯,准备好了吗?

4. 结构体指针

结构体指针的创建其实和普通的指针变量创建没什么区别。例如:

Coordinate *point = NULL;
复制代码

上面的代码就创建了一个叫做 point 的 Coordinate 结构体指针变量(Coordinate 是我们上面定义的表示坐标的一个结构体)。

我们再来提醒一次:

一般推荐写成:

Coordinate *point = NULL; // 星号挨着指针变量名字
复制代码

而不推荐写成:

Coordinate* point = NULL;  // 星号挨着结构体名,这种写法不好!
复制代码

在指针的创建中,我们推荐第一种写法。

因为用第二种写法,如果你在一行上创建好几个指针变量时,会容易忘记在第二个之后的变量前加 * 号。例如,容易写成这样:

Coordinate* point1 = NULL, point2 = NULL;   // 编译会出错
复制代码

但这样编译会出错,因为 point2 其实是 Coordinate 结构体变量,而不是 Coordinate 结构体指针变量!

所以我们建议这样写:

Coordinate *point1 = NULL, *point2 = NULL;
复制代码

在以前的课程中,对于基础类型的指针变量,我们也是这样建议:

int *number1 = NULL, *number2 = NULL;
复制代码

特别是 int 型的指针,还很不容易察觉到错误,如果写成:

int* number1 = NULL, number2 = NULL;
复制代码

编译器是不会报错的。因为 NULL 的值就是 0,可以赋给 number2 这个 int 型变量(注意:上面的 number2 不是 int 指针)。

回顾总是很好的(“伤心总是难免的...”)。

结构体作为函数参数

这里,我们主要来学习如何将一个结构体指针(为什么是传结构体指针而不是传结构体,可以看之前的解释)传给一个函数(作为参数),使得函数内部可以真正修改此结构体。

我们来看一个实例:

#include <stdio.h>

typedef struct Coordinate
{
    int x;  // 横坐标值
    int y;  // 纵坐标值
} Coordinate;

void initializeCoordinate(Coordinate *point);  // 函数原型

int main(int argc, char *argv[]) {
    Coordinate myPoint;

    initializeCoordinate(&myPoint);  // 函数的参数是 myPoint 变量的地址

    return 0;
}

// 用于初始化结构体变量
void initializeCoordinate(Coordinate *point) {
    // 结构体初始化的代码
}
复制代码

上面的 initializeCoordinate 函数体内,我们将放置初始化结构体的成员变量的代码。

我们按顺序来看一下这段代码:

void initializeCoordinate(Coordinate *point){
    *point.x = 0;
    *point.y = 0;
}
复制代码

point 前面的 * 号是必不可少的噢。因为,传进函数的参数是一个结构体指针,我们要取到此结构体,就需要用到“解引用”符号:星号( * )。

但是,认真的读者看出上面这个函数中的错误了吗?

我们的初衷是想要:先用 * 号解引用 point 这个结构体指针,取到结构体,然后再用 . 号取到其中的变量 x 和 y。但是如果按上面的写法,其实效果相当于如下:

*(point.x) = 0;
*(point.y) = 0;
复制代码

因为 . 号的优先级是高于 * 号的。

有兴趣可以看一下 C语言运算符的优先级,不过之前的课我们也说过了,记不清怎么办呢?加括号就解决啦。

上面的代码编译是通不过的,因为结构体指针 point 并没有成员叫 x 和 y,而且,对于结构体指针我们也不能用 . 号来取到什么值。

因此,我们需要修改一下。改为如下就可以了:

void initializeCoordinate(Coordinate *point) {
    (*point).x = 0;
    (*point).y = 0;
}
复制代码

这样就对了。用括号去掉了运算符优先级的影响。

但是,之前也说过:程序员是懂得偷懒的一群人。

如果每次要取结构体的成员变量都要这么麻烦,先用 * 号,还要加括号,再用 . 号。想想都要让 Denis Ritchie(C语言的作者)老爷子醉了。他是决不允许这种事发生的,因此,他就定义了一个新的符号: -> (一个箭头。是的,就是这么“霸气侧漏”)。

用法如下:

point->x = 0;
复制代码

就相当于:

(*point).x = 0;
复制代码

是不是简便了很多?

记住:这个符号,只能用在指针上面。

因此,我们的函数可以改写为:

void initializeCoordinate(Coordinate *point) {
    point->x = 0;
    point->y = 0;
}
复制代码

我们在 main 函数里也可以这样写:

int main(int argc, char *argv[])
{
    Coordinate myPoint;
    Coordinate *myPointPointer = &myPoint;

    myPoint.x = 10;  // 用结构体的方式,修改 myPoint 中的 x 值
    myPointPointer->y = 15;  // 用结构体指针的方式,修改 myPoint 中的 y 值

    return 0;
}
复制代码

结构体是 C语言中一个非常好用且很重要的概念,希望大家好好掌握!

当然,还有不少知识细节,就要大家自己去看 C语言的经典教材了,例如《C程序设计语言》(不是谭浩强那本《C语言程序设计》!而是 C语言作者写的经典之作),《C和指针》,《C专家编程》,《C语言深度解剖》,《C陷阱和缺陷》,等等。

5. union

union 是“联合”的意思,是 C语言的关键字,也有的书上翻译为“共用体”。

我们可以来写一个 union 的例子。

union CoderHub
{
    char character;
    int memberNumber;
    double rate;
};
复制代码

乍看之下,和 struct 没什么区别么。但是真的没有区别吗?

假如我们用 C语言的 sizeof 关键字(size 表示“尺寸,大小”,of 表示“...的”)来测试此 union 的大小(大小指的是在内存中所占的字节(byte)数,一个字节相当于 8 个 bit(二进制位)):

#include <stdio.h>

typedef union CoderHub
{
    char character;  // 大小是 1 个字节
    int memberNumber;  // 大小是 4 个字节
    double rate;  // 大小是 8 个字节
} CoderHub;

int main(int argc, char *argv[]){
    CoderHub coderHub;

    printf("此 union 的大小是 %lu 个字节\n", sizeof(coderHub));

    return 0;
}
复制代码

运行程序,输出:

此 union 的大小是 8 个字节
复制代码

假如我们对结构体也做一次测试,对比一下:

#include <stdio.h>

typedef struct CoderHub
{
    char character;  // 大小是 1 个字节
    int memberNumber;  // 大小是 4 个字节
    double rate;  // 大小是 8 个字节
} CoderHub;

int main(int argc, char *argv[]){

    CoderHub coderHub;

    printf("此 struct 的大小是 %lu 个字节\n", sizeof(coderHub));

    return 0;
}
复制代码

运行程序,输出:

此 struct 的大小是 16 个字节
复制代码

为什么我们自定义的 union 的大小是 8 个字节,而 struct 是 16 个字节呢?

这就涉及到 union(共用体)和 struct(结构体)的区别了。

struct 的大小是其中所有变量大小的总和。

但是你会说:“不对啊, 1 + 4 + 8 = 13,为什么 sizeof(coderHub) 的值为 16 呢?”

好问题!这个有点复杂,涉及到内存对齐的问题,我们以后再说。如果你一定要知道,那是因为内存对齐使得第一个 char 变量对齐了第二个 int 变量的空间,也变成了 4,如此一来:4 + 4 + 8 = 16。

有兴趣的读者可以去参考《 C语言深度解剖 》的解释。

在嵌入式编程等内存有限的环境下,需要考虑内存对齐,以节省空间。

union 的大小等于其中最大( sizeof() 得到的值最大)的那个变量的大小。所以我们就知道了,其实 union 的储存是这样的:其中的每个变量在内存中的起始地址是一样的,所以 union 同一时刻只能存放其中一个变量,union 的大小等于其中最大的那个变量,以保证可以容纳任意一个成员。

union 适合用在很多相同类型的变量集,但是某一时刻只需用到其中一个的情况,比较节省空间。

6. enum

看完了 struct(结构体)和 union(联合),我们最后来学习很常用的一个自定义变量类型:enum。

enum 是 enumeration(表示“枚举”)的缩写,也是一个 C语言关键字。

枚举是一个比较特别的自定义变量类型。当初我学 C语言时,一开始还真有点不理解。但用得好,却非常实用。

我们之前学了:结构体里面包含了多个可以是不同类型的成员变量(一说“成员”就有点面向对象的感觉 :P)。

但是 enum(枚举)里面是一系列可选择的值。也就是说每次只能取其中一个值,听着和 union 有点类似啊。但是 enum 和 union 还是有区别的。

我们来举一个例子就知道区别了:

typedef enum Shape Shape;

enum Shape   // shape 表示“身材、体型”
{
    THIN,   // thin 表示“瘦”
    MEDIUM,   // medium 表示“中等”
    FAT   // fat 表示“胖”
};
复制代码

所以,我们定义了一个名叫 Shape 的 enum 变量。其中有三个值,分别是 THIN,MEDIUM 和 FAT(身材有瘦,中等和胖之分)。不一定要大写,只是习惯。

那我们怎么来创建 enum 变量呢?如下:

Shape shape = MEDIUM;
复制代码

shape 这个变量,我们在程序里也可以再将其修改为 THIN 或者 FAT。

将数值赋给 enum 的成员

大家看到 enum 和 union 以及 struct 的区别了吗?是的,enum 的定义里,每个成员没有变量类型(int,char,double,之类)!

很奇怪吧。想起来为什么 enum 的成员习惯用大写了吗?

对,就是因为 enum 的每个成员都不是变量,而是常量!但是 enum 的机制和常量定义以及 #define 还是有些区别:

像上面的代码:

typedef enum Shape {
    THIN,
    MEDIUM,
    FAT
} Shape;
复制代码

编译器会自动为其中的每一个成员绑定一个常量值,我们写程序测试一下:

#include <stdio.h>

typedef enum Shape Shape;

enum Shape
{
    THIN,
    MEDIUM,
    FAT
};

int main(int argc, char *argv[]) {
    Shape shape = THIN;
    printf("THIN = %d\n", shape);

    shape = MEDIUM;
    printf("MEDIUM = %d\n", shape);

    shape = FAT;
    printf("FAT = %d\n", shape);

    return 0;
}
复制代码

运行程序,输出:

THIN = 0
MEDIUM = 1
FAT = 2
复制代码

看到了吗?编译器自动给这三个成员赋值 0,1 和 2。如果没有指定 enum 成员的值,那么它们的值是从 0 开始,依次加 1。

我们也可以自己来定义 enum 成员的值,不一定要每次让编译器给我们自动分配。

我们可以这样写:

typedef enum Shape {
    THIN = 40,
    MEDIUM = 60,
    FAT = 90
} Shape;
复制代码

这样,我们就自己给每个成员定义了值。

我们也可以让编译器为我们自动分配几个值,再自己定义几个值,例如:

typedef enum Shape {
    THIN,
    MEDIUM,
    FAT = 90
} Shape;
复制代码

上面,我们没有为 THIN 和 MEDIUM 赋值,那么编译器会将他们赋值为 0 和 1。

而 FAT,因为我们已经指定了其值为 90,所以 FAT 就等于 90。

enum 和 #define 的区别

是不是觉得 enum 和用 #define 来定义的常量是有些类似呢?

其实,还是有些不同的:

其实做为一个学习者,有一个学习的氛围跟一个交流圈子特别重要这里我推荐一个C/C++基础交流583650410,不管你是小白还是转行人士欢迎入驻,大家一起交流成长。
上一篇下一篇

猜你喜欢

热点阅读