分类

课内:
不限
类型:
不限 毕业设计 课程设计 小学期 大作业
汇编语言 C语言 C++ JAVA C# JSP PYTHON PHP
数据结构与算法 操作系统 编译原理 数据库 计算机网络 软件工程 VC++程序设计
游戏 PC程序 APP 网站 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
年份:
不限 2018 2019

资源列表

  • 基于C语言的小球移动课程设计

    一、需求分析
    用C语言实现“小球移动”的简单图形游戏。可添加、删除小球,小球的分数和大小随机,球会在游戏区域内反弹,小球可被删除,删除时球上的数字累加到玩家的分数上。还可实现暂停、存状态等功能。实现必要的拓展。
    1.1 功能需求
    开始时没有小球,点“增加”按钮会添加一个小球,小球上的数字(1~9)、球的大小皆为随机;小球从左边生成,碰到边缘会反弹;用鼠标选中小球再点“删除”可删除一个小球,无选中小球时,会删除最早出现的小球。删除小球时,球上数字累加到玩家得分。规则:当分数恰好为50分或分数为球数的10倍时,游戏结束。难度可以调节。具备功能齐全、友好的主界面。可记录玩家的成绩、可保存导入游戏状态。
    1.2 数据需求
    输入数据:用户名、导入已保存的数据,对球的操作指令(增加,删除等)。中间数据:球的位置、大小、数值等动态变化的信息、变化的分数、游戏进行的状态(暂停、继续)、用时。输出数据:球的动态显示、游戏数据的导出、对玩家用户名、分数和用时的记录。
    1.3 界面需求
    图形界面。
    有“增加”、“删除”、“退出”、“导出状态”、“导入状态”、“暂停”、“继续”、“难度调节”等按钮,对应相应的功能;
    具有一个游戏区域,小球在游戏区域中运动。

    1.4 开发与运行环境需求
    开发工具:Visual Studio 2012
    环境需求:Windows系统(静态编译,不需EasyX库)

    1.5 其他方面需求程序健壮性良好,考虑到用户各种可能的操作行为,以避免BUG。
    二、概要设计模块化设计。主要模块有:

    绘制主界面
    相应按钮点击
    小球的移动和反弹
    小球的生成和删除
    选择小球
    玩家分数累加
    更新用户名和分数
    游戏数据导出
    游戏数据导入
    暂停和继续
    导出用户名和分数
    退出并保存相关信息
    计时器

    2.1 程序总体结构

    main:初始化,主循环。
    input_player_info:输入玩家信息。
    display_ball_count:更新球的数量。
    move_ball:球的各种行为,如移动、反弹等。
    button_respond:对玩家的点击做出反应。
    read_game:载入游戏。
    display_player_nifo:显示玩家信息。
    delete_ball:删除球。
    update_score:更新分数。
    add_ball:增加球。
    save_game:导出游戏状态。
    load_game:导入游戏状态。
    exit_game:退出游戏、保存分数。
    select_ball:选择小球。
    main_interface(及其子函数):绘制界面
    ball_timer:计时器

    2.2 主要数据结构
    用户信息作为结构体(用户名、分数)
    小球信息作为结构体(数字、大小、形状等)
    大量小球的信息作为结构体链表
    可导入、导出的数据用txt存储,其中包含:小球的信息、玩家的信息

    三、界面小球移动示意图
    1 评论 15 下载 2018-10-21 19:33:22 下载需要2点积分
  • 基于Easyx插件的俄罗斯方块游戏的设计与实现

    一 需求分析1.1 设计内容:设计一个俄罗斯方块游戏,根据实际游戏的规则完成设计。
    游戏设计方案:

    在一个图形绘制区域的正上方随机产生四种不同方块中的任意一种的初始位置,并使其向下运动
    定义键盘,游戏玩家用键盘上定义的键控制方块的翻转、左右移动、加速和暂停等
    当某行被填满则该行消除,1次消除1行加10分,1次消除2行加30分,1次消除3行加50分,1次消除4行加100分
    当未被消除的行达到绘图区域的最大行数时,则游戏结束,给出游戏最终得分

    1.2 要解决的问题
    怎么在VC++6.0中画出各种图形、填充各种颜色
    怎么样根据坐标画出方块
    怎么随机产生不同形状、颜色和旋转状态的方块
    怎么控制方块的移动、旋转等操作

    1.3 如何做
    在 www.easyx.com 网站中下载Easyx插件,使VC++6.0实现图形化界面
    查看easyx的帮助,解决easyx.h和graphics.h库中需要用到的绘图函数。从而会出各种形状、颜色的方块
    利用kbhit()函数获取玩家键盘输入,在编写不同输入对应的函数,从而实现对方块的控制
    利用time.h库中srand()函数来根据时间产生随机数,从而在游戏中实现随机产生不同形状、颜色和旋转状态的方块

    二 算法设计2.1 总体设计游戏流程图如下所示:

    2.2 关键数据结构和主要算法for(i = 0; i < 16; i++, b <<= 1) if (b & 0x8000) { x =block.x + i % 4; y = block.y - i / 4; if (y < HIG) DrawUnit(x,y, Blocks[block.id].color); }
    b是一个十六进制数,和0x8000做“与”运算是为了测试最高位是否为1,从而将b的二进制等值数中的1的坐标记录下来,从而准确的画出各种形状的方块。
    while(true){ new_time = GetTickCount(); // 不控制,默认自动下落一格 if (new_time - old_time > 500) { old_time = new_time; return DOWN; } // 如果有按键,返回按键对应的功能 if (kbhit()) { switch(getch()) { case 72: cmd = ROTATE; return cmd; case 80: cmd = DOWN; return cmd; case 75: cmd = LEFT; return cmd; case 77: cmd = RIGHT; return cmd; case 27: //ESC键 cmd = QUIT; return cmd; case ' ': cmd = SINK; return cmd; } }}
    通过GetTickCount()函数获取时间,计算出时间差,大于规定的时间差,则默认方块下落,通过改变时间差的大小可以控制下落的速度,从而控制游戏难度。在通过kbhit()函数接受键盘输入,不同的按钮相应不同的函数,从而有效的控制的方块的位置和旋转状态。
    2.3 主要函数
    main()函数:是程序的入口,通过调用各个函数来启动游戏
    Init()函数:通过rectangle()、line()、outtextxy()函数在黑窗口内画出矩形、线和文字,通过控制它们的位置和大小来画出游戏启动画面
    NewGame()函数:先清空游戏区(为下次继续游戏做准备)、初始化分数,在创建新的方块和预备方块
    CreatBlock()函数:通过rand()函数随机初始化不同形状、旋转状态和大小的方块。初始化后通过调用DrawBlock()函数来画出方块。创建方块的同时,通过GetTickCount()函数设置计时器,用于判断自动下落
    DrawBlock()函数:通过一些算法,将初始化的十六进制数转换为等值二进制,将16位的二进制看成4*4的矩阵,将矩阵中的1用坐标(x, y)记录下来,在将方块的每个单元坐标(x, y)和颜色发送给DrawUnit()函数处理
    DrawUnit()函数:将DrawBlock()函数发送过来的坐标和颜色信息,在游戏区内画出来。从而使玩家看到有形有色的方块
    ClearBlock()函数、ClearUnit()函数:原理和DrawBlock()函数、DrawUnit()函数一样,只是用BLACK背景色“画”出方块,从而实现擦掉方块的效果
    GetCommand()函数:通过GetTickCount()函数获取新的时间,计算出时间差,大于规定的时间差,则默认方块下落,通过改变时间差的大小可以控制下落的速度,从而控制游戏难度。再通过kbhit()函数接受键盘输入,将输入信息发送给SendCommand()函数,从而实现对玩家的输入做出准确的响应
    SendCommand()函数:将GetCommand()函数发送过来的输入信息,通过switch()分别调用不同的函数,从而实现玩家对方块的有效控制
    CheckBlock()函数:对方块执行左、右、下移和旋转时执行该函数,目的是检验方块目前的位置和旋转状态是否满足执行下一步的条件,起到判断的作用
    Go_Down()函数:先检查是否满足下移的条件,如果满足,改变方块的坐标,使方块实现下移,否则,调用Go_Sink()函数,执行沉淀操作
    Go_Left()函数:先检查是否满足左移的条件,如果满足,改变方块的坐标,使方块实现左移。否则,不执行任何操作
    Go_Right()函数:先检查是否满足右移的条件,如果满足,改变方块的坐标,使方块实现右移。否则,不执行任何操作
    Go_Rotate()函数:先检查是否满足旋转的条件,如果满足,改变方块的旋转状态,使方块实现旋转。否则,不断改变方块的条件,使方块满足旋转的条件,使方块完成旋转操作
    Go_Sink()函数:先不断的改变方块的坐标,使方块下移,直到不满足下移条件时,记录方块的坐标,画出固定方块。如果此时方块累加的高度大于游戏区的高度,调用GameOver()函数,游戏结束。否则用二维数组FixBlock[x][y]记录方块的位置,表示固定方块区。在判断固定方块区是否有满行方块,如果没有,不做处理,继续固定。如果有满行,就消掉该行,并将所有固定方块区的纵坐标下移。并且累加刷新分数。再调用CreatBlock()函数,创建新的方块
    GameOver()函数:弹出对话框,提示玩家是否继续游戏。继续就调用NewName()函数,否则执行Quit()函数
    Quit()函数:关闭游戏窗口

    三 程序实现3.1 实现说明主要实现的功能如下:

    在一个图形绘制区域的正上方随机产生四种不同方块中的任意一种的初始位置,并使其向下运动
    定义键盘,游戏玩家用键盘上定义的键控制方块的翻转、左右移动、加速和暂停等
    当某行被填满则该行消除,1次消除1行加10分,1次消除2行加30分,1次消除3行加50分,1次消除4行加100分
    当未被消除的行达到绘图区域的最大行数时,则游戏结束,给出游戏最终得分
    采用图形化界面实现
    在一个图形绘制区域的正上方随机产生七种不同方块中的任意一种的初始位置
    七种方块有七种不同的颜色,并且颜色是随机出现在七种方块中的
    在消除方块时产生过渡动画的效果
    给游戏设置难度等级,难度由玩家开始游戏之前选择,分为“简单”、“正常”、“困难”
    随着玩家分数的提高,游戏难度和等级自动提高,即方块下落的速度加快
    进入游戏前和游戏结束后为游戏添加多读动画,使游戏运行更自然

    四 程序运行4.1 游戏手册进入游戏时,游戏区的上方会自动下落方块,游戏窗口的右上方会显示方块的形状、颜色和旋转状态,使玩家产生心理准备。游戏窗口的右方显示玩家的分数。游戏窗口的右下方是游戏的操作说明。玩家键盘输入左、右、下分别是控制方块的位置,使方块根据玩家的控制移动起来。键盘输入上是改变方块的旋转状态,使方块旋转。按Esc键是退出游戏。按空格键是使方块直接沉底。当方块的高度大于游戏区的高度时,游戏结束。友情提示:当某行被填满则该行消除,1次消除1行加10分,1次消除2行加30分,1次消除3行加50分,1次消除4行加100分。所以想要拿高分,需要一次消多行。
    4.2 运行实例如下图所示:

    五 参考资料[1] Stephen Prata编著 C primer plus[M].北京:人民邮电出版社,2012
    [2] 合肥学院计算机科学与技术系编著《C语言程序设计》讲义[M].合肥学院计科系,2016-2-28
    [3] http://www.easyx.cn/
    1 评论 49 下载 2018-10-21 19:28:52 下载需要4点积分
  • 基于C++的数据库可扩容哈希

    一、项目介绍主要涉及可扩展哈希在数据库中的应用。
    读入由 tpc-h 生成的 lineitem.tbl,以 L_ORDERKEY 属性作为键值将记录放入合适的哈希桶内。读入测试文件 testinput.in 内的数据,数据中包含多个需要查询的键值。将通过键值查询得到的所有记录都输出到 testoutput.out 文件中。算法实现分为两大部分,第一部分是建立索引,第二部分是查询。建立索引是将输入的每一条记录根据指定的键值放入合适的哈希桶内,当哈希桶已满时,需要进行分裂。查询是根据输入的键值返回具有相同键值的记录,返回的记录可能有不止一条。
    二、项目环境
    系统:Windows 8.1 专业版 64 位
    处理器:Intel® Core(TM) i3 CPU M 350 @ 2.27GHz 2.27 GHz
    内存:2 GB 金士顿 DDR3 1333MHZ
    硬盘:希捷 ST9320 320GB 7200 转/分
    语言:C++
    编辑器:Visual Studio 2013

    三、项目架构本项目共有7 个文件:

    main.cpp:主文件,程序的入口
    Manager.h (.cpp):主控程序,实现功能的主体
    Buffer.h (.cpp):缓存池,管理内存中各缓存页
    Index.h (.cpp):目录页,存储哈希值和桶号的对应关系
    Page.h (.cpp):页的基本类,实现一个页的基本功能
    Function.h (.cpp):项目中要用到的通用函数,如取哈希值,取键值等
    option.h:程序的如最大使用页数,哈希方式等参数的定义文件

    无论最大使用页数和哈希方式,项目运行时目录页占用 1 页,读写文件缓冲用占用一页,其他页都用来存储记录桶。
    四、主要数据结构4.1 基本页
    每个 Page 页在内存中的分布如下:

    其中蓝色背景的为 contents 的内容。记录的具体内容从 contents 的头向尾扩展,对应的记录信息如长度,偏移值等从 contents 的尾向头扩展。需要写入或写出时将 Tong 强制转换为 char*类型,按长度为 8192 进行存储或读取。
    4.2 缓冲池
    每个缓冲池有一个Page*的数组,用来存储各Page页的指针。pageId数组用来存储每个Page对应的pageId。ref_bit, pin_count数组分别存储各page的时钟标志和pin标记(用于时钟算法),dirty数组标志各page的修改状态,以确定换出时是否需要写回硬盘。
    4.3 目录页
    size 表示目录页的总大小,包括在硬盘的部分。globalDepth 表示全局深度,start 表示目录当前在内存部分的头下标,pageId 表示以 start + (数组下标)为哈希值的桶的 pageId。
    目录总的结构为:

    五、主要算法5.1 低位哈希算法5.1.1 哈希方法直接将 key 值截取低 globalDepth 位。比如 13 二进制为 1101,则当全局深度为 1 时返回 1, 全局深度为 2 时返回 1,全局深度为 3 时返回 5, 全局深度大于等于 4 时返回值都为 13。
    程序中使用的方法是 key 值对 1 左移 globalDepth 位后的值取余:key % (1 << depth)
    5.1.2 桶分裂算法假设原来桶中的记录哈希值为 hashKey,则分裂后桶中的记录的哈希值可能为 hashKey 或者 1 << localDepth + hashKey,即原来的 hashKey 最高位前补 0 或 1。所以桶分裂后桶中的记录会根据记录的 key 值的哈希判断是留在旧桶还是移动到哈希值为 1<< localDepth + hashKey 的桶中。另外由于采取的是变长记录,删除掉一条记录后需要对之后的所有记录进行移动,所以旧桶对移出的记录不立即删除,而是将其长度置为-1,等桶分裂完成后再一次性将旧桶中所有长度为-1 的记录删除。
    void Buffer::spiltPage 当桶中存在下一条记录时循环: 计算该条记录的哈希值并与当前桶的哈希值进行比较 两者相同则进行下一次循环 两者不同则将该记录插入新桶中,并将该记录在当前桶的长度置为-1对旧桶进行更新操作
    void Page::adjustPage 当桶中存在下一条记录时取该条记录进行循环得到该条记录的长度,判断其是否为-1 是,则继续循环 否,则取在其前面的距离最远一条长度为-1的记录,覆盖为该条记录更新桶的记录数和可用空间偏移
    5.1.2.1 目录维护桶分裂时需要对目录进行相应的维护,这里分两种情况:
    localDepth < globalDepth
    这时需要将目录中新的哈希值所对应的桶号改为新桶桶号,同时如果需要的话,还要把另外一些关联的哈希值的对应桶号也改为新桶桶号。比如当前全局深度为 7,一个局部深度为 5,哈希值为 00110 的桶要进行分裂,这时不仅要将哈希值 01 00110 对应的桶更改为新桶桶号,还要将哈希值 11 00110 对应桶号做相同更改。
    void Manager::insert Value = 0 循环 Offset = value << (localDepth + 1) Hash = oldHash + (1 << localDepth) + offset 当得到的哈希值超出允许位数时退出循环 设置hash对应的桶号为新桶号 Value++
    localDepth == globalDepth
    这时仅需要将目录翻倍后将哈希值为 oldHash + 1 << globalDepth 对应的桶号更改为新桶的桶号即可。
    5.2 高位哈希算法5.2.1 哈希方法从第 23 位开始从高到低将 key 值截取 globalDepth 位。比如 2816 二进制为 0000 0000 0000 1011 0000 0000,则当全局深度小于等于 12 时都返回 0, 全局深度为 13 时返回 1,全局深度为 14 时返回 4, 全局深度为 15 时返回值为 5 等等。
    程序中使用的方法是 key 值右移 23-globalDepth 位后与 007fffff(16)取与的结果:
    (key ≫ (23 − globalDepth) ) & 007
    5.2.2 桶分裂算法假设原来桶中的记录哈希值为 hashKey,则分裂后桶中的记录的哈希值可能为 hashKey << 1 或者(hashKey << 1) + 1,即原来的 hashKey 的两倍或者两倍加一。这里不会将旧桶中的记录分到两个新桶,而是将旧桶对应的哈希值翻倍,之后再像低位扩展一样,根据旧桶中的记录的哈希值进行判断,哈希值恰好是原哈希值两倍的记录留在旧桶,其他移往新桶,并在全部记录都移动好后再对旧桶进行整理。
    5.2.2.1 目录维护桶分裂时需要对目录进行相应的维护,这里也分两种情况:
    localDepth < globalDepth
    这里跟低位扩展一样,不仅需要将新桶对应的哈希值更新,还需要对关联的哈希桶的哈希值进行更新。与低位扩展不同的是,高位扩展是在最低位后补 0 或 1。比如当前全局深度为 7,一个局部深度为 5,哈希值为 00110 的桶要进行分裂,这时不仅要将哈希值 00110 10 对应的桶更改为新桶桶号,还要将哈希值 00110 11 对应桶号做相同更改。
    void Manager::insert start = localHash << (globalDepth - localDepth) size = 1 << (globalDepth - localDepth - 1) for (int j = size; j < (size << 1);j++) 设置start + j 对应的桶号为新桶号
    localDepth == globalDepth
    这时仅需要将目录翻倍后将哈希值为 (oldHash << 1) + 1 对应的桶号更改为新桶的桶号即可。
    5.3 时钟算法每个桶都有一个 pin 标志位和 ref 标志位。Pin 表示该页不可被换出,ref 表示当前页的时钟标志。
    int Buffer::choseByClock 从 currentPage 开始循环取 buffer 中的下一页(首尾循环) 若当前页的 pin 与 ref 均为 false 则退出循环 否则如果当前页 pin 为 false,将其 ref 设为 false 返回当前位,并把 currentPage 设为下一页
    5.4 插入记录算法 void Manager::insert 对将要插入的记录的键值进行哈希得到哈希值 通过得到的哈希值查找 index 目录,找到其应放入的页的pageID 如果该页不在内存中 用时钟算法得到一可用来替换的页 如果该页的 dirty位为 true,表示该页已修改 将该页写回硬盘 从硬盘读入要插入的页,放于替换出去的页的内存中将该记录插入内存中对应的页 如果插入不成功,则需要进行分裂 页分裂 递归调用自身再次进行插入 修改该页dirty位为 true
    六、性能测试 以下为软件在本人电脑中运行的截图:
    低位扩展哈希,8 页

    低位扩展哈希,128 页

    高位扩展哈希,8页

    高位扩展哈希,128 页

    可得到表格:
    哈希方式和缓存页数与程序效率的联系

    通过对比低位哈希和高位哈希时的建立索引耗时和查找记录耗时,可以看出建立索引时高位扩展比低位扩展速度要快很多,相差了两个数量级。这很大程度上是因为 listitem.tbl 文件本身是有序的,这就导致高位扩展建立索引时数据会按照顺序写入桶中,并且前期因为数据的哈希值都为 0,导致桶和目录的不断分裂,而由于数据的有序性,后来插入的数据的桶命中率会很高,这就大大降低了 I/O 次数,最终导致了高位哈希的建立索引的速度较快。
    而查找数据时低位哈希却比高位哈希快了 3 倍多,这应该是因为测试时 testinput.in 文件中的数据是随机生成的,高位扩展发挥不了其优势,由于数据的随机性目录页的命中率和缓存页的命中率都下降的缘故,而低位扩展数据分布得较为均匀,所以命中率会较高,最终导致低位扩展速度较快。
    而对比 8 页和 128 页还可以发现无论是低位还是高位扩展,更多的缓存页都会一定程度上提高索引和搜索时的速度,但提高程度并不是特别明显。
    哈希方式和缓存页数与存储空间和 I/O的联系

    首先可以明显看出高位哈希的 I/O 次数和 I/O 用时占比明显低于低位扩展,其中的原因在上一步已经分析过,是因为高位扩展本身所需的 I/O 次数比较少,占比也就会比较少。
    目录大小方面,高位索引的目录也比低位索引的小,但哈希桶的数量却比低位扩展的少,这应该是由于低位扩展的数据分布不均匀才导致桶的数目少但目录却更大的结果。
    七、性能优化
    尽量使用位运算

    在一些频繁使用的函数如哈希函数中尽量使用位运算而避免使用乘除法,一些简单的如乘二除二的运算也用左右移位来替代
    将调用次数较多的函数改为宏函数或去掉函数调用直接计算

    优化程序时利用 VS 的性能报告发现一些需要频繁使用的函数用时占比较多,将其改为宏调用或直接计算后可加快 10% ~ 15%的程序用时
    使用Win API代替 C 库函数 (暂未实现)

    直接调用 Win API:CreateFile,ReadFile,WriteFile,SetFilePointer。使用 Win API 会降低程序的可移植性,但是对于数据库这种对性能要求较高的系统,必然会针对平台进行优化,所以使用 Win API 是很正确的做法
    预计效率要比使用 fread 等高不少。但由于 CreateFile 和 ReadFile 和 WriteFile 会自动把原本单字符的\n 读入或写出为\n\r 的双字符,修改成本高,经过一天的尝试均告失败后,由于时间关系,最终决定暂时不做这部分的优化

    让 BUFF 在空间上连续

    申请 BUFF 时候,让所有页位于一个连续的空间内,这样能够显著提高 Cache 的命中率,最终能够提升速率
    0 评论 3 下载 2018-10-21 16:17:33 下载需要8点积分
  • 基于Newban的Nancyj字体Email签名工具

    一 需求分析Newban是一个输出mail签名的工具,能够在终端将字符进行“图形化”输出。本程序就是用所给字体文件,在屏幕上输出该字体的签名,并要求实现如下功能:

    能设定输出宽度
    能设定输出对齐方式,可以设置左对齐、居中对齐、右对齐
    能指定字体

    基本要求如下所示:

    字体信息必须以文件形式存放,文件名为 nancyj。输出时,从字体文件中取出相应字符图形
    输出宽度信息以命令行参数形式传递给程序,如果省略宽度信息,默认为80个字符宽度。参数开关为-w。例如,如果程序名为newban,则运行时指定输出宽度为200个字符的命令为:newban –w 200
    对齐方式以命令行参数形式传递给程序,如省略,默认为左对齐。参数开关为-l,-c,-r。对齐方式可以指定其中之一。-l表示左对齐,-c表示居中对齐,-r表示右对齐。例如:指定输出为右对齐方式,则命令格式为:newban –r
    可以同时设定输出的宽度和对齐方式。例如,命令:newban–r–w 80则指定输出右 对齐,输出宽度为80字符
    (提高)可以设定输出字体,命令行参数用-f,后面接字体文件名。例如,以字体standard.data字体为输出字体,则命令为:newban –f standard.data
    程序运行后,接受用户输入,按回车后以指定格式和宽度显示内容,直到用户输入quit 结束
    字体文件中应含如下字符的图形信息:a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9

    二 程序设计2.1 总体设计
    2.2 详细设计2.2.1 主程序设计 1.主函数原型:int main(int argc, char*argv[])
    2.功能:在主函数里调用其他功能函数,实现功能。
    流程图如下所示:
    说明:

    主函数中没有结束标志,当调用输入函数时,遇到 quit命令,会退出程序
    num_word是单词数,每次循环都需要初始化,以免对二次输入产生影响
    判断命令行参数,将输出宽度信息存储在width中,将对其方式信息存储在 alignment中,将字体文件名信息存储在 filename中

    判断命令行参数的具体流程如下:

    2.2.2 读取文件 1.函数原型:void read()
    2.功能:读取字体文件中的图形信息,并存入三维数组 Type中。
    流程图如下所示:

    说明:

    读取的高度信息,用 atoi 函数转化为整型数字,存储在 high 中
    读取的图形信息,存储在三维数组 Type 中
    由于读取是用的 fgets 函数,每个字符串后面均有一个换行符,所以要去掉换行符

    2.2.3 用户输入 1.函数原型:void put_in()
    2.功能:接收用户输入,并计算单词数。
    流程图如下所示:

    说明:

    用户输入的信息存储在 Text中,用 strcmp 函数与“quit“比较,判断是否退出程序
    计算的单词数存储在 num_word中
    调用控制单词长度函数,根据单词长度增加单词数,以达到控制单词长度的目的

    2.2.4 控制单词长度 1.函数原型:void iswordover()
    2.功能:判断每个单词的图形字符数总和,如果一个单词的字符数总和大于了规定宽度,则增加单词数,以便于下面利用储存单词函数把过长的部分储存成另一个单词。
    流程图如下所示:

    说明:

    功能主要判断每一个单词是否超过 width,根据超过的部分的长度增加单词数,以达到控制单词长度的目的。
    2.2.5 储存单词 1.函数原型:void make_word()
    2.功能:把每一个单词存放到在主函数中申请的num_word个动态数组中,以便于输出时调用。同时调用相邻字母衔接处的判断函数函数 issame()和相邻字母衔接处的修改函数alter() 实现相邻字母共用相同的字符。
    流程图如下所示:

    说明:

    将Text中的每一个字符进行处理
    将单词存储在动态数组 p_word 中,根据指针移动控制单词序数
    遇到空格,或者 p_word 储存的单词的长度达到规定宽度,移动指针到下一个单词,将下面的字符图形储存到下面的单词,达到将单词一个一个储存的目的
    在将字符图形连接到 p_word 前,先调用衔接处判断函数,判断是否需要修改衔接处,需要则调用衔接处修改函数

    2.2.6 判断相邻字母衔接处 1.函数原型:int issame(int j)
    2.功能:判断相邻字母的衔接处的字符是否相同或者有一方为空格。
    流程图如下所示:

    说明:

    当首尾衔接处的字符都相同,或者由一方为空格,则返回真值 1;否则返回 0
    2.2.7 修改相邻字母衔接处 1.函数原型:void alter(int j)
    2.功能:修改相邻字母衔接处的字符,实现相邻字母共用相同的字符串。
    流程图如下所示:

    说明:

    copy、alter3是动态数组。
    在修改之前,要将图形信息复制给copy,以防改变字体形状数组 Type 中的值

    2.2.8 缓存处理 1.函数原型:void buffer()
    2.功能:将储存好的单词进行连接处理,判断何时输出与怎样输出。 流程图如下所示:
    说明:

    Out_word是用于输出的动态数组,储存的单词在这里进行连接和输出
    当判断out_word的长度加上空格长度加上将要连接的单词长度超过规定长度,则先输出,在进行覆盖,以达到控制输出宽度的目的
    空格,指的是在字体文件最后储存的一个空格图形,在 Type中的下标为62

    2.2.9 输出 1.函数原型:void put_out()
    2.功能:将out_word中储存的短语输出,同时调用输出对齐方式的控制函数out_type()函数计算出每行左边要输出的空格数。
    流程图如下所示:

    说明:

    为了控制输出的对齐方式,便采用在左端输出空格的方式实现,调用 out_type()函数计算空格数,在每行输出 out_word之前先输出空格
    2.2.10 控制输出格式 1.函数原型:int out_type()
    2.功能:根据规定的输出宽度和out_word中字符串宽度,计算出输出时左方应输出的空格数。
    流程图如下所示:

    说明:

    alignment为对齐方式信息,width为规定的宽度,strlen(out_word->line)是要输出图形的长度
    当对齐方式为左对齐时,返回 0
    当对齐方式为左对齐时,返回(width-strlen(out_word->line))/2
    当对齐方式为左对齐时,返回 width-strlen(out_word->line)

    2.2.11 字体文件
    文件概况:第一行为此字体文件的字体形状的高度信息;再往下为字体形状信息,按竖排分布,每次用 fgets函数读取一行
    文件制作程序:为了方便制作字体文件,编写了一个程序,可以将横排分布的字体文件转换为竖排分布
    文件截图如下所示:


    三 运行测试3.1 默认字体(nancyj)、对齐方式(左对齐)、输出宽度(80)
    3.2 居中对齐”-c”开关
    3.3 右对齐”-r”开关
    3.4 输出宽度”-w”开关
    3.5 切换文件”-f”开关(big字体)

    (nscript字体)

    3.6 字体文件制作程序 (源文件)

    (目的文件)
    1 评论 2 下载 2018-10-21 10:55:52 下载需要9点积分
  • 基于C语言实现的流星雨模拟课程设计

    一、课程设计内容及要求程序模拟一组流星飞向地面的情景。地面用多行#来表示,流星用大写字母不表示。程序产生一组流星(比如10个),从屏幕顶部下降飞向地面。一组流星中,每个流星的字符颜色是随机的,下降的位置是随机的,下降的速度也是随机的。一个流星下落只能去掉一个#号,当最后一行地面有#被去掉时,程序终止。
    二、设计思路
    程序在下降过程中,程序必须知道流星的字符、颜色、位置、速度,因此程序需要定义以下几个数组变量:存放流星字符的数组,存放流星字符颜色的数组,存放流星行位置的数组,存放流星列位置的数组,存放流星下降速度的数组
    可定义二维数组screen表示地面和天空,此数组是一个24行81列的字符数组。上面的行表示天空,数组单元的值是空格;最下面的几行(如5行)表示地面,数组单元的值是’#’;整个屏幕的大小是80*25,即25行80列, 为了在输出最后一行时不换行滚屏,程序只能利用上面的24行空间。把数组定义成81列的目的是,每行的最后字符赋值成’\0’,就可以按照字符串的方式输出每行文本了
    程序首先输出地面和天空,即输出定义的二维数组screen中的字符串,前19行是空行,后5行是#号。这样screen[24][81]的字符矩阵就与整个屏幕对应起来
    然后随时机产生一组流星数据,包括字符、颜色、位置和速度。速度用一次下降多少行来表示,最大的速度是4。由于要随机产生这些数据,因此需要调用random函数。random函数的原型是 int random(int num); 这个函数产生一个0—num-1之间的一个随机数。 流星字符可以这样产生:random(26)+’A’; 流星字符的颜色可以这样产生:random(16)+1;流星下降的位置可以这样产生:random(4)+1;流星的行位置一开始都是1; 流星的列位置可以这样产生:random(80)+1;但要保证所有流星的列位置不能相同(设计一个算法来判断)
    调用random之前,用randomize()库函数进行初始化。 两个库函数都在stdlib.h文件中
    每个流星按照自己的速度下落,所谓的下落就是逐行移动流星字符:在下一行新的位置上显示流星字符,在原来的位置上显示空格以便擦除流星字符,然后再延迟等待几十毫秒。这样循环往复就构成了流星下落的动画。 但要注意,流星的速度各不相同,而一次下落多行的流星也要逐行下落
    如果流星的新位置所对应的screen的单元格的值是’#’,则表示撞到了地面。 这种情况下在流星的新位置上输出空格,擦除#号,并且对screen相应的单元赋值为空格,流星字符也要赋值为空格,以表示流星消失
    当screen[23]中任何一个单元格是空格时,程序终止

    三、程序框图
    四、程序中遇到的问题及解决方案在C++程序设计中,我发现所给的设计思路是对的,但是里面提到的引用的库函数是不可执行的,例如randomize()这个函数,在VC中不能使用,于是我便选择了一个与其功能相似的rand()函数代替它;另外,在控制字母颜色的函数选择上,起初选用的textcolor()函数,但是一样不适用,于是经过再三思考后,选择了在课本中学习过的switch()语句,达到了预期的效果。
    0 评论 34 下载 2018-10-20 22:48:36 下载需要2点积分
  • 基于C++的图书管理系统

    一、需求分析图书管理系统功能如下:

    添加书籍
    删除书籍:当系统中存在此书时,删除该书籍,否则提示用户删除失败
    借阅书籍:当系统中存在此书且未被借阅时,将书的状态设置为“借出”,否则提示用户借阅失败
    归还书籍:当系统中存在未归换的该书时,将书的状态设置为“可用”,否则提示用户归还失败
    查询全部:输出系统中所有的书籍(书名升序)以及他们的状态
    查询书名:输出系统中所有与书名同名的书以及他们的状态
    书籍计数:输出系统的藏书量或某一本书的副本数量

    二、文件间关系

    MyLibrary.h与MyLibrary.cpp。定义并实现了Book类和Library类。前者用以表示书籍,后者构成整个系统。这部分属于类的设计范畴。(注:类设计者部分没有实现输出,输出由“使用者”实现)
    frame.h与frame.cpp。定义并实现了在main.cpp中使用了的函数。这些函数全部与输出有关
    main.cpp。程序运行的入口。直接调用library库和frame的函数。此文件内实现了简单的交互界面
    主要工作委托给std::multiset实现

    三、数据设计
    3.1 State 枚举enum State {Borrowed, Available}; //Borrowed表示被借出,Available表示书籍可用。
    3.2 Book 类class Book {public: //定义类型别名以实现抽象 typedef multiset<Book> data_t; //不允许书籍默认构造。只允许通过传入书名构造。将书籍状态设为“可用” Book() = delete; Book(const name_t& name): name(name), state(Available) { } ~Book() = default; //使用书名比较书籍间的相互关系 bool operator<(constBook& other) const { return name < other.name; } bool operator==(constBook& other) const { return name == other.name; } //一些访问和修改的函数 //关于书籍名字和书籍状态(“可用”,“已借”) State GetState() const {return state; } name_t GetName() const {return name; void SetName(constname_t& _name) { name = _name; } void SetState(State newState){ state = newState; }private: name_t name; State state;};
    3.3 std::multiset<Book>用以储存数据3.4 Library类classLibrary {public: //定义一系列类型类型别名实现抽象 typedef multiset<Book> data_t; typedef typename Book::name_t name_t; typedef typename data_t::iterator Iterator; typedef vector<Book> Info_t; typedef string file_name_t; //只允许该类默认构造,不允许复制 Library() = default; Library(const Library&) = delete; Library& operator=(const Library&) =delete; //分别为增加,删除,借阅,归还书籍。 void Insert(const Book& book) {libraryData.insert(book); } bool Remove(const name_t& name); bool Borrow(const name_t& name); bool GiveBack(const name_t& name); //返回该类中书籍的相关信息 vector<Book> GetBookInfo(constname_t& name) const ; vector<Book> GetAllBookInfo() const; //返回该类中书籍的数目 size_t Count(const name_t& name) const {return libraryData.count(Book(name)); } size_t NumOfBook() const { returnlibraryData.size(); } //保存信息到文件,从文件读取信息 //接受文件名为参数 void SaveToFile(const string& fileName)const; bool LoadFromFile(const string& fileName);private: //储存书籍的容器 data_t libraryData; //内部辅助函数。输入书籍的名字,返回一对迭代器(std::pair)指向一个区间范围。该范围内的书籍都有相同名字name inline auto _find(const name_t& name)const -> const pair<Iterator, Iterator> { return libraryData.equal_range(Book(name)); }};
    四、函数间关系(指frame文档内的函数)
    说明:

    所有函数的第一个参数都为ostream& os,用以接受输出流。在main中都将该参数赋为cout。调用者可根据实际情况修改
    所有函数都返回void
    下层的四个函数相互独立

    // 打印“>>>”用以实现交互界面inline void PrintPromt(ostream& os);// 打印指导信息。每次运行该系统都会输出该内容void PrintInstroduction(ostream& os);// 打印帮助信息。在系统中输入help会调用该函数输出帮助void PrintHelp(ostream& os);// 打印错误信息。该函数会将参数s直接打印出来(加上换行符)inline void PrintError(ostream& os, const string& s);// 将系统中的全部书籍以表格形式输出。接受library类。通过调用PrintTitle(打印表格)和PrintAllBook(打印所有书籍)执行打印inline void PrintTable(ostream& os, const Library& lib);// 打印表格的题头。执行实际的打印行为inline void PrintTitle(ostream& os);// 打印系统中的全部书籍。通过调用PrintBookInOneLine打印书籍void PrintAllBook(ostream& os, const Library& lib);// 实际执行打印行为的函数。每次只打印一行。接受Book的对象为参数inline void PrintBookInOneLine(ostream& os, const Book& book);
    五、数据交流
    注:

    user指这个程序实际的用户(甲方)
    main指Libraryclass的使用者
    Library指Libraryclass的底层实现
    file指储存信息的本地文件

    不提供跨层交流数据的接口。如main向file交换数据必须通过library。

    从user到main。通过cin交换信息。
    从main到user。通过frame.h和frame.cpp中定义的一系列函数进行输出到终端。(见函数间关系)
    从Library到main。vector<Book> Library::GetBookInfo(constname_t& name) const ;vector<Book> Library::GetAllBookInfo()const;通过Library类中定义的两个函数传送书籍信息到main。
    以及通过Library成员函数的返回值传递信息。
    从main到class。通过Library类中的成员函数的参数传递来传递数据。
    从Library到file。main向Library提供文件名,library根据该文件名存放数据入file中。
    file到library。main向library提供文件名,library根据该文件名向file读取数据。

    六、main函数的算法流程

    输入:输入使用getline读取整行
    是否语法错误:使用正则表达式检查语法错误。若格式不匹配,输出错误提示信息,重新等待输入
    是否逻辑错误:调用Library类并查看返回值。若返回false表示出现逻辑错误。输出提示信息,重新等待输入
    执行命令:命令被执行

    七、合法的命令
    add <bookname>:添加一本书。
    rm <bookname>:删除一本书
    brw <bookname>:借出一本书
    ret <bookname>:归还一本书
    cnt <bookname>:统计该书的数目
    cnta:统计所有(all)书的数目
    prt <bookname>:打印该书相关信息
    prta:打印所有书的信息
    save:将信息储存到本地文件
    load:载入本地文件中的信息

    八、使用的正则
    匹配命令的格式:^(quit|help|save|load|prta|cnta)(\s.*)?$|^(add|rm|brw|ret|cnt|prt)
    匹配整个输入的格式:^(quit|help|save|load|prta|cnta|(add|rm|brw|ret|cnt|prt)[a-z\sA-Z_]+(\w|_)*)$
    1 评论 63 下载 2018-10-20 18:38:59 下载需要8点积分
  • 基于C++的多项式计算器

    一、 实验环境1.1 编程语言和开发工具
    语言采用ANSI C++(C++11)
    开发工具:vim,g++,git,Visual Stdio(用于生成exe文件)

    1.2 编码规范编码规范中所有没有涉及到的内容,参见googlestyle C++

    类名

    类名采用开头大写的方式命名
    变量

    局部变量采用小驼峰法命名类私有和保护的成员变量末尾加下划线类公用变量命名同局部变量
    函数

    函数采用大驼峰命名法命名类的私有成员函数末尾加下划线,如 voidshrink_();get/set/isXXX等函数采用开头小写get/set/is,其后接开头大写单词的方式命名.如isValid, setName
    其他

    常量采用字母k + 大驼峰命名法命名。但类中的const变量依旧采用类变量的方式命名类型名称(typedef)采用下划线命名法命名。单词最后加_t。如name_t左大括号“{”与前一语句在同一行

    二、分析与设计2.1 需求分析
    系统储存多项式
    多项式相加
    多项式相减
    多项式与常数 数乘运算
    多项式代入某点求值
    系统显示储存的多项式
    显示帮助

    2.2 系统功能图
    2.3 类结构设计
    2.4 细节设计以下+表示public,-表示private,#表示protected
    以 [+-#] operation(type param) : return-type
    或 [+-#] attribute : type等形式给出。self代表类本身(Polynomial)

    接口设计:

    +typedef vector<double> coefficient_t;
    +typedef Polynomial self;
    +Polynomial(string); // 构造函数,对输入作parsing。输入应形为”(1, 3)(2, 4)(4, 6)”
    +operator rel(const self&) : bool //关系运算符。rel 为 == 与 !=,判断两个多项式是否相等。
    +operator op(const self&) : self //运算符。op为+,-,*中的一个
    +operator op(const self&) : self& //运算符。op为+=,-=,*=中的一个
    +operator op(double) : self //数乘运算符。op为, =中的一个
    +operator<<(ostream&, self) :ostream& // 作cout用
    +getDegree() : size_t // 返回该多项式的次数
    +setDegree(int) : void // 设置多项式的次数。若设置的次数低于多项式原本的次数,设置被拒绝。
    +getCoefficient(): vector<double> // 返回一个数组,储存多项式系数
    +derivation() : self // 返回该多项式求导后得到的多项式
    +evaluate(double) : double // 返回多项式代入某点值后的值

    成员函数:

    -shrink_() : void // 简化多项式。即将多项式系数为零且没有必要储存的项舍去。
    数据成员:

    -coefficient : vector<double> //储存多项式的系数
    静态成员:

    +isValid(string) : bool // 接受一个代表多项式的字符串,判断其是否合法+pattern_ : const string // 用于正则表达式的字符串。判断多项式输入是否合法。用于isValid+regex_ : regex //判断输入是否合法的正则。使用了pattern_+pattern_iterate_ : string //用于正则表达式的字符串。用于提取括号。+regex_iterate_ : const regex // 提取括号。使用了pattern_iterate_+poly_name_pattern_ : const string //用于正则表达式。检测用户提供的多项式的名字是否合法+regex_poly_name_ : regex // 正则表达式。判断用户提供的多项式的名字是否合法。

    2.5 正则匹配
    匹配一个单词


    提取一个括号


    判断输入是否合法

    三、测试心得本次项目相比于前几次项目,难度较低。唯一的难点是parsing,即如何将用户的输入正确地转换为多项式。parsing可以用灵活的正则表达式完成。其次的难点是用户交互界面,即如何用尽量简短的代码完成交互界面的需求,写出DRY(don’t repeat yourself)的代码。
    相比之下,多项式类的实现反而显得难度较低。可以采用vector<double>储存按索引储存系数,或采用足够大的double[]储存系数。(本项目不考虑指数为负的情况)。
    本次项目有些地方可以更加细致。例如根据“面向接口编程”思想,“交互”应该与“数据”和“接口”分离开。Polynomial类应该从一个Interface_Polynomial类继承而来。由前者作为接口,后者作为实现。main函数中所有函数的参数都使用Interface_Polynomial而非Polynomial。
    本次项目还可以拓展成允许矩阵运算的计算器。将两者结合在一起。
    本次项目让我对C++的使用更加熟练。让我对正则的书写更为了解。加深了我的对面向对象的理解。
    2 评论 11 下载 2018-10-20 16:32:23 下载需要6点积分
  • 基于C++的学生选课管理系统的设计与实现

    一 需求分析
    系统添加课程:将一门课程加入到系统数据中。课程提交重复时给出提示信息
    系统删除课程:以课程编号为索引删除课程。系统无此课程时给出提示
    课程添加学生:把学生的姓名、学号等信息加入到课程中。学号重复时给出提示信息
    课程删除学生:以学号为索引从课程中删除学生。课程无此学生时给出提示
    课程添加教师:把教师的姓名、教工号等信息加入到课程中。教工号重复时给出提示信息
    课程删除教师:以教工号为索引从课程中删除教师。课程无此教师时给出提示
    课程输出数据:提供某课程的学生、教师信息

    二 程序设计2.1 功能结构图如下所示:

    2.2 数据设计数据架构如下图所示:


    Object(abstractbaseclass):抽象基类。定义了“姓名”和“编号”两种属性。它是Student(class),Teacher(class),Course(class) 最终的父类
    Person(class):表示人。没有额外定义属性。Student(class)和Teacher(class)直接继承这个类
    Teacher(class):表示教师。没有额外定义属性
    Student(class):表示学生。没有额外定义属性
    std::set(templateclass)
    Group(template class):对std::set作轻度封装
    Course(class):表示课程。包含成员Group<Student>和Group<Teacher>,定义一系列添加、删除、访问的操作
    System(class):表示整个系统的类。是整个程序的核心。包含成员Group<Course>。定义了一系列添加、删除、访问操作

    2.3 函数设计函数结构如下图所示:
    // 格式化输出的辅助函数。以上几乎所有函数都调用OutputHelpervoid OutputHelper(const string& s1, const string& s2);// 调用PrintTitle和PrintCourse以表格的形式打印所有的课程void PrintAllCourse(const System& sys);// 输入一个课程的全部信息void PrintCourse(const Course&crs);// 打印“>>> ”用以实现交互界面inline void PrintPromt();// 打印“… ”用以等待用户输入更多信息inline void PrintPromtForInput();// 打印指导信息。每次运行该系统都会输出该内容void PrintInstroduction();// 打印帮助信息。在系统中输入help会调用该函数输出帮助void PrintHelp();// 打印错误信息。该函数会将参数s直接打印出来(加上换行符)inline void PrintError(const string& s);// 打印表格的题头。执行实际的打印行为inline void PrintTitle();// 模板函数,接受Name和Id,返回T。T是应当是Student,Teacher,Course中的一个T InputNameAndId();// 模板函数,接受Id,返回T。同上T InputId();
    三 程序实现3.1 实现思路3.1.1 结构间关系系统中包含了一系列课程,课程由名称和编号构成;课程包含了任课教师和选课学生,学生和教师都含有名称和学号/教工号等信息。系统负责管理课程,课程负责管理教师和学生。
    3.1.2 具体逻辑“添加”操作需要“名称”和“编号”两项信息。“删除”操作只需要“编号”作为索引。编号保证唯一,是确定身份的凭证。
    3.2 数据流流向 数据流流向图如下所示:


    User指用户,即系统管理员
    User Interface指程序的交互层,包括文件main.cpp,IOHelper.*
    BusinessLogic指业务逻辑层,包括除上述文件以外的所有文件
    Dataaccess 数据访问层,系统采用文件IO的方式储存数据

    3.3 main函数算法流程如下图所示:


    输入:输入使用getline读取整行
    是否语法错误:使用正则表达式检查语法错误。若格式不匹配,输出错误提示信息,重新等待输入
    是否逻辑错误:调用System类并查看返回值。若返回false表示出现逻辑错误。输出提示信息,重新等待输入
    执行命令:命令被执行

    四 程序运行case1:

    case2:

    case3:

    case4:

    case5:

    case6:

    case7:
    1 评论 80 下载 2018-10-19 16:30:49 下载需要10点积分
  • 基于C++的物资管理系统的设计与实现

    一 需求分析程序需实现以下功能:

    新物资信息录入(编号、名称、库存)
    查询已录入的所有物资信息(编号或名称为索引)
    添加物资信息(编号或名称为索引)
    领物资并生成领料单(编号或名称为索引)
    浏览领料单或物资库存清单
    物资信息保存,创建格式化文本

    二 程序设计程序流程图如下所示:


    三 程序实现3.1 开发环境
    Windows XP
    Microsoft Visual C++ 6.0

    3.2 程序结构3.2.1 函数头#include <iostream.h> //包括cin , cout , 等函数 #include <string.h> //包括 类,对象,数组,等函数定义#include <fstream.h> //包括 文本文档的读/写/复制 等函数#include <stdlib.h> //包括 exit等函数
    3.2.2 主函数void main ()
    3.2.3 子函数void Entering(fstream); //新物资信息录入void Inquire(fstream); //单一物资查询void Append(fstream); //旧物资的添加void Getthing(fstream); //领物资void Showthing(fstream); //领料单及库存清单void CreateTxt(fstream); //物资信息生成
    3.3 关键类与函数实现3.3.1 物资类建立一个物资类,功能模块为成员函数,物资信息为私有成员,封装性得到保证,其中,ID[10]用来存放物资编号,thingname[40]用来存放物资名称,sum是物资库存量。
    class Material {public: void Entering(fstream); //新物资信息录入 void Inquire(fstream); //单一物资查询 void Append(fstream); //旧物资的添加 void Getthing(fstream); //领物资 void Showthing(fstream); //物资库存清单 void CreateTxt(fstream); //物资信息保存private: char ID[10]; char thingname[40]; long sum;};
    3.3.2 录入函数函数开头和结尾都使用system(“cls”)清除所有显示的信息。
    fs.write((char*)this,sizeof(Material));
    将新物资信息持续录入到文件中储存
    在for()中镶嵌if语句,输入choice,if(choice==0)break;退出信息录入
    返回主菜单
    3.3.3 查询函数考虑到使用时不可能记得所有物资编号,所以运用了2种查询方式,包含按名称查询和按编号查询2种模式查询,实用性更高一点。
    在for语句中内嵌switch语句,进行选择操作,
    do { fs.read((char*)this,sizeof(Material));}while(strcmp(thingname,name)!=0&&fileend!=fs.tellg());
    当输入物资名称与保存在文件中的物资名称不同时,程序将文件中的信息写入内存中。
    每次将信息读入内存后读指针后移,不断移动后,当输入要查询的物资名name与文件中保存的物资信息名称相同时,输出物资信息到显示器上。
    按编号查询功能同上。
    3.3.4 添加函数同样运用2种模式进行添加(按名称进行添加,按编号进行添加)
    在该功能模块中
    do { fs.read((char*)this,sizeof(Material));}while(strcmp(thingname,name)!=0&&fileend!=fs.tellg());
    当输入物资名称与保存在文件中的物资名称不同时,程序将文件中的信息写入内存中,当名称相同时就输出物资的信息(相当与库存多少),而后cin>>num,当num正确时,就添加物资库存数量。
    完成一次添加操作后,
    fs.seekp(-long(sizeof(Material)), ios::cur ); // 指针复位fs.write( (char *)this , sizeof(Material)); // 写入文件cout << "现库存量:"<< sum<< endl;
    将写指针复位,将实行了添加的物资信息重新写入到文件中,同时显示已添加物资的库存信息,确保已经完成添加工作。
    按编号添加功能模块大致同上
    3.3.5 领物资函数do { fs.read((char*)this,sizeof(Material));}while(strcmp(thingname,name)!=0&&fileend!=fs.tellp());
    将物料信息读取到内存中
    if(strcmp(thingname,name)==0)
    当输入的物资名称和已报存的物资名称相同时,领取物料,减少库存
    outfile<<"第"<<i<<"次:\t"<<"领取"<<thingname<<"数量: "<<num<<" "<<"库存:"<<sum<<endl;
    将领料记录用outfile输入到”领料记录文件”中,方便领料单的查询。
    将领料之后的剩余库存信息重新保存到物资库存清单中,修改实际库存量。
    编号查询模式大致功能代码同上。
    3.3.6 库存清单及领料单函数该函数包含领料单浏览和物资库存清单浏览2个模块。
    模块1:领料单浏览
    char s[1024];while(fh.getline(s,1024)) cout<<s<<endl;
    fh.getline从领料记录文件中持续读取一行字符存放到内存s[1024]中,然后将领料记录完全展示在显示器上,实现领料单的浏览。
    模块2:物资库存清单浏览
    将读指针移动到“物资库存清单.dat”文件的开始,然后用do..while及成员函数read将“物资库存清单.dat”中的物资信息全部读取到内存中,而后逐个输出到显示器上,实现物资库存清单的浏览。
    3.3.7 信息生成函数从二进制文件“物资库存清单.dat”中读记录,不断将信息导入到ASCII文件“物资信息文本”中,可以直接查看物资信息。
    ASCII文件“物资信息文本”建立成功。提示是否查看,输入指令,运用getline函数将信息读取到内存,展示到显示器上,实现对物资信息的保存,创建格式化文本。
    四 程序运行主菜单如下图所示:


    物资添加功能 选中后,可以选择2种物资添加模式(按编号或名称添加),而后输入要添加的物资的编号(或名称),即可显示该物资现有信息,输入需要添加的数量,即添加成功;添加成功后可以选择继续添加或是返回上一级菜单
    物资领取功能 选中后,可以选择2种物资领取模式(按编号或名称领取),而后输入要领取的物资的编号(或名称),即可显示该物资现有信息,输入需要领取的数量,即领取成功,同时保存领料记录;物资领取成功后,可以选择继续领取物资或是返回主菜单
    库存清单和领料单功能 选中后,输入要显示的模块(库存清单或领料单);完成物资库存清单或领料单的显示后,可以选择继续或是返回主菜单
    物资信息生成功能 选中后,程序会显示物资信息文档保存成功,是否查看(可选择是否查看);完成该功能模块后,以选择继续或是返回主菜单
    退出程序 选中后,按任意键,退出程序

    五 参考文献《C++程序设计(第二版)》 谭浩强 编著
    1 评论 18 下载 2018-10-19 14:40:42 下载需要11点积分
  • 基于C++的平面形状编辑器的设计与实现

    一 需求分析参考如下给出的类层次关系,实现一个平面上的形状编辑程序序。要求如下:


    按照下面类图给出的层次关系来定义类
    所有形状支持无参数构造,有参数构造,拷贝构造,析构
    所有形状支持平移操作,需要重载 operator+
    所有形状(除去无意义的),均支持计算周长
    所有形状(除去无意义的),均支持 Draw()操作,此时只要要显示形状的名称,位置等信息
    需要实现一个 CShapeArray类,该类类似一个数组,用来存放放编辑过程中的平面形状。该类需要支持:添加,插入,删除,查询,复制等等操作。可以支持形状编辑中需要的针对形状的操作
    主程序中实现用户输入形状及其参数,然后把形状存入 6中定义的 CShapeArray。在输入形状的同时,用户可以查询当前已经输入入的形状(可按名称(需要对每个平面形状加入名称),位置来查询)。支持用用户对形状的复制,粘贴(粘贴时假设用户指定粘贴的位置)。同时支持用户对对形状的删除操作
    输入和处理好的形状可以存入文件,并从文件中读入
    支持对当前所有形状的 Draw()

    二 程序实现2.1 开发环境本程序使用Visual Studio 2010编写,并按照规范风格,将类、函数、变量的声明、其他头文件的引用等写在头文件(*.h文件)中,并使用头文件保护符(#pragma once)保护以免重复编译,具体函数的定义等则写在对应的源文件中。函数的注释说明写在头文件函数声明处,函数内部具体代码说明写在源文件代码处。
    2.2 类与变量声明2.2.1 类声明本程序定义了以下几个类(其中由于 CPoint为VS中的保留类名,改用 CCPoint):
    class CShape;class CCircle:public CShape;class CCPoint:public CShape;class CLine:public CShape;class CRectangle:public CShape;class CPolygon:public CShape;class CTriangle:public CPolygon;class CShapeArray;
    2.2.2 全局变量及全局函数声明声明如下所示:
    /**********************全局变量声明**********************///保存到的文件名const static char *fname = "D:\\dat.txt";// CShapeArray对象CShapeArray arr;//记录要复制的形状所在位置int copyPos = -1;/************************函数声明************************///输入数字进行选择int input(char *, int);//清屏void clr();//暂停void pause();//选择操作void inputOp();//选择形状void inputShape();//查询void inputQuery();//输入位置查询void inputPos();//输入名称查询void inputName();//复制void inputCopy();//粘贴void inputPaste();//删除void inputDel();//保存至文件void saveToFile();//从文件读取void loadFromFile();//输入矩形CRectangle* inputRectangle();//输入圆形CCircle* inputCircle();//输入多边形CPolygon* inputPolygon();//输入三角形CTriangle* inputTriangle();//输入点CCPoint* inputPoint();//输入直线CLine* inputLine();
    2.3 类定义2.3.1 CShape 基类 CShape定义如下:
    class CShape{ public: const static int SHAPE_CIRCLE = 1; const static int SHAPE_POINT = 2; const static int SHAPE_LINE = 3; const static int SHAPE_POLYGON = 4; const static int SHAPE_RECTANGLE = 5; const static int SHAPE_TRIANGLE = 6; //无参数构造 CShape(void); //有参数构造 CShape(char*); //析构函数 ~CShape(void); //拷贝构造 CShape(const CShape&); //获取名称 char *getName(); //显示名称等信息,使用虚函数 virtual void Draw(void); //计算并显示周长,使用虚函数 virtual void Calc(void); //保存至文件 virtual void saveToFile(ofstream&); //从文件读取 virtual CShape& loadFromFile(ifstream&); //判断形状是否存在 virtual int exist(); protected: //图形的名称 char* name; int shape;};
    其中函数
    virtual void Draw(void);virtual void Calc(void);virtual void saveToFile(ofstream&);virtual CShape& loadFromFile(ifstream&);virtual int exist();
    为虚函数,具体根据继承的子类不同而有不同定义,体现了 C++的多态性;
    变量 const static int SHAPE_CIRCLE等用于区分不同子类的具体类型,用于全局函数 loadFromFile()从文件加载形状信息。
    子类 CCircle, CCPoint, CLine, CRectangle, CPolygon继承于基类 CShape,类CTriangle 继承于类 CPolygon,分别表示具体的形状,并分别定义了各自的 Draw,Calc,saveToFile,loadFromFile等函数。
    2.3.2 CShapeArray类 CShapeArray用于存放编辑过程中的平面形状,支持:添加,插入,删除,查询,复制等操作。使用 vector<CShape*> vec来保存添加进的 CShape 对象的指针。
    其定义如下:
    class CShapeArray{ public: CShapeArray(void); ~CShapeArray(void); //添加 void add(CShape*); //插入 void insert(int, CShape); //删除 void del(int); //清除全部 void clear(void); //查询 CShape* get(int); //复制 void copy(int, int); //显示所有元素 void drawAll(void); //获取元素数目 int getSize(void); //根据名称查询 int findByName(char*); private: vector<CShape*> vec;};
    三 程序运行过程程序启动即进入功能选择主菜单。用户通过输入数字并按回车进行功能的选择,如果用户输入的不是数字,或数字超出了输入范围,则提示输入错误需要重新输入(该功能由 input函数实现)。添加形状时,根据用户选择的形状不同,要求输入的参数也不同,如果输入的参数能构成相应形状(如三角形两边之和要大于第三边,此功能由 exist函数实现),则添加该形状至对象 CShapeArray arr中,否则提示错误而不保存。
    输入形状的名称时,支持输入中英文,并可包含空格。
    本程序能自动纠正部分用户输入错误,以及从文件读取时发生的错误,并给出相应提示信息。
    四 运行测试启动后提示输入数字选择操作:

    选择操作输入形状,选择要输入的形状为圆形:

    输入圆心坐标和半径,按回车键确认:

    选择操作“2.查询已输入”,选择“3.显示全部”:

    保存至文件(默认保存至 D:\dat.txt中):

    从文件读取(默认从 D:\dat.txt中读取):
    1 评论 5 下载 2018-10-18 15:10:45 下载需要5点积分
  • 基于C++的民航订票系统的设计与实现

    一、需求分析设计一个民航管理系统,使更广大的用户群体可以借助这个平台订飞机票。
    C++作为一个面向对象的语言,利用多态性和封装性可以更好地表达出这些功能,相比 JAVA 等语言,C++具STL,可以表达更强的封装性。
    二、程序的主要功能
    登录和注册用户,管理员的登录
    存储用户的身份验证信息,账户信息,历史购票记录
    用户使用地址或者是航班号对特定的航班进行查询
    用户可以修改自己的密码、账户号和账户密码
    用户能够对航班信息进行确认
    用户对余额进行充值操作
    管理员可以对航班进行新建、修改、删除等操作

    三、程序运行平台
    qt-opensource-windows-x86-msvc2013_64-5.4.1+ Qt Creator 3.3.2
    Microsoft Visual C++ Compiler 14.0 (amd64)

    具体操作如下:
    qmake.exe -r -spec win32-msvc2013 CONFIG+=debugjom.exe -f Makefile.Debug cl -c -nologo -Zm200-Zc:wchar_t -Zi -MDd -GR -W3 -w34100 -w34189 -EHsc/Fddebug\CPP_Curriculum_Design.pdb -DUNICODE -DWIN32 -DQT_WIDGETS_LIB-DQT_GUI_LIB -DQT_CORE_LIB
    注:程序需要手动在build目录下建立userdb.dat,flightdb.dat 两个空二进制文件作为数据存储
    四、系统总框架图
    五、模块分析
    登录模块
    用户输入用户名和密码进行登录操作,从而得到权限进入主界面
    注册模块
    用户新建一个用户
    乘客模块
    乘客可以通过城市或者是航班号对特定航班进行查询
    查询模块
    系统显示符合条件的航班,让用户进行选择然后进行订票
    确认模块
    将航班详细信息显示出来,同时提示余额变化情况,以让用户进行最后确认
    修改信息模块
    用户对自己的密码,卡账号和密码进行修改
    充值模块
    用户通过输入正确的卡账号和密码进行充值操作
    查看历史订票信息模块
    用户查看自己曾经预订过的航班
    管理员模块
    管理员用户对航班进行添加,修改和删除

    六、类的说明User:
    class User{protected: string Name; // 姓名 string Password; // 密码public: User() {} // 默认构造函数 User(string,string); // 姓名和密码的构造函数 string getName() { return Name;} string getPassword() { return Password; } void setPassword(string s) { Password = s;} void setName(string s) { Name = s;} virtual void editInfo(string,string,string) = 0; // 虚函数用来让子类根据情况来修改信息};
    Passenger:
    class Passenger : public User{ string Id; // 身份证号 string CreditCardId; // 信用卡账号 string CreditCardPassword; // 信用卡密码 int CreditCardMoney; // 信用卡金额public: vector<FlightInfo> bookInfo; // vector容器保存的已经订购的航班信息 Passenger() {} // 默认构造函数 Passenger(const Passenger&); // 拷贝构造函数 Passenger(string,string); // 有账号和密码的构造函数 Passenger& operator = (const Passenger&); // 重载赋值运算符 void setId(string); void setCreditCardId(string); void setCreditCardPassword(string); void setCreditCardMoney(int); bool validateCreditCard(string,string); // 验证信用卡信息 bool recharge(int); // 充值账户 string getCreditCardId(); string getCreditCardPassword() ; int getCreditCardMoney() ; string getId(); void editInfo(string,string,string); // 编辑信息};
    Manager :
    class Manager : public User{ string phonenumber; string extra; // 管理人员的备注信息public: void editInfo(string,string,string); // 修改信息};
    UserManager:
    class UserManager { vector<Passenger> userlist; // vector容来用来存放用户清单public: UserManager(); // 构造函数 UserManager(const UserManager&); // 拷贝构造函数 ~UserManager(); // 析构函数 void findUserByName(string,Passenger&); // 通过名字来查找用户 bool addUser(string,string); // 增加用户 bool deleteUser(string); // 删除用户 bool editUser(string,const Passenger&); // 编辑用户信息 bool validate(string,string); // 验证身份信息};
    FlightInfo:
    class FlightInfo{ string No; // 航班号 string StartCity; // 起飞地点 string EndCity; // 降落地点 string StartDate; // 日期 string StartTime; // 起飞时间 string EndTime; // 降落时间 int FirstTicket; // 头等舱剩余票数 int FirstPrice; // 头等舱价格 int SecondTicket; // 二等舱剩余票数 int SecondPrice; // 二等舱价格public: FlightInfo() {} //默认构造函数 FlightInfo(string, string); //带两个参数的构造函数,参数分别为航班号和日期 ~FlightInfo(); //析构函数 FlightInfo(const FlightInfo&); // 拷贝构造函数 FlightInfo& operator = (const FlightInfo&); //重置赋值运算符 void setDateTime(string,string,string);//设置航班起飞的日期和时间 void setFirstPrice(int,int); //设置头等舱的价格和剩余票数 void setSecondPrice(int,int); //设置二等舱的价格和剩余票数 void setBaseInfo(string,string);//设置出发地和目的地 void setNo(string); //设置航班号 string getNo() { return No; } string getStartCity() {return StartCity;} string getEndCity() {return EndCity;} string getStartDate() {return StartDate;} string getStartTime() { return StartTime;} string getEndTime() { return EndTime;} int getFirstTicket() {return FirstTicket;} int getFirstPrice() {return FirstPrice / 100;} // 为方便操作,将数值扩大一百倍来表示角和分,返回时则缩小一百倍 int getSecondTicket() {return SecondTicket;} int getSecondPrice() {return SecondPrice / 100;}};
    FlightManager:
    class FlightManager{public: vector<FlightInfo> flightlist; // 用vector容器来保存flighinfo对象,表示航班信息 FlightManager(); // 构造函数 ~FlightManager(); // 析构函数 FlightManager(const FlightManager&); // 拷贝构造函数 bool addFlight(const FlightInfo&); // 增加航班 bool editFlight(string,string,const FlightInfo&); // 编辑航班 bool deleteFlight(string,string); // 删除航班 void findFlightByPlace(string,string,string,vector<FlightInfo>&); // 给定起降位置来搜索航班 void findFlightByNo(string,string,vector<FlightInfo>&); // 给定航班号来搜索航班};
    七、关键函数// 使用md5对密码进行处理bool UserManager::addUser(string name, string password);bool UserManager::validate(string name, string password);
    八、总结 不足

    Qt GUI编程是第一次使用,在环境调试过程就花费了许多的时间。在许多细节上也存在很多的问题和缺陷,整个程序的效率受此影响并不高
    代码略显臃肿
    文件读写某些情况还是会出现问题

    体会

    文件读写过程非常的繁琐,而且十分容易出错
    1 评论 87 下载 2018-10-18 11:07:16 下载需要12点积分
  • 基于C++的简易数据库的开发与测试

    一 开发说明1.1 总体说明本次项目以c++语言编写简易数据库,数据库为<key:value>的简单形式,在本项目中,限定key为整数且不考虑溢出问题,value为字符串类型,不可为空,长度最长为19(其中第20位为\0字符)。主体程序面向用户提供四种主要操作,分别为查找、添加、删除和修改。文件中数据结构主要采用B+树,实现了对删除的结点的空间回收。数据库cache模拟系统中的cache以利用文件读取的局部性来增加读写速度。文件以二进制形式打开以便于管理。
    1.2 文件设计说明1.2.1 索引文件设计说明索引文件前4个字节为根节点所在地址,若为0则树为空,初始时。接着8个字节为第一个空白位置,初始时为8,即文件尾。然后依次是每个节点。每个节点分为三个部分,第一部分为12个字节,四个整数,分别是父节点地址、父节点在节点中的位置(从1开始)和当前节点关键码的个数,根节点父节点地址为0。第二部分为当前节点的关键码和其孩子的地址,若节点为叶节点,则为当前节点的关键码和关键码对应的值在数据文件中的地址的负数(因此可以根据孩子地址的正负来直接区别内部节点和叶子结点)。第三部分为下一个叶子节点的地址,若节点为内部节点,则该部分无意义。空白位置组成单项链表,最后一项始终为文件末尾。删除节点后将地址链接到链表头部。
    1.2.2 数据文件设计说明数据文件前四个字节为第一个空白位置,初始时为4。之后为数据,每条数据占用20个字节。空白位置组成单项链表,最后一项始终为文件末尾。删除节点后将地址链接到链表头部。
    1.2.3 日志文件设计说明用户版和正确性测试版中每次操作会将相关信息写入日志文件,主要用于程序调试。
    1.2.4 正确性测试文件及性能测试文件设计说明文件第一个数字为需要进行的操作,数字1、2、3的含义与用户版对应,其后是需要操作的key,根据操作种类确定后面是否有value。正确性测试文件中4的含义为测试所有key。性能测试文件名最后数字问测试循环单位,而非测试数据量。
    1.3 类设计说明1.3.1 Cache类(Cache.h)1.3.1.1 总体说明cache类模拟系统中的cache,将最近使用过的数据放入其中,利用局部性加快数据的读写速度。cache大小固定,为16384,即2^14。cache中每个数据有四个值:int key、string value、bool dirty、bool valid。其中valid表示这个数据是否有效,dirty表示这个key对应的值是否被修改过。
    1.3.1.2 主要函数// 构造函数,确定cache大小,将所有有效位置为0Cache();// 从cache中取值,将取回的值放在value中,返回值表示是否取值成功bool select(const int key, string &value);// 向cache中插入值,若cache中原来有值且被修改过,则将原来的key和value存入*oldKey和*oldValue。返回值含义为0表示插入失败,1表示原来没有值或值没有被修改,2表示原来有值且被修改过int insert(const int key, const string&value,int *oldKey,string*oldValue);// 从cache中删除,若cache中存有key且有效位为true,则将有效位置为falsevoid remove(const int key)// 更新cache中的值,若cache中没有key,返回false,否则更新值且将dirty置为truebool update(const int key, string &value)// 将cache中的内容写会,所有没有保存修改的key和value存入*key和*value,交由索引文件和数据文件保存void save(vector<int> *key, vector<string>*value)
    1.3.2 Database类1.3.2.1 总体说明Database类为数据库的主体实现。持有变量scale为B+树的阶数。三个文件流分别对应索引文件、数据文件和日志文件,常量ZERO用于写入空指针。
    1.3.2.2 主要流程(对应用户版)查找

    用户输入关键码
    查找cache,若找到,返回对应的值
    否则查找索引文件,若没有找到,返回false
    否则根据地址查找数据文件,取出相应的值
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    添加

    用户输入关键码和值
    若cache中存在关键码,返回false
    否则查找索引文件,若存在关键码,返回false
    否则在数据文件中插入值并获得位置
    根据查找结果在索引文件中加入关键码和数据地址
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    删除

    用户输入关键码
    在cache中删除此关键码
    查找索引文件,若关键码不存在,返回false
    否则根据查找结果在数据文件中删除值
    根据查找结果在索引文件中删除关键码
    根据返回值输出结果

    修改

    用户输入关键码和值
    在cache中更新关键码和值,若成功,返回true
    否则在索引文件中查找关键码,若没找到,返回false
    否则根据查找结果在数据文件中更新值
    将关键码和值插入cache中
    若有必要,更新cache中原来的关键码和值
    根据返回值输出结果

    1.3.2.3 主要函数// 构造函数,文件不存在初始化文件Database();// 数据文件查找void dataFile_find(const int dataAddress, string &value);// 数据文件添加,返回数据地址int dataFile_add(const string&value);// 数据文件删除void dataFile_delete(const int dataAddress);// 数据文件替换void dataFile_replace(const int dataAddress, string &value);// 索引文件查找,*size为最后一个节点关键码个数。返回值含义为0表示树为空或key小于最小值,1表示命中,2表示在两节点之间,4表示大于最后一个节点最大值。若命中,*indexAddress存放命中叶节点地址,*pos为节点位置,*dataAddress为值在数据文件中的地址。若不命中,*indexAddress存放应该插入叶节点地址,*pos为插入位置(1表示在第一个关键码之后),*dataAddress无意义。对每个节点采用二分查找法int indexFile_find(int key, int *indexAddress, int *pos, int *size,int *dataAddress);// 索引文件添加void indexFile_add(const int key, const int dataAddress, const int indexAddress,int pos, int size);// 索引文件添加并处理上溢void indexFile_addAndOverflow(const int key, const int dataAddress,const int indexAddress, const int pos, const int size);// 索引文件删除bool indexFile_delete(const int indexAddress, const int pos, int size);// 索引文件当前节点向左兄弟借一个关键码void indexFile_borrowLeft(const int indexAddress,int size, const int left, int leftSize,int parent, int parentPosition);// 索引文件当前节点向右兄弟借一个关键码void indexFile_borrowRight(const int indexAddress,int size, const int right, int rightSize,int parent, int parentPosition);// 索引文件当前节点和左兄弟合并void indexFile_mergeLeft(const int indexAddress,const int size, const int left, int leftSize);// 索引文件当前节点和右兄弟合并void indexFile_mergeRight(const int indexAddress,int size, const int right, const int rightSize);// 索引文件删除并处理下溢void indexFile_deleteAndUnderflow(const int indexAddress,const int pos, int size);// 更新到文件bool file_update(const int key, string &value);// 对外接口查找bool select(const int key, string &value);// 对外接口插入bool insert(const int key, const string&value);// 对外接口删除bool remove(const int key);// 对外接口更新bool update(const int key, string &value);// 析构函数,保存cache,关闭文件~Database();
    1.4 版本设计说明1.4.1 用户版用户版无法测试,故没有数据结构维护正确答案,每次打开数据库若索引文件或数据文件不存在则新建文件并初始化,用户操作的相关信息会写入日志文件。
    1.4.2 正确性测试版默认情况下每次清空索引文件和数据文件,从测试文件中读取测试数据进行测试。若遇到错误则停止,否则输出通过并进入手动测试界面。可以通过更改文件打开方式和使用注释中的代码进行数据库关闭后再打开的测试(主要用于测试cache)。在简易测试模式下需手动注释cache的引用进行测试。(否则数据均在cache中,难以找出错误)简易测试为针对B+树规模为5时构造层数为3的B+树,之后人为计算各种增删改查的特殊情况进行测试。随机测试模式随机生成测试数据和增删改查操作进行测试。
    1.4.3 性能测试版默认情况下每次清空索引文件和数据文件,从测试文件中读取数据测试数据进行测试。测试完成后显示测试所用时间(单位为毫秒),用户输入0退出。
    1.5 开发环境
    代码编写环境:Microsoft Visual Studio 2017 community
    测试环境:windows 8.1

    1.6 其他说明辅助项目auxiliary目录下CreateTestFile项目用于生成正确性测试和性能测试所需要的文件。
    二 测试报告2.1 正确性测试分别运行简易测试和随机测试文件,测试结果均没有异常。

    2.2 性能测试2.2.1 测试结果当B+树的规模不同,数据量为1000000,插入次数为10000、循环测试次数为10000、删除次数为1000(删除后会随机读取10条记录)时,平均每次操作所用时长如下(总操作次数1033088)



    规模
    64
    128
    256
    512
    1024
    2048




    操作总时间/ms
    795088
    677865
    643788
    634600
    630898
    664984


    单次操作时间/ms
    0.76962
    0.65615
    0.62317
    0.61427
    0.61069
    0.64369



    数据折线图如下所示:

    在测试中,程序所占CPU为15%左右,所占内存为2-3M。由此推测程序没有进入死循环且没有内存泄漏。
    测试过程截图如下所示:







    2.2.2 测试分析和猜想根据B+树性质,对于阶数m数据量为N的单次操作时间复杂度为logmN。在本次测试中N几乎不变,故随着m增大,单次操作所需时间减少,这在m不很大的情况下有所体现。说明B+树对查找效率有更大的帮助,当m过大时,单次查找所需时间增加,猜想这是因为结点大小超过内存访问时单个物理页的面积,从而破坏整体读写,使B+树的优点无法充分体现。这说明B+树设计的合理性,也体现了本次设计达到了预期要求。
    1 评论 11 下载 2018-10-18 10:32:08 下载需要7点积分
  • 基于C++的通讯录系统的设计与实现

    一 需求分析通讯录系统可帮助使用者管理归纳通讯录名单,达到添加,删除,修改,保存等需求。
    二 系统设计2.1 功能模块设计通讯录主要功能为:添加通讯录成员,修改成员,删除成员,按需求搜索查看成员,保存为文档。
    如下图所示:

    系统各模块的功能具体描述为:
    1、添加成员模块
    提供界面让使用者输入希望加入的通讯录成员的各类信息(姓名,电话,住址, QQ,邮箱等),并检查格式是否有误。若格式无误,则将该通讯录信息通过二进制文件方式储存在./contact文件目录下。
    2、修改成员模块
    使用者可以重写已有的通讯录成员,增加或删除除姓名以外的各个信息。一条通 讯录成员可以拥有多个电话号码或QQ。
    3、删除成员模块
    使用者可以选择某个不希望继续使用的通讯录成员并删除他们。
    4、搜索查看成员模块
    使用者通过各种方式查询已添加的通讯录成员,并决定是否修改或删除它们。提供的方法有:精准查询,模糊查询,按分类查询等。
    2.2 系统架构设计系统开发使用Template Method设计模式和Strategy Patten 两种设计模式,较好的封装所需函数,使得主程序入口开发环节只需关注Contact.h头文件即可实现。
    具体类之间的耦合关系见下图:

    类的关系设计如下图所示:

    各类的具体功能和说明如下:

    class Person; 提供基本的数据存储结构
    classContactInterface; 提供主函数菜单,策略选择方法。是Contact类的一个接口,MainStrategy的调用者
    classContactInit; 提供初始化程序所需函数。同样是Contact类的一个接口
    classContact; 具体实现了两个接口的方法。MainStrategy的决策者。同时面向调用者(main.cpp)。但注意Contact不提供任何public方法。需要通过两个接口调用
    classCheckInterface; 提供检查函数
    classMainStrategy; Strategy Patten设计模式。同时包含子类公用的方法
    classMainNewMenu; class MainDelMenu; class MainMdfMenu; 分别override doMainStrategy()函数,实现新建,删除,修改功能
    classMainVewMenuInterface; override doMainStrategy()函数,ViewStrategy的调用者
    classMainVewMenu; ViewStrategy的决策者
    class ViewStrategy; StrategyPatten
    classViewAllMenu; class ViewExactMenu;

    class ViewFuzzyMenu; class ViewCategoryMenu; 分别override doViewStrategy()函数,实现所有查找,精确查找,模糊查找,按类查找功能。
    三 系统实现3.1 系统实现流程 通讯录系统实现流程如下图所示:

    3.2 类的实现 系统包含Person,Contact, ContactInterface, ContactInit等类,具体类结构声明如下:
    Person类:
    class Person {Public: char name[MAXNAME]; char sex; char tel[MAXTEL]; char addr[MAXADDR]; char zip[MAXZIP]; char mail[MAXMAIL]; char qq[MAXQQ]; char category[MAXCTGY]; Person(); ~Person();};
    ContactInterface类:
    class CheckInterface {public: bool check(Person&, const bool _check_repe) const; bool check_exact(const Person&, const string) const; virtual ~CheckInterface(){};private: vector<string> part_tq(const Person&, const char* const) const;};
    ContactInit类:
    class ContactInit {public: virtual int refresh() const = 0; virtual void welcome() const= 0; virtual ~ContactInit(){};};
    Contact类:
    class Contact : public ContactInterface, public ContactInit{private: MainStrategy* setMainStrategy(int); public: Contact(); ~Contact(); int refresh() const; void welcome() const;};
    MainStrategy类:
    class MainStrategy : public CheckInterface{public: MainStrategy(); virtual ~MainStrategy(); virtual int doMainStrategy() = 0;protected: void printAll() const; void print_prsn(const Person&, const string, bool) const; bool delete_prsn(Person&) const; int modify_prsn(Person&) const; //Way to modify a spefic Person member, with 0->success, -1->fail};
    MainViewMenuInterface类:
    class MainVewMenuInterface : public MainStrategy{public:private: ViewStrategy* viewStrategy; virtual ViewStrategy* setViewStrategy(int) = 0; virtual int view(Person* v_Person) const; public: MainVewMenuInterface(); virtual ~MainVewMenuInterface(); virtual int doMainStrategy();};
    四 系统测试4.1 登录界面系统运行开始的界面如图所示:

    主要通过选择结构和循环结构实现界面的前进和后退。例如,第一个登录界面出现5个选择:1.新建,2.删除,3.修改,4.浏览,5.退出
    4.2 添加联系人在开始界面输入“1”即添加新的成员:

    若显示 Information Entry Success! 则录入数据成功。若显示Information Error! 则录入数据失败。如图则因为在电话(TEL)中出现了中文字符。随后将返回主界面。

    4.3 删除联系人在主界面输入2可删除成员:

    如我们希望删除(2)数据,则键入2:

    就可以得到(2)的详细数据。输入y/n即可选择是否删除该成员。随后程序将返回主界面。
    4.4 修改联系人在主界面下输入3可以修改已有的成员:我们希望修改刚刚加入的成员(1)的电话号码,同时加入他所有常用的QQ号码:

    键入1, 可修改第一个人的信息,并按照需求修改信息:


    确认无误后即可修改信息,随后返回主界面。
    4.4 搜索联系人输入4即可按需求分类查看搜索成员:

    键入1进行精准匹配,该模式下只匹配名字:如输入“严恩伟”后匹配信息如图:

    随后可以根据自身需求选择1-3选项。此处不再演示。
    在view模式下键入2进行模糊匹配,该模式匹配名字,电话,地址。只要出现匹配字符即给与显示。如输入“181”后显示:

    选择1-3即可进入其详细页面。此处不再演示。
    在view模式下键入3进行分类(category)匹配。该模式会列出所有的分类,并可根据用户选择的分类进行罗列(其中未设置的分类被标记为Unset):

    根据提示选择1-2即可进入相应页面。选择0即可退出。
    在view模式下键入4进行全局匹配。该模式会列出所有的成员:

    在view模式下键入5退出view模式。
    在主菜单中键入5退出程序。
    1 评论 27 下载 2018-10-16 10:52:44 下载需要4点积分
  • 基于C++的学生生活系统设计与实现

    一 需求分析需要设计并实现如下场景:在那山的那边,湖的那边,有一所学校,学校里有一幢宿舍楼,宿舍楼有若干层,每一层有若干房间 ,一群学生快乐地生活在这里。他们每天可做的事情有:

    换宿舍,从一个房间搬到另一个房间
    退学,亦即搬出宿舍楼
    入学,亦即搬入宿舍楼
    吃饭,吃饭会增加体重,花费金钱
    学习,学习会消耗体重,增加魅力(注:学习是唯一可以直接提升魅力值的活动,这个养成游戏的价值导向还是蛮正确的)
    化妆(女生独有),化妆会花费金钱,提升容貌
    运动(男生独有),运动会消耗体重,增加健康(注:体重、容貌、健康与魅力之间存在一定的转化关系)
    谈恋爱,学生可以向其他的某个学生提出恋爱请求,被求爱的学生依据二者魅力值之差按照某种概率答应对方的求爱请求,从而建立双方的恋爱关系
    分手,处于恋爱关系的两个学生中的任何一方都可以提出分手,依据二者魅力值之差按照某种概率分手成功,从而断开二者之间的恋爱关系

    此外,宿舍楼还可进行扩建,亦即增加楼层,增加某层的房间数,以及增加某个房间可容纳的人数(不考虑这种任意增加的物理可能性),经过一段时间的生活后可对学生的状态信息进行查询。
    二 系统架构设计系统功能模块如下图所示:

    三 程序设计思路按照功能模块将程序分为初始化模块、读取执行活动指令模块以及查询模块。
    3.1 初始化模块要从sample_config.txt文件读取初始配置,并且将数据保存为全局变量从而可供所有文件使用,该文件内容在各个文件中都能使用到;从dispatch.txt文件读取初始宿舍的具体信息,并且建立最初的宿舍。
    3.2 读取执行活动指令模块文件读取方面,首先用文件流的方式读取,并且都使用split函数将指令分解成vector\<string\>,然后通过比较字符串判断指令的格式以及翻译指令内容,然后根据指令调用对应的功能函数。
    对各种指令和功能进行分析,可以归纳出主要有两个具体的对象进行活动:学生和宿舍,因此需要分别构造Student类和Dorm类并且对活动结果进行记录。
    学生从事吃饭、学习、恋爱活动。学生自身具有名字、性别、魅力值等属性,但是根据性别的差异有不同的特殊属性,因此用类的继承即可记录共有的属性值以及各自特有的属性值;学生从事的吃饭、学习等活动应作为类的成员函数进行实现,才更加符合对象独自完成的活动,而且吃饭、学习虽是公有活动,但是具体的实现方法根据性别会有不同,因此将这些共有的活动用虚函数在Student类中声明,并且在派生类中根据性别调用特有的实现方法;因学生的性别不同而导致的活动实现差异通过Student抽象基类指针统一调用,在具体的派生类中则分别实现其具体的活动过程;恋爱活动为多对象的活动,因此作为全局函数来实现;所有的活动都会引起一些属性的改变,只需根据规则或者公式进而实现即可。
    宿舍则主要进行入学、退学、搬宿舍、改变楼层等活动。首先是宿舍的结构,用结构体ROOM表示房间,其中包含人员、容量、性别(不能男女混住,因此需要有性别区别)等具体信息,再用vector\<ROOM\>表示一层楼,再嵌套一层vector表示一幢宿舍;其次改变楼层的活动add可以用重载的方法使得在增加楼层、房间、床位的函数实现上简化编写,在增加楼层等时需要按初始值初始化房间;入学等人员的活动则是对宿舍内部的信息进行修改,将这些活动设计为Dorm的成员函数更为妥当;由于需要存储所有具体的学生对象,在Dorm类中声明为共有成员变量可以使Dorm类与Student类的关联更为紧密,并且用映射的方法可以使学生对象得到共享,并且方便了查询;入学、退学、搬宿舍则是对学生对象的修改和房间信息的修改,需要两个地方同时修改,而且内存的分配和回收也要注意。
    3.3 查询模块显示查询界面,通过输入序号选择查询,并且对屏幕进行适时的更新或者清屏;
    调用查询函数,根据输入的指令调用相应的功能函数,并且在屏幕中输出查询的结果;
    实现功能,找出属性值的最值以及范围查询可以通过将所有的Student类的指针用vector保存,再按属性的值得大小排序,则可求出最值,范围查询则直接遍历比较即可输出结果;倘若需要分性别查询,则分别建立男生vector和女生vector进行排序;对于学生具体信息可通过名字直接在map中找到对应的对象,查找房间号则可以按下标找到对应房间,最后输出查找结果即可。
    四 详细设计4.1 对象结构及功能Student类作为抽象基类为男生和女生声明共同的成员变量和类成员函数来表示男女学生共有的属性以及学生个体的活动,例如:名字、性别、体重等共有的属性值,以及学习、吃饭等共有的学生活动。将共有的活动设计成虚函数是由于活动相同但是具体的实现不同,例如女生的魅力值的计算公式则与男生魅力值计算公式不同。
    Girl类、Boy类通过继承Student类并且实现其所有虚函数从而实现学生活动得出可实例化的具体类,并且分别添加各自的特有属性:容貌值和健康值;引用了Student指针使得学生活动会根据具体Girl类或Boy类来进行函数的调用,实现多态。Girl类和Boy类中还添加实现了weight_add()函数等私有函数,是为了使调用eat、study等学生活动函数时对于其它属性值的相应改变更为明显、直观,而且对学生活动的公式修改更为方便,直接修改增量函数即可。
    Dorm类实现建立宿舍的结构,用vector的方式分别记录楼层、房间,每个房间则用结构体保存人名、宿舍人员性别以及容量;用map\<string, Student\*\>记录所有入住的学生,实现从名字映射到具体的学生,从而通过名字共享了学生数据,方便对学生信息进行修改,并且还方便了在宿舍进行搬宿舍等活动中一旦对学生进行了修改,在修改学生信息时可直接通过名字找到该学生对象并且修改房号或者查询;add成员函数分别重载实现了增加楼层、增加房间和增加床位;enrol、quit、move分别实现入学、退学、搬宿舍。
    Init_value.h中的命名空间变量是用于保存初始化配置信息,并且可供各个文件使用。
    4.2 公有函数实现Initial.h包含初始化的功能函数:initv,读入配置信息初始为Init_value中的配置信息赋值;inits,读入宿舍的初始人员信息构建宿舍,通过调用Dorm的enrol函数即可实现入学。
    love.h的court和breakup分别实现求爱和分手功能,因为恋爱功能是两个对象,因此作为全局函数更为合适;通过公式计算出是否成功恋爱或者分手,从而对两个对象属性分别进行修改。
    read.h则从instructions.txt文件读取指令,并且通过split和比较选择将读入的数据翻译为指令并且调用对应的指令函数,需要对指令的格式进行判断并且显示出异常的指令。
    query.h包含了各种关于查询的显示和调用函数,首先对各个界面的内容进行显示,然后通过输入的内容对查询的内容进行具体选择查询,然后分别访问Student类或者Dorm类的成员变量对其具体信息进行查询。
    五 关键代码实现Student类虚函数:
    public: virtual bool eat(int){return true;} virtual bool study(int){return true;} virtual bool ownact(int){return true;} virtual int person_value(){return 0;}
    实现学生活动:
    ownact(int m){ cost_add(0, m); charm_add(0, looks_add(m), 0);}//根据固定公式计算得出增量cost_add(int f, intm){}
    Dorm类:
    class Dorm{public: vector<FLOOR_NUM> floors; map<string,Student*> students; };
    实现宿舍活动:
    //入学enrol(){ if (s) p = new Girl(); else p = new Boy(); floorsfl.member.push_back(n); students[n] = p;}//搬宿舍move(){ tmp.erase(iter); floorsflfrom.member.push_back(n); students[n]->floor_number = flto; students[n]->room_number = rmto;}//退学quit(){ tmp.erase(it); delete iter->second; students.erase(iter); }//增加fls层楼add(int fls){}//对fl层增加rms间房add(int fl, intrms){}//对fl层、rm房增加beds张床add(int fl, int rm,int beds){}
    实现追求功能(分手相类似):
    court(Student *a,Student *b){ if (fall_love(a,b)) { a->lover.push_back(b->name); b->lover.push_back(a->name); }}bool fall_love(Student*a, Student *b){}
    体重、金钱、魅力值查询:
    sort(vec.begin(),vec.end(),cmp1); sort(g_vec.begin(),g_vec.end(),cmp1); sort(b_vec.begin(),b_vec.end(),cmp1); qw_output(vec); // 若范围是全体学生则输入vec qw_output(b_vec); // 若范围是全体男生则输入b_vec qw_output(g_vec); // 若范围是全体女生则输入g_vec
    查询容貌、健康信息:
    query_person(){ sort(vec.begin(),vec.end(),cmp4); cout << (*iter)->person_value();}
    查询学生具体信息:
    query_student(){ i = d.students.find(str);}
    1 评论 5 下载 2018-10-15 22:37:56 下载需要6点积分
  • 基于重载算法的内存泄漏检测和内存越界检测

    通过重载`new`,`delete`实现对在动态内存分配中内存越界和内存泄露的自动检测1. 内存泄漏1.1 简介`内存泄漏`是当程序不正确地进行内存管理时出现的一种资源泄漏,表现为程序不再需要使用的内存空间并没有及时被释放掉。内存泄漏并非指物理内存的消失,而是在程序分配了某段内存后,由于设计错误,失去了对该段内存的控制,造成了内存的浪费.
    1.2 危害内存泄漏减少计算机可用内存,从而影响了计算机的性能。如果内存泄漏过于严重,整个操作系统、应用程序甚至会崩溃,计算机性能会大打折扣。但是,一般情况下,在现代操作系统中,当一个应用程序结束的时候,该应用程序所占用的内存会被操作系统自动地全部释放,因此,内存泄漏的后果往往不会很严重,甚至不会被察觉到。但是,当长时间运行程序或者设备内存较小时,内存泄漏的问题就不容忽视。作为程序员,我们有必要尽力避免内存泄漏,养成良好的编程习惯.
    1.3 分类内存泄漏尤其会发生在没有垃圾回收机制(Garbage collection)的编程语言,例如:C和C++,也就是说程序并不会自动实现内存管理。对于C和C++这两种语言,我们主要关心两种类型的内存泄漏:

    堆内存泄漏:程序通过`malloc`,`realloc`,`new`等函数在堆空间中申请了一块内存,但是在使用完后并没有用`free`,`delete`等函数将所申请的内存的内存释放掉,导致相应的那块内存一直被占用。
    系统资源泄漏:程序在使用系统分配的资源比如Bitmap,handle等之后,并没有用相应的函数释放掉,导致相应内存的占用和系统资源的浪费。

    本次只针对堆内存泄漏提出自动检测的方法。
    1.4 解决内存泄漏解决内存泄漏的困难之处在于:

    编译器不能发现这些问题
    在程序运行时才有可能捕捉到这些错误,而且这些错误没有明显的症状,时隐时现
    一般解决内存泄漏必须需要程序员获得源码,通过修改源码的方式解决,比较耗时

    因此,我们需要想出一种简便的方法,可以较大程度地自动检测出内存泄漏,及时提醒程序员对程序进行修正,在此我们通过重载`new`、`delete`函数的方式实现了自动检测的功能,下面将介绍`new`和`delete`函数。
    2.`new`和`delete`及其重载初步介绍2.1 系统提供的`new`和`delete`标准库定义了`operator new`函数和`operator delete`函数的8个重载版本。其中前4个版本可能会抛出`bad_alloc`异常,后4个版本则不会抛出异常。
    //这些版本可能会抛出异常void *operator new (size_t); //分配一个对象void *operator new[] (size_t); //分配一个数组void operator delete (void* ) noexcept; //释放一个对象void operator delete[] (void* ) noexcept; //释放一个数组
    //这些版本承诺不会抛出异常void *operator new (size_t, nothrow_t&) noexcept;void *operator new[] (size_t,nothrow_t&) noexcept;void operator delete (void*, nothrow_t& ) noexcept;void operator delete[] (void*, nothrow_t& ) noexcept;
    2.2 `operator new`和`operator delete`函数及其参数的说明重载`new`函数必须有一个`size_t`参数,这个参数由编译器产生并传递给我们,它是要分配内存的对象的长度。类型`nothrow_t`是定义在`new.h`中的一个`struct`,在这个类型中不包含任何成员。加上这个参数后会阻止参数抛出异常。
    对于`operator new`函数或者`operator new[]`函数来说,它的函数返回类型必须是`void*` ,第一个形参必须是`size_t`且该形参不能含有默认实参。如果我们想自定义`operator new`函数,则可以为它提供额外的形参。对于`operator delete`函数或者`operator delete[]`函数来说,它们的返回类型必须是`void`,第一个形参类型必须是`void*` ,也可以提供额外的形参。
    但是以下函数却无论如何不能被用户重载:
    void *operator new (size_t, void *),
    这种形式只能供标准库使用,不能被用户重新定义。
    2.3 `new`和`delete`表达式操作过程当我们使用一条`new`表达式的时候,实际上执行了三步操作:第一步,`new`表达式调用一个名为`operator new`(或者`operator new[]`)的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。第二步,编译器运行相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配空间并构造完成,返回一个指向该对象的指针。
    当我们使用一条`delete`表达式删除一个动态分配的对象时,实际执行了两步操作:第一步,对指针所指对象或者所指数组中的元素执行对应的析构函数。第二步,编译器调用名为`operator delete`(或者`operator delete[]`)的标准哭函数释放内存空间。
    2.4 `new`和`delete`重载的本质一条`new`表达式的执行过程总是先调用`operator new`函数以获取内存空间,然后在得到的内存空间中构造对象。与之相反,一条`delete`表达式的执行过程总是先析构对象,然后调用`operator delete`函数释放对象所占内存。因此,用户提供的的`operator new`函数和`operator delete`函数的目的在于改变内存的分配方式,但是不管怎么样,我们都不能改变`new`运算符和`delete`运算符的基本含义。(不能让new去做加法)
    3.通过重载`new`/`delete`实现对动态内存分配中内存泄漏的检测3.1 方法一:3.1.1 算法1.创建一个位于静态数据区域的类`Trace`的对象`m_trace`,这样当`main`函数退出的时候,`m_trace`的析构函数一定会执行,那么可以通过这个一定执行的函数来判断动态申请的内存是否被释放。
    2.`Trace`类中建立一个`map`,该`map`是内存空间到存储内存信息类的映射,在每次执行`new`时都会在`map`中添加这样一个映射,而在每次执行`delete`时会删除`map`中相应内存的信息。这样,如果`new`分配的每一段内存都经过`delete`释放,那么`map`在`main`结束时应该为空,反之,如果在程序结束时,`map`不为空则说明有内存未被释放,即发生了内存泄漏。将上述判断`map`是否为空的函数放到刚才所说的析构函数中,这样就能保证判断一定会执行该函数,程序也能够自动判断内存是否发生内存泄漏。
    3.一些细节值得我们注意。在重载了`new`以后,`string`类的建立和删除是要调用`new`和`delete`的,因此为了减少麻烦,我们这里是不使用`string`类的,我们将使用`string`类的地方用C风格的字符串`char*`来替代。
    本程序中使用了`map`这种数据结构,这样做使得我们建立的“内存清单”更方便查找(我们也可以自己建立一个`链表`来实现类似的功能,不过这样做比较麻烦),但是需要注意的是,`map`中的数据在删除时是会调用`delete`的,所以我们应该想出一种机制,避免重载后`delete`对`map`中的数据释放有任何的影响(后面还会详细地解释)。
    3.1.2 代码
    文件名:MemoryLeakCheck.h

    /* /** MemoryLeakCheck.h** Trace类中成员函数的声明,重载new,delete的声明**/#ifndef MEMORYLEAKCHECK_H_#define MEMORYLEAKCHECK_H_#include <map>using namespace std;class Trace{public: class Entry //Entry类用来记录分配内存的信息 { public: char *File; //用来记录发生内存泄漏的源文件名称 int Line; //用来记录未调用delete释放内存的new所在行数 Entry():Line(0){} //默认构造函数 Entry(char m_file[],int m_line):Line(m_line),File(m_file){} //重载构造函数 ~Entry(){} //析构函数 }; class Lock /*Lock类用来避免delete对map在 清楚数据时使用delete造成干扰*/ { public: Lock(Trace &tracer):_trace(tracer){_trace.lock();} ~Lock(){_trace.unlock();} private: Trace& _trace; //成员变量是类Trace的引用 };private: int m_locktimes; //用来判断是不是map调用的delete map<void*,Entry > m_map; /*记录内存分配的map,键值为new分配的 内存地址,对应的值为类Entry,储存new 所在的文件和行数*/ typedef map<void*,Entry>::iterator m_iterator; //重命名map<void*,Entry>迭代器的名字public: int getMapSize(); //用来获取m_map的长度 bool on; /*用来表示Trace的对象是否存在,即 main函数是否结束,true表示未结束, false表示析构函数执行,即主函数结束*/ void lock(); //用来使m_locktimes++ void unlock(); //用来使m_locktimes-- void addMap (void *p,char* m_file,int m_line); //用来添加map中的信息 void deleteMap (void *p); //用来删除map中的信息 void checkMap (); /*用来检查m_map中的是否存有元素, 并进行汇报,如果发生内存泄漏, 释放占用内存*/ Trace():on(true),m_locktimes(0){} /*程序开始后,会将on设为true, m_locktimes的值设为0。即若on==true, 则主函数未结束*/ ~Trace();};void* operator new (size_t size,char *m_file,int m_line); /*重载new运算符,char* m_file是文件名,int m_line是行数*/void* operator new[] (size_t size,char *m_file,int m_line);void* operator new (size_t size);void operator delete (void *p);void operator delete[] (void *p);#endif

    文件名:MemoryLeakCheck.cpp

    /** MemoryLeakCheck.cpp** Trace类中成员函数的定义,重载new,delete的定义**/#include "MemoryLeakCheck.h"#include <fstream> //将检测的结果输入到了文件log.txt中#include <iostream>Trace m_trace; //类m_trace的全局实例对象Trace::~Trace() //类Trace的析构函数{ checkMap(); //在析构函数执行的时候调用checkMap()函数,检查是否发生内存泄漏 if (on==true) //当析构函数执行的时候,即主函数结束的时候,将on设为false on=false;}void Trace::lock () /*类Lock的构造函数执行的函数lock函数。即Lock类建立时,m_locktimes会加1*/{ m_locktimes++;}void Trace::unlock () /*类Lock的析构函数执行的函数unlock函数。即Lock类被析构时,m_locktimes会减1*/{ m_locktimes--;}void Trace::addMap(void *p,char *m_file,int m_line) //向数据结构map中添加新的数据{ if(m_locktimes>0) //正常情况下,一开始由构造函数 初始化使得m_locktimes==0 return; Trace::Lock locker(*this); /*如果陷入了迭代循环,每次迭代会使m_locktimes++,那么上面的如果(m-locktimes>0)为真,则表明陷入了迭代,为避免程序崩溃,直接return*/ m_map[p] = Entry (m_file,m_line);}void Trace::deleteMap(void *p){ if (m_locktimes>0) /*我们在抹去m_map中的数据时,erase函数会调用delete来删除数据,那么就可能产生一个死循环的迭代。*/ return ; Trace:: Lock locker(*this); /*当用户使用delete函数时,通过if判断(没有直接return)执行deleteMap函数,此时m_locktimes==0,但是但此处使得m_locktimes==1,即下一次迭代会退出*/ cout<<"delete memory,"<<m_map[p].File<<",line " <<m_map[p].Line<<endl; m_iterator temp= m_map.find(p); m_map.erase(temp); /*然后调用erase函数,erase函数会再次调用delete函数,但如果再次进入deleteMap函数,由于m_locktimes==1会直接返回,然后到上一层执行free函数,然后完成erase函数,然后再返回原来的delete函数,完成内存空间的释放*/}void Trace::checkMap(){ void *temp=NULL; ofstream out("log.txt",ofstream::app); //建立文件输出流,将每次内存泄漏的检测结果记录到文件log.txt中 out<<__DATE__<<"\t"<<__TIME__<<endl; //输出程序运行的具体时间 if (m_map.size()) /*在程序结束时,如果m_map没有清空,那么说明有用户分配的内存空间没有被释放,即发生了内存泄漏*/ { cout<<"Memory Leak Detected!"<<endl; out<<"Memory Leak Detected!"<<endl; for(m_iterator it =m_map.begin();it!=m_map.end(); it=m_map.begin()) /*检测到程序发生了内存泄漏,则自动释放用户忘记释放的内存*/ { cout<<"file:"<<it->second.File<<",line:" <<it->second.Line<<endl; out<<"file:"<<it->second.File<<",line:" <<it->second.Line<<endl; temp=it->first; deleteMap(temp); //删除m_map中数据 free(temp); //删除用户分配的空间 } cout<<"Leak memory released!"<<endl; out<<"Leak memory released!"<<endl; } else //如果m_map为空,那么说明没有发生内存泄漏 { cout<<"Memory Leak Not Detected!"<<endl; out<<"Memory Leak Not Detected!"<<endl; } out<<endl;}int Trace::getMapSize(){ return m_map.size(); /*为了数据安全,将m_map类型设为private, 用public的getMapSize函数来获取m_map的长度*/}void* operator new (size_t size,char *m_file,int m_line){ void* p = malloc (size); if (m_trace.on==true) /*on==true的时候,表示主函数正在运行; 如果on变为false,则跳过if中的语句。 (可能程序在main之后还会调用new,这时需 要使new恢复原来的样子)*/ m_trace.addMap(p,m_file,m_line); //在m_map中插入数据 return p;}void* operator new[] (size_t size,char *m_file,int m_line) //数组版的new{ void* p = malloc (size); if (m_trace.on==true) m_trace.addMap(p,m_file,m_line); return p;}void operator delete (void *p) { if (m_trace.on==true&&m_trace.getMapSize()) /*当m_map的长度为0时,不用调用我们自己 的deleteMap函数。程序可能会在main函数结 束后调用delete,这种情况m_map中没有存有 相关的信息,因此不能执行deleteMap函数。 同时,这样的条件判断又可以保证用户分配的内 存都可以被执行到*/ m_trace.deleteMap (p); free(p);}void operator delete[] (void *p) //delete数组{ if (m_trace.on==true&&m_trace.getMapSize()) m_trace.deleteMap(p); free(p);}void* operator new (size_t size)//一些容器会调用这个new(即使new重载也不会调用重载版本的new){ void* p = malloc (size); if (m_trace.on==true) m_trace.addMap(p,"?",0); return p;}

    文件名:define.h

    /** define.h** new的宏定义**/#define new new(__FILE__, __LINE__) /*宏定义,把new替换成我们定义的operator new (size_t size,char *m_file,int m_line)函数用到了__FILE__,__LINE__宏来获取当前文件名和当前行数*/
    3.1.3 功能实现了通过重载`new`和`delete`自动检测内存泄漏,当使用`delete`时,会输出调用了`delete`的信息,并输出`delete`对应的`new`所在的文件和行数。当发生内存泄漏时,输出`Memory Leak Detected!`,并且自动释放用户忘记释放的内存;如果没有发生内存泄漏,那么输出`Memory Leak Not Detected!`,并将是否发生内存泄漏的信息写到log.txt中。
    3.1.4 测试case1:
    文件名:main.cpp

    #include "MemoryLeakCheck.h"#include "define.h"#include <iostream>int main (){ int *a=new int[5]; a[0]=3; return 0;}

    没有及时释放内存,控制台输出结果:文件写入的内容:

    case2:
    文件名:main.cpp

    #include "MemoryLeakCheck.h"#include "define.h"#include <iostream>int main (){ int *a=new int[5]; delete a; return 0;}

    正常操作,没有发生内存泄漏,控制台输出结果:文件写入内容:

    case3:
    文件名:main.cpp

    #include "MemoryLeakCheck.h"#include "define.h"#include <iostream>class cat{private: int name; int weight; int length;public: cat():name(0),weight(0),length(0){} ~cat(){}};int main (){ cat* a=new cat; return 0;}

    new新建了一个类,发生了内存泄漏,控制台输出结果:文件写入内容:

    case4:
    文件名:main.cpp

    #include "MemoryLeakCheck.h"#include "define.h"#include <iostream>#include <string>int main (){ string b; return 0;}

    没有发生内存泄漏,但是调用了new和delete,控制台输出结果:文件写入内容:

    3.1.5 进一步探讨通过重载`new`和`delete`的方法,实现了内存泄漏的自动检测。但是,在编写程序的过程中,我发现重载了`operator new` 函数
    void* operator new (size_t size,char* m_file,int m_line);
    并且通过宏定义重新定义了`new`
    #define new new(__FILE__,__LINE__)
    但是系统在创建容器类对象(`string`,`map`,`set`等)时,并不会调用我们宏定义的`operator new`函数,而是会去调用系统提供的`operator new`的默认版本,也就是
    void* operator new (size_t size);
    但是`operator delete`函数却不是这样,并不能使用类似`new`的宏定义,而是只会调用:
    void delete (void* p);
    本程序本程序直接重载了
    void operator delete (void* p);
    也就是说用户使用的`delete`运算符以及容器类实例自动删除时都会调用该重载函数。但是,`operator new`重载了两个版本,分别是:
    void* operator new(size_t size);void* operator new(size_t size,char* m_file,int m_line);
    这样就保证了只要调用`new`,那么一定会在`m_map`中建立相应的映射,相应地`delete`就不会出错(否则,可能删除”不存在的信息”而导致程序崩溃)。而`void *operator new (size_t size)`版本中向`m_map`添加的文件名是`?`,行数是`0`(因为参数只有`size_t`,没有接口可以获取`string`,`set`等调用`new`所对应的文件名和行数)。
    这种方法记录的信息很详细全面(包括内存泄漏的文件名以及行数),但是这种方法似乎有些麻烦,我们可能需要一个更简洁(不需要如此详细的)版本,来检查是否发生了内存泄漏。
    3.2 方法二:3.2.1 算法:我们可以不使用数据结构`map`来存储`new`分配的内存的信息,我们可以把`operator new`函数返回的`void*p`指向的内存地址存储起来,同时把`delete`的指针所指向的内存地址也存储起来。最后在一个全局类的实例对象的析构函数中执行比较函数。
    为了更方便地比较,我们这里使用了数据结构`set`,其存储的数据为`string`,即指针所指向内存的地址。因为`set`对于`string`是按照`字典排序`的顺序排列的(这个排序过程是自动的),所以如果没有发生内存泄漏,那么两个`set`中的元素个数是一样的,同时,对应位的`string`也应该是相同的,如果不同时满足上述两个条件,那么说明发生了内存泄漏。
    这种方法没有存储new表达式所在的文件名和行数,因此,直接重载了:
    void* operator new (size_t size);
    即可满足要求(根据前面的讨论可知,`string`,`set`等容器类对象建立的时候,会调用该函数)。
    3.2.2 代码:
    文件名:MemoryLeakCheck.h

    /** MemoryLeakCheck.h** Examine类中成员函数的声明,重载new,delete的声明**/#ifndef MEMORYLEAKCHECK_H_ #define MEMORYLEAKCHECK_H_#include <set>#include <string>using namespace std;class Examine{private: set<string> fromNew; //两个存储分配的内存地址的集合 set<string> fromDelete; typedef set<string>::iterator m_iterator; int locktimes; //防止死循环public: Examine():on(true),locktimes(0){} ~Examine(); bool on; //析构函数是否执行 void compareSet (); void addSetFromNew (void *p); void addSetFromDelete(void *p); void reportLeak(); void reportNotLeak(); void lock(); void unlock();public: class Lock //防止死循环 { Examine& _exam; public: Lock(Examine&r):_exam(r){_exam.lock();} ~Lock(){_exam.unlock();} };};void* operator new (size_t size);void* operator new[](size_t size);void operator delete (void* p);void operator delete[] (void* p); #endif

    文件名:MemoryLeakCheck.cpp

    /** MemoryLeakCheck.cpp** Trace类中成员函数的定义,重载new,delete的定义**/#include "MemoryLeakCheck.h"#include <iostream>#include <fstream>#include <string>#include <sstream>Examine m_exam;Examine::~Examine(){ on = false; compareSet();}void Examine::compareSet(){ int lengthNew = fromNew.size(), lengthDelete = fromDelete.size(); if (lengthNew != lengthDelete) { reportLeak(); return; } m_iterator i,j; //比较两个集合是否一样 for (i = fromNew.begin(),j = fromDelete.begin(); i != fromNew.end(); ) { if (*i != *j) { reportLeak(); return; } i++; j++; } reportNotLeak();}void Examine::lock(){ locktimes++;}void Examine::unlock(){ locktimes--;}void Examine::reportLeak () //发生内存泄漏的输出{ ofstream out("log.txt",ios::app); cout <<"Memory Leak Detected!"<<endl; out<<__DATE__<<" "<<__TIME__<<endl<<"Memory Leak Detected!"<<endl<<endl;}void Examine::reportNotLeak() //未发生内存泄漏的输出{ ofstream out("log.txt",ios::app); cout<<"Memory Leak Not Detected!"<<endl; out<<__DATE__<<" "<<__TIME__<<endl<<"Memory Leak Not Detected!"<<endl<<endl;}void Examine::addSetFromNew(void* p){ if (m_exam.locktimes>0) //防止死循环 return; Examine::Lock locker(*this); if (p == NULL) return; stringstream stream; //用string流的方式获取指针指向内存的地址(的字符串) string name; stream<<p; stream>>name; fromNew.insert (name);}void Examine::addSetFromDelete (void *p){ if (m_exam.locktimes>0) return; Examine::Lock locker(*this); if (p == NULL) return; stringstream stream; string name; stream<<p; stream>>name; fromDelete.insert (name);}void* operator new (size_t size){ void *p = malloc (size); if (m_exam.on == true) //主函数未结束,析构函数未执行 m_exam.addSetFromNew(p); return p;}void* operator new [](size_t size){ void *p = malloc(size); if (m_exam.on == true) m_exam.addSetFromNew(p); return p;}void operator delete (void* p){ if (m_exam.on == true) m_exam.addSetFromDelete(p); free(p);}void operator delete[] (void* p){ m_exam.addSetFromDelete(p); free(p);}
    3.2.3 功能:这个版本相对于上一种方法显得更简洁,如果发生内存泄漏,则显示”Memory Leak Detected!”,并在文件”log.txt”中记录;如果没有发生内存泄漏,则显示”Memory Leak Not Detected!”,并在文件”log.txt”中记录。
    3.2.4 测试:case1:

    文件名:main.cpp

    #include <iostream>#include "MemoryLeakCheck.h"int main (){ int* a = new int (5); char* b = new char; delete a; delete b; return 0;}

    及时释放了内存没有发生内存泄漏,控制台输出:
    文件写入的内容:

    case2:

    文件名:main.cpp

    #include <iostream>#include <string>#include "MemoryLeakCheck.h"int main (){ int* a = new int (5); char* b = new char; string c("cat"); delete b; return 0;}

    发生了内存泄漏,控制台输出:文件写入的内容:

    4.什么是内存越界4.1 简介内存越界可以分为`读越界`和`写越界`,`读越界`是指当用户向系统申请了一块内存后(可以 位于堆空间,可以位于栈空间,也可以位于静态数据区),在读取数据时,超出了申请的范围;`写越界`是指用户在写入数据时,超出了申请的范围。
    4.2 危害内存越界可能会导致程序的数据被篡改或者无法访问,从而使程序乃至系统发生不可预见的错误,这些错误可大可小,往往不可重现,对程序和系统的稳定性、安全性等方面构成了巨大的威胁。
    4.3 解决内存越界和内存泄漏一样,避免内存越界的发生需要程序员良好的编程习惯,同时,这些错误也是难以发现的。因此,我们希望能够想出一种自动检测的方法,在此,通过重载`new`和`delete`,可以对堆空间中的`写越界`进行自动检测。(对于`读越界`重载`new`和`delete`难以实现检测)
    5.通过重载`new`实现对内存越界_`写越界`的自动检测5.1 算法
    全局的类`WriteCheck`的实例对象的析构函数在`main`函数结束时一定会执行,所以我们的`检测函数`就位于这样一个类的析构函数中(这与处理`内存泄漏`时的算法思想类似)。
    在`WriteCheck`类中建立一个关联容器`map`的对象`m_map`(这与我们在处理`内存泄漏`时的做法类似),`m_map`的键值为`void*`,该`void*`指向`new`分配的内存,`m_map`的映射值为一个类`Entry`,这个类存储了`new`分配内存的长度(`size_t`的值),以及调用`new[]`的文件名和行数。即在`m_map` 中建立了从`内存地址`到`相应内存信息`的映射。
    重载`new`,在函数`operator new`中分配一块长于`size_t size`长度的内存,将多分配的内存初始化(例如初始化为0),然后把`void*`,`size_t size`,`文件名`,`行数`信息存入2中所提到的`m_map`中。如果发生内存写越界,这些多分配的内存空间中的值就会和初始值不同,即发生了内存越界,最后,我们在1中提到的析构函数中进行判断即可。

    注:实现内存写越界的自动检测并不用重载`delete`,也不会影响`main.cpp`的`main函数`中的顺序容器和关联容器的使用。
    5.2 代码
    文件名:MemoryOutOfBounds.h

    /** MemoryOutOfBounds.h** WriteCheck类成员函数的声明以及new重载声明***/#ifndef MEMORYOUTOFBOUNDS_H_#define MEMORYOUTOFBOUNDS_H_#include <map>using namespace std;class WriteCheck{public: class Entry //Entry类用来记录分配内存的信息 { public: char *File; //用来记录发生内存泄漏的源文件名称 int Line; //用来记录发生写越界对应的new所在行数 int Length; //用来记录new参数size_t的大小 Entry():Line(0),Length(0){} //默认构造函数 Entry(char m_file[],int m_line,int m_length) :Line(m_line),File(m_file),Length(m_length){} //重载构造函数 ~Entry(){} //析构函数 };private: map<void*,Entry > m_map; //记录内存分配的map,键值为new分配的内存地址,对应的值为类Entry// typedef map<void*,Entry>::iterator m_iterator; //重命名map<void*,Entry>迭代器的名字,方便使用public: bool on; /*用来表示WriteOut类的全局对象是否存在,true表示未结束, false表示析构函数执行,即主函数结束*/ void addMap (void *p,char* m_file,int m_line,int m_length); //用来添加map中的信息 void checkMap (); /*用来检查map中的Entry元素,如果初始值被修改, 则发生写越界,否则没有,并进行汇报*/ WriteCheck():on(true){} //程序开始后,会将on设为true。 ~WriteCheck();};void* operator new[] (size_t size,char *m_file,int m_line); //operator new[]函数,char* m_file是文件名,int m_line是行数void* operator new (size_t size,char *m_file,int m_line);//operator new函数只是形式上重载以符合宏定义#define EXTRALENGTH 100 //多分配内存的长度#endif

    文件名:MemoryOutOfBounds.cpp

    /** MemoryOutOfBounds.cpp** WriteCheck类成员函数的定义以及new重载定义**/#include "MemoryOutOfBounds.h"#include <iostream>#include <fstream>WriteCheck m_writecheck; //类WriteCheck的全局对象WriteCheck::~WriteCheck() { checkMap(); //执行m_map的检查 if (on==true) on=false; //主函数结束,置为false}void WriteCheck::addMap(void *p,char* m_file,int m_line,int m_length){ m_map[p] = Entry(m_file,m_line,m_length); //向m_map中添加信息 unsigned char* temp =(unsigned char *)p; //多分配的内存用unsigned char来初始化一个 unsigned char变量占1字节// int start = m_length / sizeof (unsigned char), //计算多分配的内存转换成unsigned char后的下标范围// end = (m_length+EXTRALENGTH) / sizeof (unsigned char); for (int i=start;i<end;i++) //初始化多分配的内存,全部置为0 temp[i] = 0;}void WriteCheck::checkMap(){ ofstream out("LOG.txt",ofstream::app); //输出文件流 unsigned char *temp=NULL; //用来将void *转换为unsigned char* bool first = false; //一个输出控制开关 int start=0,end=0,i=0; for (m_iterator it = m_map.begin();it!=m_map.end();it++) { temp = (unsigned char *)it->first; //获取m_map中Entry存储的内存地址 start = it->second.Length / sizeof(unsigned char); //计算下标范围 end = ( it->second.Length + EXTRALENGTH ) /sizeof (unsigned char); for (i = start;i<end;i++ ) //检查初始值是否改变 if (temp[i]!=0) break; //输出部分 if (i!= end) { if (first==false) { cout<<"Write memory out of bounds DETECTED!"<<endl; out<<__DATE__<<"\t"<<__TIME__<<endl<< "Write memory out of bounds DETECTED!"<<endl; first = true; } cout<<"file:"<<it->second.File<<"\tline:" <<it->second.Line<<endl; out<<"file:"<<it->second.File<<"\tline:" <<it->second.Line<<endl; } } if (first==false) { cout<<"Write memory out of bounds NOT DETECTED!"<<endl; out<<__DATE__<<"\t"<<__TIME__<<endl<< "Write memory out of bounds NOT DETECTED!"<<endl; } out<<endl;}void* operator new[] (size_t size,char *m_file,int m_line){ void* p = malloc (size+EXTRALENGTH); //多分配EXTRALENGTH字节的长度的内存 if (m_writecheck.on==true) //如果main函数没有结束 m_writecheck.addMap(p,m_file,m_line,size); return p;}void* operator new (size_t size,char* m_file,int m_line) //形式上的重载为了符合define宏定义{ void *p = malloc(size); return p;}

    文件名:define.h

    /** define.h** new的宏定义**/#define new new(__FILE__, __LINE__) /*宏定义,把new替换成我们定义的operator new (size_t size,char *m_file,int m_line)函数用到了__FILE__,__LINE__宏来获取当前文件名和当前行数*/
    5.3 功能本程序可以实现对`写越界`的自动检测,如果发生了`写越界`,则输出`Write memory out of bounds DETECTED!`,并且输出发生`写越界`对应的`new`的所在文件名和行数;如果没有发生`写越界`,则输出`Write memory out of bounds NOT DETECTED!`。最后将是否发生内存越界写入文件LOG.txt。
    5.4 测试*case1:
    文件名:main.cpp

    #include <iostream>#include "MemoryOutOfBounds.h"#include "define.h"using namespace std;int main (){ int *a = new int[10]; a[12] = 5; return 0;}

    发生了内存越界,控制台输出:文件输出:

    *case2:
    文件名:main.cpp

    #include <iostream>#include "MemoryOutOfBounds.h"#include "define.h"using namespace std;class dog{public: char name[20]; int speed; int loyalty;};int main (){ dog *a = new dog[10]; a[11].speed = 100; return 0;}

    发生了内存写越界,控制台输出文件输出:

    *case3:
    文件名:main.cpp

    #include <iostream>#include "MemoryOutOfBounds.h"#include "define.h"using namespace std;int main (){ char name[20]; name[0] = 'L'; name[1] = 'Y'; name[2] = 'R'; return 0;}

    没有发生内存越界,控制台输出:文件输出:

    5.5 进一步探讨其实,我们也可以实现对内存越界中`读越界`的自动检测。我们可以重载中括号运算符`[]`,但是,`[]`运算符只能够在类中重载,并不能在全局重载,所以,这种方法对int,char等数据类型的`读越界`也是无能为力的。
    对于类中`[]`的重载,其基本思路是记录分配的数组长度,然后在调用`operator [int index]`时,比较记录的长度和`index`的大小,即可判断是否发生`读越界`。
    6.结语我们通过重载全局`new`和`delete`的方法,实现了对`内存泄漏`和`内存越界`的自动检测。同时在本次程序设计中,作者兼顾了`C++`的模块化,易复用的特点,做到了代码易移植性。
    但是,每个程序员应该谨记的是,在编程过程时要始终保持良好的编程习惯,这样才能在根本上避免内存泄漏,内存越界以及其他问题。
    7.参考资料
    《C++ Primer(第5版)》《Thinking in C++》https://en.wikipedia.org/wiki/Memory_leakhttp://blog.csdn.net/na_he/article/details/7429171http://blog.csdn.net/realxie/article/details/7437855http://blog.csdn.net/wyg1065395142/article/details/50930395http://blog.csdn.net/ghevinn/article/details/18359519http://blog.csdn.net/chinabinlang/article/details/8331704http://blog.sina.com.cn/s/blog_6b2a69300100xrpw.html
    1 评论 5 下载 2018-10-15 16:42:42 下载需要12点积分
显示 885 到 900 ,共 15 条
eject