【C语言进阶】【小项目】实现一个通讯录【C语言知识点汇总项目】通过这个项目,掌握C语言重要知识点

【C语言进阶】【小项目】实现一个通讯录【C语言知识点汇总项目】通过这个项目,掌握C语言重要知识点

欢迎来到#西城s的博客,今天,博主带着大家用C实现一个通讯录!干货满满不要错过噢!

作者: #西城s
这是我的主页:@小小Programmer
在食用这篇博客之前,博主在这里介绍一下其它高质量的编程学习栏目:
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
算法专栏:算法 这里可以说是博主的刷题历程,里面总结了一些经典的力扣上的题目,和算法实现的总结,对考试和竞赛都是很有帮助的!
力扣刷题专栏Leetcode想要冲击ACM、蓝桥杯或者大学生程序设计竞赛的伙伴,这里面都是博主的刷题记录,希望对你们有帮助!

在这里插入图片描述
先赞后看好习惯 打字不容易,这都是很用心做的,希望得到支持你 大家的点赞和支持对于我来说是一种非常重要的动力。看完之后别忘记关注我哦!️️️

这个小项目几乎包含了C语言学习阶段所有知识点,实现了这个项目,我们的C语言,就没问题了!

本篇为不收藏必后悔篇~

我们要实现的通讯录的功能:

  1. 可以动态开辟空间存放联系人的信息。
  2. 联系人的信息包括名字、性别、年龄、电话、地址。
  3. 同时,我们要实现通讯录中:增加,删除,查找,修改联系人的功能,打印联系人的功能。
  4. 联系人的信息可以通过文件进行保存。

前言

实现这种类似的简易通讯录有很多种方式,这里博主选用的是顺序表来进行实现,其实博主认为,使用链表是最好的,但是我们如果是刚学习完C语言的伙伴,应该是还没有开始学习数据结构的,所以这里我们用最简单的——顺序表,也就是结构体数组来实现这个通讯录。
当然,如果对链表有兴趣的伙伴,可以通过博主提供的传送门过去食用~
【链表】单链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
【链表】双向链表的介绍和基本操作(C语言实现)【保姆级别详细教学】
当然,它们也收录在博主:数据结构这个专栏里面。

当然,在本期博客中,我们要所用到的核心知识有:动态内存的管理,结构体和文件。博主以前总结过这些知识点,这里提供传送门,如果伙伴们在阅读的时候遇到困难,不妨食用完博主这几篇总结再来实现这个小项目,这几篇博客同样是干货满满噢!
【算法】【C语言进阶】C语言字符串操作宝藏级别汇总 strtok函数 strstr函数该怎么用?【超详细的使用解释和模拟实现】
【文件】C语言文件操作及其使用总结篇【初学者保姆级别福利】
【动态内存】C语言动态内存管理及使用总结篇【初学者保姆级福利】

实现通讯录总体框架

当然,我们实现的时候采用分模块实现的方式。
创建3个源文件:

test.c 总体测试源文件(main函数的位置)
contact.c 实现接口
contact.h 接口的声明

首先我们来看通讯录的结构

#define NAME_MAX 20 //一个联系人名字的最大空间
#define SEX_MAX 5   //性别
#define TELE_MAX 12  //电话
#define ADDR_MAX 30  //地址  
#define DEFAULT_SZ 3 //默认初始容量大小-后面有需要会接着增容
//一个人的信息的结构
typedef struct PeoInfo {
	char name[NAME_MAX];//名字
	char sex[SEX_MAX];//性别
	int age;//年龄
	char tele[TELE_MAX];//电话
	char addr[ADDR_MAX];//地址
}PeoInfo;
//通讯录结构
//顺序表实现
typedef struct Contact {
	PeoInfo* data;
	int sz;//记录当前通讯录里面有几个人
	int capacity;//记录最大容量
}Contact;

//实现的结构名称
//让代码的可读性提升了
enum Option {//利用枚举,给接口所对应的数字换成有意义的单词,提升可读性
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT
};

确定好总体结构之后,我们回到test.c文件里面,开始创建通讯录。

#include"contact.h"
//通讯录
//自己写一个小目录
void menu() {
	printf("----------------------------\n");
	printf("*          THE MENU        *\n");
	printf("----------------------------\n");
	printf("*          1.add           *\n");
	printf("*          2.del           *\n");
	printf("*          3.search        *\n");
	printf("*          4.modify        *\n");
	printf("*          5.sort          *\n");
	printf("*          6.print         *\n");
	printf("*          0.exit          *\n");
	printf("----------------------------\n");
}
void _test() {
	int _input = 0;
	//创建通讯录
	Contact con;
	//初始化通讯录
	//所以我们要改造这个初始化函数,给它三个struct的空间
	_initContact(&con);//这里同时也要把东西加载进去
	do {
		system("cls");//这个是格式优化,我们可以暂时不管
		menu();
		printf("there is %d PeoInfos in the Contact\n", con.sz);
		printf("----------------------------\n");
		printf("please choose:>");
		scanf("%d", &_input);
		switch (_input)
		{
		case ADD://ADD,DEL这些博主都通过枚举的方式,给它们赋好值了
			_addContact(&con);//增加联系人
			break;
		case DEL:
			_delContact(&con);//删除联系人
			break;
		case SEARCH:
			_searchContact(&con);//查找联系人
			break;
		case MODIFY:
			_modifyContact(&con);//修改联系人
			break;
		case SORT:
			//_sortByAge(&con);//按照年龄排序-这个博主就不带大家实现了,其余的结构博主都给大家写好了,这个可以自己尝试一下
			break;
		case PRINT:
			_printContact(&con);
			break;
		case EXIT:
			//改成动态版本之后,要一个接口来销毁通讯录
			//退出通讯录之前,我们还需要将内容保存一下
			_saveContact(&con);
			_destroyContact(&con);
			printf("exit contact\n");
			system("pause");
			exit(-1);
			break;
		default:
			printf("err\n");//选择错误
			system("pause");
			break;
		}
	} while (_input);
}
int main() {
	_test();
	return 0;
}

我们可以测试一下我们的目录:
在这里插入图片描述

当然,写到这里,我们的实现接口是还没有写的,等一下我们要一一实现!

核心接口实现

初始化通讯录接口

初始化注意事项:

  • 首先我们需要开辟出3(一开始设定好的默认值)的空间,全部置为0
  • 然后,我们还要加载以前保存下来的文件(contact.bat)里的内容
  • 加载的时候,要检查当前通讯录的容量是否够放,所以我们还需要一个检查容量,并在不够时增容的一个函数:_checkCapacity(),这个博主在后面的讲解带大家实现,在这里大家先写好,我们一会儿实现。
//加载通讯录
void _loadContact(Contact* pc) {
	//打开文件
	FILE* pf = fopen("contact.dat", "rb");
	if (pf == NULL) {
		perror("_loadContact::fopen");
		return;
	}
	//读文件
	PeoInfo tmp = { 0 };
	while (fread(&tmp, sizeof(PeoInfo), 1, pf)) {
		//放进去
		_checkCapacity(pc);//检查容量是否够放
		pc->data[pc->sz] = tmp;
		pc->sz++;
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
}


//改造成文件的版本
void _initContact(Contact* pc) {
	assert(pc);
	pc->sz = 0;
	pc->capacity = DEFAULT_SZ;//创建默认个数的空间先
	pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));
	if (pc->data == NULL) {
		perror("init::malloc");
		exit(-1);
	}
	memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
	//加载文件信息到通讯录
	_loadContact(pc);
}

增加联系人接口

思路很简单,一个一个信息输入,放进去即可,但是要注意:增加的时候先检查容量。

//增加联系人
void _addContact(Contact* pc) {
	assert(pc);
	//满了要增容
	_checkCapacity(pc);
	//录入信息
	printf("please input the name:");
	scanf("%s", pc->data[pc->sz].name);
	printf("please input the age:");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("please input the sex:");
	scanf("%s", pc->data[pc->sz].sex);
	printf("please input the tele_num:");
	scanf("%s", pc->data[pc->sz].tele);
	printf("pleae input the address:");
	scanf("%s", pc->data[pc->sz].addr);

	pc->sz++;//通讯录人物数量记得++
	printf("----------------------------\n");
	printf("successfully added!\n");
	printf("----------------------------\n");
	printf("\n");
	system("pause");
}

检查并增加容量接口

同样:思路非常的简单,不够了,再开辟一个即可!
这里注意realloc()函数的使用!

//增容
void _checkCapacity(Contact* pc) {
	if (pc->sz == pc->capacity) {
		PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));
		if (tmp != NULL) {
			pc->data = tmp;
		}
		pc->capacity += 2;
		printf("contact is successfully enlarged!\n");
		printf("---------------------------------\n");
	}
}

打印接口

一个简单的顺序表的遍历即可实现,当然我们要花点功夫让它们打印得更美观!

//打印通讯录
void _printContact(const Contact* pc) {
	assert(pc);
	//打印
	int i = 0;
	printf("\n");
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->sz; i++) {
		printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[i].name, pc->data[i].age,
			pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
	printf("\n");
	system("pause");
}

在这里插入图片描述

查找,删除,修改联系人信息接口

这几个接口我们写在一起,因为删除,修改,我们总要找到这个人吧,所以其实它们都需要用到一个叫做 通过联系人名字查找联系人的一个接口。

思路也非常的简单,但是有很多细节,我们在写的时候注意一下即可,具体我们看注释。

//通过名字查找人的信息
//如果找到这个人,返回在顺序表中的下标i,如果没找到,返回-1
int _findByName(const Contact* pc, char* name) {
	assert(pc);
	int i = 0;
	for (i = 0; i < pc->sz; i++) {
		if (strcmp(pc->data[i].name, name) == 0) {
			return i;
		}
	}
	return -1;
}

//删除指定联系人信息
void _delContact(Contact* pc) {
	assert(pc);
	if (pc->sz == 0) {//里面没有联系人,不能删除
		printf("null Contact! err!\n");
		system("pause");
		return;
	}
	//删除
	//1.找到
	char name[NAME_MAX] = { 0 };
	printf("please input the name:");
	scanf("%s", name);
	int pos = _findByName(pc, name);//找到了,返回下标;找不到,返回负一
	if (pos == -1) {//找不到
		printf("cannot find the PeoInfo,err!");
		system("pause");
		return;
	}
	//2.删除
	//这里要稍微分析清楚,是sz-1,不要越界了
	for (int j = pos; j < pc->sz - 1; j++) {
		pc->data[j] = pc->data[j + 1];
	}
	pc->sz--;//这个不要忘记了
	printf("successfully deleted\n");
	system("pause");
}



//查找一个人的信息
void _searchContact(const Contact* pc) {
	char name[NAME_MAX] = { 0 };
	printf("please input the name:>");
	scanf("%s", name);
	int pos = _findByName(pc, name);
	if (pos == -1) {
		printf("cannot find the PeoInfo,err!");
		return;
	}
	printf("--------------------------------------------------\n");
	printf("the info is:\n");
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[pos].name, pc->data[pos].age,
		pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
	printf("--------------------------------------------------\n");
	system("pause");
}


void _modifyContact(Contact* pc) {
	printf("whose Info do you want to modify:\n");
	char name[NAME_MAX] = { 0 };
	scanf("%s", name);
	int pos = _findByName(pc, name);//找一下这个人,看能不能找到
	if (pos == -1) {//找不到
		printf("cannot find the PeoInfo,err!\n");
		printf("--------------------------------------------------\n");
		system("pause");
		return;
	}
	//找到了-修改
	//修改
	printf("--------------------------------------------------\n");
	printf("you are modifing %s's info\n", pc->data[pos].name);
	printf("please input the name:");
	scanf("%s", pc->data[pos].name);
	printf("please input the age:");
	scanf("%d", &(pc->data[pos].age));
	printf("please input the sex:");
	scanf("%s", pc->data[pos].sex);
	printf("please input the tele_num:");
	scanf("%s", pc->data[pos].tele);
	printf("pleae input the address:");
	scanf("%s", pc->data[pos].addr);
	printf("\n");
	printf("successfully modified!\n");
	printf("--------------------------------------------------\n");
	printf("\n");
	system("pause");
}

保存联系人信息、退出通讯录接口

这里我们又要用到文件的知识了,把当前通讯录的信息写到文件里去。

//保存通讯录
void _saveContact(const Contact* pc) {
	FILE* pf = fopen("contact.dat", "wb");//以二进制的形式写进去
	if (pf == NULL) {
		perror("_saveContact::fopen");
		return;
	}
	//写文件
	for (int i = 0; i < pc->sz; i++) {//一个一个写进去
		fwrite(pc->data+i,sizeof(PeoInfo),1,pf);
	}
	printf("sucessfully saved!\n");
	printf("--------------------------------------------------\n");
	//关闭文件
	fclose(pf);
	pf = NULL;
}


//销毁通讯录
void _destroyContact(Contact* pc) {
	free(pc->data);
	pc->data = NULL;
	pc->capacity = 0;
	pc->sz = 0;
}

完整代码展示

test.c

#define _CRT_SECURE_NO_WARNINGS 1

#include"contact.h"
//通讯录

//改成动态版本
void menu() {
	printf("----------------------------\n");
	printf("*          THE MENU        *\n");
	printf("----------------------------\n");
	printf("*          1.add           *\n");
	printf("*          2.del           *\n");
	printf("*          3.search        *\n");
	printf("*          4.modify        *\n");
	printf("*          5.sort          *\n");
	printf("*          6.print         *\n");
	printf("*          0.exit          *\n");
	printf("----------------------------\n");
}
void _test() {
	int _input = 0;
	//创建通讯录
	Contact con;
	//初始化通讯录
	//所以我们要改造这个初始化函数,给它三个struct的空间
	_initContact(&con);//这里同时也要把东西加载进去
	do {
		system("cls");
		menu();
		printf("there is %d PeoInfos in the Contact\n", con.sz);
		printf("----------------------------\n");
		printf("please choose:>");
		scanf("%d", &_input);
		switch (_input)
		{
		case ADD:
			_addContact(&con);
			break;
		case DEL:
			_delContact(&con);
			break;
		case SEARCH:
			_searchContact(&con);
			break;
		case MODIFY:
			_modifyContact(&con);
			break;
		case SORT:
			//_sortByAge(&con);//按照年龄排序
			break;
		case PRINT:
			_printContact(&con);
			break;
		case EXIT:
			//改成动态版本之后,要一个接口来销毁通讯录
			//改造成文件版本
			_saveContact(&con);
			_destroyContact(&con);
			printf("exit contact\n");
			system("pause");
			exit(-1);
			break;
		default:
			printf("err\n");
			system("pause");
			break;
		}
	} while (_input);
}
int main() {
	_test();
	return 0;
}

contact.h

#pragma once
#include<stdio.h>
#include<memory.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#define NAME_MAX 20 //一个联系人名字的最大空间
#define SEX_MAX 5   //性别
#define TELE_MAX 12  //电话
#define ADDR_MAX 30  //地址  
#define DEFAULT_SZ 3 //默认初始容量大小-后面有需要会接着增容
//一个人的信息
typedef struct PeoInfo {
	char name[NAME_MAX];
	char sex[SEX_MAX];
	int age;
	char tele[TELE_MAX];
	char addr[ADDR_MAX];
}PeoInfo;

//静态版本
#if 0
typedef struct Contact {
	PeoInfo data[MAX];//可以存放1000个人的信息
	int sz;//记录通讯录种已经保存的信息个数
}Contact;
#endif

typedef struct Contact {
	PeoInfo* data;//可以存放
	int sz;
	int capacity;//记录最大容量
}Contact;



//实现的结构名称
//让代码的可读性提升了
enum Option {
	EXIT,
	ADD,
	DEL,
	SEARCH,
	MODIFY,
	SORT,
	PRINT
};


void _initContact(Contact* pc);
void _addContact(Contact* pc);
void _printContact(const Contact* pc);
void _delContact(Contact* pc);
void _searchContact(const Contact* pc);
void _modifyContact(Contact* pc);
void _sortByAge(Contact* pc);
void _destroyContact(Contact* pc);
void _saveContact(const Contact* pc);

contact.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"contact.h"
//增容
void _checkCapacity(Contact* pc) {
	if (pc->sz == pc->capacity) {
		PeoInfo* tmp = (PeoInfo*)realloc(pc->data, (pc->capacity + 2) * sizeof(PeoInfo));
		if (tmp != NULL) {
			pc->data = tmp;
		}
		pc->capacity += 2;
		printf("contact is successfully enlarged!\n");
		printf("---------------------------------\n");
	}
}



//初始化通讯录(静态)
#if 0
void _initContact(Contact* pc) {
	assert(pc);
	pc->sz = 0;
	memset(pc->data, 0, sizeof(pc->data));
}
#endif

//动态版本初始化
#if 0
void _initContact(Contact* pc) {
	assert(pc);
	pc->sz = 0;
	pc->capacity = DEFAULT_SZ;
	pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));
	if (pc->data == NULL) {
		perror("init::malloc");
		exit(-1);
	}
	memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
}
#endif
//加载通讯录
void _loadContact(Contact* pc) {
	//打开文件
	FILE* pf = fopen("contact.dat", "rb");
	if (pf == NULL) {
		perror("_loadContact::fopen");
		return;
	}
	//读文件
	PeoInfo tmp = { 0 };
	while (fread(&tmp, sizeof(PeoInfo), 1, pf)) {
		//放进去
		_checkCapacity(pc);
		pc->data[pc->sz] = tmp;
		pc->sz++;
	}
	//关闭文件
	fclose(pf);
	pf = NULL;
}


//改造成文件的版本
void _initContact(Contact* pc) {
	assert(pc);
	pc->sz = 0;
	pc->capacity = DEFAULT_SZ;
	pc->data = (PeoInfo*)malloc(pc->capacity * sizeof(PeoInfo));
	if (pc->data == NULL) {
		perror("init::malloc");
		exit(-1);
	}
	memset(pc->data, 0, pc->capacity * sizeof(PeoInfo));
	//加载文件信息到通讯录
	_loadContact(pc);
}

//销毁通讯录
void _destroyContact(Contact* pc) {
	free(pc->data);
	pc->data = NULL;
	pc->capacity = 0;
	pc->sz = 0;
}

//增加联系人
void _addContact(Contact* pc) {
	assert(pc);
	//if (pc->sz == MAX) {
	//	printf("Contact is already full,err\n");
	//	exit(-1);
	//}

	//满了要增容
	_checkCapacity(pc);
	//录入信息
	printf("please input the name:");
	scanf("%s", pc->data[pc->sz].name);
	printf("please input the age:");
	scanf("%d", &(pc->data[pc->sz].age));
	printf("please input the sex:");
	scanf("%s", pc->data[pc->sz].sex);
	printf("please input the tele_num:");
	scanf("%s", pc->data[pc->sz].tele);
	printf("pleae input the address:");
	scanf("%s", pc->data[pc->sz].addr);

	pc->sz++;
	printf("----------------------------\n");
	printf("successfully added!\n");
	printf("----------------------------\n");
	printf("\n");
	system("pause");
}

//打印通讯录
void _printContact(const Contact* pc) {
	assert(pc);
	//打印
	int i = 0;
	printf("\n");
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	for (i = 0; i < pc->sz; i++) {
		printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[i].name, pc->data[i].age,
			pc->data[i].sex, pc->data[i].tele, pc->data[i].addr);
	}
	printf("\n");
	system("pause");
}

//通过名字查找人的信息
int _findByName(const Contact* pc, char* name) {
	assert(pc);
	int i = 0;
	for (i = 0; i < pc->sz; i++) {
		if (strcmp(pc->data[i].name, name) == 0) {
			return i;
		}
	}
	return -1;
}

//删除指定联系人信息
void _delContact(Contact* pc) {
	assert(pc);
	if (pc->sz == 0) {
		printf("null Contact! err!\n");
		system("pause");
		return;
	}
	//删除
	//1.找到
	char name[NAME_MAX] = { 0 };
	printf("please input the name:");
	scanf("%s", name);
	int pos = _findByName(pc, name);//找到了,返回下标;找不到,返回负一
	if (pos == -1) {
		printf("cannot find the PeoInfo,err!");
		system("pause");
		return;
	}
	//2.删除
	//这里要稍微分析清楚,是sz-1,不要越界了
	for (int j = pos; j < pc->sz - 1; j++) {
		pc->data[j] = pc->data[j + 1];
	}
	pc->sz--;//这个不要忘记了
	printf("successfully deleted\n");
	system("pause");
}



//查找一个人的信息
void _searchContact(const Contact* pc) {
	char name[NAME_MAX] = { 0 };
	printf("please input the name:>");
	scanf("%s", name);
	int pos = _findByName(pc, name);
	if (pos == -1) {
		printf("cannot find the PeoInfo,err!");
		return;
	}
	printf("--------------------------------------------------\n");
	printf("the info is:\n");
	printf("%-20s %-5s %-5s %-12s %-30s\n", "姓名", "年龄", "性别", "电话", "地址");
	printf("%-20s %-5d %-5s %-12s %-30s\n", pc->data[pos].name, pc->data[pos].age,
		pc->data[pos].sex, pc->data[pos].tele, pc->data[pos].addr);
	printf("--------------------------------------------------\n");
	system("pause");
}


void _modifyContact(Contact* pc) {
	printf("whose Info do you want to modify:\n");
	char name[NAME_MAX] = { 0 };
	scanf("%s", name);
	int pos = _findByName(pc, name);
	if (pos == -1) {
		printf("cannot find the PeoInfo,err!\n");
		printf("--------------------------------------------------\n");
		system("pause");
		return;
	}
	//修改
	printf("--------------------------------------------------\n");
	printf("you are modifing %s's info\n", pc->data[pos].name);
	printf("please input the name:");
	scanf("%s", pc->data[pos].name);
	printf("please input the age:");
	scanf("%d", &(pc->data[pos].age));
	printf("please input the sex:");
	scanf("%s", pc->data[pos].sex);
	printf("please input the tele_num:");
	scanf("%s", pc->data[pos].tele);
	printf("pleae input the address:");
	scanf("%s", pc->data[pos].addr);
	printf("\n");
	printf("successfully modified!\n");
	printf("--------------------------------------------------\n");
	printf("\n");
	system("pause");
}


//保存通讯录
void _saveContact(const Contact* pc) {
	FILE* pf = fopen("contact.dat", "wb");//以二进制的形式写进去
	if (pf == NULL) {
		perror("_saveContact::fopen");
		return;
	}
	//写文件
	for (int i = 0; i < pc->sz; i++) {//一个一个写进去
		fwrite(pc->data+i,sizeof(PeoInfo),1,pf);
	}
	printf("sucessfully saved!\n");
	printf("--------------------------------------------------\n");
	//关闭文件
	fclose(pf);
	pf = NULL;
}

尾声

看到这里,我们的通讯录小项目就完成了!这里面包含了C语言大部分的重要知识点,对复习是很有帮助的。这也是我们学习C语言来为数不多的一个项目。看完这篇博客,是不是感觉自己的C又行了呢哈哈哈。如果你觉得这篇博客对你有帮助,千万不要忘了点赞关注和收藏后再走噢!

posted @ 2022-04-09 21:11  背包Yu  阅读(12)  评论(0编辑  收藏  举报  来源