一、引言
在数字化时代,通讯录作为我们日常生活中不可或缺的一部分,扮演着记录和管理联系人信息的重要角色。随着智能手机的普及,人们对于通讯录的功能和性能要求也在不断提高。为了更好地满足这些需求,我们有必要对通讯录的实现方式进行深入研究和探索。
在众多的数据结构中,单链表以其独特的优势成为了实现通讯录的一种理想选择。单链表是一种线性数据结构,它通过每个节点中的指针链接在一起,形成一个有序的链表。相比于数组等其他数据结构,单链表在插入、删除操作上具有更高的效率,因为它不需要像数组那样移动大量的元素。此外,单链表在内存使用上也更加灵活,可以根据需要动态地分配和释放内存空间。
因此,本文旨在探讨如何使用单链表来实现一个高效、灵活的通讯录项目。我们将首先介绍单链表的基本概念和基本操作,然后分析通讯录项目的需求,并设计相应的数据结构和接口。接下来,我们将详细实现通讯录类的各个功能,并进行测试和验证。最后,我们将对项目进行总结和反思,并提出改进方向。
通过本文的介绍和实践,读者将能够深入理解单链表在通讯录项目中的应用,掌握使用单链表实现通讯录的基本方法和技巧。同时,本文也为读者提供了一个实际的项目案例,有助于提升读者的编程能力和解决问题的能力。
二、单链表的基本概念
通讯录项目的实现直接借用了单链表实现的头文件SLinkList.h 和 源文件SLinkList.c
关于单链表的问题请参照前置文章
对单链表有了深入的理解之后才能更好的实现通讯录项目
三、通讯录项目的需求分析
- 能够存储较多的联系人信息,并且能够高效地管理内存,避免不必要的内存浪费
- 能够保存用户信息:名字、性别、年龄、电话、地址等
- 增加联系⼈信息
- 删除指定联系⼈
- 查找指定联系⼈
- 修改指定联系⼈
- 显示联系⼈信息
- 实现数据的导入导出
四、通讯录的数据结构
以下是用结构体记录通讯录单个联系人信息的信息,对应单链表单个节点的数据部分的数据类型
#define NAME_MAX 100 #define SEX_MAX 4 #define TEL_MAX 11 #define ADDR_MAX 100 typedef struct PersonInfo { char name[NAME_MAX];//姓名 char sex[SEX_MAX]; //性别 int age; //年龄 char tel[TEL_MAX]; //电话 char addr[ADDR_MAX];//地址 }PeoInfo;
同时,单链表头文件中对单链表结构的数据部分定义有所修改
typedef PeoInfo SLTDataType;//类型重定义 typedef struct SListNode { SLTDataType data;//数据 struct SListNode* next;//指针 }SLTNode;
注意:
因为单链表的头文件需要用到通讯录的头文件的联系人结构体定义,所以在单链表头文件中包含了通讯录头文件。
但通讯录头文件中又需要用到单链表中对节点的定义,头文件不能互相包含,所以应当在通讯录头文件中包含一条对单链表的结构体的前置声明
typedef struct SListNode contact;//声明并重命名
接下来,就可以来实现通讯录项目的方法了
五、通讯录的接口
通讯录的基本方法接口包括
- 初始化与销毁通讯录
- 数据的导入导出
- 对联系人的增删改查
- 以及展示通讯录中的联系人信息
需要对单链表数据进行修改的函数,应该传址调用,实参传递地址,形参使用二级指针接收
//初始化通讯录 void InitContact(contact** con); //添加通讯录数据 void AddContact(contact** con); //删除通讯录数据 void DelContact(contact** con); //展示通讯录数据 void ShowContact(contact* con); //查找通讯录数据 void FindContact(contact* con); //修改通讯录数据 void ModifyContact(contact** con); //销毁通讯录数据 void DestroyContact(contact** con);
1.通讯录初始化 / 导入外部数据
单链表实现的通讯录因为不带有额外的头节点,并且链表每个节点都是独立的,所以初始化不需要额外的操作,只需要从外部文件导入通讯录数据即可
这里将导入数据单独封装成一个函数,以便代码复用
由初始化函数来调用导入数据函数
导入外部数据的函数功能:
- 以二进制读方式打开文件(注意:以读的方式打开文件,必须保证文件存在,否则会出错)
- 判断是否成功
- 循环读取数据,每读取一条就尾插到单链表
void LoadContact(contact** con)//导入数据到通讯录 { FILE* pf = fopen("contact.txt", "rb");//以二进制读方式打开文件 if (pf == NULL)//判空 { perror("fopen"); return; } PeoInfo po; while (fread(&po, sizeof(po), 1, pf))//循环读取数据 { SLTPushBack(con, po);//尾插到单链表 } printf("数据载入成功!\n"); fclose(pf); pf = NULL; }
/初始化通讯录 void InitContact(contact** con) { LoadContact(con);//导入外部数据 }
2.添加联系人信息
添加联系人信息的函数功能:
- 创建一个联系人的结构体变量
- 逐个录入信息至该变量
- 将该结构体变量和通讯录链表的首节点地址作为参数,一起传给底层的单链表尾插函数
//添加通讯录数据 void AddContact(contact** con) { assert(con);//二级指针判空 PeoInfo po; printf("请按提示输入要添加的联系人信息\n"); printf("请输入姓名:\n"); scanf("%s", po.name); printf("请输入性别:\n"); scanf("%s", po.sex); printf("请输入年龄:\n"); scanf("%d", &po.age); printf("请输入电话:\n"); scanf("%s", po.tel); printf("请输入地址:\n"); scanf("%s", po.addr); SLTPushBack(con, po);//调用单链表函数尾插 printf("添加联系人成功\n"); }
3.删除联系人信息
删除联系人记录需要封装一个查找联系人函数(单独实现查找,无其他功能)
单独的查找函数的函数功能:
- 这里以姓名作为关键值查找,接收一个链表首地址和关键值信息
- 遍历链表,如果找到该联系人,返回节点地址
- 否则返回空指针
//封装的单独查找函数 contact* FindByname(contact*con,char name[]) { assert(con);//二级指针判空 contact* pcur = con;//遍历链表的指针 while (pcur) { if (strcmp(pcur->data.name, name) == 0)//字符串比对 return pcur; pcur = pcur->next; } return NULL; }
删除联系人记录的函数功能:
- 录入要删除的联系人姓名
- 调用封装的查找函数
- 如果找到,调用单链表实现的删除指定节点函数
- 如果找不到,报错
//删除通讯录数据 void DelContact(contact** con) { assert(con&&*con);//二级指针判空,链表判空 printf("请输入要删除的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* del = FindByname(*con, name);//调用单独的查找函数 if (del == NULL) { printf("要删除的联系人不存在!\n"); return; } SLTErase(con, del);//调用单链表删除指定元素 printf("删除联系人成功!\n"); }
4.查找联系人信息
查找联系人信息的函数功能:
- 这里不同于上面的功能单一的查找函数
- 作用是根据录入的关键值查找并打印该联系人信息或报错
- 这里依然以姓名作为关键值查找
//查找通讯录数据 void FindContact(contact* con) { printf("请输入要查找的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* pcur = FindByname(con, name);//调用已经实现的查找函数 if (pcur == NULL) { printf("要查找的联系人不存在!\n"); return; } printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名", "性别", "年龄", "电话", "地址" );//打印该联系人信息 printf("%-10s%-10s%-10d%-10s%-10s\n", pcur->data.name, pcur->data.sex, pcur->data.age, pcur->data.tel, pcur->data.addr ); }
5.修改联系人信息
修改联系人信息的函数功能:
- 依然以姓名作为关键值(因为可以重复利用封装的查找函数)
- 录入姓名,调用单独的查找函数
- 若找到指定联系人,依次修改该联系人的各部分信息,赋值给该节点
- 否则,该联系人不存在,报错
//修改通讯录数据 void ModifyContact(contact** con) { assert(con); printf("请输入要修改的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* pcur = FindByname(*con, name);//调用已经实现的查找函数 if (pcur == NULL) { printf("要修改的联系人不存在!\n"); return; } printf("请输入修改后的联系人姓名: "); scanf("%s", pcur->data.name); printf("请输入修改后的联系人性别: "); scanf("%s", pcur->data.sex); printf("请输入修改后的联系人年龄: "); scanf("%d", &pcur->data.age); printf("请输入修改后的联系人电话: "); scanf("%s", pcur->data.tel); printf("请输入修改后的联系人地址: "); scanf("%s", pcur->data.addr); printf("修改联系人信息成功!\n"); }
6.展示联系人信息
展示联系人信息的函数功能:
- 先打印表头信息
- 创建一个遍历链表的指针
- 逐个访问链表的每一个节点,每行打印该节点的数据部分
//展示通讯录数据 void ShowContact(contact* con) { if (con == NULL)//对空链表的特殊处理 { printf("NULL\n"); return; } printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名", "性别", "年龄", "电话", "地址" );//表头 contact* pcur = con; while (pcur)//遍历链表,打印每个节点的联系人信息 { printf("%-10s%-10s%-10d%-10s%-10s\n", pcur->data.name, pcur->data.sex, pcur->data.age, pcur->data.tel, pcur->data.addr ); pcur = pcur->next; } }
7.导出数据到文件
数据导出的函数功能:
- 以二进制写方式打开文件
- 循环遍历链表,将每个节点的数据(每个联系人信息)输出到外部文件
//导出数据 void SaveData(contact* con) { FILE* pf = fopen("contact.txt", "wb");//以二进制写方式打开文件 if (pf == NULL) { perror("fopen\n"); exit(1); } contact* pcur = con; while (pcur)//遍历链表,将通讯录数据输出到文件中 { fwrite(pcur, sizeof(contact), 1, pf); pcur = pcur->next; } fclose(pf); free(pf); pf = NULL; }
8.通讯录销毁
通讯录销毁的函数功能:
- 销毁之前需要先调用导出数据的函数,将联系人信息保存下来
- 调用单链表中已实现的链表销毁函数——循环遍历链表,释放每一个动态申请空间的节点
//销毁通讯录数据 void DestroyContact(contact** con) { SaveData(*con);//调用函数导出数据到文件 SListDesTroy(con);//调用单链表函数销毁通讯录 }
六、主函数中通讯录操作
1.通讯录菜单
菜单函数的功能:
- 封装一个函数向用户展示通讯录项目的功能以及每项功能对应的选项
void menu() { printf("\n######################################\n"); printf("###########--——通讯录菜单——--#########\n"); printf("#####1.添加联系人 2.删除联系人######\n"); printf("#####3.修改联系人 4.查找联系人######\n"); printf("######### 5.展示全部联系人 ##########\n"); printf("######### 0.退出通讯录程序 ##########\n"); printf("######################################\n\n"); }
2.通讯录人机交互操作
通讯录人机交互部分功能:
- 首先主函数运行,初始化通讯录,从外部载入数据
- 然后通过一个 do while循环(保证程序至少运行一次)和 swtich语句(用户选择指定选项对应指定的功能)配合完成对通讯录的操作
- 最后,用户选择结束操作后,将数据保存至外部文件,销毁通讯录
contact* con = NULL; InitContact(&con);//初始化 int op = 0;//选项 do { menu(); printf("请选择您的操作: "); scanf("%d", &op); switch (op) { case 1: AddContact(&con);//添加 break; case 2: DelContact(&con);//删除 break; case 3: ModifyContact(&con);//修改 break; case 4: FindContact(con);//查找 break; case 5: ShowContact(con);//展示 break; case 0: printf("退出通讯录!\n"); break; default: printf("您选择的数字有误,请重新输入;\n"); break; } } while (op); DestroyContact(&con);//销毁
七、各文件的实现代码
单链表中已有文件——
SLinkList.h
单链表结构定义及函数声明头文件
//SLinkList.h #define _CRT_SECURE_NO_WARNINGS 1 //单链表结构定义及函数声明头文件 #include<stdio.h> #include<assert.h> #include<stdlib.h> #include"Contact.h" #include<string.h> typedef PeoInfo SLTDataType;//类型重定义 typedef struct SListNode { SLTDataType data;//数据 struct SListNode* next;//指针 }SLTNode; void SLTPrint(SLTNode* phead);//链表打印 SLTNode* NewNode(SLTDataType x);//申请节点 //头部插入删除/尾部插入删除 void SLTPushBack(SLTNode** pphead, SLTDataType x); void SLTPushFront(SLTNode** pphead, SLTDataType x); void SLTPopBack(SLTNode** pphead); void SLTPopFront(SLTNode** pphead); //查找 SLTNode* SLTFind(SLTNode* phead, SLTDataType x); //在指定位置之前插入数据 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x); //删除pos节点 void SLTErase(SLTNode** pphead, SLTNode* pos); //在指定位置之后插入数据 void SLTInsertAfter(SLTNode* pos, SLTDataType x); //删除pos之后的节点 void SLTEraseAfter(SLTNode* pos); //销毁链表 void SListDesTroy(SLTNode** pphead);
SLinkList.c
单链表方法实现源文件
//SLinkList.c //单链表方法实现源文件 #include"SLinkList.h" //void SLTPrint(SLTNode* phead)//链表打印 //{ // SLTNode* pcur = phead; // while (pcur)//pcur!=NULL // { // printf("%d->", pcur->data);//打印该节点数据 // pcur = pcur->next;//指针指向下一个节点 // } // printf("NULL\n"); //} SLTNode* NewNode(SLTDataType x)//申请节点 { SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode)); if (newnode == NULL)//注意这里不要写成一个= { perror("newnode"); exit(1);//如果申请节点失败,异常退出程序 } newnode->data = x; //数据初始化 newnode->next = NULL;//指针初始化 return newnode;//返回新申请节点的地址 } //头部插入删除/尾部插入删除 void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插 { assert(pphead);//二级指针不能为空,否则解引用就会报错 SLTNode* newnode = NewNode(x); if (*pphead == NULL) { *pphead = newnode;//如果链表为空,新节点即为第一个节点 } else { SLTNode* pcur = *pphead; while (pcur->next!=NULL)//找到链表的尾节点 { pcur = pcur->next; } pcur->next = newnode;//如果不对空链表分别处理 } //此处就会对空指针解引用 } void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插 { assert(pphead);//二级指针不能为空,否则解引用就会报错 SLTNode* newnode = NewNode(x); newnode->next = *pphead;//新节点next指针指向原来的首节点 *pphead = newnode;//新节点成为首节点 } void SLTPopBack(SLTNode** pphead)//尾删 { assert(pphead && *pphead);//二级指针不能为空,链表不能为空 if ((*pphead)->next == NULL)//处理链表只有一个节点的情况 { free(*pphead); *pphead = NULL; } else//处理正常情况 { SLTNode* pcur = *pphead;//找到指针的最后一个节点 SLTNode* prev = *pphead;//找到指针的倒数第二个节点 while (pcur->next != NULL) { prev = pcur; pcur = pcur->next; } free(pcur); pcur = NULL; prev->next = NULL;//如果不做特殊处理,此处就会对空指针解引用 } } void SLTPopFront(SLTNode** pphead)//头删 { assert(pphead && *pphead);//二级指针不能为空,链表不能为空 SLTNode* next = (*pphead)->next;//存储第二个节点 free(*pphead);//删除第一个节点 *pphead = next;//链表指向第二个节点 } //查找 //SLTNode* SLTFind(SLTNode* phead, SLTDataType x) //{ // SLTNode* pcur = phead; // while (pcur) // { // if (pcur->data == x) // return pcur;//如果找到,返回节点地址 // } // return NULL;//对未找到的情况和链表为空的情况都可以处理 //} //在指定位置之前插入数据 void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) { assert(pphead && *pphead);//二级指针不能为空,链表不能为空 assert(pos);//指定位置必须存在 SLTNode* newnode = NewNode(x); if (*pphead == pos)//如果要插入到第一个节点之前 { SLTPushFront(pphead, x);//可以直接调用头插 } else { SLTNode* prev = *pphead; while (prev->next != pos)//需要先找到pos的前一个节点 { //如果不分别处理,最后就会对空指针解引用 prev = prev->next; } newnode->next = pos;//新节点指向pos prev->next = newnode;//原pos的前一个节点指向新节点 } } //在指定位置之后插入数据 void SLTInsertAfter(SLTNode* pos, SLTDataType x) { assert(pos);//pos节点必须存在 SLTNode* newnode = NewNode(x); newnode->next = pos->next;//新节点的next指针指向原pos的下一个节点 pos->next = newnode;//pos的next指针指向新节点 } //删除pos节点 void SLTErase(SLTNode** pphead, SLTNode* pos) { assert(pphead && *pphead);//二级指针不能为空,链表不能为空 assert(pos);//指定位置必须存在 if (pos == *pphead)//如果删除的是首节点,或链表只有一个节点 { SLTPopFront(pphead); } else//链表有多个节点,且删除的不是首节点 { SLTNode* prev = *pphead; while (prev->next != pos)//如果不分开处理,这里就可能对空指针解引用 { prev = prev->next; } prev->next = pos->next;//找到前一个指针,并将其next指针指向pos的next指针所指节点 free(pos); pos = NULL; } } //删除pos之后的节点 void SLTEraseAfter(SLTNode* pos) { assert(pos && pos->next);//pos节点必须存在且pos之后必须存在节点 SLTNode* del = pos->next;//预先存储要删除的节点,防止修改指针之后找不到要删除的节点 pos->next = del->next; free(del); del = NULL; } //销毁链表 void SListDesTroy(SLTNode** pphead) { assert(pphead);//二级指针不能为空,链表不能为空 SLTNode* pcur = *pphead; while (pcur) { SLTNode* next = pcur->next;//需要预先存储当前要删除节点的下一个节点 free(pcur); //否则就找不到下一个节点了 pcur = next; } *pphead = NULL;//删除完所有节点之后,链表置空 }
Contact.h
通讯录结构定义及函数声明头文件
//Contact.h #pragma once #define NAME_MAX 100 #define SEX_MAX 4 #define TEL_MAX 11 #define ADDR_MAX 100 //前置声明 typedef struct SListNode contact;//声明并重命名 //用户数据 typedef struct PersonInfo { char name[NAME_MAX]; char sex[SEX_MAX]; int age; char tel[TEL_MAX]; char addr[ADDR_MAX]; }PeoInfo; //初始化通讯录 void InitContact(contact** con); //添加通讯录数据 void AddContact(contact** con); //删除通讯录数据 void DelContact(contact** con); //展示通讯录数据 void ShowContact(contact* con); //查找通讯录数据 void FindContact(contact* con); //修改通讯录数据 void ModifyContact(contact** con); //销毁通讯录数据 void DestroyContact(contact** con);
Contact.c
通讯录方法实现源文件
//Contact.c #include"Contact.h" #include"SLinkList.h" //导入数据到通讯录 void LoadContact(contact** con) { FILE* pf = fopen("contact.txt", "rb");//以二进制读方式打开文件 if (pf == NULL)//判空 { perror("fopen"); return; } PeoInfo po; while (fread(&po, sizeof(po), 1, pf))//循环读取数据 { SLTPushBack(con, po);//尾插到单链表 } printf("数据载入成功!\n"); fclose(pf); pf = NULL; } //初始化通讯录 void InitContact(contact** con) { LoadContact(con);//导入外部数据 } //添加通讯录数据 void AddContact(contact** con) { assert(con);//二级指针判空 PeoInfo po; printf("请按提示输入要添加的联系人信息\n"); printf("请输入姓名:\n"); scanf("%s", po.name); printf("请输入性别:\n"); scanf("%s", po.sex); printf("请输入年龄:\n"); scanf("%d", &po.age); printf("请输入电话:\n"); scanf("%s", po.tel); printf("请输入地址:\n"); scanf("%s", po.addr); SLTPushBack(con, po);//调用单链表函数尾插 printf("添加联系人成功\n"); } //封装的单独查找函数 contact* FindByname(contact*con,char name[]) { assert(con);//二级指针判空 contact* pcur = con;//遍历链表的指针 while (pcur) { if (strcmp(pcur->data.name, name) == 0)//字符串比对 return pcur; pcur = pcur->next; } return NULL; } //删除通讯录数据 void DelContact(contact** con) { assert(con&&*con);//二级指针判空,链表判空 printf("请输入要删除的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* del = FindByname(*con, name);//调用单独的查找函数 if (del == NULL) { printf("要删除的联系人不存在!\n"); return; } SLTErase(con, del);//调用单链表删除指定元素 printf("删除联系人成功!\n"); } //展示通讯录数据 void ShowContact(contact* con) { if (con == NULL)//对空链表的特殊处理 { printf("NULL\n"); return; } printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名", "性别", "年龄", "电话", "地址" );//表头 contact* pcur = con; while (pcur)//遍历链表,打印每个节点的联系人信息 { printf("%-10s%-10s%-10d%-10s%-10s\n", pcur->data.name, pcur->data.sex, pcur->data.age, pcur->data.tel, pcur->data.addr ); pcur = pcur->next; } } //查找通讯录数据 void FindContact(contact* con) { printf("请输入要查找的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* pcur = FindByname(con, name);//调用已经实现的查找函数 if (pcur == NULL) { printf("要查找的联系人不存在!\n"); return; } printf("%-10s%-10s%-10s%-10s%-10s\n", "姓名", "性别", "年龄", "电话", "地址" );//打印该联系人信息 printf("%-10s%-10s%-10d%-10s%-10s\n", pcur->data.name, pcur->data.sex, pcur->data.age, pcur->data.tel, pcur->data.addr ); } //修改通讯录数据 void ModifyContact(contact** con) { assert(con); printf("请输入要修改的联系人姓名:"); char name[NAME_MAX]; scanf("%s", name); contact* pcur = FindByname(*con, name);//调用已经实现的查找函数 if (pcur == NULL) { printf("要修改的联系人不存在!\n"); return; } printf("请输入修改后的联系人姓名: "); scanf("%s", pcur->data.name); printf("请输入修改后的联系人性别: "); scanf("%s", pcur->data.sex); printf("请输入修改后的联系人年龄: "); scanf("%d", &pcur->data.age); printf("请输入修改后的联系人电话: "); scanf("%s", pcur->data.tel); printf("请输入修改后的联系人地址: "); scanf("%s", pcur->data.addr); printf("修改联系人信息成功!\n"); } //导出数据 void SaveData(contact* con) { FILE* pf = fopen("contact.txt", "wb");//以二进制写方式打开文件 if (pf == NULL) { perror("fopen\n"); exit(1); } contact* pcur = con; while (pcur)//遍历链表,将通讯录数据输出到文件中 { fwrite(pcur, sizeof(contact), 1, pf); pcur = pcur->next; } fclose(pf); free(pf); pf = NULL; } //销毁通讯录数据 void DestroyContact(contact** con) { SaveData(*con);//调用函数导出数据到文件 SListDesTroy(con);//调用单链表函数销毁通讯录 }
test.c
主函数测试文件
#include"Contact.h" #include"SLinkList.h" void menu() { printf("\n######################################\n"); printf("###########--——通讯录菜单——--#########\n"); printf("#####1.添加联系人 2.删除联系人######\n"); printf("#####3.修改联系人 4.查找联系人######\n"); printf("######### 5.展示全部联系人 ##########\n"); printf("######### 0.退出通讯录程序 ##########\n"); printf("######################################\n\n"); } int main() { //test1(); contact* con = NULL; InitContact(&con);//初始化 int op = 0;//选项 do { menu(); printf("请选择您的操作: "); scanf("%d", &op); switch (op) { case 1: AddContact(&con);//添加 break; case 2: DelContact(&con);//删除 break; case 3: ModifyContact(&con);//修改 break; case 4: FindContact(con);//查找 break; case 5: ShowContact(con);//展示 break; case 0: printf("退出通讯录!\n"); break; default: printf("您选择的数字有误,请重新输入;\n"); break; } } while (op); DestroyContact(&con);//销毁 return 0; }
八、测试与验证
九、写在最后
本文所有代码已经过多轮测试,可以直接复制使用。
如果您发现某处代码存在问题以及对程序的改进意见,欢迎私信或评论指点。
💓 博客主页:倔强的石头的CSDN主页