上一篇:从0开始学c语言-33-动态内存管理_阿秋的阿秋不是阿秋的博客-CSDN博客
我发现,理论的学习文章没几个人看,如果是这种实践类型的文章,看的人很多,偏偏我之前都是扔完代码就走了,也没认真写这种类型文章的思考过程,所以这篇打算好好写下。
先放上网盘链接,(我设置的永久有效)
链接:https://pan.baidu.com/s/1Renm5M2NRlXkbb-IX3r5QA?pwd=1f9i
提取码:1f9i
代码中还有个优化没做,是输入年龄那里判断是否整型并重新输入的问题
我在新的文章里写了解决方法,链接给上
学习小发现 - 03 - 如何判断输入的是不是整型数据并重新输入,纠错(异常)scanf与strcmp,_阿秋的阿秋不是阿秋的博客-CSDN博客
目录
通讯录版本介绍
静态通讯录
*大概思路
*文件
1·功能入口书写 test.c
2·结构体思路 contact.h
联系人结构体
通讯录结构体
3·完善主函数内容 test.c
创建与初始化通讯录
确定分支语句函数
4·初始化通讯录函数 contact.h - contact.c
5·添加信息 contact.h - contact.c
演示
6·打印信息 contact.h - contact.c
演示
7·删除信息 contact.h - contact.c
找人 (名字查找)
打印信息
确认删除
演示
8·查找信息 contact.h - contact.c
演示
9·修改信息 contact.h - contact.c
演示
动态通讯录
1·创建和初始化通讯录
2·检查容量 contact.h - contact.c
3·归还空间 contact.h - contact.c
文件通讯录
1·读取文件
2·保存信息
排序功能
1·菜单
2·enum枚举类型
3·switch分支
4·排序
注:跟着写,只看文章会忘记前面写了什么。
功能:
1·存放信息 2·增加信息
3·删除信息 4·修改信息
5·查找信息 6·排序信息
信息:名字 + 年龄 + 性别 + 电话 + 地址
静态版本:存放1000个人的信息
动态版本:增设容量,根据有效信息拓容
文件版本:程序开始读取文件,程序结束把信息写到文件中。
注:功能实现的过程都放在了静态通讯录版本中进行介绍。
功能:
1·存放信息 2·增加信息
3·删除信息 4·修改信息
5·查找信息 6·排序信息
信息:名字 + 年龄 + 性别 + 电话 + 地址
首先,我看到这个功能就想到了switch分支语句。
其次,又想到了enum枚举类型,把分支的数字和功能名字对应起来。
最后,因为是通讯录,所以通讯录中有以联系人为单位的集合,那么就应该有对应的结构体来保存联系人的信息。
这是最初的大概思路。
头文件contact.h是用来声明函数和定义一些变量的。
源文件contact.c是用来写函数实现功能的具体过程的。
test.c可以理解为功能的入口。
想要进入不同的功能区,要先书写相应的菜单。
void menu()
{
printf(" ***** 0.EXIT 1.ADD 2.DEL *****\n");
printf(" ***** 3.SEARCH 4.MODIFY *****\n");
printf(" ***** 5.SORT 6.PRINT *****\n");
}
其次,把这些 菜单名字与数字 用enum类型对应起来。
enum Option
{
EXIT, //0
ADD, //1
DEL, //2
SEARCH,//3
MODIFY,//4
SORT, //5
PRINT //6
};
在主函数中书写switch分支语句实现不同功能的入口。
int main()
{
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case EXIT: //EXIT就代表数字0
break;
case ADD: //ADD就代表数字1
break;
case DEL: //后面的以此类推,和enum中的数字对应起来
break;
case SEARCH:
break;
case MODIFY:
break;
case SORT:
break;
case PRINT:
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
因为通讯录和联系人都会跨文件使用,所以写在头文件中更好一些。
信息:名字 + 年龄 + 性别 + 电话 + 地址
判断每个信息对应的类型,以及是否需要数组来储存就可以写出来了。
#define MAX_NAME 20
#define MAX_SEX 10
#define MAX_TEL 12
#define MAX_ADDR 30
//定义一个人的结构体信息
struct PeoInfo
{
char name[MAX_NAME];
char sex[MAX_SEX];
int age;
char tel[MAX_TEL];
char addr[MAX_ADDR];
};
后又因为之后会多次使用这个结构体类型,为了书写方便,把struct PeoInfo重命名为PeoInfo。
//定义一个人的结构体信息
typedef struct PeoInfo
{
char name[MAX_NAME];
char sex[MAX_SEX];
int age;
char tel[MAX_TEL];
char addr[MAX_ADDR];
}PeoInfo; //把struct PeoInfo定义为PeoInfo
首先通讯录结构体中肯定要有联系人结构体,而我们要实现的是存放1000个人的信息,所以把这个联系人结构体设为数组。
其次,为了方便添加信息,删除信息之类的,我们需要一个变量来记录目前有几个有效信息。
同样,我们也进行了重命名,把struct Contact定义为Contact。
#define MAX 1000
//定义通讯录——静态
typedef struct Contact
{
PeoInfo data[MAX]; //存放过来的个人信息集合(数组
int sz; //记录已经有几个信息,相当于data数组的下标
//比如已经有一个人的信息,那么下一个要增加信息对应下标就是1
}Contact;
之前我们在test.c中写了switch分支语句来实现不同功能的入口,但我们还没有创建一个通讯录结构体变量以及初始化通讯录。
int main()
{
int input = 0;
do {
//创建通讯录
Contact con;
//初始化通讯录
IniContact(&con);
//这里要取地址传参,因为我们要
//通过这个函数改变通讯录
//单纯传数值没办法改编数据
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
//后面先省略
我们知道不同分支的情况对应不同的功能,这个功能我们通过函数实现。所以这里我们先要确定好函数的命名与参数。参数都是通讯录的地址,因为无论是改变通讯录信息还是不改变,传地址都效率高且节省空间。命名方面我可能不是很规范,第二个单词没大写,太懒了哈哈哈。
int main()
{
//创建通讯录
Contact con;
//初始化通讯录
IniContact(&con);
int input = 0;
do {
menu();
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case EXIT:
Destroycontact(&con); //销毁信息,动态版本用
break;
case ADD:
Addcontact(&con); //添加信息
break;
case DEL:
Delcontact(&con); //删除信息
break;
case SEARCH:
Searcontact(&con); //删除信息
break;
case MODIFY:
Modifcontact(&con); //修改信息
break;
case SORT:
Sortcontact(&con); //分类信息
break;
case PRINT:
Princontact(&con); //打印信息
//虽然打印不需要改变通讯录内容
//但是传地址节省空间
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
我们在头文件中进行这个函数的声明,在contact.c中书写这个函数的定义。
声明:
//初始化通讯录
void IniContact(Contact* pc);
定义:
通讯录结构体中就两个变量,一个是记录有效信息的 int sz 变量,这个初始化就很简单,等于0就好了。
另一个是 联系人信息结构体 数组 ,数组初始化可以使用循环,或者用之前在这里
从0开始学c语言-31-关于字符串的各种函数+内存函数+字符串旋转判断_阿秋的阿秋不是阿秋的博客-CSDN博客
学过的memset函数,以字节为单位设置内存。
//初始化通讯录_静态
void IniContact(Contact* pc)
{
pc->sz = 0;
//memset 内存设置
memset(pc->data, 0, sizeof(pc->data));
}
我们在头文件中进行这个函数的声明,在contact.c中书写这个函数的定义。
声明:
//添加信息
void Addcontact(Contact* pc);
定义:
这里要明确的就是 sz 的值就是下一个添加信息的下标,比如,现在通讯录中有一个联系人的信息,这个信息对应下标是0,此时的sz是1,把sz当做 pc->data[pc->sz] 的下标,正好是下一个添加信息的下标。
void Addcontact(Contact* pc)
{
//因为都是数组,便没加&符号
printf("请输入名字:>");
scanf("%s", pc->data[pc->sz].name);
printf("请输入性别:>");
scanf("%s", pc->data[pc->sz].sex);
printf("请输入年龄:>");
scanf("%d", &(pc->data[pc->sz].age));
//这里只有age是int类型,所以需要取地址
printf("请输入电话:>");
scanf("%s", pc->data[pc->sz].tel);
printf("请输入地址:>");
scanf("%s", pc->data[pc->sz].addr);
pc->sz++;
printf("增加成功\n");
}
为了确认是否有我们想要的信息,现在进行打印信息的设计。
我们在头文件中进行这个函数的声明,在contact.c中书写这个函数的定义。
声明:
//打印信息
void Princontact(const Contact* pc);
定义:
因为我们要实现这样的打印效果,便需要打印标题,以及对应的数组信息。
首先我考虑到,如果通讯录为空,那便不需要打印。
其次,标题之间的大空格该如何设定。
最后,要打印的信息该如何访问。
这些都在代码里。
void Princontact(const Contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空,没有需要打印的。\n");
return;
}
//打印标题
printf("%-15s\t%-5s\t%-s\t%-10s\t%-20s\n", "名字", "性别", "年龄", "电话", "地址");
//加上负号是左对齐
//打印信息
int i = 0;
for (i = 0; i < pc->sz; i++)
{
//这个打印格式和前面要对应起来,整齐
printf("%-15s\t%-5s\t%-d\t%-10s\t%-20s\n",
pc->data[i].name,
pc->data[i].sex,
pc->data[i].age,
pc->data[i].tel,
pc->data[i].addr
);
}
}
然后进行删除信息的书写。
我们在头文件中进行这个函数的声明,在contact.c中书写这个函数的定义。
声明:
//删除信息
void Delcontact(Contact* pc);
定义:
整体逻辑介绍:
1·查人
2·输入下标,检查输入下标是否正确(因有重名)
3·确认删除
首先要知道的是,你删除一个人的信息,大多是通过名字来寻找的,所以这里主要设置名字搜索来删除信息。
void Delcontact(Contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空,不需要删除\n");
return;
}
char name[MAX_NAME] = { 0 };
//1·找人并打印 找到\找不到
printf("请输入删除人的名字:>");
scanf("%s", name);
int ret = NameFind(pc, name); //通过名字寻找的函数
//找到返回不为0的数,找不到返回-1
if (ret == -1)
{
printf("找不到\n");
return;
}
return;
}
所以我们现在要书写 NameFind(pc, name); 这个函数的实现过程。因为我们设计的函数是有返回值的,所以函数的返回类型设为了int。
我们需要设一个循环,来比对 有无 和我们输入的 名字相同的联系人。(这里我考虑到了重名问题,所以又设计了打印信息的函数。)
//按照名字查找
//static表示该函数不能跨文件使用
static int NameFind(const Contact* pc, char* name)
{
int i = 0;
int flag = 0;
for (i = 0; i sz; i++)
{
if (strcmp(pc->data[i].name, name) == 0)
{
Prin_del(pc, i);
flag++;
}
}
if (flag)
return flag;
return -1;
}
这个很简单,所以不再说。
//打印查找到的
void Prin_del(const Contact* pc, int i)
{
printf("************************************************\n");
printf("%-15s\t%-5s\t%-s\t%-10s\t%-20s\n", "名字", "性别", "年龄", "电话", "地址");
//加上负号是左对齐
//打印信息
printf("%-15s\t%-5s\t%-d\t%-10s\t%-20s\n",
pc->data[i].name,
pc->data[i].sex,
pc->data[i].age,
pc->data[i].tel,
pc->data[i].addr
);
printf("名字是%s的人标号为%d\n", pc->data[i].name, i);
printf("************************************************\n");
}
void Delcontact(Contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空,不需要删除\n");
return;
}
char name[MAX_NAME] = { 0 };
//1·找人并打印 找到\找不到
printf("请输入删除人的名字:>");
scanf("%s", name);
int ret = NameFind(pc, name); //通过名字寻找的函数
//找到返回不为0的数,找不到返回-1
if (ret == -1)
{
printf("找不到\n");
return;
}
return;
}
我们之前写了这样的代码,假设现在找到了,那么现在要进行确认是否删除的代码书写。
因为之前设计的找人函数并没有设置为带回来数组对应的下标,所以在打印信息中设计了打印下标。就像这样张三的下标是1。
所以我们需要一个变量来储存下标,并加入 if 语句确定输入下标是否正确,来决定是否进入删除环节。
printf("请输入删除人的标号:>");
int input = 0;
scanf("%d", &input);
if (strcmp(pc->data[input].name, name) == 0)
{
printf("请确认删除(1/0):>");
int a = 0;
scanf("%d", &a);
if (a == 1)
{
}
else if (a == 0)
printf("未删除\n");
else
printf("输入错误,未删除\n");
}
else
{
printf("输入标号不对\n");
}
如果输入确认删除,那么就要进行 if (a == 1) 语句中的书写。
首先删除的话,我们不能简单的把有效信息 sz 变量 减一,这样只会删除最后一个联系人。
其次,我们要明确这是一个联系人数组,那就可以类比,如果你想删除数组中的一个数据,并且保留其他数据,数组的有效信息-1,该如何做呢?
如图,我们要删除数组中的2,就需要3向前挪,4向前挪,再删除最后一个4的空间。
而这其中,最最关键的步骤就是确定好向前移动数据的范围,如上图,数据的有效信息 sz是5,假设 2 对应下标是 i ,那么向前挪动应该这么写,pc->data[i] = pc->data[i + 1];
所以我们的下标 i,走到sz -2,也就是3对应的下标,就可以实现3挪到2位置,4挪到3位置。
if (a == 1)
{
//删除
int i = 0;
//要确定好向前移动的数据范围
for (i = input; i < pc->sz - 1; i++)
{
pc->data[i] = pc->data[i + 1];
}
printf("删除成功!\n");
pc->sz--;
}
现在打印确认是否删除成功。
已经删除了。
我们在头文件中进行这个函数的声明,在contact.c中书写这个函数的定义。
声明:
//查找信息
void Searcontact(Contact* pc);
定义:
实际上,在书写删除信息函数的时候就已经写了查找和 打印查找到的信息 的函数了。这里直接用就行。
//查找
void Searcontact(Contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空,查不到\n");
return;
}
printf("请输入查找人姓名:>");
char name[MAX_NAME] = { 0 };
scanf("%s",name);
int ret = NameFind(pc, name);
if (ret == -1)
{
printf("找不到\n");
return;
}
printf("找到了\n");
return;
}
OK,找到了。
声明:
//修改信息
void Modifcontact(Contact* pc);
定义:
其实上,这个修改信息的逻辑和删除信息的逻辑差不多。
都是查找,是否查到,查到输入下标确认修改,下标不正确退出函数,下标正确确认下标,确认下标后进行输入信息修改覆盖的操作。
//修改信息
void Modifcontact(Contact* pc)
{
if (pc->sz == 0)
{
printf("通讯录为空,没有需要修改的。\n");
return;
}
printf("请输入修改人姓名:>");
char name[MAX_NAME] = { 0 };
scanf("%s", name);
int ret = NameFind(pc, name);
if (ret == -1)
{
printf("找不到\n");
return;
}
printf("请输入修改人下标:>");
int input = 0;
scanf("%d", &input);
if (strcmp(pc->data[input].name, name) == 0)
{
printf("请确定下标(1/0):>");
int a = 0;
scanf("%d", &a);
if (a == 1)
{
printf("***进行修改***\n");
printf("请输入名字:>");
scanf("%s", pc->data[input].name);
printf("请输入性别:>");
scanf("%s", pc->data[input].sex);
printf("请输入年龄:>");
scanf("%d", &(pc->data[input].age));
printf("请输入电话:>");
scanf("%s", pc->data[input].tel);
printf("请输入地址:>");
scanf("%s", pc->data[input].addr);
printf("修改成功\n");
}
else if(a == 0)
printf("未修改\n");
else
printf("输入错误,未修改\n");
}
else
{
printf("输入下标不正确,修改失败\n");
}
return;
}
现在我们要修改名字为1的那个。
修改成功。
包括输入不正确的检查也成功了。
静态版本到此为止。
下面介绍动态版本。
动态通讯录是需要有动态内存分配基础的。
从0开始学c语言-33-动态内存管理_阿秋的阿秋不是阿秋的博客-CSDN博客
首先回顾我们的动态通讯录的功能,(静态通讯录已经实现了大多数功能)。
动态版本:增设容量,根据有效信息拓容
我们具体化这句话,比如我们设定一开始的容量是5个人,然后在添加第六个人的时候发现满了,那就需要拓容,拓容也要设计好是一次拓容几个。后面的整体思路是这样的。
之前那个通讯录的结构体需要再加一个变量,叫做capacity(容量)。以及需要把数组换为指针,指向堆区上开辟的动态内存分配空间。
//动态定义通讯录
typedef struct Contact
{
PeoInfo* data; //指向动态开辟的空间,存放个人信息
int sz; //记录当前通讯录中有效信息个数
int capacity; //记录当前通讯录最大容量
}Contact;
开辟动态内存空间,我们使用malloc函数,并用define定义最初的容量和每次拓容的大小。
#define START 2
#define INC 2
//这俩在头文件中
//动态初始化通讯录 -contact.c
void IniContact(Contact* pc)
{
pc->sz = 0;
PeoInfo*ptr = malloc(START * (sizeof(PeoInfo)));
if (ptr == NULL)
{
perror("Addcontact");
printf("初始化失败\n");
return;
}
pc->data = ptr; //指针指向动态开辟的空间
pc->capacity = START;
}
声明(头文件):
//增容
void Checkcapacity(Contact* pc);
定义:
首先要明确,我们什么时候需要拓容。
//动态定义通讯录
typedef struct Contact
{
PeoInfo* data; //指向动态开辟的空间,存放个人信息
int sz; //记录当前通讯录中有效信息个数
int capacity; //记录当前通讯录最大容量
}Contact;
当然是 sz有效信息 == 最大容量capacity 的时候了。
//检查容量
void Checkcapacity(Contact* pc)
{
if ((pc->sz) == (pc->capacity))
{
//扩容
PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (INC + pc->capacity) * sizeof(PeoInfo));
if (ptr != NULL)
{
printf("增容成功\n");
pc->data = ptr; //指针指向动态开辟的空间
pc->capacity += INC; //变量capacity也需要跟着变大
}
else
{
printf("拓容失败\n");
perror("Addcontact");
return;
}
}
}
很有可能这句不太懂。
//扩容
PeoInfo* ptr = (PeoInfo*)realloc(pc->data, (INC + pc->capacity) * sizeof(PeoInfo));
其实就是对这个realloc函数的返回值进行了强制类型转换,转换为了联系人结构体指针类型。
第一个参数 pc->data 是是要调整的内存地址,
第二个参数(INC + pc->capacity) * sizeof(PeoInfo)是调整后的空间大小。因为是调整后的空间大小,所以千万别忘了加上原有的capacity大小。
注:这个函数使用在ADD(添加信息)函数中,记得自己要在这个add函数里调用。
动态开辟的空间要尤其注意释放空间,所以在EXIT这个功能中要进行新函数的书写。
声明(头文件):
//退出,释放空间
void Destroycontact(Contact* pc);
定义:
除了释放动态空间外,也要把有效信息 sz和 容量 capacity进行归0。
这个函数需要在EXIT功能入口处调用。
//退出销毁信息
void Destroycontact(Contact* pc)
{
free(pc->data);
pc->data = NULL;
pc->capacity = 0;
pc->sz = 0;
}
这个需要有文件基础,不过我还没写,后续补上链接。
回顾功能:
文件版本:程序开始读取文件,程序结束把信息写到文件中。
注意每次读取是以联系人结构体大小为单位进行读取的就行。
看注释。这个函数要在初始化通讯录的函数中调用,自己记得写。
//读取文件
void Readcontact(Contact* pc)
{
//1·打开文件
FILE* pf = fopen("contact.dat", "r");
//以只读形式打开文件contact.dat
if (pf == NULL)
{
perror("Readcontact");
return;
}
//2·读取文件
PeoInfo tmp = { 0 };//创一个存信息的中间量
//从pf中读取1个sizeof(PeoInfo)大小的数据到&tmp中
while (fread(&tmp, sizeof(PeoInfo), 1, pf))
{
Checkcapacity(pc); //看是否需要增容
pc->data[pc->sz] = tmp;
//sz是有效信息
//sz=0,tmp从文件读取信息放进去后sz=1
//而sz=1正好是下一个读取tmp要放进去的下标
pc->sz++; //这个千万不能丢
}
//3·关闭文件
fclose(pf);
pf = NULL;
}
其实上面两个版本都有个缺陷,就是每次运行程序都是重新添加信息,上一次的信息不会被保存起来,所以我们需要书写这样一个函数,来保存每次输入的信息到初始化通讯录需要读取的文件中。
这个函数需要在EXIT功能入口中调用。在Destroycontact函数前把数据保存好。
void Savecontact(Contact* pc)
{
//以输出方式打开文件
FILE* pf = fopen("contact.dat", "w"); //注意这是'w'
if (pf == NULL)
{
perror("Savecontact");
return;
}
//写文件
int i = 0;
for (i = 0; i < pc->sz; i++)
{
fwrite(pc->data + i, sizeof(PeoInfo), 1, pf);
//从pc->data中传1个sizeof(PeoInfo)到pf中
}
//关闭文件
fclose(pf);
pf = NULL;
}
基本功能已经完善了,但是我还没写排序功能的实现,因为比较长,所以我放最后说。
要排序,就需要明确我们按照什么来排序。
这个的思路和我们一开始的菜单设计思路差不多,通过menu函数、enum枚举类型、switch分支语句来实现不同选项的分类。
这个菜单的排序设置,是根据联系人结构体的成员变量来设计的。
void menus()
{
printf(" ***** 0.EXIT 1.NAME *****\n");
printf(" ***** 2.SEX 3.AGE *****\n");
printf(" ***** 4.TEL 5.ADDR *****\n");
}
注意要和上面的菜单对应起来。
enum Option2
{
EXIT, //0
name, //1
sex, //2
age, //3
tel, //4
addr, //5
};
注意我在每个分支设计的函数参数,这是为了方便后续的排序有根据。
//排序
void Sortcontact(Contact* pc)
{
printf("请选择排序方式\n");
menus();
int input = 0;
scanf("%d", &input);
switch (input)
{
case EXIT:
printf("退出排序\n");
return;
case name:
sort(pc,name);
break;
case sex:
sort(pc,sex);
break;
case age:
sort(pc,age);
break;
case tel:
sort(pc,tel);
break;
case addr:
sort(pc,addr);
break;
}
printf("排序成功\n");
}
其实在之前我们就学过冒泡排序和qosort函数进行排序,这两个各有好处。
相应的文章学习链接给上
从0开始学c语言-28-qsort函数、 数组和指针参数、函数指针数组(转移表)、回调函数_阿秋的阿秋不是阿秋的博客-CSDN博客
从0开始学c语言-16-数组以及数组传参应用:冒泡排序_阿秋的阿秋不是阿秋的博客-CSDN博客
void Change(Contact* pc, int j)
{
PeoInfo A= pc->data[j];
pc->data[j] = pc->data[j + 1];
pc->data[j + 1] = A;
}
void sort(Contact* pc,int opt)
{
int i = 0;
int j = 0;
for (i = 0; i < pc->sz - 1; i++)
{
for (j = 0; j < pc->sz - i - 1 ; j++)
{
if (opt == name)
{
int tmp = strcmp(pc->data[j].name, pc->data[j + 1].name);
if (tmp > 0)
Change(pc, j);
}
else if (opt == sex)
{
int tmp = strcmp(pc->data[j].sex, pc->data[j + 1].sex);
if (tmp > 0)
Change(pc, j);
}
else if (opt == age)
{
int tmp = pc->data[j].age - pc->data[j + 1].age;
if (tmp > 0)
Change(pc, j);
}
else if (opt == tel)
{
int tmp = strcmp(pc->data[j].tel, pc->data[j + 1].tel);
if (tmp > 0)
Change(pc, j);
}
else if (opt == addr)
{
int tmp = strcmp(pc->data[j].addr, pc->data[j + 1].addr);
if (tmp > 0)
Change(pc, j);
}
else
{
printf("传参错误\n");
}
}
}
}
这是我写的冒泡排序方法,如果你想用qsort函数也行,那个更简便。
现在,这个通讯录就完整了,
相信很少有人能看完,不过我还是会认真写的。
毕竟咱就爱这口~