西南交通大学数据结构C课设:学生数据管理系统

(现已添加文件内容)

(版本:2.0,修改内容:排序,存储,装入)

要求:8286c1f1e26545918fc81bd448384471.jpeg

         首先对题目进行简单的分析:数据管理,只需要简单的链表即可完成。管理的方式有录入(将从键盘中打入的数据加入链表的最后),存储(将程序的学生数据存储到文件中),装入(将文件中的学生数据装入程序中),修改(修改学生数据中的某一项),插入(将学生数据插入到链表之中),删除(删除学生数据),查找(通过学号查找学生的数据),排序(将学生由学号从小到大排序),显示(显示全部学生数据)。

        难点非常清楚:1.文件和程序的数据相互转移。2.链表的排序。

1.模块化编程。

        为了使得代码的修改和理解更加简单,在本课设中我推荐用模块化编程。

        这种大程序少说也有500行,如果只放在一个文件,将会导致深度过深,代码又长又臭。

        为了实现模块化编程,在这里我会讲一下模块化编程的简单实现。

A.创建头文件

        很多大学生都忽视了头文件的重要性,通过一个头文件,我们可以将一个结构体用于两个c文件中(在本课设中充分体现)。有了头文件,以后若需要使用到现在编写的代码,也可以通过头文件快速调用。头文件的编写如下:

        0130c1bdb7554685ab171139dd3657ac.png

        其中最重要的就是用红色框圈定的部分。这是每个头文件不可缺少的内容,ifndef和define后面的内容就是头文件名,格式入上,注意前面有两个下划线。

        将c文件中的函数放在头文件中声明,将结构体也放在头文件中使用,将会大大减少c文件的编写量。

B.在c文件中的引用

        引用方式如下图

8cad1f860e2040f3b743ad4d45b6fd5e.png

 62ee403ac5f749f89b07bfbfb5e51f92.png

 用该格式即可引用头文件。

2.整体代码展示

        整体代码先放在这里,在后文将会一一解释。

main.cpp文件代码

#include <stdio.h>		
#include "line_list.h"	//线性表操作头文件
#include <stdlib.h>

extern struct Student;
//名称:学生数据管理。
//内容:录入,存储,装入,修改,插入,删除,查找,排序,显示。
//数据结构:线性表。
//数据对象:班或系的学生记录。
//学生记录:学号,姓名,年龄,性别,家庭住址,入学时间,家庭电话号码。

int main()
{
	struct Student* head;		//学生数据的头指针。
	int state= -1;	//初始状态,以进入和在选择操作中使用。
	head = (struct Student* )malloc(150);
	head->pNextNode=NULL;
	printf("欢迎使用本学生数据管理系统");
	while(state!=0)
	{
		printf("请输入您想要进行的操作的数字序号:0.退出  1.录入  2.修改  3.插入  4.删除  5.查找  6.排序  7.显示 8.装入 9.存储\n");
		scanf("%d",&state);
		getchar();	//吃回车
		while(state==1)
		{
			List_LuRu(head);				//将尾指针放进去,新建链表来改变尾指针,最后将尾指针赋值回去。
			printf("如果需要继续录入,则输入1;退出录入,则输入-1。\n");
			scanf("%d",&state);
			getchar();
		}
		while(state==2)
		{
			int lin,col;		//输入序号和列数以修改
			List_XianShi(head);	//显示帮助输入
			printf("请问您要修改序号几的第几列内容?(一次修改一个内容,空格隔开输入)\n");
			scanf("%d %d",&lin,&col);
			List_XiuGai(head,lin,col);	//修改函数
			printf("如果需要继续修改,则输入2;退出修改,则输入-1。\n");
			scanf("%d",&state);	
		}
		while(state==3)
		{
			int lin;	//输入序号
			List_XianShi(head);	//显示帮助输入
			printf("请问您要插入到第几序号后:\n");
			scanf("%d",&lin);
			List_ChaRu(head,lin);	//插入函数
			printf("如果需要继续插入,则输入3;退出插入,则输入-1。\n");
			scanf("%d",&state);
		}
		while(state==4)
		{
			int lin;
			List_XianShi(head);
			printf("请问您要删除第几序号的学生数据:");
			scanf("%d",&lin);
			List_ShanChu(head,lin);	//删除函数
			printf("如果需要继续删除,则输入4;退出删除,则输入-1。\n");
			scanf("%d",&state);
		}
		while(state==5)	//查找
		{
			int aim;	//查找方式
			printf("请问您是要什么方式查找数据?\n");
			printf("1.学号	2.序号\n");
			scanf("%d",&aim);
			List_ChaZhao(head,aim);	//查找函数,输入为查找方式
			printf("如果需要继续查找,则输入5;退出查找,则输入-1。\n");
			scanf("%d",&state);
		}
		while(state==6)	//排序
		{
			printf("正在进行学号排序in....(若数据过多,则缓冲时间会比较长)\n");
			List_PaiXv(head);
			printf("如果需要继续排序,则输入6;退出排序,则输入-1。\n");
			scanf("%d",&state);
		}
		while(state==7)
		{
			List_XianShi(head);				//将头指针放进去,以获得整个链表的所有信息。
			printf("如果需要继续显示,则输入7;退出显示,则输入-1。\n");
			scanf("%d",&state);
		}
		while(state==8)
		{
			printf("正在装入data中。。。。。\n");
			List_fLuRu(head);
			printf("录入成功!\n");
			state=-1;
		}
		while(state==9)
		{
			printf("正在储存数据到sum中。。。。\n");
			List_fCunChu(head);
			printf("存储成功!\n");
			state=-1;
		}
	}
}

line_list.cpp代码

#include <stdio.h>
#include <stdlib.h>
#include "line_list.h"
#include <string.h>

void List_LuRu(struct Student *head)
{
	struct Student* Temp;
	struct Student* Temp2;
	Temp2 = head;
	while(Temp2->pNextNode!=NULL)
	{
		Temp2=Temp2->pNextNode;
	}
	printf("请输入学生的学号 ,姓名 ,年龄 ,性别(b/g) ,家庭住址 ,入学时间(xx/xx/xx)  家庭电话号码。(中间空格隔开)\n");
	Temp = (struct Student*)malloc(150);	//新建一个动态链表
	scanf("%s %s %s %s %s %s %s",Temp->ID,Temp->name,Temp->age,Temp->sex,Temp->address,Temp->Time,Temp->number);//该表内容的输入
	Temp2->pNextNode=Temp;	//将目前尾指针的指向该链表
	Temp2=Temp;
	Temp2->pNextNode=NULL;	//尾指针的下一项指向NULL
}
void List_XianShi(struct Student *head)	//显示函数
{
	int temp=0;
	struct Student* List;
	List = head->pNextNode;	//初始指向头指针下一个链表。头指针是空的
	while(List!=NULL)	//只要List不是空的,就执行
	{
		temp=temp+1;
		printf("序号%d:  ",temp);
		printf("%s %s %s %s %s %s %s\n",List->ID,List->name,List->age,List->sex,List->address,List->Time,List->number);
		List=List->pNextNode;	//指向下一个List
	}
}
void List_XiuGai(struct Student *head,int lin,int col)	//修改函数	
{
	struct Student* List;
	List=head->pNextNode;
	int Temp=lin;
	while(lin>1)	//指针不断指向下一个,直到找到目标指针
	{
		lin=lin-1;
		List=List->pNextNode;
	}
	if(lin==1)	//找到了
	{
		if(List==NULL)		//如果是空指针,报错
		{
			printf("请输入合法的序号。\n");
		}
		else if(col==1)
		{
			printf("定位到%d号的学号\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->ID);
		}
		else if(col==2)
		{
			printf("定位到%d号的姓名\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->name);
		}
		else if(col==3)
		{
			printf("定位到%d号的年龄\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->age);
		}
		else if(col==4)
		{
			printf("定位到%d号的性别\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->sex);
		}
		else if(col==5)
		{
			printf("定位到%d号的地址\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->address);
		}
		else if(col==6)
		{
			printf("定位到%d号的入学时间\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->Time);
		}
		else if(col==7)
		{
			printf("定位到%d号的电话号码\n",Temp);
			printf("请输入您的修改后的内容:");
			scanf("%s",List->number);
		}
		else	//若col不在范围内,报错
		{	
			printf("请输入合法的列号\n");
		}
	}
	else		//如果是负或0的序号
	{
		printf("请输入合法的序号\n");
	}
}
void List_ChaRu(struct Student *head,int lin)	//插入函数
{
	struct Student* List_begin;	//前一个
	struct Student* List_end;	//后一个
	struct Student* Temp;		//中间插的
	List_begin=head->pNextNode;	
	List_end=List_begin->pNextNode;//将后一个指针接于前一个指针后
	while(lin>1)	//寻找目标位置
	{
		lin=lin-1;
		List_begin=List_begin->pNextNode;
		List_end=List_begin->pNextNode;
	}
	if(lin==1)		//找到了
	{
		Temp=(struct Student*)malloc(150);	//创建新链表
		printf("请输入学生的学号 ,姓名 ,年龄 ,性别(b/g) ,家庭住址 ,入学时间(xx/xx/xx)  家庭电话号码。(中间空格隔开)\n");
		scanf("%s %s %s %s %s %s %s",Temp->ID,Temp->name,Temp->age,Temp->sex,Temp->address,Temp->Time,Temp->number);
		List_begin->pNextNode=Temp;//插在目标位置
		Temp->pNextNode=List_end;
	}
	else	//序号为0或负,报错
	{
		printf("输入不合法!\n");
	}
}
void List_ShanChu(struct Student* head,int lin)
{
	struct Student* List_begin;	//前一个
	struct Student* Temp;		//删除目标
	Temp=head->pNextNode;
	List_begin=head;
	while(lin>1)		//寻找
	{
		lin=lin-1;
		List_begin=Temp;
		Temp=Temp->pNextNode;
	}
	if(lin==1)		//找到了,删除
	{	
		List_begin->pNextNode=Temp->pNextNode;	//让前一个指针指向后一个指针
		printf("删除成功!\n");
	}
	else	//序号错误
	{
		printf("输入非法序号\n");
	}
}
void List_ChaZhao(struct Student* head,int aim)	//目前只有两种查找方式
{
	struct Student* List;
	char ID[15];
	int sum;	//查找的序号或者学号
	int Temp;	//临时存储信息
	List=head->pNextNode;
	if(aim==0)
	{
		printf("请输入查找的序号:");
		scanf("%d",&sum);
		Temp=sum;
		while(sum>1)		//寻找查找序号目标
		{
			List=List->pNextNode;
			sum=sum-1;
		}
		if(sum==1)			//找到了
		{
			printf("序号%d:  ",Temp);
			printf("%s %s %s %s %s %s %s\n",List->ID,List->name,List->age,List->sex,List->address,List->Time,List->number);
		}
		else				//没找到
		{
			printf("输入序号不合法\n");
		}
	}
	else if(aim==1)			//寻找查找学号目标
	{
		printf("请输入查找的学号:");
		scanf("%s",ID);
		while(List!=NULL)
		{
			if(strcmp(List->ID,ID)!=0)
			{
				List=List->pNextNode;
			}
			else
			{
				break;
			}
		}
		if(strcmp(List->ID,ID)==0)		//找到了
		{
			printf("学号%s:  ",ID);
			printf("%s %s %s %s %s %s %s\n",List->ID,List->name,List->age,List->sex,List->address,List->Time,List->number);
		}
		else				//没找到
		{
			printf("没有该学号的数据!\n");
		}
	}
	else			//查找方式错误
	{
		printf("输入查找目标不合法\n");
	}
}
void List_PaiXv(struct Student* head)
{
	//本代码将采用简单的冒泡排序法.
	struct Student* List_begin_1;	//第一轮循环前一个
	struct Student* List_begin_2;	//第二轮循环前一个
	struct Student* List_in_2;	//第二轮循环中间一个
	struct Student* List_end_2;	//第二轮循环后一个
	List_begin_1 = head;
	while(List_begin_1->pNextNode->pNextNode!=NULL)		//以下为简单的冒泡排序法
	{
		List_begin_2 = head;
		List_in_2 = List_begin_2->pNextNode;
		List_end_2 = List_in_2->pNextNode;
		while(List_end_2!=NULL)
		{
			if(strcmp(List_in_2->ID,List_end_2->ID)>0)
			{
				List_begin_2->pNextNode=List_end_2;
				List_in_2->pNextNode=List_end_2->pNextNode;
				List_end_2->pNextNode=List_in_2;
				List_in_2 = List_begin_2->pNextNode;
				List_end_2 = List_in_2->pNextNode;
			}
				List_begin_2=List_begin_2->pNextNode;
				List_in_2=List_in_2->pNextNode;
				List_end_2=List_end_2->pNextNode;	
		}
		List_begin_1=List_begin_1->pNextNode;
	}
}
void List_fLuRu(struct Student* head)		//装入函数
{
	FILE* fp;	//文件指针
	int n=0;		//数组的位置
	int state=0;//链表的位置
	char ch;	//录入字符
	struct Student* List;//链表指针
	struct Student* Temp;
	List=(struct Student*)malloc(150);
	head->pNextNode=List;
	if((fp=fopen("C:\\Users\\池上桜\\Desktop\\学习\\课设\\Project\\data.txt","r+"))==NULL)	//打开文件
	{
		printf("No file!\n");
		exit(0);
	}
	while(!feof(fp))
	{
		ch=fgetc(fp);
		if(ch!='\n')//换行
		{
			if(ch!=' ')	//空格切换一次链表位置
			{
				if(state==0)	//链表在ID位置,以下同理
				{
					List->ID[n]=ch;
					List->ID[n+1]='\0';
				}
				else if(state==1)
				{
					List->name[n]=ch;
					List->name[n+1]='\0';
				}
				else if(state==2)
				{
					List->age[n]=ch;
					List->age[n+1]='\0';
				}
				else if(state==3)
				{
					List->sex[n]=ch;
					List->sex[n+1]='\0';
				}
				else if(state==4)
				{
					List->address[n]=ch;
					List->address[n+1]='\0';
				}
				else if(state==5)
				{
					List->Time[n]=ch;
					List->Time[n+1]='\0';
				}
				else if(state==6)
				{
					List->number[n]=ch;
					List->number[n+1]='\0';
				}
				else
				{
					printf("格式有误!");
					exit(0);
				}
				n++;
			}
			else
			{
				state=state+1;
				n=0;
			}
		}
		else
		{
			Temp = (struct Student*)malloc(150);
			List->pNextNode=Temp;
			List=Temp;
			List->pNextNode=NULL;
			n=0;
			state=0;
		}
	}
	fclose(fp);
}
void List_fCunChu(struct Student* head)
{
	FILE* fp;	//文件指针
	struct Student* List;//链表指针
	List=head->pNextNode;
	if((fp=fopen("C:\\Users\\池上桜\\Desktop\\学习\\课设\\Project\\sum.txt","w+"))==NULL)	//打开文件
	{
		printf("No file!\n");
		exit(0);
	}
	while(List->pNextNode!=NULL)
	{
		fputs(List->ID,fp);
		fputc('\0',fp);
		fputs(List->name,fp);
		fputc('\0',fp);
		fputs(List->age,fp);
		fputc('\0',fp);
		fputs(List->sex,fp);
		fputc('\0',fp);
		fputs(List->address,fp);
		fputc('\0',fp);
		fputs(List->Time,fp);
		fputc('\0',fp);
		fputs(List->number,fp);
		fputc('\n',fp);
		List=List->pNextNode;
	}
		fputs(List->ID,fp);
		fputc('\0',fp);
		fputs(List->name,fp);
		fputc('\0',fp);
		fputs(List->age,fp);
		fputc('\0',fp);
		fputs(List->sex,fp);
		fputc('\0',fp);
		fputs(List->address,fp);
		fputc('\0',fp);
		fputs(List->Time,fp);
		fputc('\0',fp);
		fputs(List->number,fp);
		fclose(fp);
}

 line_list.h代码

#ifndef __LINE_LIST_H
#define __LINE_LIST_H

struct Student {
    char ID[15];          	//学生学号
    char name[10];     	//学生姓名
    char age[3];			//学生年龄
	char sex[5];			//性别
	char address[30];	//住址
	char Time[15];		//入学时间
	char number[15];		//电话号码
    struct Student * pNextNode;
};

void List_LuRu(struct Student *head);
void List_XianShi(struct Student *head);
void List_XiuGai(struct Student *head,int lin,int col);
void List_ChaRu(struct Student *head,int lin);
void List_ShanChu(struct Student* head,int lin);
void List_ChaZhao(struct Student* head,int aim);
void List_PaiXv(struct Student* head);
void List_fLuRu(struct Student *head);	//装入
void List_fCunChu(struct Student* head); //存储
#endif

注意在头文件的最后一行,留一行空行,以防某些玄学错误.

3.代码全分析

A.主函数main.cpp中的代码:

        由于main.cpp中代码逻辑简单,所以就只需要一栏就可以解释清楚.

        首先,创建一个空链表作为头节点,这种创建方式是官方数据结构书上的标准线性表创建方式,能够方便后文许多操作(如排序).然后让该链表的下一项指向NULL,作为结尾的判断,这个链表就是所有数据存储的位置了.

        然后用scanf接收用户想要进行的操作,在while循环中不断询问用户的需求,以达到管理系统的目的.每个二层while中需要添加第二个scanf,来接收用户在使用完一个功能后的意向.

        这大致就是主函数的内容了.

B.头文件line_list.h的代码:

        line_list.h就是简单的头文件,在这里需要说的就是里面的结构体,在这里,结构体不能够将重命名定为Student,不然就会报错,虽然我也不知道为什么,但是只要按照上面代码的做法就不会被玄学干扰.

C.函数库line_list.cpp的代码:

        从上到下开始分析

a.录入函数

        录入,是将数据接到目前链表的下一位,由此,需要输入为头结点,由while循环找到目前链表的最后一个结点,然后创建链表,将内容录入,将链表接到最后一个结点,然后将最后一个结点的下一位指向NULL即可.       

        NULL是用于判断链表的尾部的,在这里无疑体现了其优异的功能性.

       代码:d8d7579564384e8dba5d0a5d0ab43335.png

 b.显示函数

        为什么把显示函数放在这么上面的位置?这当然是因为有了显示才能知道自己的编写有无问题.

        显示函数就是从头结点之后开始,显示每一个表中的数据,直到NULL.

        d69dfa6fc18d48f3b070ae3808cb901a.png

c.修改函数

        修改需要到用户自己选择修改的内容,所以输入为序号和列号, 序号是指通过显示而展现出的目前链表的数据的序号,而列号就是修改的目标(如学号为1)

        同样先通过while找到需要修改的数据,再用if语句确定列号,即可精准修改.

     da99426d2bac45f6a67f4e984577251f.png

(部分) 

d.插入函数

        先将链表显示一遍,询问需要插入在序号几之后.在函数中定义前中后三个链表的指针,找到位置后将三个指针相连即可.

9afcfb747a0c4914b9772a30e00eb7c2.png

e. 删除函数

        删除操作同样需要多个指针,找到位置后,将前指针的后一个变成删除目标的后一个,即可直接跳过删除目标,最后free即可

b4aae42c65c6464bb7ef91134cf056dd.png

 f.查找函数

        查找函数是逻辑上最简单的,只需要判断和查找目标是否一致即可.因此在这里不再赘述.

g.排序函数

        在排序函数中,使用的是冒泡排序法,具体实现如下,在本函数中体现了将头指针变成空的好处,多一个指针,避免了if来判断是不是头指针.

        

void List_PaiXv(struct Student* head)
{
	//本代码将采用简单的冒泡排序法.
	struct Student* List_begin_1;	//第一轮循环前一个
	struct Student* List_begin_2;	//第二轮循环前一个
	struct Student* List_in_2;	//第二轮循环中间一个
	struct Student* List_end_2;	//第二轮循环后一个
	List_begin_1 = head;
	while(List_begin_1->pNextNode->pNextNode!=NULL)		//以下为简单的冒泡排序法
	{
		List_begin_2 = head;
		List_in_2 = List_begin_2->pNextNode;
		List_end_2 = List_in_2->pNextNode;
		while(List_end_2!=NULL)
		{
			if(strcmp(List_in_2->ID,List_end_2->ID)>0)
			{
				List_begin_2->pNextNode=List_end_2;
				List_in_2->pNextNode=List_end_2->pNextNode;
				List_end_2->pNextNode=List_in_2;
				List_in_2 = List_begin_2->pNextNode;
				List_end_2 = List_in_2->pNextNode;
			}
				List_begin_2=List_begin_2->pNextNode;
				List_in_2=List_in_2->pNextNode;
				List_end_2=List_end_2->pNextNode;	
		}
		List_begin_1=List_begin_1->pNextNode;
	}
}

h.装入函数

        灵活运用文件函数,在本代码中使用的是fgetc来一个个从文件中获得数据,方便判断\n等内容,需要注意的是,最后一行不能打回车键。

void List_fLuRu(struct Student* head)		//装入函数
{
	FILE* fp;	//文件指针
	int n=0;		//数组的位置
	int state=0;//链表的位置
	char ch;	//录入字符
	struct Student* List;//链表指针
	struct Student* Temp;
	List=(struct Student*)malloc(150);
	head->pNextNode=List;
	if((fp=fopen("C:\\Users\\池上桜\\Desktop\\学习\\课设\\Project\\data.txt","r+"))==NULL)	//打开文件
	{
		printf("No file!\n");
		exit(0);
	}
	while(!feof(fp))
	{
		ch=fgetc(fp);
		if(ch!='\n')//换行
		{
			if(ch!=' ')	//空格切换一次链表位置
			{
				if(state==0)	//链表在ID位置,以下同理
				{
					List->ID[n]=ch;
					List->ID[n+1]='\0';
				}
				else if(state==1)
				{
					List->name[n]=ch;
					List->name[n+1]='\0';
				}
				else if(state==2)
				{
					List->age[n]=ch;
					List->age[n+1]='\0';
				}
				else if(state==3)
				{
					List->sex[n]=ch;
					List->sex[n+1]='\0';
				}
				else if(state==4)
				{
					List->address[n]=ch;
					List->address[n+1]='\0';
				}
				else if(state==5)
				{
					List->Time[n]=ch;
					List->Time[n+1]='\0';
				}
				else if(state==6)
				{
					List->number[n]=ch;
					List->number[n+1]='\0';
				}
				else
				{
					printf("格式有误!");
					exit(0);
				}
				n++;
			}
			else
			{
				state=state+1;
				n=0;
			}
		}
		else
		{
			Temp = (struct Student*)malloc(150);
			List->pNextNode=Temp;
			List=Temp;
			List->pNextNode=NULL;
			n=0;
			state=0;
		}
	}
	fclose(fp);
}

i.存储函数

        用fputs将链表内容全部存储入文件,记得在吗,每一个后面加‘\0’,表示结束。

void List_fCunChu(struct Student* head)
{
	FILE* fp;	//文件指针
	struct Student* List;//链表指针
	List=head->pNextNode;
	if((fp=fopen("C:\\Users\\池上桜\\Desktop\\学习\\课设\\Project\\sum.txt","w+"))==NULL)	//打开文件
	{
		printf("No file!\n");
		exit(0);
	}
	while(List->pNextNode!=NULL)
	{
		fputs(List->ID,fp);
		fputc('\0',fp);
		fputs(List->name,fp);
		fputc('\0',fp);
		fputs(List->age,fp);
		fputc('\0',fp);
		fputs(List->sex,fp);
		fputc('\0',fp);
		fputs(List->address,fp);
		fputc('\0',fp);
		fputs(List->Time,fp);
		fputc('\0',fp);
		fputs(List->number,fp);
		fputc('\n',fp);
		List=List->pNextNode;
	}
		fputs(List->ID,fp);
		fputc('\0',fp);
		fputs(List->name,fp);
		fputc('\0',fp);
		fputs(List->age,fp);
		fputc('\0',fp);
		fputs(List->sex,fp);
		fputc('\0',fp);
		fputs(List->address,fp);
		fputc('\0',fp);
		fputs(List->Time,fp);
		fputc('\0',fp);
		fputs(List->number,fp);
		fclose(fp);
}

总结:

        难度极低(不包含文件的话),作为课设确实是过于简单了.

        本课设能够巩固如排序,查找,链表,结构体,文件等内容,涉及面广,内容丰富.对于初学者而言是不错的练习题.

        弘扬开源精神,从大一做起~

猜你喜欢

转载自blog.csdn.net/ChiShangying/article/details/130815242