可克达拉市网站建设_网站建设公司_jQuery_seo优化
2026/1/16 20:12:44 网站建设 项目流程

《数据结构》课程设计报告

一、设计内容及其要求

资源管理器的模拟实现(A)

  • 用树结构,模拟实现资源管理器的功能,包括:
  • 文件夹的建立;
  • 文件的增加;
  • 文件的删除;
  • 文件夹的删除。
  • 在此基础上增加了复制/移动/重命名文件

二、基本原理

  • C/C++ 基本语法
    兄弟孩子法表示的多叉树的存储结构及其操作
  • 存储多叉树的链式栈的存储结构及其操作
  • 结构体指针的熟练使用
  • 文件读写操作
  • 双序确定一棵唯一二叉树
  • Windows 命令行操作文件的命令

三、具体设计方案(设计说明、框图、流程图等)

设计说明:

Ⅰ、需求分析

用树结构设计一个资源管理器,能够模拟资源管理器的基本功能,如创建文件/文件夹,删除文件/文件夹,重命名文件/文件夹,复制/移动文件,查看文件/文件夹的路径等。

  • 输入的形式和输入值的范围:
    程序的输入形式以字符串和整数为主,字符串主要是输入文件夹/文件的名字,而整数主要是定义文件的类型,1 为文件夹类型,2 为文件类型,还有-1 是退出程序的命令。
  • 输出的形式:
    输出的形式主要是字符串类型,如执行完命令的提醒,函数返回的文件路径,函数输出的文件系统结构等,都由字符串构成。
  • 程序所能达到的功能:
    能够在程序内的文件树中模拟文件的操作过程,结点的增加或删除代表文件的增加或删除,在此基础上,通过命令行将文件树的结构映射到系统盘中,以实现在修改文件树的结点的同时会在操作系统中完成相应的操作,实现真正能够管理部分系统文件的功能,同时能够把程序的操作数据保存下来,在程序下次启动时可以恢复上次操作完的状态,以达到程序的循环重复使用。
  • 测试数据:
    测试数据根据用户使用而定,只要按照用户手册要求输入数据即可。

Ⅱ、概要设计

抽象数据类型

typedef struct TreeNode { } TreeNode, *Tree; //定义存储文件系统的多叉树(兄弟孩子表示法) typedef struct Stack { } Stack, *LinkStack; //定义遍历多叉树需要用到的链式栈

设计各程序模块及各模块之间的调用关系

bool initStack(LinkStack &S)//链栈的初始化 bool Push(LinkStack &S, Tree T) //入栈 bool Pop(LinkStack &S, Tree &T) //出栈 bool getTop(LinkStack S, Tree &T) //取栈顶元素 bool isStackEmpty(LinkStack S) //判断栈是否为空 void createFileSystem(Tree &T) //建立文件系统(创建文件树的根结点) void createNewTreeNode(Tree &T, string name, int type, int level) //创建一个新结点,用于初始化文件系统,新建文件,移动文件,复制文件等操作。 bool initFilePath(Tree &T) //初始化文件系统 (创建文件树的必要结点) void saveFileSystem(Tree T) //保存文件系统数据到TXT文件中 void traverseFilePath(Tree T) //遍历文件系统,并输出文件系统结构

重载:

Tree findFileNode(Tree T, string filename) //在文件树上找到名为filename的文件夹,用于查找、新建、删除、重命名、移动、复制文件等操作 Tree findFileNode(Tree T, string parentname, string filename) //在文件树上找到父母为parentname,名为filename的文件,用于查找、新建、删除、重命名、移动、复制文件等操作 Tree findFirstNextEmptyRoot(Tree T) //找到第一个nextSibling为空的根节点,在插入(创建新文件)时要使用 bool checkSameName(Tree T, string filename, int type) //重名判别函数(判定一个文件夹中是否已有同名文件),在新建、移动、复制、重命名文件时要用到此函数

重载:

string getFilePath(Tree T, string filename) //获取名为filename的文件夹在系统中的路径 string getFilePath(Tree T, string parentname,string filename) //获取父母为parentname,名为filename的文件在系统中的路径 bool createNewFile(Tree &T, string filename, string newfilename, int type) //创建一个新的文件/文件夹

重载:

bool deleteFile(Tree &T, string defilename) //删除名为defilename的文件夹 bool deleteFile(Tree &T,string parentname, string defilename) //删除父母为parentname,名为defilename的文件夹

重载:

bool reNameFile(Tree &T, string filename, string newfilename) //重命名名为filename的文件夹为newfilename bool reNameFile(Tree &T, string parentname,string filename, string newfilename) //重命名父母为parentname,名为filename的文件为newfilename bool lsFileChild(Tree T, string filename) //列出名为filename的文件夹下的所有文件 bool moveFile(Tree &T, string mvparentname, string mvfilename, string tofilename) //移动父母为mvparentname,名为mvparentname的文件到名为tofilename的文件夹中 bool copyFile(Tree &T, string parentname, string cpfilename, string tofilename) //复制父母为parentname,名为cpfilename的文件到名为tofilename的文件夹中 //从外存中读入文件系统树数据 void readFile() //利用读入的数据重建树 Tree readSystemFromTxt(MainData *preorder, MainData *midorder, int len) //将重建后的树的双亲和前驱还原 void connectPreParent(Tree &T) void menu() //程序运行显示的菜单 void run(Tree &T) //main函数的入口

各模块之间的调用关系:(下页)

程序执行的流程图:

组员分工:无。

Ⅲ、详细设计

抽象数据类型的具体实现:

typedef struct TreeNode { string fileName; //文件名 int fileType; //fileType为1代表文件夹 为0代表文件 根节点为文件夹类型 int nodeLevel; //存结点的层次,注意并不是在树中的深度,因为采用的是孩子兄弟表示法 存这个数主要是为了输出文件系统目录 struct TreeNode *pre; //前驱 struct TreeNode *parent; //父亲 struct TreeNode *firstChild; //第一个孩子 struct TreeNode *nextSibling; //下一个兄弟 } TreeNode, *Tree; typedef struct Stack { Tree data; struct Stack *next; } Stack, *LinkStack;

主要操作的伪码算法:

bool createNewFile(Tree &T, string filename, string newfilename, int type) // { TreeNode *p = findFileNode(T, filename);//找到要新建的文件所在文件夹的指针 if(type == 1 && checkSameName(T, newfilename, type))//判别文件夹中是否有重名文件夹 { cout<<"Warning:"<<endl; cout<<"文件系统中已存在名为 "<<newfilename<<" 的文件夹!"<<endl; cout<<endl; return false; } else if(type == 0 && checkSameName(p, newfilename, type))//判别文件夹中是否有重名文件 { cout<<"Warning:"<<endl; cout<<"文件夹中已存在名为 "<<newfilename<<" 的文件!"<<endl; cout<<endl; return false; } if(p == NULL) //指针等于空,说明要新建的文件所在文件夹不存在 { cout<<"文件夹不存在!"<<endl; return false; } else if(p->fileType == 0) //结点的类型为文件,文件下面不能再存储文件 { cout<<"你不能在一个文件下创建文件,请选择一个文件夹!"<<endl; return false; } int level = p->nodeLevel+1; //新建分两种情况 if(p->firstChild == NULL) //第一种情况,文件夹是空的,即文件夹的firstChild为空,直接创建 { createNewTreeNode(p->firstChild, newfilename, type, level); //新建一个结点 //修改指针 p->firstChild->pre = p; p->firstChild->parent = p; } else //第二种情况,文件夹不是空的,要找到一个兄弟的下一个兄弟为空的结点,然后创建 { TreeNode *q = findFirstNextEmptyRoot(p); //找到第一个兄弟的下一个兄弟为空的结点 createNewTreeNode(q->nextSibling, newfilename, type, level);//新建一个结点 //修改指针 q->nextSibling->pre = q; q->nextSibling->parent = p; } string path = getFilePath(T, filename,newfilename); //获得新建文件的路径 string cdcmd = path.substr(8, 1)+":"; //cdcmd为切换盘符的命令 system(cdcmd.c_str()); //在命令行中执行 if(type == 1) //创建文件夹 { string cmd = "mkdir "+path.substr(8, path.length()-8); //mkdir为cmd中创建文件的命令 system(cmd.c_str()); 在命令行中执行 } else if(type == 0) //创建文本文件 { string cmd = "type nul>"+path.substr(8, path.length()-8)+".txt"; // type nul>为cmd中创建文本文件的命令 system(cmd.c_str()); //在命令行中执行 } return true; //创建成功 返回true } bool deleteFile(Tree &T, string defilename) //删除文件夹 { if(defilename == "D:" || defilename == "E:" || defilename == "F") //不允许删除D: E: F:盘符 { cout<<"禁止删除的文件夹!!"<<endl; return false; } TreeNode *p = findFileNode(T, defilename); //找到要删除的文件夹所在的结点 if(p == NULL) //如果指针为空,说明要删除的文件夹不存在 { cout<<"要删除的文件不存在!"<<endl; //删除文件夹时给予适当的提醒 string ensure; cout<<"你选择删除的是一个文件夹,你确定要这么做吗?如果这么做的话,该文件夹下的文件和文件夹将都被删除!"<<endl; cout<<"如果你依然要这么做,请输入yes,如果你要放弃操作,请输入no:"; cin>>ensure; if(ensure == "no") return false; string path = getFilePath(T, defilename); //拿到文件夹的路径 string cdcmd = path.substr(8, 1)+":"; //cd切换盘符 system(cdcmd.c_str()); //cmd中执行命令 LinkStack S; initStack(S); //初始化存储该文件夹的所有孩子的链式栈 Tree rootDel = p; Tree tmp; if(p->firstChild != NULL) //把该文件夹下所有结点压入栈 { Push(S, p->firstChild); = p->firstChild; while(p->nextSibling != NULL) { Push(S, p->nextSibling); = p->nextSibling; } } while(!isStackEmpty(S)) //递归删除文件夹下的所有文件及文件夹 { Pop(S, tmp); if(tmp->fileType == 1) deleteFile(T, tmp->fileName); else deleteFile(T, rootDel->fileName,tmp->fileName); } //最后执行删除该文件夹的操作 分两种情况 if(rootDel->pre->firstChild == rootDel) //情况1 .删除文件夹结点的前驱是该文件夹结点的父母 { //修改相应指针并删除该结点 rootDel->pre->firstChild = rootDel->nextSibling; if(rootDel->nextSibling != NULL) rootDel->nextSibling->pre = rootDel->pre; delete(rootDel); } else //情况2. 删除文件夹结点的前驱是该文件夹结点的兄弟 //修改相应指针并删除该结点 rootDel->pre->nextSibling = rootDel->nextSibling; if(rootDel->nextSibling !=NULL) rootDel->nextSibling->pre = rootDel->pre; delete(rootDel); } string cmd = "rmdir /s "+path.substr(8, path.length()-8); //rmdir 为cmd中递归删除文件夹的命令 system(cmd.c_str()); //执行递归删除文件夹的命令 return true; //删除文件夹成功,返回true } bool deleteFile(Tree &T,string parentname, string defilename) //删除文件 { TreeNode *p = findFileNode(T, parentname,defilename); //找到要删除的文件结点 if(p == NULL) //若指针为空,说明要删除的文件不存在 { cout<<"要删除的文件不存在!"<<endl; } string path = getFilePath(T, parentname,defilename); //获取该文件的路径 string cdcmd = path.substr(8, 1)+":"; //切换盘符的命令 system(cdcmd.c_str()); //在cmd中执行切换盘符的命令 string cmd = "del "+path.substr(8, path.length()-8)+".txt"; //del为cmd中删除文件的命令 system(cmd.c_str()); //在cmd中执行删除文件的命令 //在树中删除该文件结点 分两种情况 if(p->pre->firstChild == p) //情况1. 删除文件夹结点的前驱是该文件夹结点的父母 { //修改相应指针并删除该结点 p->pre->firstChild = p->nextSibling; if(p->nextSibling != NULL) p->nextSibling->pre = p->pre; delete(p); } else //情况2. 删除文件夹结点的前驱是该文件夹结点的兄弟 { //修改相应指针并删除该结点 p->pre->nextSibling = p->nextSibling; if(p->nextSibling != NULL) p->nextSibling->pre = p->pre; delete(p); } return true; } //从外存中读入文件系统树数据 void readFile() { preorder = new MainData[100]; midorder = new MainData[100]; ifstream infile("fileone.txt"); if(!infile) cerr<<"文件打开失败!"; int k = 0; while(!infile.eof()) { infile>>midorder[k].filename>>midorder[k].fileType>>midorder[k].nodeLevel; ++k; } infile.close(); ifstream input("filetwo.txt"); if(!input) cerr<<"文件打开失败!"; int j = 0; while(!input.eof()) { input>>preorder[j].filename>>preorder[j].fileType>>preorder[j].nodeLevel; ++j; } input.close(); LEN = k-1; } //利用读入的数据重建树 Tree readSystemFromTxt(MainData *preorder, MainData *midorder, int len) { MainData rootKey = preorder[0]; Tree root = new TreeNode; root->fileName = rootKey.filename; root->fileType = rootKey.fileType; root->nodeLevel = rootKey.nodeLevel; root->firstChild = NULL; root->nextSibling = NULL; root->parent = NULL; root->pre = NULL; if(len == 1 && preorder->filename == midorder->filename) return root; MainData *rootMidOrder = midorder; int leftLen = 0; while(rootMidOrder->filename != rootKey.filename && rootMidOrder <= (midorder+len-1)) { ++rootMidOrder; ++leftLen; } //if(rootMidOrder->filename != rootKey.filename ) return NULL; //error if(leftLen > 0) { root->firstChild = readSystemFromTxt(preorder+1, midorder, leftLen); } if(len-leftLen-1 >0) { root->nextSibling = readSystemFromTxt(preorder+leftLen+1, rootMidOrder+1, len-leftLen-1); } return root; } //将重建后的树的双亲和前驱还原 void connectPreParent(Tree &T) { LinkStack S; initStack(S); TreeNode *par; TreeNode *now; TreeNode *prenow; TreeNode *p = T; TreeNode *q = new TreeNode; while(p || !isStackEmpty(S)) { if(p) { Push(S, p); par = p; if(p->firstChild != NULL) { p->firstChild->parent = par; p->firstChild->pre = p; now = p->firstChild; while(now->nextSibling != NULL) { prenow = now; now = now->nextSibling; now->parent = par; now->pre = prenow; } } = p->firstChild; } else { Pop(S, q); = q->nextSibling; } } } //以上只给了创建和删除以及缓存数据相关的代码,其它的详见源码文件

函数和过程的调用关系图:

Ⅳ、调试分析

实现过程中遇到的问题及产生的思考

如何存储文件系统易于操作? 答:多叉树与文件系统的结构是非常相似的,但是一般的多叉树不易于存储和操作,所以选择兄弟孩子树来存储多叉树。这样的话,一个根节点就是一个文件夹,而他的孩子以及他的孩子的兄弟就是它下面的文件,结构清晰明了。

怎么区分文件系统中的文件和文件夹? 答:很容易知道文件和文件夹在文件系统中的操作是截然不同的,比如要创建一个文件,这个文件只能放在一个文件夹下,而不能放在一个文件下,再比如要删除一个文件,只需要找到它并删除就可以了,但是要删除一个文件夹,就要先把它目录下的所有文件及文件夹删除掉,再删除这个文件夹,很显然这是一个递归的过程。所以在定义文件树结点的时候,引入了 type 这一变量,当这个变量等于 1 时,文件的类型为文件夹,当这个变量等于 0 时,文件的类型为文件,这样对文件的类型加以区分以后,再分别对文件和文件夹进行创建、删除等操作。

怎么避免文件系统中的重名问题? 答:当你要在一个文件夹中创建文件时,如果这个文件夹中已经有一个同名的文件,就会造成查找文件时的文件不唯一的错误,所以要保证每个文件夹中不能有重名文件。为此引入 checkSameName()函数,在执行创建、移动、复制时,要首先检查该文件夹目录下是否已存在重名文件。

怎么去遍历整个文件系统并把文件系统的结构输出? 答:对兄弟孩子树采用先序非递归的方式去遍历即可,这里不采用递归的方式的原因是:如果单纯的输出每个结点的名称是体现不出文件系统的结构的,因此我在程序中增加了控制文件系统层次的循环,要在函数中插入这个循环,最好是采用非递归的方式去遍历,因为非递归方式的遍历比较好控制。还有就是这个循环要借助于文件结点的 nodeLevel 来实现,nodeLevel 及此结点在文件系统结构中的层次,比如第一级文件夹的层次为 1,而它内部的文件和文件夹的层次为 2…以此类推。

在创建新文件时要注意的问题? 答:首先如果要新建文件的文件夹存在的话,第一步要执行判重操作,也就是判断该文件夹下是否有重名函数,如果没有,则要考虑接下来的两种情况:1.该文件夹目前是空的 2.该文件夹目前不是空的 如果是情况的话,说明该文件的 firstchild 是空的,插入到此位置即可,如果是情况二,则要先找到该文件结点的孩子的第一个兄弟为空的兄弟结点,在此位置插入新的文件结点。

在删除文件或文件夹时要注意的问题? 答:比较特殊的情况是删除文件夹的操作,之前提到过,删除文件夹要先删除该文件夹下的所有文件及文件夹,然后再删除这个文件,这是一个递归的过程。刚开始我犯了个错误,就是直接删除了这个文件夹结点,这样的后果就是这个文件夹结点的兄弟结点也被我删掉了,这是肯定不行的,因为我只想删除该文件夹结点的孩子和孩子的兄弟。于是我在函数中引入了一个栈来实现递归删除文件夹,在删除文件夹时,首先让它的孩子和孩子的兄弟入栈,然后出栈递归删除它们,最后删除根节点(也就是要删除的该文件结点),在删除根节点时还要注意分两种情况,1.根节点的前驱是根节点的父亲 2.根节点的前驱是根节点的兄弟(这里就又蹦出一个问题就是要保存结点的前驱,因为在删除结点时要修改它的前驱结点,而它的前驱结点可不一定是它的父母,因此在文件树的定义中引入 pre 指针,存储结点的前驱) 根据这两种情况来修改指针,在删除普通文件时同样要考虑这两种情况,不过不用考虑递归删除的问题了。

有一个问题就是如果我在系统中进行了一系列操作以后,当我退出程序后,程序中的文件树就没了,这样下次再运行程序就又是一个新的文件系统,这样的可用性是非常差的,于是我就想能不能有一种办法将文件系统树的数据保存下来,在下次运行程序时能够恢复上次运行的状态,可不可行呢? 答:解决这个问题的关键就是兄弟孩子树的指针我如何保存呢?想把指针存到 txt 文本文件里是不现实的,所以只能通过复盘的方式将原来的兄弟孩子树重建。首先用到的第一个理论是根据二叉树的先序遍历和中序遍历可以确定唯一的一棵二叉树(兄弟孩子树),通过这个理论,我就可以将原来兄弟孩子树的 firstChild 和 nextSibling 指针恢复了。还有一个问题没有解决,那就是文件系统树中有前驱和双亲两个指针,并且这两个指针是非常重要的,如何将他们恢复到原来的状态呢?既然我已经恢复了唯一确定的一棵二叉树(兄弟孩子树),那么他们的前驱和双亲也是确定的,只要想出一种策略将他们恢复即可,我的思路就是让每个根结点和它的 firstChild 以及 firstChild 的 nextSibling 们根据前驱和双亲的特点来非递归恢复。(实现函数为 void connectPreParent(Tree &T))。

改进设想

虽然兄弟孩子树表示的文件系统结构很清晰明了,但是缺点也很明显,由这种结构构成的文件系统的树太高了,树太高会直接影响文件的查询速度,进而影响各个操作的执行时间,因此找一种更合适的树来存储文件系统会更好,我想的是 B-树,这种树也是多叉树,和文件系统的结构很像,而 B-树的阶数可以代表每个文件下最多存储的文件个数,但是 B-树并不只是这么简单,它还是一颗平衡树,也就是说在新建、删除的过程中,B-树的结构是不断调整的,这就产生了一个问题,在调整过程中文件的位置会改变,因此不能用 B-树直接存储文件系统,而是用 B-树建立文件索引,然后去存储文件系统,但是我现在的能力还有限,不能把目前的系统迁移,只是先实现了 B-树的各个操作,代码附在附录中,等我有能力实现了,我会再完善这个系统。

经验和体会

在调试的过程中加固了对指针的操作的理解,特别是对多个指针同时修改时的操作,以前感觉操作两三个指针就要晕了,但是对指针的操作深入理解之后,我感觉指针的操作也没有想象中的那么复杂。做这个程序我最大的体会就是要有明确的目标和解决问题的方向,比如要做一个特定功能的函数,要先确定这个函数最后能够实现什么效果,然后再去考虑这个函数的内部细节,尽量考虑全面,如果在调试过程中发现问题,从纸上把过程画出来,考虑是哪个环节出了问题,然后去检查代码,这样的方式是比较有效的。如果还是分析不出来,可以用高级的 IDE 进行逐步调试,跟着代码一步一步走,总能找出问题的关键所在。

四、设计小结

本次课程设计选择了资源管理器这个题目,感觉难度适中,刚开始是先做出了一个纯模拟系统,只能在命令行中看看文字,不能完成实际的操作,在老师指导后,老师建议我能够实现真正操作系统中的文件,我想了想,感觉单纯的在命令行中显示操作确实过于枯燥了,没有什么实际作用。于是我在原版代码的基础上,实现了程序与 windows 文件系统的连接,连接方式是我们常用的命令行。实现以后,在程序中的操作会对应到实际的系统中去,能够简单模拟资源管理器的功能,如创建、删除、重命名、复制、移动等操作,这样有些操作就不用到我的电脑里去操作了,也不用到命令行中去操作了,可以在这个程序中直接按照指令输入命令,执行命令,完成文件系统的操作,同时能够查看程序中创建的文件系统的目录结构和信息,有一定的可操作性。再后来,我感觉这个系统的功能虽然比较完善了,但是复用性太低了,只能当个摆设,于是我在最后实现了系统的数据保存以达到循环使用的目的。这也是我第一次写单个 C++ 程序超过了 1000 行代码,其实在写的过程中并没有感觉到,写完后发现居然有这么多了。在写的过程中碰到了很多困难,每当通过自己的努力解决一个困难,那种愉悦的心情是无法形容的。本次课程设计结束后,我更深刻的理解了 C/C++ 的一些语法以及树和栈等数据结构,并能够用一些数据结构去解决实际的问题,从理论到实践,大概就是这样一个过程。最后,我认识到了数据结构这门课对于计算机专业学生的重要性,它表面上看起来枯燥乏味,但如果真的去用这些东西去解决一些困难的问题时,会发现它就是你解决问题的利器。

五、设计附录(数据、图纸等)

Ⅰ、用户使用说明

程序运行后界面如图所示

你需要按照界面中显示的命令去输入,请不要输入错误的指令,那是无效的。

比如你要创建一个新文件,就输入 new 然后敲回车键 就会跳转到创建新文件的界面,你需要根据界面上的提示来输入,请严格按照界面中提示的规定输入,尽管我在程序中加入了很多判断语句来阻止错误的输入,但是我的考虑肯定是不周全的,你要避免程序出现未知的崩溃,一般情况下,输入的字符串都是以空格隔开的,当你输入完毕后,敲回车键执行。

Ⅱ、简单的测试使用演示

下面演示创建一个新的文件夹和一个新的文件的操作

  • 首先运行源码出现用户使用说明中的界面
  • 输入 new 回车 进入新建文件界面

在新建文件界面按照指令输入并执行

这时打开我的电脑,打开 E 盘,会发现在 E 盘下新建了文件夹 TESTfile

  • 输入 r 返回,然后再输入 new,进入新建文件界面

按照指令输入,在刚才创建的文件夹 TESTfile 下创建一个新文件 t1,输入完毕后回车执行,此时在我的电脑中打开刚才创建的文件夹 TESTfile,会发现里面有一个 t1.txt 文件

接着又执行了在 TESTfile 文件夹中创建 t2.txt,t3.txt,然后删除了刚才创建的 t1.txt 后的状态。

输入任意字符返回后,输入-1 正常退出程序以保证数据能够保存到外存中,以供下次运行时读入。

再次运行程序,输入 check,可以发现系统恢复了上次运行结束后的状态,你可以接着上次继续进行你的操作。

以上即为该程序的简单使用测试。

继续使用的话只要严格按照程序给出的指令操作即可。

Ⅲ、源程序

资源管理器源代码在文件 SourceCode 中,名为资源管理器 Source Manager-v1.0.cpp

另外在 SourceCode 中还附上了 B-树的实现代码,这是可能的一种改良该系统执行速度的方案。

六、参考资料

数据结构 C 语言版(第二版)--严蔚敏 李冬梅 吴伟民

C 语言程序设计(第二版)

C++ Primer Plus(第四版)

Windows Command Prompt 命令用法

♻️ 资源

大小:513KB

➡️资源下载:https://download.csdn.net/download/s1t16/87400399

注:更多内容可关注微信公众号【神仙别闹】,如当前文章或代码侵犯了您的权益,请私信作者删除!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询