项目地址:Haroxa/BillingManagement
1 实验目的
通过迭代式开发,深入掌握C语言的文件、链表、结构体、动态内存管理等技术,开发实现一个计费管理软件。
2 系统功能与描述
2.1 基本功能
1. 添加卡
通过输入卡号、密码和开卡金额来生成一张新卡,在生成前会对输入时数据进行长度和格式判断,卡号为4~10位数字字母,密码为6~12位数字字母,开卡金额位2~6位整数。生成合法的卡信息后,会以双向链表的形式,添加到当前卡链表中。
2. 查询卡
将输入卡号和密码保存到临时的卡中,通过数据检验后,会在当前卡链表中,以迭代的方式寻找卡号相同的卡信息。如果成功找到,再继续对二者的密码进行比对,若密码正确,则将会以表格的形式展现卡的相关信息。
特别地,对于密码这种隐私信息,将会采取中间部分为 * 的形式展现,以保证数据的安全性。
3. 修改卡
修改卡是以查询卡为基础,判断是否修改卡的卡号和密码的功能。如果确认修改,则需要输入合法的数据,然后用新的数据替换掉旧的数据。同时也会修改卡的更新时间。
4. 注销卡
注销卡也需要先查找卡的相关信息,然后再对是否注销进行判定。如果确认注销,则需要对卡所处的位置(表头,表中和表尾)进行判断,选择合适的方法将其从链表中分离出来,进行释放。
5. 上机与下机
从上机和下机功能开始,将会引入新的模型--账单。
查询到卡后,选择上机,会记录此时的上机时间,以及卡的部分信息,生成账单。然后将该账单以双向链表的形式,添加到当前账单链表中,同时卡的状态会被标记为上机状态。
选择下机,会先对卡的状态进行判断,只有卡的状态为上机状态,才会继续操作。此时会去寻找上机时对应的账单,并记录此时下机时间,以上下机的时间差为判断依据,通过一定的计费标准来确定消费金额。然后会将这些信息保存到账单中,同时也会修改卡中的余额和消费金额。如果此时卡的余额为负值,则将此卡标记为欠费状态,无法进行除充值以外的其它操作。
6. 充值与退费
充值与退费,都是输入合法金额后,对查询到的卡余额进行的修改操作。当然为了开发者的利益,此过程会产生一定的手续费,并生成账单,保存到账单链表中。
充值过后,如果卡的现有余额为正值,且原来为欠费状态,则将会被恢复为正常状态。
退费时,只有卡为正常状态,才会进行此操作。如果退费金额大于现有余额,则只会返回现有余额(扣除手续费后的)。
7. 导入与导出
为了防止退出程序后的数据丢失,在系统启动或退出时,会自动的以二进制形式读取或保存默认储存文件,以便数据的继续使用。
同时,系统提供了导入导出的接口,通过输入合法的文件名,将指定信息,以二进制形式读取或保存到指定文件中。也支持多个信息在同名文件中读取或保存(例如将卡信息和账单信息保存到对应文件夹下的同名文件中)。
8. 信息列表
为了更直观的了解当前的数据信息,便采用信息列表的模型,来将所需的数据信息存储,并以表格的形式,按需要展示可观看的信息。
对于不同的数据模型,会有对应的处理函数,将其存储到信息列表模型中,再以通用的表格函数输出。
2.2 扩展功能
1. 图形界面
图形界面为本系统的核心功能,所有的操作实现都是在此功能的基础上完成的。具体实现为:下载EasyX这个图形库,导入其中的graphics.h 和 conio.h 头文件,通过调用里面的相关函数,即可进行图形操作。不过由于优美的界面设计实现起来比较复杂,所以本系统只采用了相对简陋的设计。
根据实际的需求,已设计如下几个界面:首页界面、用户界面、查询界面、信息列表界面、卡服务界面、数据统计界面和导入导出界面。
2. 用户管理
为了区分管理者和普通用户,便引入用户模型。用户的增删改查与卡的增删改查类似,在此不做赘述。对于每个用户都会有一个标记来判断其是否为管理者,从而来决定其是否可以使用某些功能或者使用功能的范围。在用户登录后,其生成的卡和账单都会与此用户产生关联,用户中会记录其所拥有的卡和账单数目。
对于普通用户而言,只能查看修改删除与自己关联的信息,无法操作其它用户关联的信息,而且也无法使用数据统计功能。对于管理者来说,则没有任何限制。
3. 信息编号
如何区分不同的账单,如何让用户,卡以及账单之间产生关联呢?参考生活中的经验,在此引入信息编号。
在生成模型信息时,会记录当时的时间,将其中产生的time_t类型的时间数据做一定处理,便可作为信息编号使用,且该编号是唯一的,不会出现重复。
所以通过信息编号即可区分不同的账单,而且上机时在卡信息中存储该编号,下机时便可通过该编号查找到对应的账单,完成下机操作。
通过在卡信息中存储用户编号,则可判断其是否与当前用户相关联。判断用户与账单,卡与账单也是如此。这样就可以实现普通用户只能操作与自己相关联信息的功能。
4. 相似推荐
在查询信息时,可能会出现记不清或者打错字符的情况。为了方便操作,当查询不到信息时,便会推荐相似的信息,以帮助用户回忆和成功查询。
其具体实现是通过求两个字符串之间转换的最少操作数,来计算相似度,并通过插入排序,实现相似度从大到小的推荐顺序,由于提示框大小有限,只展示前五个信息。
5. 自动生成
为了便于添加管理员账号以及生成多组数据测试已有功能,便添加了一个自动生成的功能。
可以根据开发者的需求,来设定具体的生成数。其实质就是利用随机数和已有函数来模拟正常的操作来完成生成功能。
6. 信息分页
当数据量过多时,直接全部展现出来不便于观看,特别是在图形界面中无法超出界面范围的数据无法展现。
于是便采用分页的方式来展现信息,可以通过使用首页、上一页、跳转、下一页和尾页的选项来便捷的切换展示页面,方便更好地观看。
其实质为:初始化所有信息模型并存储,在展示表格信息时,设置页面限制,通过这些操作改变当前页数,以达到展示指定页数的效果。
7. 数据统计
数据统计功能是为了帮助开发者直观分析数据而产生的功能。通过此功能,可以查询到所有、年度、月度的数据记录,包括新增用户数、卡数和账单数,充值、退费、消费和存储金额。
引入统计模型,利用生成数据时的时间信息,将其保存在对应时间的模型中,再对不同时间进行统计,然后通过选择输入合理的年份月份,即可查看所需的统计信息。
3 典型算法分析
3.1 计算相似度算法
在计算相似度时,采取了一种专门度量文本差异度的方法--编辑距离法(Minimum Edit Distance,MED)。编辑距离是指在两个单词中,将一个单词通过插入、删除和替换,转换成另一个单词所需要的最少次数。
具体过程为先建立一个矩阵,初始化第一列和第一行的所有距离;然后开始逐行逐列的计算所有的距离;选择左边和上边距离的最小值加一后与左上方距离相比得到的最小值来作为当前行列的距离;如果当前行列对应的字符不等或不为字母的大小写转换,则在比较时将左上方距离加一后再进行比较;最后行列的距离即为所需的编辑距离,然后以此来计算相似度。
3.2 相似度排序算法
在求得相似度后需要进行排序,选取相似度大的前五个展示,此处采取的排序方法为插入排序。
每计算一个相似度时,会根据已有个数从后往前比较,如果比当前这个数大,则这个数往后移一位,否则新计算的相似度将会存储在当前数的后一位,依此类推,最后即可得到按从大到小顺序排列的列表。
3.3 模拟生成算法
模拟顾名思义就是用程序来模仿正常的操作流程。模拟生成时需要注意几个模型之间的从属关系,要按照先生成用户,再生成卡,最后生成账单的顺序执行,以实现模型之间的关联性。
在具体实现时主要利用了随机的特性。对一些时间数值相关的数据利用随机数来生成;生成卡时,利用随机数来决定用户遍历的深度,作为当前用户使用,生成账单时类似的找到当前用户和当前使用卡;对于账单的类型也会利用随机数进行选择,确定本账单是上机、充值还是消费。
容易出错的是卡的状态标记,在生成账单后,如果卡的金额变为负值了,就需要标记为欠费状态,下次遇到时则直接跳过这张卡,其余操作基本就是与正常的操作一致,还有部分直接就是调用了已有的函数。
3.4 暴力统计算法
由于新增数据以及营业额的时间不固定,可能每天都会有,也可能几天才有,所以直接采取暴力的方式统计。
首先开一个10*12*31的数组原来存储信息;再将所需的模型(用户,卡和账单),存储到其对应时间的信息中,并做上标记;然后从头开始遍历数组进行汇总,先汇总月份的,再汇总年份的,遇到标记的才统计,无标记的则跳过;最后根据选择以及输入的年份月份输出指定的有标记的数据即可,全部无标记则输出空。
4 开发难点与体会
4.1 开发时遇到的错误
1. 指针异常访问
在使用链表进行操作时,使用指针操作会比较方便高效,但也容易出现问题。比较常见的问题有指针没初始化就使用,函数返回局部变量地址,以及使用文件中读取的地址等,这些都会造成指针的异常访问。
在本次实验中,刚开始,在构建卡链表时出现了未初始化的操作,于是便采用calloc 在堆上开辟空间并初始化。
然后在读取文件时,从文件中读取地址,并使用时,发生了异常,不知道为什么,在调试时发现这个指针的地址已经无效了,于是就想到把指针和数据分成两个结构体,保存到文件时只保存数据,在读取时再记录现在的数据地址,这样就解决了。
在图形界面操作时,使用了局部变量的地址,虽然这样不一定会立即出现问题,但是会留下隐患。后面就采取使用全局变量或者将所需的内容以指针传入再进行修改。
因此,使用指针操作时,必须谨慎谨慎再谨慎,尽量不要犯错,否则查找问题时将会消耗大量的时间。
2. typedef结构体语法错误
在将指针和数据分成两个结构体,突然出现了语法错误,看了半天都不知道为什么后出现这种错误,最后在网上搜索后才找到原因。
原来在VS中使用.c文件时,结构体指针必须要带上struct 不然会出现语法错误,而且这个错误在不熟悉的情况,根本无法会想到是这里出错,会让人很匪夷所思,要必要牢牢记住。
3. 数组越界
对于一些错误的操作,都会弹出对应的信息来提示或警告用户,但有时文字信息过多,超出了数组范围,就导致了数组越界,这里把数组大小调大点就好了。
还有在进行插入排序时,前后的数组大小不一致,导致了数组越界,找了半天才发现错因,只要把两个大小统一就可以解决。
所以在声明数组类型变量时必须考虑全面,除非内存不够用,否则尽量把范围开大点,而且前后大小要统一,减少此种情况的发生。
4. 栈溢出
在使用信息列表和数据统计时,这些模型都是在栈上开辟的空间,当数据量稍微大一点,就容易栈溢出(容易超出VS上的默认栈内存)。
解决方法为:打开VS ,在解决方案资源管理器中,鼠标右键点击项目,找到并点击属性,点击展开链接器,找到系统并点击,便可看到堆栈保留大小,在其右侧点击修改数值即可调整堆栈大小(我修改成了100000000)。
所以在使用时注意栈的默认内存空间,如果超过了,就进行修改,可以尽量开大点。
5. 重定义
重定义是多文件编程中最容易出现的错误,非常令人头疼。常见的错误有头文件之间的相互包含,头文件中定义变量和函数等。
在头文件中一般会用ifndef、 define和 endif 来使头文件中的内容只编译一次,而且尽量减少在头文件中包含其它文件的部分,将其移动到源文件中去,这样可以减少重定义的发生。
对于变量和函数,则只在头文件中声明(变量采用的是extern外部声明,有时也可采用static静态定义),而不做具体的定义,定义写在源文件中。
本次实验中主要是在头文件中定义变量出现重定义问题,如果选择用static,在每个文件调用时,都是一个全新的值,无法做到单值公用。
总的来说,对于重定义问题,只要熟练掌握以上方法,一般都可以轻松解决。
4.2 开发时的难点
1. 从属关系的逻辑判断
在实际开发中,对于多个相关联的模型,处理时可能会比较的棘手。本次系统中,用户,卡和账单模型是关联在一起的,当对其中一个进行增删改查的操作时,其它的模型的信息也要跟着改变。如果稍微不注意,很容易就会忘记修改,导致运行的结果不合乎预期。所以在整体上可能需要通过结构图等形式将各个过程梳理一遍,通过这种方式来减少寻找没修改处的时间。
2. 学习使用EasyX图形库
在网络上,关于EasyX的教程比较少,很多东西的处理不知道要如何去做,只好自己去看基本的语法慢慢琢磨,特别是图形界面的设计,对与没有艺术细胞的我来说,很是头痛。在后面多次搜索中,发现了一些类似的系统(学生管理系统等),参考其源码,学到了一些比较便捷的处理方法,提高了编写的效率,但是图形界面的设计这方面还是没有太大进展,而且越是精美的界面就越复杂,所以最终只好采取简易的界面来完成,如果后续有时间的话,可能会继续来对界面进行美化。
5 实验总结
学习一门语言最重要的就是练习,所以通过本次实验,我对C语言的结构体,文件,指针等操作熟悉了很多,也学到了一些新的知识,特别是学习了关于EasyX图形库的简单使用,可谓是收获满满。
其实刚开始时,通过连续四天的写代码,已经完成了一个大致的版本,但是这个系统比较单调没有啥特色。于是后面又花了一个多星期的时间,重新完成了这个图形界面的版本。
在重写的过程中,我对上个系统中不够规范的地方进行了一定的修改。比如一些命名问题之类的,增强代码的可读性;尝试去将一个功能分化成多个函数,并减少它们之间地耦合性,特别是一些需要被多次使用的功能,这样做会相当方便快捷;继续地完善已有的功能和添加新的功能,在一些细节处理上格外注意等。
本次实验也是第一次正式接触到的相对复杂的实验,对于一些数据上的处理学到了很多,同时也可以为以后的项目操作积累经验,之后会把本次系统的代码上传到github上,以便后续的学习和完善。
总之,计算机专业需要学习的知识是很多的,想要牢固掌握,就必须通过大量的练习,这样才可以不断的提升代码能力,成为一个合格的程序员。