前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >C语言项目 图书管理系统 | 链表

C语言项目 图书管理系统 | 链表

作者头像
CtrlX
发布2023-03-21 11:53:51
发布2023-03-21 11:53:51
74400
代码可运行
举报
文章被收录于专栏:C++核心编程C++核心编程
运行总次数:0
代码可运行

Gaga-Geek图书管理系统(BMS)

项目介绍

本项目会搭建一个控制台操作的图书管理系统。

学习目标:链表操作、文件读写、分文件编写、

目录结构

V1.0.0 – 程序安装包

documents – 项目开发相关文档

  • 需求文档
  • 产品原型图
  • 产品流程图

projects/BMS – 项目文件夹

环境搭建

开发工具

工具

说明

版本

备注

MindMaster

思维导图设计工具

12.0.5

https://www.edrawsoft.cn/mindmaster/

ProcessOn

流程图绘

NULL

https://www.processon.com/

MODAO

原型图绘制工具

v1.2.5

https://modao.cc/brand

开发环境

工具

版本

备注

Windows

11

操作系统

Visual Studio

2022

https://visualstudio.microsoft.com/zh-hans/vs/

需求文档

产品原型图

产品流程图

软件架构:三层架构

核心类文件:

bms.cpp:main.cpp

base.cpp/h:结构体、初始化相关函数

BLL.cpp/h业务逻辑层

DAL.cpp/h数据访问层

UI.cpp/h:表现层

优化类文件:

startinterface.cpp/h:开始界面动画

map.cpp/h:操作界面外壳

tools.cpp/h:控制台优化类函数

point.cpp/h:操作界面外壳元素

详细介绍:

核心类文件:

bms.cpp:main.cpp

包含内容:

  • 初始化
    • 开始动画
    • 初始化头节点
    • 读取文件数据
  • 主循环:调用主页面
代码语言:javascript
代码运行次数:0
运行
复制
#include ..

void init() {
	//1.开始动画
	SetWindowSize(41, 32);//tools设置窗口大小
	SetColor(3);//tools设置开始动画颜色
	StartInterface* start = new StartInterface();//动态分配一个StartInterface类start
	start->Action();//开始动画
	delete start;//释放内存空间
	/*设置关标位置,并输出提示语,等待任意键输入结束*/
	SetCursorPosition(13, 26);
	std::cout << "Press any key to start... ";
	SetCursorPosition(13, 27);
	system("pause");

	//2.初始化头节点
	bHEAD = createbHead();
	mHEAD = createmHead();

	//3.读取文件操作
	readInfoFromFile_b("book.txt", bHEAD);
	readInfoFromFile_m("man.txt", mHEAD);
}

int main() {
	//初始化
	init();

	//调用主界面函数:进行登录-注册页面
	while (true) {
		switch (loginface()) {
		case 0:
			//调用管理界面函数
			SetCursorPosition(5,5);
			guanL();
			break;
		case 1:
			//调用用户界面函数
			SetCursorPosition(5,5);
			yongH();
			break;
		}
	}
	return 0;
}

base.h/cpp:结构体、初始化链表相关函数

具体内容:

  • 结构体:
    • _man/*man
    • _book/*book
  • 全局变量:
    • 单个man类型节点:记录当前登录的人
    • 链表表头:书籍链表表头/成员链表表头
  • 函数:
    • 创建链表表头
    • 创建链表节点
代码语言:javascript
代码运行次数:0
运行
复制
#include ..

typedef struct _man
{
	char Account[MAX];
	long key;//最多九位
	struct _man* next;
	int pw = 0;//0是管理员,1是用户
}*man;

extern man reader;//当前登录的人,不论是管理员还是用户
typedef struct _book
{
	int storage_num ;//编号
	char name[MAX];
	char writer[MAX];
	int num;//该书储存量
	struct _book* next;
}*book;

//创建全局链表表头
extern book bHEAD;
extern man mHEAD;

//创建表头:
book createbHead();
man createmHead();

//创建节点
book createNode(char* name, char* writer, int storage_num, int num);
man createNode(char* Account, long key, int pw);

BLL.cpp:业务逻辑层

主要内容:

  • 链表的核心操作
    • 插入节点(书籍、成员):头插法(首选)/尾插法
  • 删除节点(书籍):通过查找名称进行删除
  • 查找节点(书籍、成员):通过名称进行查找
  • 遍历节点(书籍):用于打印书籍
代码语言:javascript
代码运行次数:0
运行
复制
//一、核心操作

//添加节点到链表头部(头节点后面的第一个节点)
void Add(book headNode, char* name, char* writer, int storage_num, int num) {
	book newNode = createNode(name, writer, storage_num, num);
	newNode->next = headNode->next;
	headNode->next = newNode;
}
man Addm(man headNode, char* Account, long key, int pw = 1) {//默认注册只能注册用户,pw=1
	man newNode = createNode(Account, key, pw);
	newNode->next = headNode->next;
	headNode->next = newNode;
	return newNode;
}

//删除书籍节点
void Delete(char* bookName)
{
	book posLeftNode = bHEAD;
	book posNode = bHEAD->next;//创建临时变量posNode记录要删除的节点
	while (posNode != NULL && strcmp(posNode->name, bookName)) //思路二:调用Find函数
    {
		posLeftNode = posNode;
		posNode = posLeftNode->next;
	}
	if (posNode == NULL) {
		SetCursorPosition(5, 15);
		printf("Error!Try again.");
	}
	else
	{
		SetCursorPosition(5, 15);
		printf("删除成功\n");
		posLeftNode->next = posNode->next;
		free(posNode);//释放临时变量指向的堆区数据
		posNode = NULL;
	}
}

//查找书籍:返回书籍结构体指针
book Find(char* fileName, book headNode = bHEAD) {
	book posNode = headNode->next;
	while (posNode != NULL && strcmp(posNode->name, fileName)) {
		posNode = posNode->next;
	}
	return posNode;
}
//查找用户:返回用户节点
man Find_man(char* Account) {
	man node;
	for (node = mHEAD->next; node != NULL; node = node->next) {
		if (!strcmp(node->Account, Account)) {
			return node;
		}
	}
	return NULL;
}
//递归查找并打印书籍
void findBook(book temp) {
	if (temp->next != NULL) {
		findBook(temp->next);
	}
	printf("\v\t%d  书名:%s\n\t\t数量:%d\t\n", temp->storage_num, temp->name, temp->num);
}

DAL.cpp:数据访问层

主要内容:将本地文件中的数据读取到链表中,将链表中的数据存储到本地。

  • 文件读操作(书籍/用户)
  • 文件写操作(书籍/用户)
代码语言:javascript
代码运行次数:0
运行
复制
//二、数据处理部分
#include ..
//文件写操作
void saveInforToFile_b(const char* fileName, book headNode) {
	FILE* fp = fopen(fileName, "w");
	book pMove = headNode->next;
	while (pMove != NULL) {
		fprintf(fp, "%d %s %s %d\n", pMove->storage_num, pMove->name, pMove->writer, pMove->num);
		pMove = pMove->next;
	}
	fclose(fp);
}
void saveInforToFile_m(const char* fileName, man headNode) {
	FILE* fp = fopen(fileName, "w");
	man pMove = headNode->next;
	while (pMove != NULL) {
		fprintf(fp, "%s %d %d\n", pMove->Account, pMove->key, pMove->pw);
		pMove = pMove->next;
	}
	fclose(fp);
}
//文件读操作 
void readInfoFromFile_b(const char* fileName, book headNode) {
	FILE* fp = fopen(fileName, "r");
	if (fp == NULL) {
		//第一次打开程序文件肯定不存在,所以要创建一个文件
		fp = fopen(fileName, "w+");
	}
	int storage_num = 0, num = 0; char name[MAX] = { 0 }, writer[MAX] = { 0 };
	while (fscanf(fp, "%d %s %s %d\n", &storage_num, name, writer, &num) != EOF) {
		Add(bHEAD, name, writer, storage_num, num);
	}
	fclose(fp);
}
void readInfoFromFile_m(const char* fileName, man headNode) {
	FILE* fp = fopen(fileName, "r");
	if (fp == NULL) {
		fp = fopen(fileName, "w+");
	}
	char Account[MAX] = { 0 }; int key = 0, pw = 0;
	while (fscanf(fp, "%s %d %d\n", Account, &key, &pw) != EOF) {
		Addm(mHEAD, Account, key, pw);
	}
	fclose(fp);
}

UI.cpp/h:表现层

主要内容:

  • 主要页面
    • 主页面
    • 注册页面
    • 登录页面
  • 分支页面
    • 用户页面
    • 管理员页面
  • 功能页面
    • 浏览信息页面
    • 查找书籍页面
    • 添加书籍界面
    • 删除书籍页面
  • 优化页面
    • 动态选择页面
    • 绘制界面边框
    • 动态结束界面
代码语言:javascript
代码运行次数:0
运行
复制
//三、用户交互界面
#include ..
//主界面
int loginface();
//登陆成功后的初始化页面
void startface();
//开始菜单选择界面
void Select();
//登录页面
int login(long key);
//注册页面
man _register();
//1.浏览信息页面
void skimface();
void manface();
//2.查找书籍页面
void bookface();
//3.添加书籍界面
void addface();
//4.删除书籍
void DeleteBook();
//用户操作界面:1
int yongH();//1.浏览书目2.查询书籍3.个人信息4.退出
//管理员操作界面:0
int guanL();//1.浏览书目2.查询书籍3.添加书籍4.退出
//(非核心)优化界面:绘制界面边框
void DrawFace();
//结束界面
int Pause();

PS:选择效果的实现

代码语言:javascript
代码运行次数:0
运行
复制
int Select()
{
	/*初始化界面选项*/
	SetColor(3);
	SetCursorPosition(13, 26);
	std::cout << "                          ";
	SetCursorPosition(13, 27);
	std::cout << "                          ";
	SetCursorPosition(6, 21);
	std::cout << "请选择功能:";
	SetCursorPosition(6, 22);
	std::cout << "(上下键选择,回车确认)";
	SetCursorPosition(27, 22);
	SetBackColor();//第一个选项设置背景色以表示当前选中
	std::cout << "用户登录";
	SetCursorPosition(27, 24);
	SetColor(3);
	std::cout << "用户注册";
	SetCursorPosition(27, 26);
	std::cout << "开发信息";
	SetCursorPosition(27, 28);
	std::cout << "退出系统";
	SetCursorPosition(0, 31);

	/*上下方向键选择模块*/
	int ch;//记录键入值
	int key = 1;//记录选中项,初始选择第一个
	bool flag = false;//记录是否键入Enter键标记,初始置为否

	while ((ch = _getch()))//检测键盘按键输入
	{
		switch (ch)//检测输入键
		{
		case 72://UP:键盘上的上方向键按钮
			if (key > 1)//当此时选中项为第一项时,UP上方向键无效
			{
				switch (key)
				{
				case 2:
					SetCursorPosition(27, 22);//给待选中项设置背景色
					SetBackColor();
					std::cout << "用户登录";

					SetCursorPosition(27, 24);//将已选中项取消我背景色
					SetColor(3);
					std::cout << "用户注册";

					--key;
					break;
				case 3:
					SetCursorPosition(27, 24);
					SetBackColor();
					std::cout << "用户注册";

					SetCursorPosition(27, 26);
					SetColor(3);
					std::cout << "开发信息";

					--key;
					break;
				case 4:
					SetCursorPosition(27, 26);
					SetBackColor();
					std::cout << "开发信息";

					SetCursorPosition(27, 28);
					SetColor(3);
					std::cout << "退出系统";

					--key;
					break;
				}
			}
			break;

		case 80://DOWN:键盘上的下方向键按钮
			if (key < 4)
			{
				switch (key)
				{
				case 1:
					SetCursorPosition(27, 24);
					SetBackColor();
					std::cout << "用户注册";
					SetCursorPosition(27, 22);
					SetColor(3);
					std::cout << "用户登录";

					++key;
					break;
				case 2:
					SetCursorPosition(27, 26);
					SetBackColor();
					std::cout << "开发信息";
					SetCursorPosition(27, 24);
					SetColor(3);
					std::cout << "用户注册";

					++key;
					break;
				case 3:
					SetCursorPosition(27, 28);
					SetBackColor();
					std::cout << "退出系统";
					SetCursorPosition(27, 26);
					SetColor(3);
					std::cout << "开发信息";

					++key;
					break;
				}
			}
			break;

		case 13://Enter回车键
			flag = true;
			break;
		default://无效按键
			break;
		}
		if (flag) return key;//输入Enter回车键确认,退出检查输入循环

		SetCursorPosition(0, 31);//将光标置于左下角,避免光标闪烁影响体验
	}
}
优化类文件:

startinterface.cpp/h:开始界面动画

实现工具:C++ vector、deque容器

  • deque是有下标的容器,它可以在开头和结尾两边快速插入及删除元素,另外,在任意一端插入元素或者删除元素不会使指向其他元素的指针或者引用失效。
  • 与vector相反,deque的元素不是相连存储的;典型实现用单独分配的固定大小数组的序列,外加额外的登记,这意味者使用下标访问必须经过二次指针的解引用,与之相比vector的下标访问只进行一次。(左边是额外的连续的内存空间,右边是固定大小的数组,用于保存数据)

点坐标图:

通过坐标(0,14)观察队列输出特点:先进先出

9

8

7

6

5

4

3

2

1

0

14

15

16

17

18

-31

-30

-29

-23

-22

-21

-20

-19

-18

-17

-16

-15

-14

-13

-6

-5

-4

14

15

16

17

18

实现流程:

  • 开始动画
    • 第一阶段:蛇从左边出现到完全出现的过程
    • 第二阶段:蛇从左向右移动的过程
    • 第三阶段:蛇从接触右边到消失的过程
  • 开始文字

实现方法:

这一部分的实现首先是建立一个deque双端队列,用于存储点的对象,这些点就是组成蛇身的元素,然后再用一个for循环将容器中的点依次打印出来,每打印一个点停顿一会,这样就达到了移动的效果。

代码语言:javascript
代码运行次数:0
运行
复制
void StartInterface::PrintFirst()//第一阶段:蛇从左边出现到完全出现的过程
{
    for (auto& point : startsnake)//只读方式遍历容器
    {
        point.Print();
        Sleep(speed);//停顿
    }
}

全部打印完后就到了第二部分,这部分蛇的每次前进都是通过计算将要移动到的下一个点的坐标,然后将这个点打印出来,与此同时将蛇尾,亦即queue中的首端点去掉,并擦除屏幕上该点颜色。

代码语言:javascript
代码运行次数:0
运行
复制
void StartInterface::PrintSecond()//第二阶段:蛇从左向右移动的过程
{
    for (int i = 10; i != 40; ++i) //蛇头需要从10移动到40
    {
        /*计算蛇头的下一个位置,并将其压入startsnake中,绘制出来,将蛇尾去掉*/
        //计算从坐标j
        int j = (((i - 2) % 8) < 4) ? (15 + (i - 2) % 8) : (21 - (i - 2) % 8);
        startsnake.emplace_back(Point(i, j));
        //queue中的首端点去掉,并擦除屏幕上该点颜色
        startsnake.back().Print();
        startsnake.front().Clear();
        startsnake.pop_front();
        //停顿
        Sleep(speed);
    }
}

第三部分就直接依次从蛇尾擦除即可。

代码语言:javascript
代码运行次数:0
运行
复制
void StartInterface::PrintThird()//第三阶段:蛇从接触右边到消失的过程
{
    while (!startsnake.empty() || textsnake.back().GetX() < 33) //当蛇还没消失或文字没移动到指定位置
    {
        if (!startsnake.empty()) //如果蛇还没消失,继续移动
        {
            startsnake.front().Clear();
            startsnake.pop_front();
        }
        ClearText();//清除已有文字
        PrintText();//绘制更新位置后的文字
        Sleep(speed);
    }
}

同理,文字BMS的移动也基本类似,稍微改动即可,因为无需对首尾进行操作,而是要对所以点进行移动,因此容器选用vector。

注:为什么使用emplace_back() :

  1. emplace_back函数的作用是减少对象拷贝和构造次数,是C++11中的新特性,主要适用于对临时对象的赋值。
  2. 在使用push_back函数往容器中增加新元素时,必须要有一个该对象的实例才行,而emplace_back可以不用,它可以直接传入对象的构造函数参数直接进行构造,减少一次拷贝和赋值操作。
  3. emplace_back() 和 push_back() 的区别,就在于底层实现的机制不同。push_back() 向容器尾部添加元素时,首先会创建这个元素,然后再将这个元素拷贝或者移动到容器中(如果是拷贝的话,事后会自行销毁先前创建的这个元素);而 emplace_back() 在实现时,则是直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
  4. 显然完成同样的操作,push_back() 的底层实现过程比 emplace_back() 更繁琐,换句话说,emplace_back() 的执行效率比 push_back() 高。
代码语言:javascript
代码运行次数:0
运行
复制
class StartInterface
{
public:
    StartInterface() : speed(35) {
        startsnake.emplace_back(0, 14);//Éß
        startsnake.emplace_back(1, 14);
        startsnake.emplace_back(2, 15);
        startsnake.emplace_back(3, 16);
        startsnake.emplace_back(4, 17);
        startsnake.emplace_back(5, 18);
        startsnake.emplace_back(6, 17);
        startsnake.emplace_back(7, 16);
        startsnake.emplace_back(8, 15);
        startsnake.emplace_back(9, 14);
		……
        
    }//绘制动画BMS
    void PrintFirst();//第一阶段
    void PrintSecond();//第二阶段
    void PrintThird();//第三阶段
    void PrintText();//输出文字
    void ClearText();//清除文字
    void Action();//绘制
private:
    std::deque<Point> startsnake;//开始动画
    std::vector<Point> textsnake;//开始动画中的文字
    int speed;//动画的速度
};

map.h:操作界面外壳

实现原理:C++vector容器

代码语言:javascript
代码运行次数:0
运行
复制
class Map
{
public:
    //默认构造函数,将圆形各点压入initmap
    Map(){
        initmap.emplace_back(1, 1);
        initmap.emplace_back(2, 1);
        initmap.emplace_back(3, 1);
						……
        initmap.emplace_back(28, 30);
        initmap.emplace_back(29, 30);
        initmap.emplace_back(30, 30);
    }
    void PrintInitmap();//绘制初始地图
private:
    std::vector<Point> initmap;//保存初始地图
};

tools.cpp:控制台优化类函数

代码语言:javascript
代码运行次数:0
运行
复制
#include "tools.h"
#include <windows.h>
void SetWindowSize(int cols, int lines)//设置窗口大小
{
    system("title 图书管理系统->嘎嘎极客");//设置窗口标题
    char cmd[30];
    sprintf_s(cmd, "mode con cols=%d lines=%d", cols * 2, lines);//一个图形■占两个字符,故宽度乘以2
    system(cmd);//system(mode con cols=88 lines=88)设置窗口宽度和高度
}
void SetCursorPosition(const int x, const int y)//设置光标位置
{
    COORD position;
    position.X = x * 2;
    position.Y = y;
    SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), position);
}
void SetColor(int colorID)//设置文本颜色
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), colorID);
}
void SetBackColor()//设置文本背景色
{
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE),
        FOREGROUND_BLUE |
        BACKGROUND_BLUE |
        BACKGROUND_GREEN |
        BACKGROUND_RED);
}

point.h:操作界面外壳元素

实现原理:C++ vector

代码语言:javascript
代码运行次数:0
运行
复制
class Point
{
public:
    Point() {}
    Point(const int x, const int y) : x(x), y(y) {}
    void Print();
    void PrintCircular();
    void Clear();
    void ChangePosition(const int x, const int y);
    bool operator== (const Point& point) { return (point.x == this->x) && (point.y == this->y); }
    int GetX() { return this->x; }
    int GetY() { return this->y; }
private:
    int x, y;
};

版本

V1.0.0

全部整合后的第一版。

V1.0.1

进一步优化了界面。

V1.0.2

修复了注册界面Bug。

Bug描述:

  • 存放密码的类型是long而不是char[],导致了用户如果在输入字符与数字的混合密码时会出现问题,可能会导致密码为空或者导致密码只保存了数字,但是无法给用户提示,从而导致用户无法登录。
  • 用户注册时如果用户名超出了数组Account的大小的问题:溢出问题
代码语言:javascript
代码运行次数:0
运行
复制
//base.cpp
typedef struct _man
{
	char Account[MAX];
	long key;//最多九位
	struct _man* next;
	int pw = 0;//0是管理员,1是用户
}*man;

Bug1解决方案:

  1. **将key的long类型改为char***:更改难度极大,该结构体已经被应用于多个函数中,且函数之间的关系比较复杂,也需要同时对函数中的操作所涉及的一系列对字符串操作的修改。
  2. 限制只能输入数字作为密码:操作难度小,只涉及一个函数的修改。

Bug2解决方案:

  1. 使用malloc动态分配内存来存储用户名:难度大,修改量大。
  2. 截断字符串并给用户提示:操作难度小,只涉及一个函数的修改。

问题解决:TODO

代码语言:javascript
代码运行次数:0
运行
复制
//注册页面
man _register() {

	char Account[MAX] = { 0 }; int key = 0;
	SetCursorPosition(10, 10);
	printf("\n\t\t\t注册!");
	printf("\n\n\t\t\t\t\t ________________");
	printf("\n\t\t请输入用户名:(20个字符) \b|________________|\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
	SetColor(2);
    
	//TODO2:原版scanf("%s",Account);
    //如果到达文件末尾或者没有读取到任何字符,返回一个空指针。如果发生错误,返回一个空指针。
	while ((fgets(Account, MAX, stdin)) != NULL && Account[0] != '\n') 
    {
		while (getchar() != '\n') {
			continue;
		}
		SetCursorPosition(8, 16);
		std::cout << "账号:" << Account << std::endl;
		break;
	}
	
	getchar();
	SetColor(3);
	printf("\n\t\t\t\t\t ________________");
	printf("\n\t\t请输入你的密码:\t\b |________________|\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b");
	SetColor(2);
	
    //TODO1:原版:scanf("ld", &key);
	while (1) {
		scanf("ld", &key);//利用scanf将换行符留在缓存区的特点
		if(getchar() == '\n')break;
        else {
			while (getchar() != '\n') //关键步骤,清空缓存区
            {
				continue;
			}
			SetCursorPosition(8, 20);
			std::cout << "请输入数字!" << std::endl;
		}
	}
	
	return Addm(mHEAD, Account, key);
}

参考:

https://blog.csdn.net/xiaoxiaoguailou/article/details/121733862

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Gaga-Geek图书管理系统(BMS)
    • 项目介绍
      • 目录结构
    • 环境搭建
      • 开发工具
      • 开发环境
    • 需求文档
    • 产品原型图
    • 产品流程图
    • 软件架构:三层架构
      • 核心类文件:
      • 优化类文件:
      • 详细介绍:
    • 版本
      • V1.0.0
      • V1.0.1
      • V1.0.2
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档