分类

类型:
不限 游戏开发 计算机程序开发 Android开发 网站开发 笔记总结 其他
评分:
不限 10 9 8 7 6 5 4 3 2 1
原创:
不限
年份:
不限 2018 2019 2020

技术文章列表

  • JavaWeb 概述

    JavaWeb使用Java语言开发基于互联网的项目
    今后主要使用B/S架构
    软件架构C/S:Clien/Server 客服端/服务器端在用户本地有一个客户端程序,在远程有一个服务器端。如:QQ,讯雷……
    优点
    用户体验好
    缺点
    开发、安装、部署、维护,麻烦
    ★B/S:Browser/Server 浏览器/服务器端只需要一个浏览器,用户通过不同的网站(URL),客户访问不同的服务器端程序
    优点:
    开发、安装、部署、维护,简单
    缺点
    如果应用过大,用户的体验可能会受到影响对硬件要求过高(带宽要高,……)
    <!--more-->

    B/S架构详解客户端浏览器通过URL,向服务器端发送请求,请求一些资源,资源就包括“静态资源”和“动态资源”。服务器端就会响应,返回这些资源。

    B/S架构是JavaWeb开发中重要的架构


    资源分类静态资源使用静态网页开发技术发布的资源
    特点
    所有用户访问,得到的结果是一样的如:文本,图片,音频,视频,HTML,CSS,JavaScript如果用户请求的是静态资源,那么服务器会直接将静态资源发送给浏览器。浏览器中内置了静态资源的解析引擎静态资源可以直接被浏览器解析

    1.HTML,CSS,JavaScript 统称:静态网页开发技术,也称静态网页三剑客2.经过解析引擎解析后,可以在浏览器中浏览图片、文字、超链接等3.不同浏览器的解析引擎不同,最终显示的网页可能不同

    HTML用于搭建基础网页,展示页面的内容
    CSS用于美化页面,布局页面
    JavaScript控制页面的元素,让页面有一些动态的效果
    ★动态资源使用动态网页技术发布的资源

    动态资源是今后学习JavaWeb的重点之一

    特点
    所用用户访问,得到的结果可能不一样如:jsp/servlet,php,asp……如果用户请求的是动态资源,那么服务器会执行动态资源转换为静态资源,再发送给用户

    学习动态资源前,必须学习静态资源!

    网络通信三要素IP
    电子设备(计算机)在网络中的唯一标识
    端口
    应用程序在计算机中的唯一标识。值范围:0~65536
    传输协议
    规定了数据传输的规则
    基础协议:

    tcp:安全协议,三次握手。 速度稍慢。
    udp:不安全协议。 速度快。


    Web服务器软件服务器
    安装了服务器软件的计算机
    服务器软件
    接收用户的请求,处理请求,做出响应
    Web服务器软件
    接收用户的请求,处理请求,做出响应。
    在Web服务器软件中,可以部署Web项目,让用户通过浏览器来访问这些项目
    Web容器

    常见的Java相关的Web服务器软件
    WebLogic:oracle公司,大型的JavaEE服务器,支持所有的JavaEE规范,收费的。
    WebSphere:IBM公司,大型的JavaEE服务器,支持所有的JavaEE规范,收费的。
    JBOSS:JBOSS公司的,大型的JavaEE服务器,支持所有的JavaEE规范,收费的。
    Tomcat:Apache基金组织,中小型的JavaEE服务器,仅仅支持少量的JavaEE规范servlet/jsp。开源的,免费的。


    JavaEE:Java语言在企业级开发中使用的技术规范的总和,一共规定了13项大的规范

    三大组件
    Servlet:服务器小程序Filter:过滤器Listener:监听器

    参照 https://www.cnblogs.com/kefir/p/9426754.html
    0 留言 2020-03-21 14:13:07 奖励35点积分
  • 【Cocos Creator 联机实战教程(2)】——匹配系统 精华

    1.知识点讲解大型多人互动游戏一般都会有一个匹配系统,用来让玩家进行联网游戏,现在我们来讲一讲这种系统吧,我们可以做个比较简单的双人对战匹配系统。
    我们让每一对匹配成功的玩家进入一个独立的房间,所以不同的房间的通信应该互不影响,由于不同场景的通信内容不同,所以不同场景的通信也应该独立
    我们把这个游戏的匹配过程比作开房的过程,

    如果有一个人进入了宾馆,那么他最先进入的区域就是hall(大厅),当然他可能就是逛逛,又推门出去
    当他想休息时他就去前台开个房,那么他就进入了queue(队列),并断开hall的通信
    当另一个人也想休息的时候也去前台排队,当个queue里有两个人的时候,前台小姐就给了他俩一个空闲房间的钥匙,他们就一起进入了一个独立的room,并断开queue的通信
    以上循环,房间数有限,在房间满的时候不能匹配成功

    当然,你也可以根据实际情况升级这个匹配系统,比如,分等级的匹配(开不同的队列等待)。
    注意:房卡游戏虽然也用到了房间这个概念,但不是匹配,这种游戏更像唱卡拉OK。进入大厅后,组织者去开个房间,其他人一起进。或者迟到的人拿着房间号直接进去。
    2. 步骤我们的游戏分为三个场景

    游戏启动的时候进入menu场景,当玩家点击对战时进入match场景,匹配成功进入game场景,取消匹配返回menu场景,游戏结束返回menu场景
    我们在Global里定义socket
    window.G = { globalSocket:null,//全局 hallSocket:null,//大厅 queueSocket:null,//队列 roomSocket:null,//房间 gameManager:null, chessManager:null, stand:null,}
    menu场景启动时,我们连接hallSocket,开始匹配时,断开hallSocket
    cc.Class({ extends: cc.Component, onLoad: function () { G.globalSocket = io.connect('127.0.0.1:4747'); //断开连接后再重新连接需要加上{'force new connection': true} G.hallSocket = io.connect('127.0.0.1:4747/hall',{'force new connection': true}); }, onBtnStart() { G.hallSocket.disconnect(); cc.director.loadScene('match'); }});
    进入match场景,连接queueSocket,先进入queue的玩家主场黑棋先手,后进入客场白棋后手(这个逻辑是服务端判断的),匹配成功时,服务端会发送roomId,玩家进入相应的房间,并断开queueSocket的通信
    const Constants = require('Constants');const STAND = Constants.STAND;cc.Class({ extends: cc.Component, onLoad: function () { G.queueSocket = io.connect('127.0.0.1:4747/queue', { 'force new connection': true }); G.queueSocket.on('set stand', function (stand) { if (stand === 'black') { G.stand = STAND.BLACK; } else if (stand === 'white') { G.stand = STAND.WHITE; } }); G.queueSocket.on('match success', function (roomId) { cc.log('match success' + roomId); G.roomSocket = io.connect('127.0.0.1:4747/rooms' + roomId, { 'force new connection': true }); G.queueSocket.disconnect(); cc.director.loadScene('game'); }); }, onBtnCancel() { G.queueSocket.disconnect(); cc.director.loadScene('menu'); }});
    在game场景中,如果游戏结束我们就断掉roomSocket回到menu场景
    startGame() { this.turn = STAND.BLACK; this.gameState = GAME_STATE.PLAYING; this.showInfo('start game'); },endGame() { let onFinished = () =>{ G.roomSocket.disconnect(); cc.director.loadScene('menu'); } this.infoAnimation.on('finished',onFinished,this); this.gameState = GAME_STATE.OVER; this.showInfo('game over'); },
    服务端完整逻辑
    let app = require('express')();let server = require('http').Server(app);let io = require('socket.io')(server);server.listen(4747, function() { console.log('listening on:4747');});let MAX = 30;//最大支持连接房间数let hall = null;//大厅let queue = null;//匹配队列let rooms = [];//游戏房间function Hall() { this.people = 0; this.socket = null;}function Room(){ this.people = 0; this.socket = null;}function Queue(){ this.people = 0; this.socket = null;}hall = new Hall();queue = new Queue();for(let n = 0;n < MAX;n++){ rooms[n] = new Room();}function getFreeRoom(){ for(let n = 0;n < MAX;n++){ if(rooms[n].people === 0){ return n; } } return -1;}io.people = 0;io.on('connection',function(socket){ io.people++; console.log('someone connected'); socket.on('disconnect',function(){ io.people--; console.log('someone disconnected'); });})hall.socket = io.of('/hall').on('connection', function(socket) { hall.people++; console.log('a player connected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); socket.on('disconnect',function(){ hall.people--; console.log('a player disconnected.There are '+hall.people+' people in hall'); hall.socket.emit('people changed',hall.people); });});queue.socket = io.of('/queue').on('connection',function(socket){ queue.people++; console.log('someone connect queue socket.There are '+queue.people+' people in queue'); if(queue.people === 1){ socket.emit('set stand','black'); }else if(queue.people === 2){ socket.emit('set stand','white'); let roomId = getFreeRoom(); console.log(roomId+"roomId"); if(roomId >= 0){ queue.socket.emit('match success',roomId); console.log('match success.There are '+queue.people+' people in queue'); }else{ console.log('no free room!'); } } socket.on('cancel match',function(){ queue.people--; console.log('someone cancel match.There are '+queue.people+' people in queue'); }); socket.on('disconnect',function(){ queue.people--; console.log('someone disconnected match.There are '+queue.people+' people in queue'); });});for(let i = 0;i < MAX;i++){ rooms[i].socket = io.of('/rooms'+i).on('connection',function(socket){ rooms[i].people++; console.log('some one connected room'+i+'.There are '+rooms[i].people+' people in the room'); socket.on('update chessboard',function(chessCoor){ socket.broadcast.emit('update chessboard',chessCoor); }); socket.on('force change turn',function(){ socket.broadcast.emit('force change turn'); }); socket.on('disconnect',function(){ rooms[i].people--; console.log('someone disconnected room'+i+'.There are '+rooms[i].people+' people in the room'); }); });}
    3. 总结我们做的是比较简单的匹配系统,实际上还有匹配算法(选择排队的顺序不仅仅是先来后到)。
    这是我们需要掌握的新知识,除此之外我们都可以使用之前的知识点完成游戏。
    注意以下问题:

    跨场景访问变量
    在util下面有两个脚本,Constants用来存储游戏常量,然后其他地方需要常量时
    const Constants = require('Constants');const GAME_STATE = Constants.GAME_STATE;const STAND = Constants.STAND;const CHESS_TYPE = Constants.CHESS_TYPE;
    Global存储全局控制句柄,需要访问他们的时候,就可以通过(G.)的方式

    控制单位应该是脚本而不是节点
    本教程部分素材来源于网络。
    4 留言 2018-12-20 12:07:25 奖励35点积分
  • 基于AheadLib工具进行DLL劫持 精华

    背景或许你听过DLL劫持技术,获取你还没有尝试过DLL劫持技术。DLL劫持技术的原理是:

    由于输入表中只包含DLL名而没有它的路径名,因此加载程序必须在磁盘上搜索DLL文件。首先会尝试从当前程序所在的目录加载DLL,如果没找到,则在Windows系统目录中查找,最后是在环境变量中列出的各个目录下查找。利用这个特点,先伪造一个系统同名的DLL,提供同样的输出表,每个输出函数转向真正的系统DLL。程序调用系统DLL时会先调用当前目录下伪造的DLL,完成相关功能后,再跳到系统DLL同名函数里执行。这个过程用个形象的词来描述就是系统DLL被劫持(hijack)了。

    现在,本文就使用 AheadLib 工具生成劫持代码,对程序进行DLL劫持。现在就把实现原理和过程写成文档,分享给大家。
    实现过程本文选取劫持的程序是从网上随便下的一个程序“360文件粉碎机独立版.exe”,我们使用 PEview.exe 查看改程序的导入表,主要是看有程序需要导入哪些DLL文件。

    观察导入的DLL,类似KERNEL32.DLL、USER32.DLL等受系统保护的重要DLL,劫持难度比较大,所以,我们选择VERSION.DLL。至于,判断是不是受系统保护的DLL,可以查看注册表里面的键值,里面的DLL都是系统保护的,加载路径固定:
    HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\SessionManager\knowndlls然后,确定劫持的DLL文件之后,我们使用 AheadLib 工具来生成DLL劫持代码:

    接着,新建一个DLL工程,把AheadLib工具生成的代码拷贝到DLL工程中,同时,我们在DLL的入口点函数DllMain中增加一行弹窗代码,这样可以提示我们DLL劫持成功。然后编译链接,生成DLL文件。这个我们自己编译生成的DLL文件,就可以把DLL名称改成“VERSION.DLL”,放到和“360文件粉碎机独立版.exe”程序在同一目录下,运行程序,则会加载同一目录下的“VERSION.DLL”。
    为了验证DLL程序是否能成功劫持,我们把改名后的“VERSION.DLL”和“360文件粉碎机独立版.exe”放在桌面,然后,运行程序,这是,成功弹窗:

    我们使用 Process Explorer 工具查看下“360文件粉碎机独立版.exe”进程加载的DLL情况:

    可以看到,我们自己的version.dll成功被加载,而且还加载了系统的version.dll。之所以会加载系统的version.dll文件,是因为我们自己的DLL文件中,会加载version.dll文件。
    编码实现现在,我给出version.dll劫持部分入口点部分的代码,其余的代码都是使用AheadLib工具生成的,自己在入口点添加了一行弹窗的代码而已。
    // 入口函数BOOL WINAPI DllMain(HMODULE hModule, DWORD dwReason, PVOID pvReserved){ if (dwReason == DLL_PROCESS_ATTACH) { DisableThreadLibraryCalls(hModule); ::MessageBox(NULL, "I am Demon", "CDIY", MB_OK); return Load(); } else if (dwReason == DLL_PROCESS_DETACH) { Free(); } return TRUE;}
    总结有了AheadLib劫持代码生成工具的帮助,使得DLL劫持变得很轻松。本文的文档自己极力简化了,大家只要认真跟着步骤操作,应该可以看得懂的。
    注意,本文演示的例子实在 Windows7 32位系统上,对于64位系统,原理是一样的,对于代码劫持工具也可以换成 AheadLib 64位版本的。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-12-20 12:07:01 奖励40点积分
  • 驱动环境下的任务进程获取枚举

    驱动环境下的,枚举系统任务,属于新手驱动学习,还望各位大佬,发现失误能够指点一二,辛得指点,感恩各位!
    #include "ntddk.h"typedef enum _SYSTEM_INFORMATION_CLASS { SystemBasicInformation, // 0 SystemProcessorInformation, // 1 SystemPerformanceInformation, // 2 SystemTimeOfDayInformation, // 3 SystemNotImplemented1, // 4 SystemProcessesAndThreadsInformation, // 5 SystemCallCounts, // 6 SystemConfigurationInformation, // 7 SystemProcessorTimes, // 8 SystemGlobalFlag, // 9 SystemNotImplemented2, // 10 SystemModuleInformation, // 11 SystemLockInformation, // 12 SystemNotImplemented3, // 13 SystemNotImplemented4, // 14 SystemNotImplemented5, // 15 SystemHandleInformation, // 16 SystemObjectInformation, // 17 SystemPagefileInformation, // 18 SystemInstructionEmulationCounts, // 19 SystemInvalidInfoClass1, // 20 SystemCacheInformation, // 21 SystemPoolTagInformation, // 22 SystemProcessorStatistics, // 23 SystemDpcInformation, // 24 SystemNotImplemented6, // 25 SystemLoadImage, // 26 SystemUnloadImage, // 27 SystemTimeAdjustment, // 28 SystemNotImplemented7, // 29 SystemNotImplemented8, // 30 SystemNotImplemented9, // 31 SystemCrashDumpInformation, // 32 SystemExceptionInformation, // 33 SystemCrashDumpStateInformation, // 34 SystemKernelDebuggerInformation, // 35 SystemContextSwitchInformation, // 36 SystemRegistryQuotaInformation, // 37 SystemLoadAndCallImage, // 38 SystemPrioritySeparation, // 39 SystemNotImplemented10, // 40 SystemNotImplemented11, // 41 SystemInvalidInfoClass2, // 42 SystemInvalidInfoClass3, // 43 SystemTimeZoneInformation, // 44 SystemLookasideInformation, // 45 SystemSetTimeSlipEvent, // 46 SystemCreateSession, // 47 SystemDeleteSession, // 48 SystemInvalidInfoClass4, // 49 SystemRangeStartInformation, // 50 SystemVerifierInformation, // 51 SystemAddVerifier, // 52 SystemSessionProcessesInformation // 53} SYSTEM_INFORMATION_CLASS;typedef struct _SYSTEM_THREAD_INFORMATION { LARGE_INTEGER KernelTime; LARGE_INTEGER UserTime; LARGE_INTEGER CreateTime; ULONG WaitTime; PVOID StartAddress; CLIENT_ID ClientId; KPRIORITY Priority; KPRIORITY BasePriority; ULONG ContextSwitchCount; LONG State; LONG WaitReason;} SYSTEM_THREAD_INFORMATION, * PSYSTEM_THREAD_INFORMATION;typedef struct _SYSTEM_PROCESS_INFORMATION { ULONG NextEntryDelta;//构成结构系列的偏移量也就是下一个进程 ULONG ThreadCount;//线程的数目 ULONG Reserved1[6];// 暂时未知 LARGE_INTEGER CreateTime;//创建时间 LARGE_INTEGER UserTime;//用户模式的CPU时间 LARGE_INTEGER KernelTime;//内核模式下的时间 UNICODE_STRING ProcessName;//进程的名称 KPRIORITY BasePriority;//进程的优先权 ULONG ProcessId;//进程的标识符 ULONG InheritedFromProcessId;//父进程的标识符 ULONG HandleCount;//句柄数目 ULONG Reserved2[2];// VM_COUNTERS VmCounters;//虚拟存储器的机构 IO_COUNTERS IoCounters;//io计数器 //SYSTEM_THREAD_INFORMATION Threads[1];//进程相关的线程结构数组这里我们不使用} SYSTEM_PROCESS_INFORMATION, * PSYSTEM_PROCESS_INFORMATION;extern "C"NTSYSAPI NTSTATUS NTAPI ZwQuerySystemInformation( IN SYSTEM_INFORMATION_CLASS SystemInformationClass, OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength OPTIONAL );//--------------------------------------------------------------//-----------------------------------------------------------------NTSTATUS Ring0EnumProcess(){ ULONG cbuffer=0x8000; PVOID pBuffer=NULL; NTSTATUS Status; PSYSTEM_PROCESS_INFORMATION pInfo; do { pBuffer=ExAllocatePool(NonPagedPool,cbuffer); if (pBuffer==NULL) { return 1; } Status=ZwQuerySystemInformation(SystemProcessesAndThreadsInformation,pBuffer,cbuffer,NULL); if (Status==STATUS_INFO_LENGTH_MISMATCH) { ExFreePool(pBuffer); cbuffer*=2; }else if (!NT_SUCCESS(Status)) { ExFreePool(pBuffer); return 1; } } while (Status==STATUS_INFO_LENGTH_MISMATCH); pInfo=(PSYSTEM_PROCESS_INFORMATION)pBuffer; for (;;) { LPWSTR pszProcessName=pInfo->ProcessName.Buffer; if (pszProcessName==NULL) { pszProcessName=L"null"; } DbgPrint("ProcessID%d 进程名::%S 父进程ID%d",pInfo->ProcessId,pInfo->ProcessName.Buffer,pInfo->InheritedFromProcessId); if (pInfo->NextEntryDelta==0) { break; } pInfo=(PSYSTEM_PROCESS_INFORMATION)(((PUCHAR)pInfo)+pInfo->NextEntryDelta); } ExFreePool(pBuffer); return 0;}VOID Unload(IN PDRIVER_OBJECT DriverObject){}NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { DriverObject->DriverUnload = Unload; Ring0EnumProcess(); return STATUS_SUCCESS; }
    0 留言 2020-02-17 11:30:34 奖励16点积分
  • 基于SSM框架的B/S微博系统的设计与实现

    第一章 绪 论时至今日,网络对于现代人来说,早已成为人类科技发展进步的桥梁,而通过网络衍生出的一系列产品也不断的冲击着人们的日常生活,截至2016年年底,我国网名数量达到7亿,有超多一半多人在使用网络,而它天涯咫尺的作用,不但消除了人与人地域上的距离,更是拉近了心灵的距离,沟通与互动变的异常频繁与重要。
    随着互联网新时代的来临,微博借着互联网的桥梁,逐渐进入网名的视野中,没有博客的长篇大论,也不需要严谨的逻辑层次,这使得网络中一大批的原创文章被生产发掘,短短几句话便可以在网络中激起千层浪,普通人也可能在一夜之间成为拥有数千万粉丝的“网红”。微博的便捷、原创、和草根性使它成为中国网民茶余饭后的网上生活。
    早在2006年3月,Obvious推出了Twitter服务,这个世界上最早同时也是最著名的微博系统,而在那是,微博也仅仅只是为了给好友发送手机短信,在中国,饭否网的上线标志中国微博的开端,之后腾讯滔滔、叽歪、嘀咕等微博的开荒者终究也都没能站在最后。
    2010年,我国的微博得到迅速发展,这一年,无论微博的用户还是影响力都达到前所未有的高度,以新浪门户为首的腾讯、新浪、搜狐等微博展现出全新的活力。到了2013年6月,中国微博用户规模高达3.31亿人,在微博中流动的信息有超过2亿多条。
    2010年11月,新浪微博推出群组功能,这个功能不但可以与好友实时联系,又可以随时发布最新信息,2012年添加新功能“悄悄关注”,在用户关注后不通知被关注用户,也不显示给被关注用户,2013年,微博推出包括iPhone和Android的移动客户端,新增“密友”功能,同年10月份新浪微博“粉丝服务平台”上线,粉丝服务平台帮助认证用户为订阅自己的用户提供精彩内容和互动服务,自此微博由“营销”向“营销+服务”转型!而新浪微博依旧延续这自己的名人效应,一个普通账号,在没有粉丝关注的情况下,发布的微博却很难被他人看到,如果需要在微博中求助,没有“名人大V”的帮助,很难被别人所注意到。想要在微博中寻求帮助,就需要微博提供更多的服务。而本系统通过使用积分悬赏功能使得用户可以通过积分悬赏自己的问题,来让更多的用户回答自己的问题,得到更多人的帮助。
    第二章 相关技术介绍2.1 架构概述B/S架构(Browser/Server,浏览器/服务器模式):是一种通过将浏览器作为客户端的网络结构模式,利用已经逐步成熟的web浏览器技术,结合浏览器的多种功能,使用浏览器来作为早先C/S(Client/Serve)架构下复杂的客户端,使用C/S架构使得用户的客户端得到统一,将软件系统的核心功能集中在服务器端,系统的升级和维护更加简单,开发人员只需要管理服务器就可以做到对如软件系统的更新和维护,B/S架构所带来的众多优点使得它成为将来信息化发展的主流方向。
    MVC模式:即模型(Model),视图(View),控制器(Controller)是一种软件开发的设计模式,Model主要负责分离出来的业务逻辑的数据交互,View主要负责数据的提交和展示,Controller主要负责获取请求和请求的转发。
    SSM框架(SpringMVC+Spring+Mybatis):由这三个开源框架组成的web项目的框架,是标准的MVC模式,spring MVC负责请求的转发和视图管理,spring实现业务对象管理和事务控制,mybatis作为持久化引擎,相对于早先流行的SSH(Struts+Spring+Hibernate)框架来说,SSM作为轻量级框架更适合开发中小型项目。
    2.2 关键技术简介前端技术
    JSP(Java Server Pages):嵌入了java代码的html页面,本质是一个servlet,它实现了在htmld的语法中扩展java代码,使用<% %>格式。
    JavaScript:是一种弱类型的脚本语言,由ECMAScript描述JavaScript的基本对象和语法,文档对象模型(DOM),浏览器对象模型(BOM)三部分组成。
    Ajax(Asynchronous Javascript And XML):异步的JavaScript和XML,实现前后台之间的异步交互,在不需要重新加载整个页面的前提下对页面上的部分内容做局部刷新。

    后台技术java:一种面向对象的编程语言,主要特性包括有

    简单性:抛弃了C++中复杂的语法和指针多继承等特征,开发人员不需要关注底层优化,只需要关注业务逻辑就行
    面向对象性,对程序员而言,只需要注意对应的数据和处理数据的方法,不用严格按照过程的方式来编写代码,因为java语言是面对对象的,因此也拥有面向对象的三大特征,继承、封装和多态
    跨平台性,java语言的跨平台性也就是JVM(java虚拟机)的跨平台性。Java文件编译后不会直接生成机器直接运行的二进制文件,而是编译成class文件,JVM会将编译的class文件根据不同操作系统下的JVM来生成对应系统的二进制文件,只需要编译一次,就可以在不同的平台上运行

    SpringMVC:是Spring框架提供的一个模块,通过实现MVC模式来地将数据、业务与展示进行分离,简化了Web开发。和其他MVC框架一样都是请求驱动,控制转发,它的核心Servlet是DispatcherServlet,即前端控制器,用来分发请求到控制器,它和Spring框架完全整合。这也使得SpringMVC成为目前非常流行的MVC框架。
    Spring:一款轻量级java开发框架,Spring框架有着分层的体系结构,因此可以使用Spring中 独立的任意部分。而它的架构依旧是内在稳定的,Spring提供了J2EE应用各层解决方案,贯穿于三层架构的每一层,但Spring依旧可以很好的兼容其他框架,本项目主要用到了Spring中IOC(控制反转)和AOP(面向切面编程)模块。
    Mybatis:是一个简化Java持久化层的轻量级开源框架。并且支持注解的Mapper,Mybatis消除了绝大部分的JDBC代码,使得java对象持久化到数据库的过程变的更加容易,相对于其他的java持久化框架,Mybatis的优点在于,消除了大量的JDBC冗余代码、简单易学、可见的Sql语句、提供了于Spring的整合,引入更好的性能。
    2.3 开发工具Tomcat服务器:是一个Web应用服务器,它是轻量且开源的,是中小型Web项目和开发调试和学习的首选。
    Oracle数据库(Oracle Database):是由Oracle公司开发的一款关系型数据库,是商业运用第一的关系型数据库,系统使用方便,功能强大,可移植性强。适用于各种大中小环境,在大数据时代,作为分布式数据库,它实现了分布式处理的功能,具有完整的数据管理功能、完备的关系型数据库、分布式处理功能。
    Eclipse开发工具:一个基于java开源的可扩展开发平台,它不但包括java集成开发环境,还包括插件开发环境,如SVN、CVS等团队开发插件。
    2.4 本章小结本章主要介绍了开发项目用到的一些主要技术,项目所使用的架构和设计模式,项目中所使用到的主要框架技术,项目在浏览器端展示用到的前端技术和展示方式,后台代码使用的开发语言,使用的服务器技术,数据持久层所使用的数据库等,在本章最后又介绍了开发使用到的开发工具。
    第三章 系统需求分析3.1 可行性分析3.1.1技术可行性在已有技术方面,为了统一客户端,消除因版本升级和维护带来的复杂性,因此采用成熟的B/S架构在项目的实现上完全可行,在开发语言和框架方面,java和j2ee体系的强大,可以让开发人员精心的构建web项目,以及一系列的开源框架,都为项目的可行性提供了强大的依据,在服务器方面,使用开源服务器Tomcat,足以支持该小型项目的正常使用,而不断发展的前端技术和前端框架可以制作精美的前台页面,提高用户的体验和交互,这在项目的页面展示技术上完全可行,强大的关系型数据库为项目数据的持久化提供强有力的后援。综上所述,日趋成熟化的java和j2ee体系、完全开源的java框架和服务器、功能强大的关系型数据库、运用Web前端技术提供用户交互页面,所以该项目在技术方面完全可行。
    3.1.2 经济可行性在互联网发展到信息时代的今天,单一获取信息的方式已经不能满足人们,人们每天接触的信息越来越多,而获取信息的形式也越来越多,但大多数获取信息的方式,留给用户交互的方式并不多,大都数情况下,人们只能被动获取,而很难找到自己的喜好和需求来获取信息,而本微博项目可以让人们获取实时热点信息和他们所关注的信息,实时与微博信息交互。而使用大量成熟技术与开源框架下,也使得小项目的开发更加简单,经济,高效,因微博而兴起的微博效应也能带来一定的经济效益。
    3.1.3 操作可行性微博系统使用B/S架构,用户不需要下载客户端,只需要用户有浏览器,就可以在浏览器上登陆微博系统,微博系统的界面颜值高,用户交互性高,用户操作简单方便,只需要了解基本的计算机操作就能使用,用户体验性高。因此在系统操作上完全可行。
    3.2 需求分析3.2.1 系统总体需求该微博系统主要由前台用户模块和后台管理模块组成,当用户进入首页时可以选择登陆或不登陆,登陆时可以使用已有账号登陆或注册新账号。用户未登陆时,在首页显示最近更新的热门微博,而登陆后的用户可以在首页看到 自己关注用户最近更新的微博。
    未登陆用户只能搜索查看微博信息和访问用户主页,登陆用户可以登陆系统后修改自己的基本信息例如签名、性别等,在验证用户信息后还可以修改密码和密保信息。以及修改用户头像和密码,编辑自己的个人主页,对微博进行点赞、评论、收藏等功能,还可以关注/取消关注用户,拉黑用户、私聊用户等操作。
    后台管理员可以查看系统所有的数据,包括用户、微博、评论、海螺、回答的总量,最近一个的数据库,最近一周的数据量。具体所具有的功能包括管理不良微博信息与不良账号,对微博、微博评论、海螺、回答等信息的删除和恢复功能,对不良账号的封禁等操作。
    微博查找模块:用户可以输入关键字来查找相对应的微博或查找用户。
    微博发布模块:用户点击发布,在内容中添加自己想要发送的内容,可以选择表情,也可以插入图片,但对输入字符数有着限制,同时还可以插入一张图片。
    微博评论模块:用户可以查看微博的评论,发布微博评论等。
    神奇海螺模块:用户可以发布一个神奇海螺,海螺主要用来记载用户提出的各种问题,由其他用户来查看并回答问题,当回复者的答案被提问者采纳后,回答者可以增加自己的海螺积分,不同的海螺积分有着不同的海螺称号。
    积分模块:用户每天登陆,发微博,做任务等方式可以增加自己的积分,不同的积分拥有不同的称号,神奇海螺模块的积分有着不同的称号,称号显示在用户名称的后面。
    好友模块:用户可以查看系统中其他的用户,找到自己喜爱的用户然后关注他们,关注后可以在好友模块中查看自己关注的用户,以及好友最近发布的微博等信息,也可以私信好友,发送私信信息给好友。
    3.2.2 用例图需求1. 当用户进入系统时候,可以选择登陆或注册用户,如果忘记了密码还可以通过密保问题来重置密码。

    2. 当用户登陆后,可以管理用户的个人基本信息,修改用户基础信息,修改用户密保信息。修改用户头像等功能。

    3. 用户微博管理系统,当用户登陆进去系统时,可以在首页发布微博,通过关键字搜索微博内容中关键字的微博信息。产看微博,包括查看个人微博、好友微博、推荐微博。

    4. 评论管理,评论管理依赖于微博模块,用户可以查看微博的评论,对微博信息发布评论,以及删除自己的评论。

    5. 海螺管理,用户登陆后可以在海螺模块发布海螺问题,发布问题时可以选择悬赏的积分数目,同时减少自己的积分数,用户可以参加回答他人的海螺问题,当回答的答案被采纳时,就可以获得用户悬赏的积分数。

    6. 好友管理,当用户登陆时可以关注系统推荐好友,也可以自己搜索用户,查看用户的主页面,添加关注或取消关注用户,还可以给用户发送私人信息,或者拉黑用户。

    7. 消息通知,当用户的关注,微博评论,点赞,收藏时调用消息通知。

    8. 管理员:管理员登陆系统,可以管理微博用户,对不良用户进行封禁和注销账户的操作,也可以恢复用户的状态,同时也可以对微博信息进行管理,如删除微博,恢复被管理员删除的微博信息等,对微博评论的删除和恢复等操作。

    第四章 系统功能设计4.1 系统类分析4.1.1 实体类实体类主要用来传递数据,主要包括User、Friend、PointAction、PointRecord、Weibo、WeiboCollect、WeiboComm、WeiboLike、WeiboTrans等,用户类中包括用户的基本信息,微博类中包括微博的基本信息同时包含实体用户类,好友类包括好友编号,好友创建时间与关注双方的用户类等,私信类包含私信双方的用户类与私信的基本属性,微博收藏类包含收藏的微博类与用户id等收藏属性,微博评论类包含微博类和用户类以及评论内容等属性,微博转发类包含用户类和微博类以及转发时间等属性,微博点赞类包含微博类和用户类以及点赞时间等属性。实体类之间的类关联关系如图所示。

    4.1.2 控制器类在controller层包含MainController、WeiboController、FrendController、AdminController四个JAVA类,在SpringMVC框架中主要用来接收浏览发送给服务器的请求和数据处理并控制请求的转发,将从Service层中获取的数据响应给浏览器端。MainController主要用发来接收来自用户相关页面中提交的表单或链接请求,并将请求的参数传递到Controller中对应的方法中,携带数据响应给浏览器,在浏览器端显示数据,具体属性和方法如图所示。

    WeiboController主要用发来接收来自微博相关页面中提交的表单或链接请求,并将请求的参数传递到Controller中对应的方法中,并获取到业务处理层中返回的数据,携带数据响应给浏览器,在浏览器端显示数据,具体属性和方法如图4所示。

    FriendController主要用发来接收来自好友相关页面中提交的表单或链接请求,并将请求的参数传递到Controller中对应的方法中,并获取到业务处理层层中返回的数据,携带数据响应给浏览器,在浏览器端显示数据,具体属性和方法如图所示。

    AdminController主要用发来接收来自管理员相关页面中提交的表单或链接请求,并将请求的参数传递到Controller中对应的方法中,并获取到业务处理层层中返回的数据,携带数据响应给浏览器,在浏览器端显示数据,具体属性和方法如图所示。

    4.1.3 业务逻辑类在Service层中主要包含四个Service接口和他们的实现类,包括IUserService用来处理用户业务例如用户注册、登陆、修改个人信息等,如图4.6所示。IWeiboService用来处理微博相关的业务例如查找微博,发布微博,删除微博以及对微博的相关操作例如点赞、评论等功能的业务实现,如图4.7所示。IFriendService用来处理和用户好友相关的业务例如点赞、取消点赞、私信、拉黑等功能的业务实现,如图4.8所示。IAdmoinService用来处理和管理员相关的业务例如管理员登陆、图表展示、用户管理、微博管理、海螺管理功能的业务实现,如图4.8所示。

    图4.6用户业务处理类图

    图4.7微博业务处理类图

    图4.8微博控制器类图
    4.1.4 数据库交互类由于系统采用了MyBatis持久化框架,开发人员不需要关注和数据库之间具体的JDBC代码,而只需要处理业务逻辑,因此只需要在Dao层接口中声明方法而不需要写接口的实现类来实现方法,则是通过配置对应的配置文件,在配置文件中编写对应接口方法中的SQL语句和数据库交互。
    4.2 关键业务设计4.2.1 登陆系统
    图4.9系统登录顺序图
    用户进入主页后,可以在左边选择注册用户,或者在右边登陆系统,在注册模块,用户输入用户邮箱,密码,昵称等信息,当用户输入邮箱后会通过Ajax将用户输入的邮箱传到后台,查找用户邮箱是否已经被注册,如果被注册则在页面提示用户该邮箱已被注册,在输入基本信息点击注册后,保存用户注册信息。
    登陆功能:当用户输入邮箱地址和密码后,如果点击下次自动登陆密码在点击登陆后,判断用户的当前登陆时间和上次登陆时间是否为同一天,如果不是同一天则为用户增加登陆的 积分,然后跳转至微博首页。
    4.2.1 用户信息系统
    图4.10用户修改信息顺序图
    用户基本信息:在系统的首页中点击个人账号设置后,跳转到修改用户基本信息页面,用户可以修改这些基本信息,并将修改后的信息保存在数据库中。
    修改密码:在用户个人资料页面点击修改密码,跳转修改密码页面,用户可以输入用户的当前密码,系统判断密码是否正确,如果密码不正确,显示当面密码错误,如果输入密码正确,用户则可以输入新密码,点击修改后将修改后的密码更新到数据库中。
    修改用户头像:点击用户个人资料中修改头像,跳转至修改头像页面,用户选择头像文件,点击上传,将用户头像保存在服务器上,判断用户之前头像是为系统默认头像,如果不是就删除用户之前的头像图片,点击修改后将修改后的头像地址保存在数据库中。
    修改密保:用户先要根据之前设置的密保问题来填写答案,如果密保答案错误,提示用户密保答案错误,如果密保答案正确,用户可以输入新的密保问题和密保答案,点击修改保存用户新的密保问题和答案。
    4.2.2微博模块
    图4.11系统登录顺序图
    发布微博:用户可以在首页发布微博,在微博信息中可以插入表情,也可以选择插入一张图片,当用户点击发布后,浏览器发送请求将前台页面表单中微博信息和图片信息传入后台控制器,将图片信息保存在服务器中,在数据库中只保存图片路径,最后将微博信息保存在数据库中。
    搜索微博:用户可以在首页的搜索框中输入微博中提到的内容来搜索,系统在数据库中通过迷糊查询查询相关的微博信息。在页面中将搜索到的页面展示出来,并且将关键字标红显示。
    微博操作:用户可以查看个人微博、好友微博、收藏微博等信息,对微博的操作有点赞,转发,收藏,评论,删除等。
    4.2.3 好友模块关注功能:当用户登陆系统后可以在页面右侧的推荐用户中选择需要关注的用户,或者是通过页面中的搜索功能来搜索用户,对搜索到的用户进行关注。关注用户后个人关注+1,同时被关注用户粉丝数+1。
    取消关注:和关注功能类型,在导航栏中点击我的好友,在我的好友首页中查看我的关注好友,就能查看到我所关注的所有好友和好友数以及他们的个人信息,通过点击对应的取消关注就可以取消关注该用户,取消关注用户后个人关注数-1,同时被关注用户粉丝数-1。
    拉黑用户:即修改好友表中对应的记录状态,被拉黑用户所发布的私信和微博信息不会被拉黑用户所看到。
    私信:可以在页面的推荐用户上面查看当前登陆用户的未查看私信数,未读私信的数目通过数据库中对所有接收者为当前登陆用户的所有私信信息,且信息状态为未阅读的私信,将得到的数值展示在前台页面中显示。
    发送私信:用户先选择要发送的用户,输入需要发送的私信信息,可以在私信中插入表情,点击发送后会将私信信息发送给对应用户。同时增加提示该用户的未读私信数。
    搜索用户:在搜索用户页面中,用户可以输入用户昵称的关键字来模糊查询相关用户,并将查询到的用户展示在页面中,同时将用户输入的关键字标红显示。
    用户主页面:在页面中,点击任意一个用户的名称或头像都会跳转至对应用户的个人主页,用户的个人主页显示用户的个人信息和用户最近发布的微博,按照时间倒序排列,用户也可以对微博的点赞、转发、评论、收藏做操作。
    4.2.4 海螺模块发布:用户通过点击导航栏中的神奇海螺进入海螺主页,在海螺页面的上面是发布海螺的问题框,下面的可以插入的表情按钮和问题的悬赏积分数,中间的导航栏可以选择查看最新发布、已解决、待解决、我的海螺不同的筛选条件。
    查看海螺问题:用户点击任意海螺问题,系统跳转到展示海螺的具体信息的页面,上方展示发布用户的用户名、用户称号、发布日期、海螺问题、悬赏积分、问题的状态等,在中间显示问题的所有回复信息,回复人的用户名、称号、回答内容、回复日期,是否被采纳等。
    回答海螺问题:在海螺问题详细详细信息页面的底部可以回答海螺问题,在输入框中输入回复的答案,插入表情信息等,点击回复后将回复信息保存在数据库中。
    采纳问题答案:在海螺问题首页,用户通过点击我的海螺问题可以跳转至用户自己所发布的海螺问题页面,在自己所发布的海螺问题页面中可以选择自己认为最正确的答案,点击采纳后修改海螺状态,被采纳的答案变为采纳答案,增加采纳者的海螺积分。
    4.2.5 后台管理员模块管理员登陆:管理员可以通过在登陆页面中点击管理员登陆,跳转至管理员登陆页面。当管理员输入账号信息和密码信息后,如果用户名和密码正确就跳转到管理员首页。如果错误则给出提示。
    管理员首页:在管理员首页中上方显示导航栏,在页面内容方面,通过四张图表来显示微博、用户、评、海螺、评论、回答等的总数以及当月数,在下方的柱状图中则显示距离今天最近的7天的数目。
    用户管理首页:在页面上方显示导航栏,页面内容中显示所有用户的编号、邮箱、昵称、注册日期、上次登陆日期以及用户的状态,由于考虑到用户数量多以以及为了方便查找,因此在页面中一次显示10条用户数据,同时添加用户搜索功能,用户能够在输入用户昵称的关键字后来搜索用户,并且支持迷糊查询。 搜索用户:在用户管理页面输入用户昵称中的关键字来搜索用户,系统会将获取到的用户信息中昵 称为输入关键字的那部分显示为红色。
    用户封禁:管理员可以通过点击用户管理页面操作一栏中的封禁按钮来对违规用户的封禁,管理员可以输入封禁的天数,那么在这个日期之前,用户是不能正常登陆的,管理员也可以通过点击解封来提前解除封禁用户的操作。
    微博管理首页:管理员点击导航栏中的微博管理,系统会跳转至用户微博管理页面,显示系统中所用是微博信息,管理员也可以通过输入微博内容中的关键字来搜索微博,同时可以删除有不良信息的微博,或是恢复以被删除的微博。
    微博搜索:管理员可以通过输入微博内容中的关键字来搜索在微博中存在该关键字的微博,搜索到的微博内容中的关键字会使用红色标注出来。
    微博删除:在微博管理页面中,管理员可以根据微博的内容来判断微博是否违法等信息,如果微博信息中包含不良信息,管理员可以通过操作栏中的删除按钮来删除微博或者可以对已经删除的为微博做恢复操作。
    海螺管理首页:管理员点击导航栏中的海螺管理可以跳转至海螺管理页面,在页面的上方为导航栏,页面内容则显示所有的海螺信息、海螺搜索框、以及下方的分页框,海螺信息包括编号、发布人名称、海螺的内容、发布日期、海螺状态以及可以执行的操作。
    海螺搜索:在海螺管理首页中,考虑到海螺数量多不好查找的问题,因此设置了搜索功能,管理员可以能够在海螺搜索框中输入海螺内容中的关键字来搜索海螺问题,在展示搜索到的结果时会将搜索关键字使用红色标注出来,方便查看。
    4.3 数据库设计4.3.1 概述微博系统数据库表主要包括:

    1. 用户表:用来保存用户的个人信息,例如编号、昵称、姓名、密码。邮箱等信息,以用户编号作为主键,如表4-1
    2. 微博关注表:用来保存用户的关注用户信息,以关注ID作为主键,如表4-2所示
    3. 微博表:用来保存微博信息,例如微博发布时间,微博内容,微博点赞,转发,收藏数等,以微博编号作为主键,如表4-3所示
    4. 微博收藏表:用来保存用户收藏的微博信息,如微博编号,收藏时间等,以收藏编号作为主键,如表4-4所示
    5. 评论表:保存微博的评论消息,例如评论人编号、评论日期、评论内容等,以评论编号作为主键,如表4-5所示
    6. 积分表:用来保存用户获取积分的方式,例如通过每天的登陆发布微博等获取积分,以积分编号作为主键, 如表4-6所示
    7. 点赞表:用来保存微博的点赞信息,主要包括点赞人编号,点赞编号,点赞微博编号等信息。以点赞编号作为主键,如表4-7所示
    8. 积分流水表:用来保存用户获取积分的信息,主要包括积分编号、用户编号、获取日期等信息,以积分编号作为主键,如表4-8所示
    9. 转发表:用来保存用户转发的信息,主要有转发编号、转发人、微博编号等信息,以转发编号作为主键,如表4-9所示

    4.3.2 概念设计一个用户可以发布多条微博,因此用户表和微博表之间存在一对多的关系如图4.12所示。

    一条微博可以对应多个点赞、转发、收藏和评论,因此微博表和收藏、点赞、转发、收藏表之间存在一对多的对应关系,如图4.13所示。

    一个用户可以发布多个海螺,每个海螺问题可以对应多条评论。如图4.14所示。

    4.3.3 数据库表
    用户表:数据库表名USER_TAB,引用序列名SEQ_USER。


    关注表:数据库表FRIEND_TAB,引用序列名SEQ_FRIEND。


    微博表:数据库表名WEIBO_TAB,引用索引名SEQ_WEIBO。


    收藏表:数据库表名COLLECT_TAB,引用索引名SEQ_COLLECT。


    评论表:数据库表名COMM_TAB,引用索引名SEQ_COMM。


    积分表:数据库表名INTEGRAL_TAB,引用索引名SEQ_INTEGRAL。


    点赞表:数据库表名LIKE_TAB,引用索引名SEQ_LIKE。


    积分流水表:数据库表名RECORD_TAB,引用索引名SEQ_RECORD。


    转发表:数据库表名TRANS_TAB,引用索引名SEQ_TRANS。

    第五章 系统功能实现5.1 登陆系统在系统登陆注册页面当用户输入邮箱后会通过Ajax将用户输入的邮箱传到后台控制器,调用Service层中对应的方法,是Service方法中调用Dao层接口查找用户邮箱是否已经被注册,如果被注册则通过前台javaScript显示在页面提示用户该邮箱已被注册,在输入基本信息点击注册后,将会把用户输入的注册信息通过浏览器发送请求到后台控制器中,控制器控制请求的转发页面和将用户注册信息传给Service,在Service中初始化用户的一些基本信息,例如默认头像、默认状态、初始化用户积分等操作,组装用户数据源,调用Dao层方法保存用户注册信息如图5.1所示。核心代码如下:

    登陆功能:如果点击下次自动登陆密码在点击登陆后,先将用户名和密码信息传递到Service层做业务处理,再调用Dao层接口判断邮箱地址和密码信息是否正确,如果正确并且点击了下次登陆,就将邮箱名和密码保存在浏览器Cookie中,将登陆用户保存在Session中,在处理用户登陆的Service中判断用户的当前登陆时间和上次登陆时间是否为同一天,如果不是同一天则为用户增加登陆的积分,修改用户数据库中的积分数,然后跳转至微博首页,如图5.2所示。核心代码如下:



    5.2 用户信息系统用户基本信息:如图5.3所示当用户点击修改后浏览器会提交form表单,发送请求携带用户基本信息到SpringMVC的控制器中接收请求,并把form表单中的数据组装成User对象,作为参数传入Service层中对应的方法进行处理,调用Dao层接口修改用户的基本信息,并修改当前服务器中Session中的User对象信息。核心代码如下:


    修改密码:在用户个人资料页面点击修改密码,将会跳转至如图5-4所示的修改密码页面,用户可以输入用户的当前密码,前台页面通过Ajax发送异步请求,后台控制器接收请求,从数据库中获取用户当前密码是否正确,如果密码不正确,在前台页面中通过javaScript动态提示给用户当面密码错误,如果输入密码正确,用户则可以输入新密码,确认新密码,点击确认修改后浏览器提交form表单,将用户新密码传给后台Service层中对应的方法,在Service的方法中调用Dao层接口更改数据库中的用户密码,更改服务器Session中的用户信息。核心代码如下:


    修改用户头像:点击用户个人资料中修改头像,将会跳转至如图5.5所示的修改头像页面,用户选择头像文件,点击上传,form表单将图片传到后台控制器中,将用户头像保存在服务器上,判断用户之前头像是为系统默认头像,如果不是就删除用户之前的头像图片,并将用户新的头像地址保存在用户信息中,传入Service中调用Dao层接口修改数据库中用户的头像信息。核心代码如下:


    修改密保:当用户点击修改密保页面时浏览器跳转至修改密保页面如图5-6所示,用户先要根据之前设置的密保问题来填写答案,前台页面通过Ajax将用户输入的密保答案传入后台控制器,与数据库中用户的密保问题答案做判断,如果密保答案错误,在页面上通过javaScript展示用户密保答案错误,如果密保答案正确,用户可以输入新的密保问题和密保答案,点击修改提交form表单后,浏览器发送请求在后台数据库中修改用户的密保问题和答案。核心代码如下:


    5.3 微博模块发布微博:在用户主页如图5.7所示,用户可以发布微博,在微博信息中可以插入表情,也可以选择插入一张图片,在前台页面中,表情使用javaScript动态生成div标签并显示在页面上,当用户点击发布后,浏览器发送请求将前台页面表单中微博信息和图片信息传入后台控制器,将图片信息保存在服务器中,在数据库中只保存图片路径,从Session中获取当前登陆用户,组装微博数据源,将数据源传入Service中,调用Dao层接口将微博信息保存在数据库中。核心代码如下:


    搜索微博:如图5.8所示,在微博首页搜索框输入关键字点击搜索提交form表单,浏览器发送请求将关键字传到后台控制器中,在数据库中通过迷糊查询查询相关的微博信息。在Service层中具体方法中调用Dao层接口获得相关的微博信息,遍历集合,组装微博的基本信息和发布人对象,将微博中表情转换成对应的gif图片,调整微博时间格式为对应格式,修改微博内容中搜索关键字为红色显示,将修改好的数据返回到前台页面展示,如图5.9所示。核心代码如下:


    微博操作:点击微博的点赞,转发和收藏功能类型,通过Ajax将微博id提交请求到后台控制器,从Session中获取登陆用户的信息,传递参数到Service对应的方法中通过对微博不同的操作调用对应的Dao层接口将微博的点赞,转发和收藏信息保存在数据库中。点击评论后通过前台页面的点击事件跳转至JavaScript中通过Ajax发送异步请求到后台控制器中,将微博id传递到Service层中对应的方法中嗲用Dao层接口查找数据库中对应微博编号的评论信息,微博评论的分页通过对应的PageBean类控制,在数据库层通过SQL语句来控制分页要显示的条数,在控制器中传递json数据到前台页面展示。在评论信息的最后面可以发布用户自己的评论,可以添加表情,点击发表将form表单提交到后台控制器中,在后台代码中调用Dao层接口保存用户的评论信息。如图5.10所示。核心代码如下:


    个人微博:如图5.11所示:在页面上方导航栏可以点击进入用户个人主页查看我的微博,在页面中页面上方展示导航栏,下面显示用户的基本信息,中间显示用户所发布过的微博,点击微博插入的图片还可以通过JavaScript将图片放大,在页面的右侧展示系统为用户推荐的好友。点击我的微博按钮后发送的请求会被后台控制器接收,从服务器Session中获取当前登陆用户的用户id,传入Service层,在Service层中调用Dao层接口从数据库中获取当前登陆用户的微博信息组装成List集合,遍历List集合,更改微博数据源的日期格式,调用工具类将微博正文中的表情替换成对应的图片信息,在微博的分页中,使用了Oralce数据库中的伪列来获取对应区间的微博信息,实现数据库层的分页,将所有需要在页面上展示的数据传递给控制器,控制器再将数据封装在Response响应中传递到前台页面。核心代码如下:


    5.4 好友模块查看好友微博:通过点击页面导航栏中我的好友来跳转至好友微博页面,在后台控制器中先从Session中获得当前登陆用户的id值,传递给Service,在Service中调用Dao层方法先在数据库的好友表中查找对应用户的所有好友信息,然后再在数据库微博表中查看这些好友对应的微博信息按照微博发布时间倒序排列,将所有查找到的微博信息和发布用户信息封装成List集合,遍历List集合修改微博日期各式,微博表情字符转换为对应图片名称,将加工过后的集合传递给控制器,控制器将所有的组装好的数据响应到前台页面中,在前台页面中通过C标签遍历List展示微博数据如图5.12所示。核心代码如下:

    关注功能:当用户登陆系统后可以在页面右侧的推荐用户中选择需要关注的用户,点击关注后前台页面通过Ajax技术发送异步请求将被关注用户的用户id发送到后台控制器中,在控制器中获取Session中的登陆用户信息,将登陆用户的id和被关注用户的id传给Service层中对应的方法,在Service层对应的方法中做处理,组装好友信息数据源,调用Dao层接口保存用户的关注信息,同时更新用户的关注数,更新被关注用户的粉丝数,然后在前台页面中通过javaScript将页面文本信息关注修改为已关注。核心代码如下:

    取消关注:和关注功能类型,在导航栏中点击我的好友,在中间点击关注,就能查看到我所关注的所有好友和好友数以及他们的个人信息,如图5.13所示,点击取消关注,通过Ajax将好友id传递到后台控制器中,在控制器中获取请求中的用户信息,将好友id和用户id传入Service层中对应的方法,调用Dao层接口将对应用户id和好友id的好友信息删除掉,同时更改用户的关注数和被关注用户的粉丝数。然后返回响应在前台页面刷新好友列表。

    拉黑用户:和取消关注用户类似,只是在Dao层接口中,不是删除已有的用户记录而是逻辑删除,即修改好友表中对应的记录状态,被拉黑用户所发布的私信和微博信息不会被拉黑用户所看到。核心代码如下:


    私信:可以在页面的推荐用户上面查看当前登陆用户的未查看私信数,如图5.14所示,未读私信的数目通过数据库中对所有接收者为当前登陆用户的所有私信信息,且信息状态为未阅读的私信,将得到的数值传递给前台页面,前台页面中所有展示的数值通过EL表达式从服务器发送回来的响应中获取。点击未读私信或者通过图5.13中显示的好友列表中的私信按钮,跳转至用户私信页面。核心代码如下:


    查看私信:如图5.15所示,用户通过点击私信关注好友时,通过后台控制器跳转至我的私信用户页面,页面左侧显示和该用户最近的私信信息,页面右侧显示所有有过私信的用户列表,点击列表中的用户就可以直接私信这个用户,在私信中允许插入表情。在后台代码中,将要发送私信的用户id和从Session中获取的登陆用户id传递到Service层中对应的方法,调用Dao层接口在数据库中查找与该用户相关的所有私信记录同时修改和该用户的所有私信状态为以阅读,将获取的记录组装到集合中,遍历集合调用工具类修改私信的日期格式和文本中的表情格式,同时从数据库中获取和当前登陆用户有过私信记录的所有用户,将组装好的集合返回给控制器,控制器将数据响应给前台页面中,遍历集合中的数据,展示在前台JSP页面中。核心代码如下:


    发送私信:用户先选择要发送的用户,输入需要发送的私信信息,可以在私信中插入表情,点击发送后会提交form表单,浏览器发送请求到后台控制器中,控制器获取发送用户的id和接收用户的id,调用Service层中的方法,组装数据源为私信信息,设置私信信息为未阅读状态,在Service层中对应的方法中调用Dao层接口将私信信息保存在数据库中。核心代码如下:

    搜索用户:在搜索用户页面中,用户可以输入用户昵称的关键字来模糊查询相关用户,当用户输入要查询的用户昵称,浏览器发送请求携带关键字等信息跳转至控制器中特定的方法,在控制器方法内部调用Service中的方法处理逻辑,业务层调用Dao层接口中的查找方法查找用户昵称中包含有关键字的用户,将从数据库获得的对应用户组装成一个集合,遍历集合将用户昵称中包含的关键字改成红色,控制器返回响应跳转到搜索结果页面,遍历集合展示所有查找到的用户信息。如图5.16所示。核心代码如下:


    用户主页面:在页面中,点击任意一个用户的名称或头像都会跳转至对应用户的个人主页,用户的个人主页显示用户的个人信息和用户最近发布的微博,按照时间倒序排列,用户也可以对微博的点赞、转发、评论、收藏做操作。在后天代码中,当用户点击其他用户的头像或名称时,浏览器发送携带用户id的请求到后台控制器中。控制器调用Service中对应的方法,在Service方法中调用Dao层接口从数据库中查看用户的基本信息和用户的微博信息,将所有的数据存放在集合中。返回到控制器中,控制器携带数据返回到前台页面中做展示,如图5.17所示。核心代码如下:


    5.5 海螺模块发布:当用户进入海螺主页时,浏览器页面发送请求到后台控制器中,调用业务层中特定的方法,Service中调用Dao层接口在数据库中查找有关不同筛选条件的海螺问题,并将查找到的问题封装在集合中,通过控制器发送服务器响应,跳转到海螺首页,并循环展示所有的海螺问题。如果用户需要发布问题,在填写了问题描述和所要悬赏的积分数后,点击发布,浏览器提交表单数据到后台控制器中,在Service层中首先判断用户的海螺积分是否大于悬赏积分,如果小于悬赏积分就返回浏览器页面提示用户积分不足。如果积分足够就调用Dao层接口把用户的海螺问题保存在数据库中,同时减少用户的海螺积分修改用户基本信息。核心代码如下:


    查看海螺问题:用户点击海螺首页具体的问题时,浏览器发送请求给后台控制器中调用Service层对应的方法,在Service层中调用Dao层接口通过海螺问题的编号来查看海螺的具体信息,通过发布问题的用户id在数据库中查找对应的发布人信息,服务器返回响应到浏览器中,展示海螺的具体信息,上方展示发布用户的用户名、用户称号、发布日期、海螺问题、悬赏积分、问题的状态等,在中间显示问题的所有回复信息,回复人的用户名、称号、回答内容、回复日期,是否被采纳等,如图5.19所示。核心代码如下:


    回答海螺问题:在海螺问题详细详细信息页面的底部可以回答海螺问题,在输入框中输入回复的答案,插入表情信息,点击回复提交表单,浏览器请求携带表单数据到后台控制器中,被控制器中具体的方法接收,获取Session中的用户编号,组装回复信息的数据源,传递参数到Service中,在Service中调用Dao层接口保存海螺的回复信息,如图5.20所示。核心代码如下:


    采纳问题答案:在海螺问题首页,用户通过点击我的海螺问题可以跳转至用户自己所发布的海螺问题页面,在自己所发布的海螺问题页面中可以选择自己认为最正确的答案,被采纳的答案变为采纳答案,增加采纳者的海螺积分,如图5.21所示,在后台代码中,当用户点击采纳后,浏览器请求携带海螺问题id和问题回复信息被后台控制器中具体的方法接收,在控制器中调用Service层,业务层中调用Dao层接口修改数据库中海螺问题的状态为已解决,修改评论表中被采纳用户的评论状态为被采纳,刷新前台海螺问题页面。核心代码如下:


    5.6 后台管理员模块管理员登陆:管理员可以通过在登陆页面中点击管理员登陆,跳转至管理员登陆页面如图5.22所示,管理员登陆后,页面发送请求到后台控制器中,后台Controller层接收请求,将用户名和密码作为参数调用业务层中的方法,在Service层中调用Dao层接口和数据库中管理员账号表中查询,如果存在就返回管理员类,如果不存在,则抛出异常,异常层层上抛,在控制器层中接收,并将错误信息保存在方法的返回值中,在页面提示,如果用户名和密码正确,就跳转到Controller中管理员首页的处理方法中,在管理员首页的处理方法中获取需要显示的数据并展示。核心代码如下:


    管理员首页:登陆后,系统会跳转到管理员首页,在首页中上方显示导航栏,在页面内容方面,通过四张图表来显示微博、用户、评、海螺、评论、回答等的总数以及当月数,在下方的柱状图中则显示距离今天最近的7天的数目。后台首先在Controller层中跳转至管理员首页对应的处理方法中,调用Service层中对应的方法获取首页展示数据,并将获取到的数据组装到Map集合中,在服务层中调用Dao层中的方法来获取首页需要展示的用户、微博、评论、回答等数据保存在Map集合中,在控制层中获取到返回值数据并保存在服务器响应中,返回给前台页面使用EL表达式展示数据,如图5.23所示。核心代码如下:


    用户管理首页:后台代码通过调用Service层中对应的方法,Service方法里面则调用Dao层接口和数据库交互,获取数据库中所有的用户信息封装成List集合,返回给Controller层将用户List集合响应给浏览器,在页面中通过c标签遍历循环显示用户信息,页面的分页使用自定义分页类PageBean来保存分页信息,在数据库层做分页一次获取10条数据。核心代码如下:


    搜索用户:在用户管理页面输入用户昵称中的关键字来搜索用户,系统会将获取到的用户信息中昵称为输入关键字的那部分显示为红色。在后台代码中,点击搜索会将管理员输入的用户昵称关键字传给后台控制器中,在控制器中调用Service层中对应的搜索用户的方法,在Service层中调用Dao层接口在数据库中通过迷糊查询来获取用户。并将获取到的数据层层返回,在Controller中响应给前台页面,然后在页面中做展示,如图5.25所示。核心代码如下:


    用户封禁:管理员可以通过点击用户管理页面操作一栏中的封禁按钮来对违规用户的封禁,管理员可以输入封禁的天数,那么在这个日期之前,用户是不能正常登陆的,管理员也可以通过点击解封来提前解除封禁用户的操作,具体页面如图5.26所示。在后台代码中则是在Service层中调用Dao层接口,更改用户状态和封禁日期,解除封禁和封禁用户类似,因此不做具体说明。核心代码如下:


    微博管理首页:管理员点击导航栏中的微博管理,系统会跳转至用户微博管理页面,显示系统中所用是微博信息,管理员也可以通过输入微博内容中的关键字来搜索微博,同时可以删除有不良信息的微博,或是恢复以被删除的微博,微博管理页面如图5.27所示,在后台代码中,当用户点击导航栏中微博管理时,浏览器会发送相应的请求到SpringMVC框架的Controller中对用的更能处理方法中,在控制器中调用Service层中显示所有微博的方法中,在Service层中再调用Dao层方法获取所有的微博信息,在分页方面没有采用在前端页面中做分页的方法,而是在数据库中通过Oracle的伪列来做分页,一次获取10条数据,最后在Controller中将获取到的微博信息响应给浏览器,浏览器中通过c标签遍历显示微博信息。核心代码如下:


    微博搜索:管理员可以通过输入微博内容中的关键字来搜索在微博中存在该关键字的微博,搜索到的微博内容中的关键字会使用红色标注出来,具体页面如图5.28所示,在后台代码中,控制器中特定的方法接收浏览发送的搜索微博请求,调用Service层中对应的方法,将关键字作为参数传给Dao层接口中,在Dao层接口中查找数据库微博表中微博内容包含该关键字的微博信息,在数据库中则是通过模糊查询来查找对应微博。然后将查找到微博信息封装到List集合中,层层返回到Controller层中对应的方法,在方法中将数据响应给浏览器,浏览器接收响应在页面中通过c标签展示数据。核心代码如下:


    微博删除:在微博管理页面中,管理员可以根据微博的内容来判断微博是否违法等信息,如果微博信息中包含不良信息,管理员可以通过操作栏中的删除按钮来删除微博或者可以对已经删除的为微博做恢复操作,当用户点击删除时,页面会携带着微博编号等参数发送请求给服务器,请求会被控制器中对应的方法所接收,将微博编号作为参数嗲用Service中对应的方法,在方法中调用Dao层接口在修改数据库中对应微博编号的微博状态为已删除,恢复微博和删除类似,只是修改数据库中对应微博信息状态为正常即可。核心代码如下:

    海螺管理首页:管理员点击导航栏中的海螺管理可以跳转至海螺管理页面,如图5.29所示,在页面的上方为导航栏,页面内容则显示所有的海螺信息、海螺搜索框、以及下方的分页框,海螺信息包括编号、发布人名称、海螺的内容、发布日期、海螺状态以及可以执行的操作,在后台代码方面,当用户点击导航栏中的海螺管理时,浏览器发送请求,请求在控制器中被对应的方法接收,Service中调用Dao层接口在数据库海螺表中查找所有的海螺信息,保存在List集合中,遍历List集合组装发布人用户信息,修改日期格式等,最后将组装号的List集合返回到Controller中,响应给浏览器跳转海螺管理首页,通过使用c标签遍历显示查找到的海螺信息,因为系统中使用的分页方法一致,因此在这里不再赘述。核心代码如下:


    海螺搜索:在海螺管理首页中,考虑到海螺数量多不好查找的问题,因此设置了搜索功能,管理员可以通过在海螺搜索框中输入海螺内容中的关键字来搜索海螺问题,在展示搜索到的结果时会将搜索关键字使用红色标注出来,方便查看。具体页面如图5.30所示。在后台代码中,当用户输入关键字点击搜索时,浏览器发送请求到控制器中指定方法接收,在Controller中调用Service层中对应的方法处理业务逻辑,然后在Service层中调用Dao层接口通过模糊查询在数据库海螺表中查找对应的海螺信息,最后在Controller中将查找到的海螺信息响应给浏览器,在页面中通过c标签遍历展示数据。因为系统使用一样的分页方法,因此不做赘述。核心代码如下:


    第六章 总 结时光荏苒,岁月如梭,转眼之间为期半年的毕业设计以及论文的编写终于落下帷幕,回顾毕业设计的每一个阶段都使我受益匪浅,在毕设初期,经过了长达一周的深思熟虑之后我决定将B/S微博系统作为我这次毕业设计和论文的选题,因为平时对微博的接触以及近几年微博的火热程度,使我对微博系统的具体功能和优缺点都有了一个全体的把控,这也使得在之后系统功能设计时能够更加的得心应手,在毕业设计中期时进入代码编写阶段时,我选择了J2EE体系开发Web项目的B/S微博,使用面向对象语言JAVA作为开发语言,使用个人熟练掌握的SSM框架来搭建系统,SSM框架强大的功能减少了大量的冗余代码,使得系统代码的编写更加轻松,提高了系统的开发效率,然而开发一个系统并不是那么简单就能完成,在代码的编写阶段,问题接连出现,但在指导老师的指导下以及自己通过网上查阅资料,最终解决了这些问题,提高了自己代码的编写能力,同时也提高了自我学习能力,这对以后的生活学习和工作中都有着非同寻常的意义,在项目编写完成后的测试阶段,之前在编写阶段没有被发现的系统缺陷逐渐跃出水面,在每一次修复这些问题的时候,我都能感觉到自己的能力在慢慢提高,一个没有缺陷的系统是不存在的,经过对测试用例中的测试方案一一测试,然后修复掉一个又一个的缺陷后,微博系统中大多数可见性高的BUG都被修复了,但是在之后的使用中还会不断的完善系统让它变的更加成熟,而不单单只是作为一份毕设设计而存在。
    在这次毕业设计系统的开发中也让我看到了自身的一些问题,例如前端技术的不熟练导致在前台页面的修改和开发中浪费了大量的时间,微博系统中关键性的用户交互不够美观,用户体验性差,以及在系统类设计时没能正确的把控全局,设计出合理的接口,只能在后期代码编写阶段中不断的去完善。
    总而言之,在对微博系统的开发中,我学习到了很多以前没有注意到和忽略到的东西,也使我认识到了自身的一些缺陷,让我在以后的生活和工作中都能更好的认识自己,提高自己的能力,然后服务于社会,做一个对社会发展有帮助的人。
    参考文献[1] 贾文潇,邓俊杰. 基于Java的Web开发技术浅析[J]. 电子测试,2016
    [2] 李传扬. 微博分析系统的设计与实现[D]. 北京邮电大学 2015
    [3] 刘运臣. 网站设计与建设[M]. 清华大学出版社, 2008
    [4] 秦雅华. 基于WEB2.0的微博网站的设计与实现[D]. 北京工业大学 2012
    [5] 陈玲,夏汛. 利用Mybatis的动态SQL实现物理分页[J]. 数字技术与应用. 2011(11)
    [6] 萨师煊,王珊. 数据库系统概论(第三版)[M].北京:高等教育出版社,1998
    [7] 基于Java的数据库访问技术研究[J]. 科技资讯. 2009(04)
    [8] 张峰. 基于Ajax技术与J2EE框架的Web应用研究与实现[D]. 中国地质大学 2008
    [9] 基于Java多线程技术的网络编程[J]. 电脑编程技巧与维护. 2009(22)
    [10] 李威. 一种小型实用即时网络聊天通讯系统的设计[J]. 长江大学学报(自然科学版). 2011(12)
    [11] 钟睿祺. 基于微博嵌入小伙伴阅读网的分析与设计[D]. 华南理工大学 2011
    [12] 王少锋编著.面向对象技术UML教程[M]. 清华大学出版社, 2004
    [13] 徐春绵. 关于网站开发相关问题的探究[J]. 通讯世界. 2015(09)
    [14] 张宇,王映辉,张翔南. 基于Spring的MVC框架设计与实现[J]. 计算机工程. 2010(04)
    [15] 胡以谰,张立平. J2EE开发模式的选择[J]. 计算机系统应用. 2002(08)
    [16] 王丽爱. 《Java程序设计》课程网站的设计与实现[J]. 电脑知识与技术. 2016(27)
    [17] 荣艳冬. 关于Mybatis持久层框架的应用研究[J]. 信息安全与技术. 2015(12)
    6 留言 2019-12-20 17:48:45 奖励50点积分
  • 基于B-S架构的学生信息管理系统设计与实现 精华

    第1章 前言1.1 课题研究的背景及意义1.1.1 课题研究的背景目前,随着计算机和移动互联网技术的快速发展,社会正迈向大数据时代,相应地,信息管理系统也越发显得重要。信息化、自动化、智能化在日常生活中的作用越来越大,让我们从繁杂的手工劳动中解脱出来,实现高效的服务工作。在信息技术广泛应用的今天,传统手工管理已经不能适应大数据的规范和管理。采用计算机管理各类数据,不仅能提高工作效率,还能保证信息的安全性。
    计算机的技术迅速发展带动其它技术的发展,自然也推动着信息技术高度发展快速前进。信息、信息技术和信息化的重要性日益被人们所认识,高科技电子通讯也日益被大众所接受,所以现代化通讯方式是今后发展的趋势,所以建立一套符合实际的、简便快捷的、易于掌握的学生信息管理系统成为必然趋势。
    1.1.2 课题研究的意义
    通过研究,了解并掌握基于Web的平台开发流程及设计方法,系统学习Web的架构与应用开发模式,深入理解Web系统体系架构的内部机制和管控方式
    以MySQL为后台数据库的基础上,熟悉Eclipse的开发环境、编辑环境及运行环境,熟练掌握Java语言的运用
    通过对该系统开发流程及开发方式的理解,总结用户需求,归纳开发思想和设计技术,为相似系统研发提供技术支持
    通过翻阅文献,研究主要模块功能的实现,掌握功能实现中所需要用到的算法,实现系统的自动编排等功能帮助管理人员减轻负担,更好的满足用户的需求。

    完成本设计、可以培养学生系统意识。加深学生对MySQL数据库,Java语言,html5+css3前端页面设计有关技术的理解。培养学生的针对实际应用开发网页的能力。使用Eclipse完成代码的编写,实现基本功能。使用MySQL数据库实现网站前后端数据交互,实现用户所操作的增、删、改等功能。该系统的设计与使用,将大大减少工作人员的工作量和工作强度,减少人工操作不可避免的错误,提高工作效率并能够保证数据的安全性,完整性,防止数据的丢失。也得学生信息管理系统平台模式变得规范化、程序化,这也是平台管理的发展方向。
    第2章 系统分析2.1 系统需求分析需求指的是用户需求。用户需求不仅是系统的体系结构规划的主要依据,系统建设与应用的主要驱动因素,也是解决系统社会认知度低、不受用户欢迎等问题的关键所在。需求分析是从用户的角度对他们需要解决的业务问题进行顶层设计,主要包括功能需求分析。
    2.1.1 需求分析实现学校对学生信息的集中管理。可供学校管理人员对学生信息的增加、删除、修改、查询等基本操作,并对系统的可登录人员进行管理,如管理员和学生用户等。在登录管理方面,用管理员身份登录,可对本系统的可登录人员进行管理,有权增加及删除本系统的登录人员,进行自身密码的修改;本系统最终目标是实现学生信息管理的系统化、规范化和自动化。用户模块主要功能有登录模块、学生信息管理、系统管理界面等。

    登录模块:可以提供用户登入系统的入口,用户通过用户名和密码可以登入系统并进行相应模块的操作
    学生信息管理模块:管理员可以添加不同学生类型的学生信息,如小学生,中学生,大学生,通过类型不一样,所要添加的属性也不一样,添加完可以对信息进行修改,删除,查询操作
    管理员模块:管理员登陆可以修改自己的个人信息,如密码等

    2.2 可行性分析
    技术可行性:当今社会,运用计算机管理信息已经越来越普遍。该系统在技术方面较为先进,有利于对学生信息管理系统的操作。经过详细的分析和调查,本学生信息管理系统平台利用计算机信息处理的迅速、准确、可靠且有强大存储能力的突出特点,全面提高平台的管理水平和工作效率,为管理平台的及时转换提供一定的支持
    操作可行性:在进行系统设计之前,已经深入学习开发设计系统中所需的JSP、数据库等技术知识,熟练掌握Java程序设计语言,并已经安装了相关的Eclipse平台和Tomcat服务器等。上述工作为网站的设计与实现做好了技术准备
    经济可行性:传统模式开发的系统不易维护,开发费用高,也不易于升级。而该模式下系统将不存在上述问题,它易维护、易升级。系统的维护和升级所带来的费用低于需求者的预期,所以在经济上是可行的

    第3章 数据库设计3.1 系统数据库设计系统在运行过程中,各功能模块的使用通常都需要调用数据库,所以说数据库的合理设计对于系统设计来说至关重要,通常来说,整个系统的开发时间应当有40%用于数据库设计。合理的数据库结构能够简化系统工作任务流程,同时能提高数据存储的效率。数据库的设计也要有一定的可扩展性,即不仅要收集资料对数据现状进行分析,还要为未来可能扩展的功能预留数据空间。
    本系统采用Mysql数据库技术,在设计数据库前,对学生信息管理系统的数据需求进行了分析,而后才确定该数据库结构。同时,数据库设计可分为概念结构设计和物理结构设计,以下就两者展开具体讨论。
    3.1.1 概念结构设计系统设计中数据库是核心设计内容所在,概念设计为数据库设计提供了良好的设计工具。概念设计中通过E-R图,将抽象的概念进行具体关系的描述和表达。
    大学生信息E-R图

    中学生信息E-R图

    小学生信息E-R图

    管理员信息E-R图

    3.1.2 数据库表设计根据数据库物理结构设计的原则(即数据结构要具有相对的稳定性;结构设计与操作设计相结合;尽可能减少数据库冗余和重复)进行学生信息管理系统数据库表结构及代码设计。
    学生信息管理系统采用MYSQL数据库作为支撑环境,整个数据库系统由多张数据表构成。数据库各表结构涉及包括确定表的名称、表中的字段类型及长度等参数,下面将介绍本系统数据库中各数据表的详细结构。
    大学生信息表



    字段名
    类型
    字段长度
    描述




    id
    Int
    11
    编号


    stu_xuehao
    varchar
    50
    学号


    stu_realname
    varchar
    50
    真实姓名


    stu_sex
    varchar
    50
    性别


    stu_age
    varchar
    50
    年龄


    nj
    varchar
    50
    班级


    yy
    varchar
    50
    英语成绩


    sx
    varchar
    50
    数学


    yw
    varchar
    50
    语文


    dl
    varchar
    50
    地理


    ls
    varchar
    50
    历史


    address
    varchar
    50
    地址


    major
    varchar
    50
    专业


    phone
    varchar
    50
    联系电话


    type
    varchar
    50
    类型



    中学生信息表



    字段名
    类型
    字段长度
    描述




    id
    Int
    11
    编号


    stu_xuehao
    varchar
    50
    学号


    stu_realname
    varchar
    50
    真实姓名


    stu_sex
    varchar
    50
    性别


    stu_age
    varchar
    50
    年龄


    nj
    varchar
    50
    班级


    yy
    varchar
    50
    英语成绩


    sx
    varchar
    50
    数学


    yw
    varchar
    50
    语文


    dl
    varchar
    50
    地理


    ls
    varchar
    50
    历史


    address
    varchar
    50
    地址


    type
    varchar
    50
    类型



    小学生信息表



    字段名
    类型
    字段长度
    描述




    id
    Int
    11
    编号


    stu_xuehao
    varchar
    50
    学号


    stu_realname
    varchar
    50
    真实姓名


    stu_sex
    varchar
    50
    性别


    stu_age
    varchar
    50
    年龄


    nj
    varchar
    50
    班级


    yy
    varchar
    50
    英语成绩


    sx
    varchar
    50
    数学


    yw
    varchar
    50
    语文


    type
    varchar
    50
    类型



    管理员信息表



    字段名
    类型
    字段长度
    描述




    Id
    Int
    11
    编号


    loginname
    varchar
    50
    用户名


    password
    varchar
    50
    密码



    第4章 系统设计4.1 系统功能架构设计该系统软件采用B/S架构,本文设计的学生信息管理系统平台主要有界面设计、逻辑功能的实现部分以及数据库采用MySQL实现网站前后端数据交互,实现用户所操作的增、删、改等功能。
    学生信息管理系统软件主要登录界面模块、学生管理界面、管理员管理模块和系统管理界面组成。用户需要进行登录。他们必须在这个页面输入相应的信息。输入信息后由后台的数据库监测,如果匹配即可进入主页面,如果不正确则继续返回登录页面,显示用户信息错误。当完成登录后,用户进入了主页面模块进入过后,用户可以查看管理平台管理系统数据信息。用户可以查询各模块的数据。具体的系统结构如图4-1。

    4.2 JDBC数据库连接Java中主要是使用JDBC来访问数据库,JDBC API是Java语言访问数据库的一种规范,是Java数据的编程接口,是一组标准的Java接口跟类,当我们使用这些时,就可以访问不同的数据库。不同数据库的类型虽然不同,但是用它的连接步骤是一样的,只是在获取驱动的URL上有所不同而已。有了JDBC,向各种关系数据发送SQL语句就是一件很容易的事。换言之,有了JDBC API,就不必为访问Sybase数据库专门写一个程序,为访问Oracle数据库又专门写一个程序,或为访问Informix数据库又编写另一个程序等等,程序员只需用JDBC API写一个程序就够了,它可向相应数据库发送SQL调用。同时,将Java语言和JDBC结合起来使程序员不必为不同的平台编写不同的应用程序,只须写一遍程序就可以让它在任何平台上运行。
    第5章 系统详细实现5.1 系统平台的实现该系统软件采用B/S架构,系统软件的运行平台采用Windows 10,用Eclipse开发软件程序,MySQL进行数据库的设计,服务器选用Apache Tomcat7.0。整个系统软件的设计和开发使用了面向对象的方法和技术,体现了可视化的友好的人机界面。在个模块的软件的设计上,主要是对界面、逻辑功能、数据库三个方面进行了设计。其中界面设计采用JSP编程和CSS网页设计;逻辑功能的实现采用了ssh框架技术;数据库采用MySQL实现网站前后端数据交互,实现用户所操作的增、删、改等功能。系统功能模块的软件设计采用了MVC设计模式,界面层设计采用JSP技术对服务器系统主界面和数据图表等界面进行设计;逻辑层采用Servlet实现对服务器相关业务逻辑处理;数据层采用了MySQL数据库实现对服务器的系统数据存储调用。系统软件开发模式如图5-1所示。

    5.2 系统主要功能模块的实现5.2.1 登录界面系统软件主要有登录界面、个人信息界面、学生管理界面组成。该系统软件经过验证已经能够成功运行。用户需要进行登录。他们必须在这个页面输入相应的信息。输入信息后由后台的数据库监测,如果匹配即可进入主页面,如果不正确则继续返回登录页面,显示用户信息错误。当完成登录后,用户进入了主页面模块进入过后,用户可以查看学生信息管理系统。登录界面模块,界面模块如图5-2。
    首先运行学生信息管理系统Web项目,成功显示学生信息管理系统的登录页面,在登录页面的右上角有个登录信息的确认,用户登录时的passname和password输入框。输入帐号与密码信息进行验证,当提交登录信息时,触发login.js脚本文件,通过UserAction类中的方法login得到输入的帐号和密码,然后通过getUser()函数进行校验,通过Hibernate进行数据库连接,当数据库中的注册的帐号和密码与用户输入的信息相匹配时,后端UserAction类中输出的“SUCCESS”会传到前端的login.js脚本文件,js会控制界面跳转到首界面main.jsp。如果登录框中没有输入信息,点击登录则提示:用户名或密码不能为空。如果输入的帐号密码在数据库中检测后不存在,则不能实现跳转,会提示用户名与密码错误。

    5.2.2 登陆后信息界面学生个人信息界面分为三个部分分别是顶部top.jsp,中部的right.jsp,以及左边的left.jsp.第一个部分是一张图片,学生信息管理系统。第二部分是main页面,下面说的设置界面和数据查看页面都是从这里实现跳转。图中的第三部分是right一行字。在main.jsp第一个frame里面打开链接top.jsp。这里面放的一个图片,用户进入即可看到健身房管理系统这个图片。在第二个frame里面我们放的是项目所实现的功能,第三个frame里面的连接是right.jsp,打开以后里面放的也是一行字母和数字。我们点击左侧菜单栏不同的部分会显示不同的界面。我们如果已经点开了某一个页面,则不能继续点开,我们用到了add table函数,点击其他页面逻辑也相同,如果存在则不能重复打开,否则跳转至相应的页面。点击左侧的按钮可以跳转至相应页面。firstnode对应左侧的第一个数据查询。点击数据查询按钮会触发addTab1()函数跳转至leftjsp界面。学生信息主界面如图5-3。

    后台信息管理主要由四个功能模块组成,分别为:管理员信息、学生信息管理、退出系统。
    管理员信息
    管理员信息功能模块学生可以点击我的个人信息进行个人信息的编辑和修改等操作,其中涉及到的个人信息有账号、姓名、密码、状态、注册时间。
    另外可以通过该模块进行登录账号和密码的修改。
    学生管理
    分为三个模块的大学生管理,中学生管理,小学生管理。
    而每个学生信息管理包括两个子功能模块,分别为学生信息管理和学生信息添加。学生信息管理中包括学生信息列表,可以通过学生序号和学生姓名进行查询信息,并且对学生的信息里可以点击修改学生信息或者删除学生信息,具体实现界面如图5-5所示。




    添加学生信息管理界面,有三种界面。
    小学生信息
    在填写必要的信息中,还需要填写英语成绩,数学成绩和语文成绩等,具体实现界面如图5-10所示。

    中学生管理
    在添加小学生信息的基础上,还需填写地理成绩,历史成绩和家庭住址信息。添加界面5-11所示。

    大学生类型
    在添加中学生信息的基础上,还需填写专业信息,联系方式信息。具体界面实现如图5-13所示。

    添加信息的同时,系统会提示未填写信息,如果在填写有重复学号的信息,系统也会给出相应的提醒。具体界面实现如图5-14所示。

    5.2.3 学生管理代码实现学生管理的主要代码实现如下:在主页面的左侧是垂直导航菜单,按数据查询会进入大学生xuesheng/xuesheng_d.jsp页面,中学生xuesheng/xuesheng_z.jsp页面,小学生xuesheng./xuesheng_x.jsp我们可以实现用户信息查询,数据会保存在数据库中,当用户查询时,从后端数据库中调出显示在页面上。这个页面上,input为输入框,点击数据查询按钮会触发XueshengAction方法,然后通过数据库进行查询 ,最后将查询到的信息显示。界面模块如图5-6所示。其主要代码如下:
    public String selectStu1() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getStuCount1(); //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus1 = sshService.selectStuPage1(this.pageModel); return SUCCESS; } public String selectStu2() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getStuCount2(); //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus2 = sshService.selectStuPage2(this.pageModel); return SUCCESS; } public String selectStu3() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getStuCount3(); //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus3 = sshService.selectStuPage3(this.pageModel); return SUCCESS; } //通过姓名或者学号查询 public String selectStuByName1() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getCountByName1(stud); if(recordCount==0) { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "无查询记录!"); }else { //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus1 = sshService.selectStuByName1(pageModel, stud); } return SUCCESS; } //通过姓名或者学号查询 public String selectStuByName2() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getCountByName2(stuz); if(recordCount==0) { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "无查询记录!"); }else { //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus2 = sshService.selectStuByName2(pageModel, stuz); } return SUCCESS; } //通过姓名或者学号查询 public String selectStuByName3() throws Exception { //设置请求的页数 if(this.pageIndex != null) { this.pageModel.setPageIndex(pageIndex); } //查询总条数 Integer recordCount = sshService.getCountByName3(stux); if(recordCount==0) { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "无查询记录!"); }else { //把总数设置到poageModel当中 this.pageModel.setRecordCount(recordCount); //查询数据 stus3 = sshService.selectStuByName3(pageModel, stux); } return SUCCESS; } /** * 1 跳转带添加页面 * 2 提交到数据库 */ public String addStu1() throws Exception { if(flag.equals("1")) { return "showAddStu"; }else { //添加学生的时候判断下,是否有学号重复 int num= sshService.findStuxh1(stud.getStuXuehao()); if(num==0) { sshService.saveStu1(stud); return SUCCESS; }else { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "学号重复,请重新添加!"); return "showAddStu"; } } } /** * 1 跳转带添加页面 * 2 提交到数据库 */ public String addStu2() throws Exception { if(flag.equals("1")) { return "showAddStu"; }else { //添加学生的时候判断下,是否有学号重复 int num= sshService.findStuxh2(stuz.getStuXuehao()); if(num==0) { sshService.saveStu2(stuz); return SUCCESS; }else { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "学号重复,请重新添加!"); return "showAddStu"; } } } /** * 1 跳转带添加页面 * 2 提交到数据库 */ public String addStu3() throws Exception { if(flag.equals("1")) { return "showAddStu"; }else { //添加学生的时候判断下,是否有学号重复 int num= sshService.findStuxh3(stux.getStuXuehao()); if(num==0) { sshService.saveStu3(stux); return SUCCESS; }else { Map request=(Map)ServletActionContext.getContext().get("request"); request.put("mess", "学号重复,请重新添加!"); return "showAddStu"; } } } /** * 1 跳转修改页面 * 2 提交到数据库 */ public String updateStu1() throws Exception { if(flag.equals("1")) { stud = sshService.findStuId1(stuId); //大学生 return "showUpdateStu3"; }else { sshService.updateStu1(stud); return SUCCESS; } } /** * 1 跳转修改页面 * 2 提交到数据库 */ public String updateStu2() throws Exception { if(flag.equals("1")) { stuz = sshService.findStuId2(stuId); //中学生 return "showUpdateStu2"; }else { sshService.updateStu2(stuz); return SUCCESS; } } /** * 1 跳转修改页面 * 2 提交到数据库 */ public String updateStu3() throws Exception { if(flag.equals("1")) { stux = sshService.findStuId3(stuId); //小学生 return "showUpdateStu1"; }else { sshService.updateStu3(stux); return SUCCESS; } } //删除学生信息 public String removeStu1() throws Exception { System.out.println("removeStu() -->"); /** * ids传过来是字符串>>"1,2,3,4" * 通过split分割再存进数组》》》》id[0]=1 id[1]=2 id[2]=3 */ String[] temp = ids.split(","); System.out.println("temp>>>>"+temp); for(String id:temp) { sshService.removeStu1(Integer.parseInt(id)); } return SUCCESS; } //删除学生信息 public String removeStu2() throws Exception { System.out.println("removeStu() -->"); /** * ids传过来是字符串>>"1,2,3,4" * 通过split分割再存进数组》》》》id[0]=1 id[1]=2 id[2]=3 */ String[] temp = ids.split(","); System.out.println("temp>>>>"+temp); for(String id:temp) { sshService.removeStu2(Integer.parseInt(id)); } return SUCCESS; } //删除学生信息 public String removeStu3() throws Exception { System.out.println("removeStu() -->"); /** * ids传过来是字符串>>"1,2,3,4" * 通过split分割再存进数组》》》》id[0]=1 id[1]=2 id[2]=3 */ String[] temp = ids.split(","); System.out.println("temp>>>>"+temp); for(String id:temp) { sshService.removeStu3(Integer.parseInt(id)); } return SUCCESS; }第6章 测试本系统中相关的测试都是通过JS来操作完成的,通过页面提交来判断空值,然后页面上给出相应的提示框。
    管理员登陆测试:不输入用户名或者密码,页面提示,如下面

    添加学生信息不填:学号,姓名等信息不填,也给出相应的提醒

    第7章 总结与展望通过此次努力地学习,系统各个模块的功能都按照任务需求基本完成了。在网页上进行展示。主要完成登录界面、学生个人信息界面、学生管理界面的设计。给出有关模块的详细分析和逻辑代码实现。通过这个软件,用户可以方便快捷的查看自己想要了解的信息。在软件模块设计的过程中我重新回顾了以往所学习了大学课堂上学习的基础知识,而且查阅了很多网页开发文献,这些都提升了我对前沿科技有所认识,知道当下研究的方向,这些都对我开发这个系统软件有极大的帮助。开始时,软件设计需要系统能够接收数据,从现在测试的结果来看,所有结果令人满意。
    在今年开学的时候我们开始着手做这个项目,学习了一些基础的CSS,js,jQuery,MySQL,SSH框架到最后的做出简单的jsp文件,再到最后完整的做出项目中间付出了很多遇到了很多问题但是都一一解决了。虽然很困难但是当解决了之后就觉得很快乐,尤其是后期一起整合的时候遇到的问题是最多的最艰辛的时候,遇到各种各样的逻辑问题,,虽然很累很辛苦但是印证了一句俗语风雨之后总会看见彩虹,一直到最后的整合结束静下心来写论文的时候才感觉到自己学到了很多以前没有学到的知识和很多解决问题的经验。才感受到自己确实收获了很多学习了很多的知识,自己在这段时间过的很充实。
    经过几个月的努力,最终完成系统的设计与实现,这期间虽然遇到了很多难题,虽然最终完成了这个系统的所有功能模块,但是和一个上线的系统相比还是有很多的不足,需要改进的地方还是有很多。比如在页面的设计上做的还是不够,页面的样式设计的还有点瑕疵。当然,在本次的毕业设计的制作过程中,我不仅学到了许多的新知识,也巩固了以前学到的一些知识,更是将理论的知识用于实践的过程中,为以后的工作打下了扎实的基础。
    参考文献
    引用[1]Gui Xiu Ouyang. Design and Implementation of Student Information Management System Based on Java Technology[J]. Applied Mechanics and Materials,2014,3634(687).[2]Hui Yan Zhao,Qi Sun. Research on the Design and Implementation of Student Information Management System[J]. Applied Mechanics and Materials,2014,3634(687).[3] Jeremy Keith.JavaScript DOM 编程艺术[M]. 杨涛,译.北京:人民邮电出版社,2011.[4] 张容铭.JavaScript 设计模式[M].北京:人民邮电出版社,2015.[5] 大漠.图解 CSS3:核心技术与案例实践[M]. 北京:机械工业出版社,2014.[6]郑金明,佟施.基于HTML5的校园网高校管理系统的设计与实现[J].广西教育学院学报,2013(02):157-161.[7]魏蓉,常青青.学生信息管理系统的设计探讨[J].价值工程,2011,30(19):166.[8]范亮,潘永才,王雄兵.B/S架构下的学生信息管理系统的设计[J].物联网技术,2016,6 (12):55-56.[9]李超.学生信息管理系统的设计与实现[J].电脑知识与技术,2016,12(09):100-109.[10]乔普拉. JSP高级程序设计[M]. 北京:人民邮电出版社, 2006.[11]杨珏. JSP网络开发技术[M]. 北京:人民邮电出版社, 2001.[12]乔晶.高校学生成绩管理系统设计与实现[J].电脑编程技巧与维护,2015(23):59-72.[13]隋郁.高校学生信息管理的系统设计与实现[J].教育发展研究,2017(S1):13-15.[14]孙红丽.基于JSP的学生信息管理系统设计与实现[J].智能计算机与应用,2017,7(02): 108-112.
    0 留言 2020-01-22 13:49:38 奖励45点积分
  • PC微信逆向分析の绕过加密访问SQLite数据库

    作者:zmrbak(赵庆明老师)
    前言微信,无疑是国内目前最为流行的应用软件,其在社交领域的霸主地位依然无人可以撼动!它不但撑起了庞大的腾讯软件帝国的一角,而且足以让腾讯傲立于BAT中国互联网三巨头之首。微信,改变了这个世界,而腾讯也成了无数优秀程序员梦想的归宿。围绕着微信这个软件的延伸,腾讯也无私地为代码世界贡献了近百个优秀的开源项目。
    从古到今,任何一个巨人,都是站在前一辈巨人的肩膀上而成长为新的巨人。百度如此,阿里如此,腾讯亦是如此。SQLite数据库,一个不为人熟知,但已超过10000亿个设备在使用的开源数据库,已经渗入人们生活的各个环节。目前超过11亿个活跃的微信号,每时每刻都在与这个深藏功与名的SQLite数据库共舞着。
    由于微信的巨大成功,从而成为众多痴迷技术的探秘者的实验品。同样,嵌入微信内部那个深藏功名的SQLite数据库,也成为这些人的又一个解剖对象。接下来,准备好我们的工具,让我们一起来窥探其中的奥妙!
    微信中的数据库无论是在微信安装还是运行的时候,普通用户是不会觉察到微信中居然还有一个默默无闻的SQLite数据库。就算在微信的安装目录中,你也不会发现有任何对于SQLite数据库的描述、说明和感谢(基于SQLite的许可协议,不允许以SQLite的名义来推销产品),但是却在用户的文件夹下(C:\Users\xxxxxx\Documents\WeChat Files\xxxx\Msg\)留下一堆以db为扩展名的文件。对于细心的探索者来说,微信留下来的任何蛛丝马迹,都会变成重要的探秘线索。
    这些db文件,都是经过加密处理的。即使你怀疑它是一个SQLite数据库文件,但是你根本无法用任何一个SQLite管理器打开。虽然网上有不少的文章告诉你,去到微信中寻找那个密码,然后使用某某工具进行解密还原。先不说这些工具各种各样的莫名其妙的问题,就算你确实可以顺利搞到密码,再到完成了解密,再进行访问,其实已经晚了半拍。当然,你要相信,腾讯的那些顶级程序员,会在保证软件功能和性能的情况下,会为这些探秘者设置各种各样的障碍。至少我们知道,那些db文件的确是一个个SQLite数据库,只是经过了加密处理而已。
    窥探SQLite数据库SQLite数据库是一个开源的软件,也就是说,任何人都可以免费地获取它的源代码,对它进行研究、分析,当然还有修改。如果你要把你修改的内容“贡献”给SQLite官方,那么你必须申明你放弃你修改的这些代码的“版权”,不过你提供的代码基本不会原样照搬,而是被SQLite官方按照自己的标准重写一次(开源,但不开放)。当然,如果你要修改后自己用的话,那就随便你啦,SQLite官方并不会找你要钱。即便如此,谷歌依然每年给予SQLite巨额的赞助,当然国内腾讯、阿里等这些巨头也毫不吝啬。虽然这个软件包括源代码在内都是免费开放的,但由于有不少的巨头为其撑腰,就算SQLite不收你的钱,它也会活得很好。如果你非要交钱买个许可的话,那就花6000美元买一个吧,官方称这些钱会用于资助SQLite的持续改进和支持。
    既然SQLite数据库是开放源代码的,而且还有来自微信开发团队的严苛加密,那么将其与微信一起研究,则更为令人振奋。登上SQLite的官方主页,http://www.sqlite.org ,你可以随时把SQLite诞生以来近20年的各个版本抓来研究一番。网站上还有各种示例,教你如何使用SQLite数据库,详尽的各种解说,如果你愿意,你可以通过这个网站了解到它的各种之末细节和详尽的实现方式。虽然,网站没提供中文版,不过,语言并不是障碍,谷歌浏览器的翻译功能,可以让你了解个八九不离十。
    一个简单的SQLite示例如果你还不知道这个SQLite数据库怎么用,网站上提供了TCL的示例代码和C语言的示例代码。现摘抄C示例代码如下(代码来源:https://www.sqlite.com/quickstart.html)。
    #include <stdio.h>#include <sqlite3.h>static int callback(void *NotUsed, int argc, char **argv, char **azColName){ int i; for(i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } printf("\n"); return 0;}int main(int argc, char **argv){ sqlite3 *db; char *zErrMsg = 0; int rc; if( argc!=3 ){ fprintf(stderr, "Usage: %s DATABASE SQL-STATEMENT\n", argv[0]); return(1); } rc = sqlite3_open(argv[1], &db); if( rc ){ fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return(1); } rc = sqlite3_exec(db, argv[2], callback, 0, &zErrMsg); if( rc!=SQLITE_OK ){ fprintf(stderr, "SQL error: %s\n", zErrMsg); sqlite3_free(zErrMsg); } sqlite3_close(db); return 0;}
    示例很简单,也很完善。由于我们的目的是为了探索和研究,因此,我们在确保程序示例可用的情况下,再继续精简。我们删除了很多代码,只剩下核心的几条:
    #include <stdio.h>#include <sqlite3.h>static int callback(void *NotUsed, int argc, char **argv, char **azColName){ for(int i=0; i<argc; i++){ printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL"); } return 0;}int main(int argc, char **argv){ sqlite3 *db; sqlite3_open(argv[1], &db); sqlite3_exec(db, argv[2], callback, 0, &zErrMsg); sqlite3_close(db); return 0;}
    先从main函数开始,首先定义一个sqlite3的db作为数据库的句柄,然后让执行sqlite3_open打开数据库,接下来执行sqlite3_exec查询数据库,最后执行sqlite3_close关闭数据库。在sqlite3_exec执行期间,使用回调函数callback来处理查询的结果。这,就是一个完整的流程。
    执行sqlite3_open打开数据库这个操作,要执行不少的操作,相对比价耗费系统资源。如果一个软件在运行过程中要不断地查询数据库,修改数据库,那么最优的方式就是打开后不要关闭,等到软件关闭的时候再关闭数据库。这样可以节约系统资源,也可以提高数据库的响应速度。
    绕过加密访问SQLite数据库无论是给数据库加上密码,还是使用其它更强有力的方式为数据库加密,无非是为了防止数据库被非法操作。但是,由于基于性能的考虑,应用软件打开SQLite数据库后,只有等到应用程序被关闭的时候,才去关闭数据库。那么在整个应用软件运行期间,SQLite数据库一旦被打开,则将一直保持打开状态。也就是说,上面示例的db,在程序还未调用sqlite3_close函数前,一直是有效的,随时可以接受sqlite3_exec函数的查询操作。这个查询并不是指select,而是指所有可能的sql语句。我们暂且不讨论具有破坏性的操作,如增加(INSERT)、修改(UPDATE)、删除(DELETE)等,在这里我们只来讨论“无破坏性”的“查”(SELECT)的操作。
    数据库的加密验证,往往只在于打开数据库的时候,一旦数据库被打开,剩下的操作,则不再对加密进行验证。因此,我们等数据库打开之后,获取db这个数值,然后就可以“非法”地调用sqlite3_exec函数,来执行我们期望的操作。如果这样操作,那么无论软件设计人员设计的任何加密方式,都形同虚设。也就是说,我们可以轻松地实现“绕过加密来访问SQLite数据库”。
    为了实现“绕过加密来访问SQLite数据库”,接下来需要解决三个问题。

    如何取得已打开的数据库的句柄db的具体数值?
    可被非法地调用的sqlite3_exec函数在哪里?
    如何非法地调用sqlite3_exec函数?

    接下来,让我们依次来解决这三个问题。
    如何取得打开数据库的句柄?如果从正向编程的思维来思考的话,似乎没有答案。但是如果用逆向的思维,你会发现,其实很简单,在调用sqlite3_open函数的时候,其中一个参数就是db,这个db的数值,就是我们正在寻找的打开数据库的句柄。因此,这个问题又转换成,在什么地方调用了sqlite3_open函数。那么,只要我们在调用完sqlite3_open函数之后,马上获取db的值,这个问题就解决了。
    如果你使用过OD或者IDA,你就知道,这些软件可以帮你分析出,在程序的什么地方调用了这个函数,而且每一个调用的地方都可以帮你分析出来。如果再配合inline hook,取数据就如同探囊取物一般容易。
    我们知道,在调用sqlite3_open函数的时候,必然会打开一个文件。在Windows系统中,必然会调用Windows中的CreateFileW函数,而这个函数可以被OD、IDA等逆向工具所识别。因此,通过倒推的方式,就能顺藤摸瓜找到sqlite3_open函数。如果使用OD来动态调试的话,sqlite3_open就非常容易找到,而且还可以找到调用这个函数的代码段,同时还可以直观地观察到某内存地址中的具体数值。因此,db的值就可以很容易地获取。如果你再编写了inline hook后,只要程序调用了sqlite3_open函数,就可以马上获取db的值。
    sqlite3_exec函数在哪里?在软件中往往会执行一些sql语句,而sqlite3_exec函数对多条函数进行了包装,让程序员执行sql语句更为简单。因此,在应用软件中会有大量的sqlite3_exec函数调用。这恰好是一个切入点。
    sqlite3_exec函数的第一个参数是数据库句柄,其实也就是一个DWORD,第二个参数是一条被执行的sql语句,其实就是一个字符串的地址。这些字符串,往往在程序员编程的时候手工写入的,并且嵌入到软件之中,而这些字符串在OD、IDA等这些软件中,可以很容易地识别出来。从而成为找到这个函数的切入点。
    比如在软件中寻找“select * from”之类的sql语句,就可以简单地定位到sqlite3_exec函数,从而确定它在应用软件中的具体地址。
    如何调用sqlite3_exec既然我们已经获得到数据库的句柄db的值,而且已经找到和sqlite3_exec的地址,接下来我们就可以来“非法”地调用这个sqlite3_exec函数了。首先,我们来了解一下这个函数的所需的参数:
    sqlite3_exec(db, argv[2], callback, 0, &zErrMsg);
    第二个参数,是一个查询的sql语句。我们可以定义一个sql的字符串即可,比如“select * from sqlite_master”。由于这条语句是用来查询这个数据库的结构的,因此,在任何一个sqlite数据库中都可以执行成功。当然,你也可以写一个你自己喜欢的sql语句。为了确保一定能查询到信息,我们就用这个字符串。
    第三个参数,是一个回调函数,参照SQLite官方的示例代码编写。
    第四个参数,是为这个回调函数提供的参数,如果没有,或者不需要提供,那么可以直接填写0或NULL。
    第五个参数,返回执行的错误信息,如果不需要的话,可以填写为0或者NULL。
    我们只是找到了sqlite3_exec在软件中的地址而已,但地址并不是一个函数,我们也无法直接用字符sqlite3_exec来调用这个函数。但是,我们可以通过C语言内嵌汇编来调用这个函数,也可以用“函数”指针的方式来调用。使用汇编语言,不是很直观,因此使用函数指针,是一个比较好的方法。
    我们从SQLite源代码中找到sqlite3_exec函数,把它复制过来,然后做一些修改。把sqlite3_exec第一个字符变成大写Sqlite3_exec,加上typedef、__cdecl*,删除形参,再将db所在的形参改为DWORD:
    typedef int(__cdecl* Sqlite3_exec)( DWORD, /* The database on which the SQL executes */ const char *, /* The SQL to be executed */ sqlite3_callback, /* Invoke this callback routine */ void *, /* First argument to xCallback() */ char ** /* Write error messages here */);
    假如sqlite3_exec的地址是:0x12345678,那么我们可以这样定义和调用:
    Sqlite3_ exec p_Sqlite3_ exec = (Sqlite3_ exec) 0x12345678;p_Sqlite3_ exec(db, zSql, callback, 0, 0);`
    运行结果示例



    总结很多初次接触到逆向的人,会被黑压压的汇编代码、一串串的C代码、还有风格诡异的e语言所吓倒。常常有人问我“逆向分析很难吗?”。我也套用一句经典来回答:“天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣!”
    逆向分析,如同玩猜谜游戏,当你看到一个个谜底慢慢浮现的时候,只有身在其中的人,才能享受其中的刺激和乐趣。作为看客,永远无缘体验这份乐趣。你并不是孤身一人,你还有我们。加入我们,一起在玩这个其乐无穷的猜谜游戏。
    源码分享https://github.com/zmrbak/SQLiteReverse
    交流QQ群:456197310 点击链接加入群聊【软件逆向分析入门】
    0 留言 2020-01-03 12:29:22 奖励35点积分
  • PC微信逆向分析の强制输出微信调式信息

    作者:zmrbak(赵庆明老师)
    前言2019年4月份 JieKeH的一篇文章《PC微信逆向—-分析获取登录二维码的数据》一文,让不少的朋友对微信这个软件产生了浓厚的兴趣,当然也包括我。令我印象最深刻的莫过于“打开微信的Xlog日志输出”这部分内容。
    虽然腾讯公司在微信的发布版本中对调试信息进行了屏蔽,但JieKeH通过IDA反汇编的伪代码与Xlog源码对比的方式找到了输出调试信息的关键点,并提供了突破屏蔽的方法。在学习了这篇文章后,本人也通过OD中的反汇编代码与Xlog源码对比的方式,同样找到了相应的关键点,之后将其录制成一个视频分享给了身边一起研究的朋友们。
    新问题新对策很不幸,该文章出现后不久,新版本的微信进行了相应的调整,JieKeH文章中所讲述的方法完全失效。身边的朋友不时告诉我,这个方法失效了,同时请教我还有什么办法可以输出这些调试信息。其实,玩过HOOK的朋友,会马上想到另一个办法,就是写一个dll注入,在dll中编写相应的逻辑实现需要的功能。我用这种方法录制了一个小视频分享之后,再也没有人问过这个问题。看来这个办法还是不错的!
    更简单的方式编写DLL,所涉及到的知识较多,涉及到编码、编译、注入等一系列操作,不但操作复杂而且修改也不容易。近日,最新正式版的微信2.8.0.106推出,我在此新版本上进程了相关的尝试,其实只是在CE中进行简单的一些设置,即可满足需求,就算是新手也不难掌握。由于剪辑视频太耗时间,而且相比于上一个视频也没多少新意,因此仅整理成文档分享给大家。部分内容,之前的视频中有讲解,这里就不再赘述,直接进入本文主题。
    定位“是否启用调试”的地址在OD中附加微信后,查询二进制代码“74 14 FF B5 EC FE FF”(为何是这个二进制代码,请参考之前的视频讲解),定位到如下代码片段,并在接下来的CALL(WeChatWi.79164604)上下一个断点:

    ds:[0x79A6CA01],这个地址,就是是否启动调试的开关。计算出偏移地址:0x160CA01。在微信中,这个地址中的数据被设置成0,因此调试信息将不会输出。只要将这个地址中的数据更改为1,那么输出调试信息的开关就被打开了。但是,调试信息还有其他开关,那就是调试级别。
    定位“调试级别”的地址查询二进制代码“56 89 9D F0 FB FF FF”(为何是这个二进制代码,请参考之前的视频讲解),定位到如下代码片段:

    进入 WeChatWi.79154DB0这个CALL后,到达如下代码片段:

    ds:[0x79A1CD54],这个地址,就是调试级别的设置。计算出偏移地址:0x15BCD54。在微信中,这个地址中的数据被设置成2,也就是高于“kLevelDebug”的信息才会被输出。只要将这个地址中的数据更改为0,也就是“kLevelAll”或“kLevelVerbose”,也就是输出全部调试信息。
    定位调试数据的代码段在JieKeH的文章中,完成这两步设置后,调试信息即可输出,在DebugView中课捕获微信自身的调试信息。正如前文所述,随着微信版本的升级,此方法很快失效。于是,基于此设置,我再用HOOK的方式,重新让微信具备了调试信息的输出的功能。到目前最新的2.8.0.106版本,HOOK方法依然有效。当然,无论怎么做,定位调试数据这个步骤还是不能少的。
    用鼠标动一动微信,OD中程序将暂停在调用“WeChatWi.79164604”处(之前下的断点处),按F7进入该函数。注意观察堆栈窗口,持续按F8,单步执行,直到堆栈顶部出现如下类似的信息。这些信息,就是原本要输出的调试信息。接下来,在HOOK中,同样把这个数据提取出来,从调试信息中输出,然后在DebugView中捕获。

    这时候,观察程序运行的代码部分,计算出该代码片段中“add esp,0x4c“所在汇编代码的偏移地址:0x‭CE7853‬。其代码片段如下:‬

    在上一个HOOK的视频中,我在这里进行了HOOK。在本文中,我使用CE来进行一小段“代码注入”即可代替编写DLL这种难度高、而且手续繁杂的工作。
    定位堆栈中数据的地址虽然数据在堆栈顶部,但是由于下来要对堆栈进行操作,如果使用ESP来取数据,还需一些手动的计算,稍显麻烦。由于EBP在堆栈操作过程中会保持不变,因此我们使用EBP加上一定的偏移量来取数据,这样就不需要额外的手动计算。
    在OD堆栈窗口中,点右键选择“转到EBP“,在该行的地址上双击,变成”$==>“

    然后,在OD堆栈窗口中,点右键选择“转到ESP“,记录该行相对于EBP的偏移:$-40E0。也就是说,当前的数据位于[ebp-0x40E0]这个地方,也就是ESP所指向的地方。因此,我们取到了数据存储的地址:ebp-0x40E0。

    使用CE编写代码注入脚本启动CE,附加微信。点击“手动添加地址”,分别添加“WeChatWin.dll+0x160CA01”与“WeChatWin.dll+0x15BCD54”,再分别命名为“输出调试信息”和“设置调试级别”,再将其值分别更改为1与0,如下图所示:

    点击“查看内存”,定位到“WeChatWin.dll+CE7853”,并选中此行。

    接下来,在“内存浏览器”窗口中依次点击“工具\自动汇编”,在弹出的新窗口中,依次点击“模板\CT表框架代码”,再依次点击“模板\代码注入”,确保弹出对话框中内容为“”WeChatWin.dll”+CE7853”(代码选中的位置),点击“OK”。

    接下来,依次点击“文件\分配到当前的CT表”,然后点击关闭。CE的主窗口中将多出一行内容。将其改名为“OutPutDebug”。记得随时按Ctrl+s(保存)。

    双击OutPutDebug中的数值栏中的“脚本”,在打开的新窗口中对该脚本进行编辑。在newmem与originalcode之间添加如下代码(备注:0x7747efd0,就是OutputDebugStringA函数地址。相关理论,不赘述):
    pushadpush [ebp-0x40E0]call 0x7747efd0popad
    点击“确定”。记得再按Ctrl+s保存。
    开始测试
    输出调试信息:1
    设置调试级别:0
    OutPutDebug:启用(那个方框中间画一个叉)


    以管理管身份启动“DebugView”软件,用鼠标动一动微信,即可观察到微信输出的调试信息。

    源码及工具https://github.com/zmrbak/PcWeChatHooK
    0 留言 2020-01-09 20:58:43 奖励30点积分
  • 获取指定进程的加载基址

    背景之前,自己写过一个进程内存分析的小程序,其中,就有一个功能是获取进程在内存中的加载基址。由于现在Windows系统引入了ASLR (Address Space Layout Randomization)机制,加载程序时候不再使用固定的基址加载。VS默认是开启基址随机化的,我们也可以设置它使用固定加载基址。至于什么是ASLR?或者ASLR有什么作用?本文就不深入探讨了,感兴趣的,可以自己私下了解了解。
    本文就是开发这样的一个小程序,使用两种方法来获取获取指定进程的加载基址。一种是使用进程模块快照方式,然后遍历加载模块并获取基址,另一种是直接调用WIN32 API函数EnumProcessModules,遍历加载模块基址。这两种方法本质上都是一样的。现在,我就把分析过程和实现方式写成文档,分享给大家。
    函数介绍CreateToolhelp32Snapshot 函数
    可以通过获取进程信息为指定的进程、进程使用的堆[HEAP]、模块[MODULE]、线程建立一个快照。
    函数声明
    HANDLE WINAPI CreateToolhelp32Snapshot( DWORD dwFlags, DWORD th32ProcessID );
    参数

    dwFlags指定快照中包含的系统内容,这个参数能够使用下列数值(常量)中的一个或多个:



    VALUE
    MEANING




    TH32CS_INHERIT
    声明快照句柄是可继承的


    TH32CS_SNAPALL
    在快照中包含系统中所有的进程和线程


    TH32CS_SNAPHEAPLIST
    在快照中包含在th32ProcessID中指定的进程的所有的堆


    TH32CS_SNAPMODULE
    在快照中包含在th32ProcessID中指定的进程的所有的模块


    TH32CS_SNAPPROCESS
    在快照中包含系统中所有的进程


    TH32CS_SNAPTHREAD
    在快照中包含系统中所有的线程




    th32ProcessID指定将要快照的进程ID。如果该参数为0表示快照当前进程。该参数只有在设置了TH32CS_SNAPHEAPLIST或者TH32CS_SNAPMODULE后才有效,在其他情况下该参数被忽略,所有的进程都会被快照。
    返回值

    调用成功,返回快照的句柄;调用失败,返回INVALID_HANDLE_VALUE 。

    Module32First 和 Module32Next 函数当我们利用函数CreateToolhelp32Snapshot()获得指定进程的快照后,我们可以利用Module32First函数来获得进程第一个模块的句柄,Module32Next函数来获得进程下一个模块的句柄。
    OpenProcess 函数
    打开一个已存在的进程对象,并返回进程的句柄。
    函数声明
    HANDLE OpenProcess( DWORD dwDesiredAccess, //渴望得到的访问权限(标志) BOOL bInheritHandle, // 是否继承句柄 DWORD dwProcessId// 进程标示符);
    参数

    dwDesiredAccess [in]访问进程对象。 此访问权限将针对进程的安全描述符进行检查。 此参数可以是一个或多个进程访问权限。如果调用者启用了SeDebugPrivilege权限,则无论安全描述符的内容如何,都会授予所请求的访问权限。bInheritHandle [in]如果此值为TRUE,则此进程创建的进程将继承该句柄。 否则,进程不会继承此句柄。dwProcessId [in]要打开的本地进程的标识符。
    返回值

    如果函数成功,则返回值是指定进程的打开句柄。如果函数失败,返回值为NULL。要获取扩展错误信息,请调用GetLastError。

    EnumProcessModules 函数
    在指定的进程中检索每个模块的句柄。要控制64位应用程序是否枚举32位模块,64位模块或两种类型的模块,请使用EnumProcessModulesEx函数。
    函数声明
    BOOL WINAPI EnumProcessModules( _In_ HANDLE hProcess, _Out_ HMODULE *lphModule, _In_ DWORD cb, _Out_ LPDWORD lpcbNeeded);
    参数

    hProcess [in]过程的句柄。lphModule [out]接收模块句柄列表的数组。cb [in]lphModule数组的大小,以字节为单位。lpcbNeeded [out]将所有模块句柄存储在lphModule数组中所需的字节数。
    返回值

    如果函数成功,返回值不为零。如果函数失败,返回值为零。 要获取扩展错误信息,请调用GetLastError。

    实现思路该程序实现进程基址的主要原理是,遍历进程里的所有加载的模块,那么,第一个加载模块的加载基址就是该进程的加载基址。
    那么,对于进程模块加载基址的遍历就有两种两种方法。一种是根据进程模块快照获取,另一种市直接调用EnumProcessModules函数获取。现在,对这两种方法的原理分别进行介绍。
    使用进程模块快照的方式获取模块基址的原理是:

    首先,使用CreateToolhelp32Snapshot 函数获取指定进程的所有模块快照。然后,根据模块快照,使用Module32First 和 Module32Next 函数进行遍历快照,并获取快照信息。其中,就包括有模块的加载基址信息。第一个模块的加载基址便是该进程的加载基址。最后,关闭上面获取的快照的句柄。
    使用EnumProcessModules函数获取模块基址的原理:

    首先,我们需要使用OpenProcess函数打开指定进程并获取进程的句柄
    然后,根据进程句柄调用EnumProcessModules函数获取进程加载的所有模块的加载基址,并保存在数组中。那么,第一个模块的加载基址便是该进程的加载基址
    关闭打开的进程句柄

    编程实现获取指定进程模块快照的方式遍历进程模块PVOID GetProcessImageBase1(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; MODULEENTRY32 me32 = { 0 }; me32.dwSize = sizeof(MODULEENTRY32); // 获取指定进程全部模块的快照 HANDLE hModuleSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessId); if (INVALID_HANDLE_VALUE == hModuleSnap) { ShowError("CreateToolhelp32Snapshot"); return pProcessImageBase; } // 获取快照中第一条信息 BOOL bRet = ::Module32First(hModuleSnap, &me32); if (bRet) { // 获取加载基址 pProcessImageBase = (PVOID)me32.modBaseAddr; } // 关闭句柄 ::CloseHandle(hModuleSnap); return pProcessImageBase;}
    直接使用EnumProcessModules函数获取进程模块基址PVOID GetProcessImageBase2(DWORD dwProcessId){ PVOID pProcessImageBase = NULL; //打开进程, 获取进程句柄 HANDLE hProcess = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (NULL == hProcess) { ShowError("OpenProcess"); return pProcessImageBase; } // 遍历进程模块, HMODULE hModule[100] = {0}; DWORD dwRet = 0; BOOL bRet = ::EnumProcessModules(hProcess, (HMODULE *)(hModule), sizeof(hModule), &dwRet); if (FALSE == bRet) { ::CloseHandle(hProcess); ShowError("EnumProcessModules"); return pProcessImageBase; } // 获取第一个模块加载基址 pProcessImageBase = hModule[0]; // 关闭句柄 ::CloseHandle(hProcess); return pProcessImageBase;}
    程序测试我们在 main 函数中调用上述封装的函数进行测试,main 函数为:
    int _tmain(int argc, _TCHAR* argv[]){ PVOID pProcessImageBase1 = NULL; PVOID pProcessImageBase2 = NULL; pProcessImageBase1 = GetProcessImageBase1(4500); pProcessImageBase2 = GetProcessImageBase2(4500); printf("pProcessImageBase1=0x%p\npProcessImageBase2=0x%p\n", pProcessImageBase1, pProcessImageBase2); system("pause"); return 0;}
    测试结果
    我们运行程序,程序执行成功,并显示两种方法获取的进程加载基址,而且获取结果都相同。

    然后,我们使用 Process Explorer 软件查看PID为 4500 的进程的加载基址,程序基址获取正确,程序测试成功。

    总结这两种方式,本质上都是一样的,只是遍历进程加载模块所使用的方法不相同。那么,进程加载的第一个模块的加载基址,便是进程的加载基址。
    参考参考自《Windows黑客编程技术详解》一书
    2 留言 2018-12-20 12:37:03 奖励5点积分
  • C语言学习笔记

    1 愉快的开始hello world1.1 include头文件包含include是要告诉编译器,包含一个头文件;
    在C语言当中,任何库函数调用都需要提前包含头文件;

    <头文件>,代表让C语言编译器去系统目录下寻找相关的头文件;
    “头文件”, 代表让C语言编译器去用户当前目录下寻找相关的头文件;

    如果是使用了一个C语言库函数需要的头文件,那么一定是#include < >;
    如果是使用了一个自定义的h文件,那么一定是#include“ ”。
    1.2 main函数main函数是C语言中的主函数,一个C语言的程序必须有一个主函数,也只能有一个主函数。
    int main() //一个函数在C语言里面,如果只是空(),代表这个函数可以有参数,也可以没有参数int main(void) //如果是(void),就是明确的表达没有任何参数1.3 注释
    // 单行注释,代表注释,就是一个文字说明,没有实质的意义,单行注释是C++语言的注释方法;
    / / 多行注释,多行注释是标准C语言的注释方法。

    1.4 {}括号,程序题和代码块C语言所有的函数的代码都是在{}里包着的
    1.5 声明int a;
    声明一个变量名字叫a,对于C语言,变量的名称是可以自定义的
    1.6 C语言自定义名字的要求可以使用大小写字母,下划线,数字,但第一个字母必须是字母或者下划线(Linux命名法、匈牙利命名法)

    字母区分大小写
    不能用C语言的关键字作为变量名称
    每一行,必须是;结尾

    1.7 printf函数printf是向标准输出设备输出字符串的如果要输出一个字符串:例如:printf(“hello world”);如果要输出一个整数,例如:printf(“%d”,整数);Printf(“\n”);会输出一个回车换行
    1.8 return语句一个函数遇到return语句就终止了,return是C语言的关键字
    1.9 System系统调用System库函数的功能是执行操作系统的命令或者运行指定的程序
    2 C语言中的数据类型2.1 常量常量就是在程序中不可变化的量,常量在定义的时候必须给一个初值。
    2.1.1 #define#define MAX 10 //定义一个宏常量2.1.2 constConst int a=20; //定义一个const常量
    2.2 字符串常量#define STRING “hello world\n”对于#define类型的常量,C语言的习惯是常量名称为大写,但对于普通const常量以及变量,一般为小写结合大写的方式
    2.3 二进制数、位、字节与字我们习惯于十进制的数:10,12等

    一个位只能表示0,或者1两种状态,简称bit
    一个字节为8个二进制,称为8位,简称BYTE,8个比特是一个字节
    一个字位2个字节,简称WORD
    两个字为双字,简称DWORD

    2.4 八进制八进制为以8为基数的数制系统,C语言当中表示八进制0
    2.5 十六进制
    十六进制值16为基数的数制系统,C语言当中用0x表示十六进制
    十进制转化为八进制,用十进制数作为被除数,8作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果
    十进制转化为十六进制,用十进制数作为被除数,16作为除数,取商数和余数,直到商数为0的时候,将余数倒过来就是转化后的结果

    2.6 原码将最高位作为符号位(0代表正,1代表负),其余各位代表数值本身的绝对值
    2.7 反码
    一个数如果值为正,那么反码和原码相同
    一个数如果为负,那么符号位为1,其他各位与原码相反

    2.8 补码
    正数:原码、反码和补码都相同
    负数:最高位为1,其余各位原码取反,最后对整个数+1
    补码符号位不动,其他位求反,最后整个数+1,得到原码
    用补码进行运算,减法可以通过加法实现

    2.9 sizeof关键字
    Sizeof是C语言关键字,功能是求指定数据类型在内存中的大小,单位:字节,sizeof(int);
    Sizeof与size_f类型

    2.10 int类型2.10.1 int常量,变量int就是32位的一个二进制整数,在内存当中占据4个字节的空间
    2.10.2 printf输出int值
    %d,输出一个有符号的10进制整数
    %u,代表输出一个无符号的10进制整数

    2.10.3 printf输出八进制和十六进制
    %o,代表输出八进制整数
    %x,代表输出16进制整数
    %X,用大写字母方式输出16进制数

    2.10.4 short,long,long long,unsigned int
    Short意思为短整数,在32位系统下是2个字节,16个比特
    Long 意思为长整数,在32位系统下,long都是4个字节的,在64位系统下,windows还是4个字节,unix下成了8个字节。
    int不管是32位系统下,还是64位系统下,不论是windows还是unix都是4个字节的。
    Long long是64位,也就是8个字节大小的整数,对于32位操作系统,CPU寄存器是32位,所以计算long long类型的数据,效率很低

    2.10.5 整数溢出计算一个整数的时候超过整数能够容纳的最大单位后,整数会溢出,溢出的结果是高位舍弃。当一个小的整数赋值给大的整数,符号位不会丢失,会继承。
    2.10.6 大端对齐小端对齐对于ARM,intel这种x86构架的复杂指令CPU,整数在内存中是倒着存放的,低地址放地位,高地址放高位,小端对齐;
    但对于unix服务器的CPU,更多是采用大端对齐的方式存放整数。
    2.11 char类型2.11.1 char常量,变量
    Char c; //定义一个char变量
    ‘a’,char的常量
    char的本质就是一个整数,一个只有1个字节大小的整数

    2.11.2 printf输出char%c意思是输出一个字符,而不是一个整数
    2.11.3 不可打印char转义符\a 警报\b 退格\n 换行\r 回车\t 制表符\\ 斜杠\’ 单引号\” 双引号\?问号2.11.4 char和unsigned char
    Char取值范围为-128到127
    Unsigned char为0-255

    2.12 浮点float,double,long double类型2.12.1 浮点常量,变量
    Float在32位系统下是4个字节,double在32位系统下是8个字节
    小数的效率很低,避免使用,除非明确的要计算一个小数。

    2.12.2 printf输出浮点数
    %f是输出一个double
    %lf输出一个long double

    2.13 类型限定2.13.1 constConst是代表一个不能改变值的常量
    2.13.2 volatile代表变量是一个可能被CPU指令之外的地方改变的,编译器就不会针对这个变量去优化目标代码。
    2.13.3 register变量在CPU寄存器里面,而不是在内存里面,但register是建议型的指令,而不是命令型的指令。
    3 字符串格式化输出和输入3.1 字符串在计算机内部的存储方式字符串是内存中一段连续的char空间,以‘\0’结尾
    “”是C语言表达字符串的方式
    3.2 printf函数,putchar函数Printf格式字符
    字符 对应数据类型 含义d int 接受整数值并将它表示为有符号的十进制整数hd short int 短整数hu unsigned shor int 无符号短整数o unsigned int 无符号8进制整数u unsigned int 无符号10进制整数x/X unsigned int 无符号16进制整数,x对应的是abcdf,X对应的是ABCDEFf float或double 单精度浮点数或双精度浮点数e/E double 科学计数法表示的数,此处“e”的大小写代表在输出是用“e”的大小写c char 字符型,可以把输入的数字按照ASC11码相应转换为对应的字符s/S char */wchar_t * 字符串,输出字符串中的字符直至字符串的空字符(字符串以‘\0’结尾,这个‘\0’即空字符)p void 以16进制形式输出指针% % 输出一个百分号Printf附加格式
    字符 含义| 附加在d,u,x,o前面,表示长整数- 左对齐m(代表一个整数) 数据最小宽度0 将输出的前面补上0,直到占满指定列宽为止(不可以搭 配使用“-”)N(代表一个整数) 宽度至少为n位,不够以空格填充。Putchar是显式一个字符的函数
    long l = 100;Printf(“-6ld”,l);3.3 scanf函数与getchar函数Scanf通过键盘读取用户输入,放入变量中,记得参数一定是变量的地址(&)
    // #define _CRT_SECURE_NO_WARNINGS#pragma warning(disable:4996)Int a=0;Scanf(“%d”,&a);//一定要用&取变量的地址getchar得到用户键盘输入的字符char a = 0;a = getchar();//得到用户键盘的按键printf(“%c”,a);printf(“please input a:”);scanf(“%d”,&a);getchar(); //通过getchar这个函数将之前输入a时候用户按的回车键先收到4 运算符表达式和语句4.1 基本运算符4.1.1 =
    数据对象:泛指数据在内存的存储区域
    左值:表示可以被更改的数据对象
    右值:能赋给左值的量

    4.1.2 +加
    4.1.3 –减
    4.1.4 *乘
    4.1.5 /除
    4.1.6 %取余数
    4.1.7 + =加等于

    a+=5;
    4.1.8 - =减等于
    4.1.9 * =乘等于
    4.1.10 /=除等于
    4.1.11 %=取余等于
    4.1.12 ++自加1

    i++先计算表达式的值,然后再++
    ++i是先++,再计算表达式的值

    4.1.13 —自减1
    4.1.14 逗号运算符int a = 2;int b = 3;int c = 4;int d = 5;int I = (a = b, c + d);逗号表达式先求逗号左边的值,然后求右边的值,整个语句的值是逗号右边的值。
    4.1.15 运算符优先级优先级 运算符 结合性 1 ++(后缀),--(后缀),()(调试函数), 从左到右 {}(语句块),-> 2 ++(前缀),--(前缀),+(前缀),-(前缀), 从右到左 ! (前缀),~(前缀),sizeof,*(取指针值), &(取地址),(type)(类型转化) 3 *,/,% 从左到右 4 +,- 从左到右 5 <<,>> 从左到右 6 < ,>,<=,>= 从左到右 7 = =,!= 从左到右 8 & 从左到右 9 ^ 从左到右 10 | 从左到右 11 && 从左到右 12 || 从左到右 13 ? 从右到左 14 =,*=,%=,+=,-=,<<=,>>=,&=, 从右到左 |=,^= 15 ,(逗号运算符) 从左到右4.2 复合语句{}代码块
    for(i = 0; i < 3; i++) //循环语句,代表复合语句内部的代码要执行3次{ printf(“hello\n”);}4.3 空语句只有一个;号的语句就是空语句,空语句在C语言里面是合法的,并且是在某些场合必用的
    4.4 类型转化double f = (double) 3 / 2; //(double)3意思是将整数3强制转化为double型 ()为强制类型转化运算符double f = 3 /2; //C语言两个整数相除的结果自动转化为一个整数double f = 3.0 / 2;5 条件分支语句5.1 关系运算符在C语言中0代表false,非0代表真
    5.1.1 <小于
    5.1.2 <=小于等于
    5.1.3 >大于
    5.1.4 >=大于等于
    5.1.5 = =等于
    5.1.6 !=不等于
    5.2 关系运算符优先级前四种相同,后两种相同,前四种高于后两种优先级
    5.3 逻辑运算符5.3.1 &&与
    当运算符左右都是真的时候,那么整个表达式的结果为真;只有左右有一个值为假,那么整个表达式的结果为假。
    5.3.2 | |或
    当运算符左右只要有一个值是真的时候,那么整个表达式的结果为真;除非左右两个值都是假,那么整个表达式的结果为假。
    5.3.3 !非
    当值为真的时候,表达式为假;当值为假的时候,表达式为真。
    5.4 if单分支
    if(条件){ //复合语句}当条件是真的时候,复合语句才能被执行,如果条件为假的时候,复合语句不执行
    5.5 if else双分支
    if(条件){ 复合语句1;}else{ 复合语句2;}如果条件为真,那么执行复合语句1,否则执行复合语句2
    5.6 if else if多重if
    if(条件1){ 复合语句1;}else if(条件2){ 复合语句2;}else if(条件3){ 复合语句3;}else { 复合语句4;}当有多个else的时候,else总是和上方最近的那个if语句配对
    5.7 switch与break,default多重选择
    switch(i){case 0: break; //跳出switch的复合语句块……default: //如果所有条件都不满足,那么执行default语句}什么时候用if,什么时候用switch?当条件很复杂,一个条件有&&,||,!存在,那么用if语句如果条件很简单,但分支很多,那么适合用switch
    5.8 条件运算符?一个求绝对值的例子
    int i = -8;int x = (i < 0) ? –i : i;先求?左边的条件,如果条件为真,那么等于:左边的值,否则等于:右边的值一个求最大值的例子
    int c = ( a > b ) ? a : b ;5.9 goto语句与标号无条件跳转goto
    goto end; //无条件的跳转到一个标号去执行……end://标号不建议使用goto语句,goto语句会使你的程序可读性变得很差
    6 循环语句6.1 whilewhile(条件),如果条件为真,循环继续,条件为假,循环结束
    while(1) //是死循环的写法{ 复合语句;}6.2 continue循环遇到continue语句,不再执行continue下面代码,而是直接返回到循环起始语句处继续执行循环
    6.3 break循环遇到break语句,立刻中断循环,循环结束
    6.4 do whiledo{ 复合语句;}while(条件);对于do while来讲,循环的复合语句至少可以被执行一次对于while来讲,有可能复合语句一次执行的机会都没有
    6.5 for
    先执行i=0,对于一个for循环,第一步只执行一次;
    判断i是否小于10,如果i小于10,那么循环继续,否则循环中断
    i++,第一次执行for的时候,不执行i++

    for(int i = 0 ; i < 10 ; i++){ 复合语句;}等同于:
    int = 0;while(i < 10){ i++;}6.6 循环嵌套打印三角形
    * *** ***** ****************int main(){ int i,j; for(i=1;i<7;i++) { for(j=1;j<7-i;j++) { printf(“ ”);}for(j=0;j<(i*2-1);j++){ printf(“*”);}printf(“\n”);}return 0;}7 数组数组的本质就是可以一次定义多个类型相同的变量,同时一个数组中所有的元素在内存中都是顺序存放的。但要记得在C语言中如果定义了如下数组:
    Char s[100] ;//s[0] – s[99],切记没有s[100]这个元素,而且C语言编译器不会帮你检查数组的下标是否有效。Char array[2][3][4] = {};//原则,数组维数越多,代码的可读性就越差,所以要尽可能的用维数少的数组7.1 一维数组定义与使用int array [10]; //定义一个一维数组,名字叫array,一共有10个元素,每个元素都是int类型的array[0] = 20 ;array[1] = 30 ;array[9] = 90 ;//array[10] = 100 ; //错误,没有array[10]这个元素7.2 数组在内存的存储的方式数组在内存中就是一段连续的空间,每个元素的类型是一样的
    7.3 一维数组初始化int array[10] = {1,2,3,4,5,6,7,8,9,10} ;//定义数组的同时为数组的成员初始化值int array[10] = {3,4,5} ;//将数组的前三个元素赋值,其余元素置为0int array[10] = {0} ;//将数组所有的元素都置为0int i;for (i = 0; i < 10; i++){ array[i] = 0 ;//通过循环遍历数组的每个元素,将元素的值置为0 //scanf(“%d”,&array[i]);}求数组中最大元素的值
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int max = 0;int i;for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ if(max < array[i]) max = array[i]; } printf(“max = %d\n”,max); return 0;}求数组中最小元素的值,和最小值对应的数组下标
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};int min = array[0];int index = 0; //在没有遍历数组之前,默认数组的第0号元素就是最小的元素int i;for (i = 1; i < 10; i++) //想找最小的值,一定要把数组先遍历一遍{ if(min > array[i]) { index = i; min = array[i]; } } printf(“min = %d index = %d\n”, min , index); return 0;}求数组中所有元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;//存放数组和的变量for (i = 0; i < 10; i++) //想找最大的值,一定要把数组先遍历一遍{ sum += array[i]; } printf(“sum = %d\n”,sum); return 0;}将数组元素逆置
    int main(){ int array[10] = {32,5,67,98,12,54,8,78,457,10};/*int tmp = array[1]; //中间变量实现两个值的互换array[1] = array[0];array[0] = tmp;*/int min = 0; //数组最小下标int max = 9; //数组最大下标while (min < max) //两头往中间堵{ int tmp = array[min]; array[min] = array[max]; array[max] = tmp; min++; max--;} printf(“max = %d min = %d\n”, max , min); return 0;}求100到999之间的水仙花数
    int main(){ int i; for(i = 100 ;i < 1000 ;i++) { Int i1=i%10; // Int i2=i/10%10; // Int i3=i/100; // If((i1*i1*i1+i2*i2*i2+i3*i3*i3) = = i) Printf(“%d\n”,i);}return 0;}求一个int数组中,所有奇数元素的和
    int main(){ int array[10] = {1,2,3,4,5,6,7,8,9,10};int i;int sum = 0;for (i = 0; i < 10; i++) { if((array[i]%2) = = 1) { sum += array[i]; } } printf(“sum = %d\n”,sum); return 0;}求从3到100之间所有素数打印出来 3 5 7 11 13 17 ……
    int main(){int i; //素数是除了1和自己以外,不能被其他整数整除的整数for (i = 3; i < 100; i++) { int j; int ststus = 0; for(j =2 ;j < i ; j++) //判断i是否为素数 { if((i %j) = = 0) { status = 1; break;} } if(status= = 0) //代表这是个素数 { printf(“%d\n”,i);} } return 0;}7.4 二维数组定义与使用int array[2][3];//定义了一个二维数组,有两个array[3]int array[2][3] = { {1,2,3},{4,5,6} };//定义一个二维数组的同时初始化成员7.5 二维数组初始化int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}};int array[2][3] = {0};//将二维数组中每个元素的值都初始化为0 int array[2][3] = { {1,2,3},{4,5,6} }; int i,j; for(j=0;j<3;j++) { int sum=0; for(i=0;i<2;i++) { sum += array[i][j]; } printf("%d\n",sum);//打印列的和 }#include<stdio.h>int main()//冒泡排序{ int array[10] = {34,14,8,54,23,89,56,4,45,22}; int i; int j; for(i = 0;i < 10; i++) { for(j = 1; j < 10 - i; j++) { if(array[j-1] > array[j])//如果前面的元素大于后面的元素,那么就让这两个元素交换位置 { int tmp = array[j]; array[j] = array[j - 1]; array[j-1] = tmp; } } } for(i=0;i<10;i++) { printf("array[%d]=%d\n",i,array[i]); } return 0;}8 字符串与字符数组字符串一定是在内存中以0结尾的一个char数组。
    8.1 字符数组定义char array[100];8.2 字符数组初始化char array[100] = {'a','b','c','d'};char array[100] = ”abcd”;char array[100] = {0};char array[] = “abcd”;8.3 字符数组使用#include<stdio.h>//字符串倒序int main(){ char buf[100] = "hello world"; int len = 0; while (buf[len++]); len--; int i = 0; int j = len -1; while (i < j) { char tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; i ++; j --; } printf("%s\n",buf); return 0;}ASCII一个字节存放一个字符。GBK用两个字节存放一个汉字。
    8.4 随机数产生函数rand与srand头文件stdilb.hrand是伪随机数产生器,每次调用rand产生的随机数是一样的。如果调用rand之前先调用srand就出现任意的随机数。只要能保证每次调用srand函数的时候,参数的值是不同的,那么rand函数就一定会产生不同的随机数。
    Srand() //随机数种子发生器Rand() //随机数产生器#include<stdio.h>#include<time.h>#include<stdlib.h>int main(){ int t = (int)time(NULL); srand(t); for(int i=0; i<10; i++) { printf("%d\n",rand()); } return 0;}8.5 用scanf输入字符串“%s”的作用就是输入一个字符串的,scanf是以回车键作为输入完成标志的,但回车键本身并不会作为字符串的一部分如果scanf参数中的数组长度小于用户在键盘输入的长度,那么scanf就会缓冲区溢出,导致程序崩溃。
    8.6 字符串的结束标志scanf将回车、空格都认为是字符串输入结束标志。
    8.7 字符串处理函数8.7.1 getsgets(s);Gets认为回车是输入结束的标志,空格不是输入结束的标志,所以用gets这个函数就可以实现输入带空格的字符串,但是gets和scanf一样存在缓冲区溢出的问题。
    Gets不能用类似“%s”或者“%d”之类的字符转义,只能接受字符串的输入。
    8.7.2 fgets函数gets函数不检查预留缓冲区是否能够容纳用户实际输入的数据。多出来的字符会导致内存溢出,fgets函数改进了这个问题。由于fgets函数是为读取文件设计的,所以读取键盘时没有gets那么方便。
    Char s[100] = {0};fgets(s ,sizeof(s) ,stdin);//第一个参数是char的数组,第二个参数是数组的大小,单位:字节,第三个参数stdin代表标准输入的意思fgets是安全的,不存在缓冲区溢出的问题,调用fgets的时候,只要能保证第二个参数小于等于数组实际的大小,那么就可以避免缓冲区溢出的。
    8.7.3 puts函数Puts函数打印字符串,与printf不同,puts会在最后自动添加一个’\n’
    Char s[] = “hello world”;Puts(s);8.7.4 fputs函数fputs是puts的文本操作版本
    char s[] = “hello world”;fputs(s,stdout);8.7.5 strlen,字符串长度size_t strlen(const char * _str);返回不包含字符串结尾’\n’的字符串长度
    Char s[100] = “hello world”;Int len = strlen(s);Printf(“len = %d\n”,len);8.7.6 strcat,字符串追加size_t strcat(char * _str1,const char * _str2);将参数_str2追加到_str1后尾
    Char s1[100] = “fsfgg”;Strcat(s,s1);Strcat也存在缓冲区溢出的问题
    8.7.7 strncat,字符串有限追加size_t strncat(char * _str1,const char * _str2,size_t len);Strcat(s,s1,3); //合并的时候可以限制追加多少个字符8.7.8 strcmp,字符串比较int strcmp(const char * _str1,const char * _str2);比较两个字符串是否相等,相等返回0,不相等返回非0
    If(strcmp(s1,s2)) = = 0){ Printf(“相同\n”);}8.7.9 strncmp,字符串有限比较If(strncmp(s1,s2,5) = = 0) //strncmp的意思是只比较指定数量的字符8.7.10 strcpy字符串拷贝Char *strcpy(char * _str1,const char * _str2);将参数_str2拷贝到参数_str1中
    Strcpy(s1,s2);8.7.11 strncpy字符串有限拷贝Strncpy(s1,s2,3);8.7.12 sprintf格式化字符串和printf函数功能类似,printf函数将格式化结果输出到屏幕,sprintf将格式化结果输出到字符串。
    int i = 200;char s[100] = {0};sprintf(s, “I = %d”,i);8.7.13 Sscanf函数Sscanf类似于scanf函数,scanf从键盘读取用户输入,scanf从指定格式化字符串读取输入。
    8.7.14 strchr查找字符Char * strchr(char * _Str, int _Ch);在参数_str中查找参数_ch指定字符,找到返回字符_ch在_str中所在位置,没有找到返回NULL;
    8.7.15 strstr查找子串Char * strstr(char * _Str,const char * _SubStr);在参数_str中查找参数_SubStr指定子串,找到返回子串在_Str中所在位置,没有找到返回NULL;
    8.7.16 strtok分割字符串字符在第一次调用时strtok()必需给予参数s字符串,往后的调用则将参数s设置成NULL每次调用成功则返回指向被分割出片段的指针。
    如果strtok没有找到指定的分割符号,那么返回NULL
    Char buf [] = “abcd@efg@h”;Char *p = strtok(buf,”@”);While(p){ Printf(“%s\n”,p); P = strtok(NULL,”@”);}8.7.17 atoi转化为intint i1 = atoi(a); //将字符串转化为一个整数需要包含头文件stdlib.h
    8.7.18 atof转化为float8.7.19 atol转化为long#include <string.h> //计算字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}int main(){ const char *s = "32 + 56 ="; printf("%d\n",calc_string(s)); return 0;}9 函数9.1 函数的原型和调用在使用函数前必须定义或者声明函数
    9.2 函数的形参与实参在调用函数的时候,函数大多数都有参数,主调函数和被调用函数之间需要传递数据。在定义函数时函数名后面括弧中的变量名称为“形式参数”,简称形参。在调用函数时,函数名后面括号中的变量或表达式称为“实际参数”,简称实参。

    形参在未出现函数调用时,他们并不占用内存单元,只有在发生函数调用的时候形参才被分配内存,函数调用完成后,形参所占的内存被释放;
    实参可以是变量,常量或者表达式;
    在定义函数时,一定要指定形参的数据类型;
    形参与实参的数据类型一定要可兼容;
    在C语言中,实参与形参的数据传递是“值传递”,即单向传递,只由实参传递给形参,而不能由形参传递给实参。

    如果函数的参数是个数组,那么是可以通过形参修改实参的值的
    9.3 函数的返回类型与返回值
    函数的返回值通过函数中的return获得,如果函数的返回值为void可以不需要return语句;
    函数return语句中的返回值数据类型应该与函数定义时相同;
    如果函数中没有return语句,那么函数将返回一个不确定的值。

    如果C语言一个函数没有明确的标明函数的返回类型,那么函数的返回类型就是int;如果一个函数没有返回值,那么函数的返回类型是void;
    9.4 main函数与exit函数与函数的return语句exit(0); //在子函数中调用exit同样代表程序终止,但在子函数中调用return只是子函数终止,程序正常执行。exit是C语言的库函数,调用exit的结果就是程序终止,在main函数中调用exit与调用return是一样的;
    main函数return代表程序终止。
    9.5 多个源代码文件程序的编译9.5.1 头文件的使用如果把main函数放在第一个文件中,而把自定义函数放在第二个文件中,那么就需要在第一个文件中声明函数原型。如果把函数原型包含在一个头文件里,那么就不必每次使用函数的时候都声明其原型了。把函数声明放入头文件是很好的习惯。
    9.5.2 #include与#define的意义#include就是简单的文件内容替换#define就是简单的文件替换而已9.5.3 #ifndef 与#endif在头文件.h中,
    #ifndef _宏名_#define _宏名_//具体宏的名字是自定义的//函数的声明#endif作用:防止多次include的同一个头文件的时候,重复预编译头文件内容防止头文件被重复包含#ifndef的意思就是条件预编译,如果#ifndef后面的条件成立,那么就预编译从#ifndef开始到#endif之间的代码,否则不会去预编译这段代码。在#ifndef中的宏,一定要大写和下划线,必要的时候加数字,目的是为了避免和其他头文件中的宏名字冲突。#ifdef,#ifndef叫条件编译语句;#ifdef 宏,如果宏被定义了,那么编译语句;#ifndef 宏,如果宏被定义了,那么就不编译语句。9.6 函数的递归函数可以调用自己,这就叫函数的递归。
    #include <stdio.h>void test(int n){ if(n > 0) { n --; printf("先序n = %d\n",n);//先序递归,如果是先序递归,那么代码是顺序执行的 test(n);//函数自己调用自己,就叫函数的递归 printf("后序n = %d\n",n);//后序递归,如果是后序递归,那么代码是逆序执行的 }}int main(){ int i = 3; test(i); return 0;}9.6.1 递归的过程分析案例:将十进制转换为二进制
    #include <stdio.h>void test(int n){ int i = n % 2; printf("%d\n",i); if(n > 0) { test(n / 2); }}int main(){ int i = 11; test(i); return 0;}斐波那契数列例子:斐波那契数列指的是这样一个数列0,1,1,2,3,5,8,13,21,34,55,89,144,…第0项是0,第1项是第一个1;这个数列从第2项开始,每一项都等于前两项之和。
    int fib(int n){ if (n == 0) return 0; if (n == 1) return 1; else { return fib(n - 1) + fib(n - 2); }}9.6.2 递归的优点递归给某些编程问题提供了最简单的方法。
    9.6.3 递归的缺点一个有缺陷的递归会很快耗尽计算机的资源,递归的程序难以理解和维护。
    10 指针10.1 指针10.1.1 指针的概念指针变量也是一个变量;指针存放的内容是一个地址,该地址指向一块内存空间
    10.1.2 指针变量的定义可以定义一个指向一个变量的指针变量
    Int *p; //表示定义一个指针变量*p; //代表指针所指内存的实际数据切记,指针变量只能存放地址,不能将一个int型变量直接赋值给一个指针。Int *p = 100;Int *p = &a; //得到变量a的地址,将这个地址赋值给变量pInt *p1;//定义一个变量,名字叫p1,它可以指向一个int的地址P1=&b;//指针变量的值一般不能直接赋值一个整数,而是通过取变量地址的方式赋值10.1.3 &取地址运算符&可以取得一个变量在内存当中的地址
    10.1.4 无类型指针定义一个指针变量,但不指定它指向具体哪种数据类型。可以通过强制转化将void 转化为其他类型指针,也可以用(void )将其他类型指针强制转化为void类型指针。
    Void *p;10.1.5 NULLNULL在C语言中的定义为(void *)0;当一个指针不指向任何一个有效内存地址的时候,我们应该把指针设置为NULL。
    10.1.6 空指针与野指针指向NULL的指针叫空指针,没有具体指向任何变量地址的指针叫野指针。空指针是合法的,但野指针是危险的,是导致程序崩溃的主要原因。
    10.1.7 指针的兼容性指针之间赋值比普通数据类型赋值检查更为严格,例如:不可以把一个double *赋值给int *;原则上一定是相同类型的指针指向相同类型的变量地址,不能用一种类型的指针指向另一种类型的变量地址。
    10.1.8 指向常量的指针与指针常量Const char * p; //定义一个指向常量的指针Char *const p;//定义一个指针常量,一旦初始化之后其内容不可改变。10.1.9 指针与数组的关系一个变量有地址,一个数组包含若干个元素,每个元素在内存中都有地址。
    Int a[10];Int *p = a;比较p和&a[0]的地址是否相同。
    10.1.10 指针运算指针运算不是简单的整数加减法,而是指针指向的数据类型在内存中占用字节数作为倍数的运算。
    Char *p;P++; //移动了sizeof(char)这么多的字节数int *p1;P1++; //移动了sizeof(int)这么多的字节数赋值:int *p = &a;求值:int i = *p;取指针地址int **pp = &p;将一个整数加(减)给指针:p+3;p-3;增加(减少)指针值 p++,p--求差值,p1-p2,通常用于同一个数组内求两个元素之间的距离比较p1= =p2,通常用来 比较两个指针是否指向同一个位置10.1.11 通过指针使用数组元素P+1代表&a[1],也可以直接使用p[1]表示a[5]P +5代表&a[5];P++
    寻找数组第二大元素第一步:假设数组中前2个元素就是最大的和第二大的MaxSmax第二步:从数组的第2号元素开始遍历数组当有元素大于max的时候,Smax = maxMax= 最大的那个元素,如果当前元素小于max,并且大于smax,那么就让smax等于当前那个元素
    int smax(int *s)//求数组中第二大元素{ int max = 0; int s_max = 0; int i; if (*s > *(s +1)) { max = *s; s_max = *(s + 1); } else { max = *(s + 1); s_max = *s; }//将max等于s[0]和s[1]中大的那个元素的值 for(i = 2;i < 10;i++)//从第3个元素开始遍历数组 { if(max < *(s + i))//如果遇到大于max的元素,那么让s_max等于max,让max等于这个元素 { s_max = max; max = *(s + i); } else if(max > *(s + i) && *(s + i) > s_max)//如果这个元素是介于max和s_max之间,那么就让这个元素等于s_max { s_max = *(s + i); } } return s_max;//返回s_max的值}int main(){ int buf[10] = {34,21,56,4,87,90,15,65,72,48}; printf("%d\n",smax(buf)); return 0;}#include<string.h>//通过指针将字符串逆置int main(){ char str[100]="you"; char *str_start = &str[0]; char *str_end = &str[strlen(str) - 1]; while(str_start < str_end) { char *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}对于VS的汉字是GBK编码,一个汉字2个字节;对于QT汉字是UTF8编码,一个汉字是3个字节。
    #include<string.h>//通过指针将汉字字符串逆置int main(){ char str[100]="我爱你"; short *str_start = &str[0]; short *str_end = &str[strlen(str) - 2]; while(str_start < str_end) { short *tmp = * str_start; * str_start = * str_end; * str_end = tmp; str_start ++; str_end --; } printf("%s\n",str); return 0;}10.1.12 指针数组int *a[10];//定义了一个指针数组,一共10个成员,其中每个成员都是int *类型10.1.13 指向指针的指针(二级指针)指针就是一个变量,既然是变量就也存在内存地址,所以可以定义一个指向指针的指针。通过二级指针修改内存的值;
    Int I = 10;Int *p1 = &I;Int **p2 = &p1;Printf(“%d\n”,**p2);以此类推可以定义3级甚至多级指针。C语言允许定义多级指针,但是指针级数过多会增加代码的复杂性,考试的时候可能会考多级指针,但是实际编程的时候最多用到3级指针,但是3级指针也不常用,一级和二级指针是大量使用的。
    10.1.14 指向二维数组的指针Int buf[3][5] 二维数组名称,buf代表数组首地址Int (*a)[5] 定义一个指向int[5]类型的指针变量aa[0],*(a+0),*a 0行,0列元素地址a+1 第1行首地址a[1],*(a+1) 第1行,0列元素地址a[1]+2,*(a+1)+2,&a[1][2] 第1行,2列元素地址*(a[1]+2),*(*(a+1)+2),a[1][2] 第1行,2列元素的值//二维数组的指针计算二维数组行列的平均值int main(){ int buf[3][5] = {{2,4,3,5,1},{7,2,6,8,1},{7,3,9,0,2}}; int i; int j; int sum; for(i = 0;i < 3;i ++) { sum = 0; for(j = 0;j < 5;j ++) { sum += (*(*(buf + i) + j)); //sum += buf[i][j]; } printf("%d\n",sum / 5); } for(i = 0;i < 5;i ++) { sum = 0; for(j = 0;j < 3;j ++) { sum += (*(*(buf + j) + i)); //sum += buf[j][i]; } printf("%d\n",sum / 3); } return 0;}10.1.15 指针变量作为函数的参数函数的参数可以是指针类型 *,它的作用是将一个变量的地址传送给另一个函数。通过函数的指针参数可以间接的实现形参修改实参的值。
    10.1.16 一维数组名作为函数参数当数组名作为函数参数时,C语言将数组名解释为指针
    int func(int array[10]);//数组名代表数组的首地址10.1.17 二维数组名作为函数参数二维数组做函数参数时可以不指定第一个下标。
    int func(int array[][10]);将二维数组作为函数的参数用例不是特别多见。
    10.1.18 const关键字保护数组内容如果讲一个数组作为函数的形参传递,那么数组内容可以在被调用函数内部修改,有时候不希望这样的事情发生,所以要对形参采用const参数。
    func(const int array[]);10.1.19 指针作为函数的返回值return NULL;10.1.20 指向函数的指针指针可以指向变量、数组,也可以指向一个函数。一个函数在编译的时候会分配一个入口地址,这个入口地址就是函数的指针,函数名称就代表函数的入口地址。函数指针的定义方式:
    int (*p)(int);//定义了一个指向int func(int n)类型函数地址的指针。
    定义函数指针变量的形式为:函数返回类型(*指针变量名称)(参数列表)
    函数可以通过函数指针调用

    int(* p)()代表指向一个函数,但不是固定哪一个函数Void *p(int ,char *);//声明了一个函数,函数的名字叫p,函数的返回值是void *,函数的参数是int和char *Void (*p)(int ,char *);//定义了一个指向参数为int和char *,返回值为void的函数指针Int *(*p)(int *);//定义一个参数为int *,返回值为int *的指向函数的指针Int *p(int *);//声明了一个函数,返回值是int *,参数是int *Int (*p)(int , int );//定义了一个指向函数的指针,可以指向两个参数,都是int,返回值也是int类型在回调函数和运行期动态绑定的时候大量的用到了指向函数的指针。
    10.1.21 把指向函数的指针作为函数的参数将函数指针作为另一个函数的参数称为回调函数。
    int max(int a,int b){ if(a > b) return a; else return b;}int add(int a,int b){ return a + b;}int func(int (*p)(int,int),int a,int b)//第一个参数是指向函数的指针{ return p(a,b);//通过指向函数的指针调用一个函数}int main(){ int i = func(add,6,9);//add函数在这里就叫回调函数 printf("i = %d\n",i); return 0;}10.1.22 memset,memcpy,memmove函数这三个函数分别实现内存设置,内存拷贝,内存移动使用memcpy的时候,一定要确保内存没有重叠区域。
    memset(buf, 0, sizeof(buf));//第一个参数是要设置的内存地址,第二个参数是要设置的值,第三个参数是内存大小,单位:字节memcpy(buf2, buf1, sizeof(buf1));//将buf1的内存内容全部拷贝到buf2,拷贝大小为第三个参数:字节memmove(buf2, buf1, sizeof(buf1));//并没有改变原始内存的值
    10.1.23 指针小结定义 说明Int I 定义整形变量Int *p 定义一个指向int的指针变量Int a[10] 定义一个int数组Int *p[10] 定义一个指针数组,其中每个数组元素指向一个int型变量的地址Int (*p)[10] 定义一个数组指针,指向int[10]类型的指针变量Int func() 定义一个函数,返回值为int型Int *func() 定义一个函数,返回值为int *型Int (*p)() 定义一个指向函数的指针,函数的原型为无参数,返回值为intInt **p 定义一个指向int的指针的指针,二级指针11 字符指针与字符串11.1 指针和字符串在C语言中,大多数字符串操作其实就是指针操作。
    Char s[] = “hello world”;Char *p = s;P[0] = ‘a’;11.2 通过指针访问字符串数组char buf[100] = "hello world";char *p = buf;//*(p + 5) = 'a';//p[5] = 'b';p += 5;*p = 'c';p[3] = ' ';printf("buf = %s\n",buf);11.3 函数的参数为char *void print_array(int *p,int n)//如果参数是一个int数组,那么就必须传递第二个参数用来标示数组的长度{ int i; for(i = 0; i <n; i++) { printf("p[%d] = %d\n", i, p[i]); }}void print_str(char *s)//如果参数是个字符串,那么就不需要包含第二个参数//因为字符串是明确的以‘\0’结尾的,所以在函数内部是有条件来作为循环终止依据的{ int i = 0; while(s[i]) { printf("%c",s[i++]); }}11.4 指针数组作为main函数的形参Int main(int argc, char *argv[]);main函数是操作系统调用的,所以main函数的参数也是操作系统在调用时候自动填写的argc代表命令行参数的数量argv代表命令行的具体参数,是char *类型的
    12 内存管理12.1 作用域一个C语言变量的作用域可以是代码块作用域,函数作用域或者文件作用域。代码块是{}之间的一段代码。出现在{}之外的变量,就是全局变量。
    12.1.1 auto自动变量一般情况下代码块内部定义的变量都是自动变量。当然也可以显示的使用aotu关键字
    12.1.2 register寄存器变量通常变量在内存当中,如果能把变量放到CPU的寄存器里面,代码执行效率会更高register int i;//建议,如果有寄存器空闲,那么这个变量就放到寄存器里面使用对于一个register变量,是不能取地址操作的
    12.1.3 代码块作用域的静态变量静态变量是指内存位置在程序执行期间一直不改变的变量,一个代码块内部的静态变量只能被这个代码块内部访问。static int i = 0;//静态变量,只初始化一次,而且程序运行期间,静态变量一直存在
    12.1.4 代码块作用域外的静态变量代码块之外的静态变量在程序执行期间一直存在,但只能被定义这个变量的文件访问Static int a=0;//一旦全局变量定义static,意思是这个变量只是在定义这个变量的文件内部全局有效
    12.1.5 全局变量全局变量的存储方式和静态变量相同,但可以被多个文件访问
    12.1.6 外部变量与extern关键字extern int i;
    12.1.7 全局函数和静态函数在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态
    12.2 内存四区12.2.1 代码区代码区code,程序被操作系统加载到内存的时候,所有的可执行代码都加载到代码区,也叫代码段,这块内存是不可以运行期间修改的。
    12.2.2 静态区所有的全局变量以及程序中的静态变量都存储到静态区,比较如下两段代码的区别:
    int a=0; int a=0;int main() static int b=0;{ int main() static int b=0; {printf(“%p,%p\n”,&a,&b); printf(“%p,%p\n”,&a,&b);return 0; retrun 0;} }int *getb() //合法的{ static int a=0; return &a;}12.2.3 栈区栈stack是一种先进后出的内存结构,所有的自动变量,函数的形参都是由编译器自动放出栈中,当一个自动变量超出其作用域时,自动从栈中弹出。
    对于自动变量,什么时候入栈,什么时候出栈,是不需要程序控制的,由C语言编译器实现栈不会很大,一般都是以K为单位的
    int *geta()//错误,不能将一个栈变量的地址通过函数的返回值返回{ int a = 0; return &a;}12.2.4 栈溢出当栈空间已满,但还往栈内存压变量,这个就叫栈溢出。对于一个32位操作系统,最大管理4G内存,其中1G是给操作系统自己用的,剩下的3G都是给用户程序的,一个用户程序理论上可以使用3G的内存空间。
    12.2.5 堆区堆heap和栈一样,也是一种在程序运行过程中可以随时修改的内存区域,但没有栈那样先进后出的顺序。堆是一个大容器,它的容量要远远大于栈,但是在C语言中,堆内存空间的申请和释放需要手动通过代码来完成。
    int *geta()//可以通过函数的返回值返回一个堆地址,但记得一定要free{ int *p=malloc(sizeof(int ));//申请了一个堆空间 return p;}Int main(){ Int *getp=geta(); *getp=100; free(getp);}12.3 堆的分配和释放操作系统在管理内存的时候,最小单位不是字节,而是内存页。
    12.3.1 mallocvoid * malloc(size_t _Size);int *p=(int *)malloc(sizeof(int)*10);//在堆中间申请内存,在堆中申请了一个10个int这么大的空间malloc函数在堆中分配参数_Size指定大小的内存,单位:字节,函数返回void *指针。Void getheap(int *p){ P=malloc(sizeof(int) * 10);}//getheap执行完以后,p就消失了,导致它指向的具体堆空间的地址编号也随之消失了Void getheap1(int **p){ *p=malloc(sizeof(int) * 10);}Int main(){ Int *p=NULL; Printf(“p=%p\n”,&p); //getheap(p);//实参没有任何改变 getheap1(&p);//得到了堆内存的地址}12.3.2 freeVoid free(void *p);free(p);//释放通过malloc分配的堆内存free负责在堆中释放malloc分配的内存。参数p为malloc返回的堆中的内存地址。Int *array = malloc(sizeof(int) * i);//在堆当中动态创建一个int数组free(array);12.3.3 callocVoid * calloc(size_t _Count, size_t _Size);Calloc与malloc类似,负责在堆中分配内存。
    第一个参数是所需内存单元数量,第二个参数是每个内存单元的大小(单位:字节),calloc自动将分配的内存置0
    Int *p=(int *)calloc(100,sizeof(int));//分配100个int12.3.4 realloc重新分配用malloc或者calloc函数在堆中分配内存空间的大小。
    Void * realloc(void *p, size_t _NewSize);第一个参数p为之前用malloc或者calloc分配的内存地址,_NewSize为重新分配内存的大小,单位:字节。成功返回新分配的堆内存地址,失败返回NULL;如果参数p等于NULL,那么realloc与malloc功能一致。
    Char *p = malloc(10);//分配空间,但原有数据没做清洁Char *p1 = calloc(10, sizeof(char));//分配空间以后,自动做清洁Char *p2 =realloc(p1,20);//在原有内存基础之上,在堆中间增加连续的内存//如果原有内存没有连续空间可扩展,那么会新分配一个空间,将原有内存copy到新空间,然后释放原有内存。//realloc和malloc,只分配内存,但不打扫Char *p2 = realloc(NULL,5);//等于malloc(5)13 结构体,联合体,枚举与typedef13.1 结构体13.1.1 定义结构体struct和初始化Struct man{ Char name[100]; Int age;};Struct man m = {“tom”,12};Struct man m = {.name = “tom”, .age = 12};#include <string.h>#pragma warning(disable:4996)struct student{ char name[100]; int age; int sex;};//说明了一个结构体的数据成员类型int main(){ struct student st;//定义了一个结构体的变量,名字叫st; st.age = 20; st.sex = 0; strcpy(st.name,"刘德华"); printf("name = %s\n",st.name); printf("age = %d\n",st.age); if(st.sex == 0) { printf("男"); } else { printf("女"); } return 0;}13.1.2 访问结构体成员.操作符
    13.1.3 结构体的内存对齐模式编译器在编译一个结构的时候采用内存对齐模式。
    Struct man{ Char a; Int b; Shor c; Char d; Long long e;};13.1.4 指定结构体元素的位字段定义一个结构体的时候可以指定具体元素的位长;
    Struct test{ char a : 2;//指定元素为2位长,不是2个字节长};13.1.5 结构数组Struct man m[10] = {{“tom”,12},{“marry”,10},{“jack”,9}};Int I;for(i=0; i<5; i++){ Printf(“姓名=%s,年龄=%d\n”,m[i].name,m[i].age);}#include <stdio.h>#include <string.h>#pragma warning(disable:4996)//结构体数组排序struct student{ char name[100]; int age; int score; char classes[100];};void swap(struct student *a,struct student *b){ struct student tmp = *a; *a = *b; *b = tmp;}int main(){ struct student st[5] = {{"chen",12,78,"A"},{"li",10,90,"B"},{"wang",13,59,"C"},{"fei",12,91,"D"},{"bai",9,59,"E"}}; int i; int j; for(i = 0; i < 5; i++) { for(j = 1; j < 5-i; j++) { if(st[j].age < st[j - 1].age) { swap(&st[j],&st[j - 1]); } else if(st[j].age == st[j - 1].age) { if(st[j].score <st[j-1].score) { swap(&st[j],&st[j - 1]); } } } } for(i = 0; i < 5; i++) { printf("姓名=%s,年龄=%d,成绩=%d,班级=%s\n",st[i].name,st[i].age,st[i].score,st[i].classes); } return 0;}13.1.6 嵌套结构一个结构的成员还可以是另一个结构类型
    Struct names{ Char a; Int b;};Struct man{ Struct names name; Int c;};Struct man m = {{“wang”,10},20};13.1.7 结构体的赋值Struct name m = b;
    结构体赋值,其实就是结构体之间内存的拷贝
    13.1.8 指向结构体的指针—>操作符Struct A a;Struct A *p = &a;p->a = 10;13.1.9 指向结构体数组的指针13.1.10 结构中的数组成员和指针成员一个结构中可以有数组成员,也可以有指针成员,如果是指针成员结构体成员在初始化和赋值的时候就需要提前为指针成员分配内存。
    Struct man{ Char name[100]; Int age;};Struct man{ Char *name; Int age;};13.1.11 在堆中创建的结构体如果结构体有指针类型成员,同时结构体在堆中创建,那么释放堆中的结构体之前需要提前释放结构体中的指针成员指向的内存。
    Struct man{ Char *name; Int age;};Struct man *s = malloc(sizeof(struct man) * 2);S[0].name = malloc(10 * sizeof(char));S[1].name = malloc(10 * sizeof(char));13.1.12 将结构作为函数参数将结构作为函数参数将结构指针作为函数参数
    Struct student{ Char name[10]; Int age;};Void print_student (struct student s)//一般来讲,不要把结构变量作为函数的参数传递,因为效率比较低,一般用指针来代替//void print_student(const struct student *s){ Printf(“name = %s, age = %d\n”,s.name,s.age);}Int main(){ Struct student st = {“tom”,20}; Print_student(st);}13.1.13 结构,还是指向结构的指针在定义一个和结构有关的函数,到底是使用结构,还是结构的指针?指针作为参数,只需要传递一个地址,所以代码效率高。
    Void set_student (struct student *s, const char *name, int age){ Strcpy(s->name, name); s->age = age;}Int main(){ Set_student(&st, “maik”,100); Print_student(st);}结论:当一个结构作为函数的参数时候,尽量使用指针,而不是使用结构变量,这样代码效率很高。
    13.2 联合体联合union是一个能同一个存储空间存储不同类型数据的类型。联合体所占的内存长度等于其最长成员的长度,也有叫做共用体。联合体虽然可以有多个成员,但同一时间只能存放其中一种。
    union A{ Int a; Char b; Char *c; //注意};Int main(){ Union A a; a.a = 10;a.c = malloc(100);//c指向了一个堆的地址 free(c);//如果联合体中有指针成员,那么一定要使用完这个指针,并且free指针之后才能使用其他成员 a.b = 20;}13.3 枚举类型13.3.1 枚举定义可以使用枚举声明代表整数常量的符号名称,关键字enum创建一个新的枚举类型。实际上,enum常量是int类型的。枚举是常量,值是不能修改的。
    enum spectrum{ red,yellow,green,blue,white,black};enum spectrum color;color = black;if(color != red)13.3.2 默认值默认时,枚举列表中的常量被指定为0,1,2等
    enum spectrum{ red,yellow,green,blue,white,black};printf(“%d,%d\n”,red,black);指定值可以指定枚举中具体元素的值
    enum spectrum{ red=1,yellow=2,green=3,blue,white,black};13.4 typedeftypedef是一种高级数据特性,它能使某一类型创建自己的名字。
    typedef unsigned char UBYTE;typedef char BYTE;
    与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
    typedef是编译器处理的,而不是预编译指令;
    typedef比#define更灵活

    直接看typedef好像没什么用处,使用BYTE定义一个unsigned char。使用typedef可以增加程序的可移植性。
    typedef struct{ Int a;}A2;A2 a;13.5 通过typedef定义函数指针typedef const char *(*SUBSTR)(const char *,const char *);const char *getsubstr(const char *src, const char *str){ return strstr(src,str);}Const char *(*p[3])(const char *,const char *);14 文件操作14.1 fopenChar s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件fputs(“hello world”,p);//向文件写入一个字符串//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));//fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针printf(“%s”,s);}fclose(p);//关闭这个文件r以只读方式打开文件,该文件必须存在r+ 以可读写方式打开文件,该文件必须存在rb+ 读写打开一个二进制文件,允许读写数据,文件必须存在rw+ 读写打开一个文本文件,允许读和写w 打开只写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;w+ 打开可读写文件,若文件存在则文件长度清0,即该文件内容会消失。若文件不存在则建立该文件;a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)a+ 以附加的方式打开可读写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(原来的EOF符不保留)。
    Void code(char *s){ While(*s)//遍历一个字符串 { (*s)++; s++;}}Int main()//文件加密{ Char s[1024] = {0};FILE *p = fopen(“D:\\temp\\a.txt”,”r”);//用读的方式打开一个文件FILE *p1 = fopen(“D:\\temp\\a.txt”,”w”);//用写的方式打开一个文件//feof(p);//如果已经到了文件结尾,函数返回真While(!feof(p))//如果没有到文件结尾,那么就一直循环{ memset(s,0,sizeof(s));fgets(s,sizeof(s),p);//第一个参数是一个内存地址,第二个参数是这块内存的大小,第三个参数是fopen返回的文件指针code(s);//文件加密fputs(s,p1);}fclose(p);//关闭这个文件fclose(p1);}14.2 二进制和文本模式的区别
    在windows系统中,文本模式下,文件以“\r\n”代表换行。若以文本模式打开文件,并用fputs等函数写入换行符”\n”时,函数会自动在”\n”前面加上“\r”。即实际写入文本的是”\r\n”。
    在类Unix/Linux系统中文版模式下,文件以”\n”代表换行。所以Linux系统中在文本模式和二进制模式下并无区别。

    14.3 fclosefclose关闭fopen打开的文件
    14.4 getc和putc函数Int main() int main(){ { FILE *fp=fopen(“a.txt”,”r”); FTLE *fp=fopen(“a.txt”,”w”); Char c; const char *s=”hello world”; While((c=getc(fp))!=EOF) int I; { for(i=0;i<strlen(s);i++) Printf(“%c”,c); {} putc(s[i],fp);fclose(fp); }return 0; fclose(fp);} return 0; }14.5 EOF与feof函数文件结尾程序怎么才能知道是否已经到达文件结尾了呢?EOF代表文件结尾如果已经是文件尾,feof函数返回true。
    While((c=getc(p))!=EOF)//EOF代表文件最后的一个结束标识{ //c=getc(p);//一次只读取一个字符 Printf(“%c”,c);}##14.6 fprintf,fscanf,fgets,fputs函数这些函数都是通过FILE *来对文件进行读写。都是针对文本文件的行读写函数fprintf(p,”%s”,buf);//和printf功能一样,fprintf将输入的内容输入到文件里面fscanf(p,”%s”,buf); //fscanf与scanf用法基本一致,fscanf是从一个文件读取输入,scanf是从键盘读取输入#include <stdio.h>#include <stdlib.h>#include <string.h>//计算文本中的字符串int calc_string(const char *s){ char buf1[100] = {0}; char oper1 = 0; char buf2[100] = {0}; int len = strlen(s);//得到字符串的长度 int i; int start; for(i=0;i<len;i++) { if(s[i] == '+'|| s[i] == '-' || s[i] == '*' || s[i] == '/') { strncpy(buf1, s, i); oper1 = s[i]; break; } } start = i + 1; for(;i<len;i++) { if(s[i] == '=') { strncpy(buf2,&s[start],i-start); } } printf("buf1 = %s,oper1 = %c,buf2 = %s\n",buf1,oper1,buf2); switch(oper1) { case '+': return atoi(buf1) + atoi(buf2); case '-': return atoi(buf1) - atoi(buf2); case '*': return atoi(buf1) * atoi(buf2); case '/': { int a = atoi(buf2); if(a) return atoi(buf1) / atoi(buf2); else return 0; } }}void cutereturn(char *s)//把字符串最后的回车字符吃掉{ int len = strlen(s); if(s[len - 1] == '\n') s[len - 1] = 0;}int main(){ FILE *p = fopen("D:\\main\\a.txt","r");//32+56=88 FILE *p1 = fopen("D:\\main\\b.txt","w"); char buf[1024]; char buf1[1024]; int value; while(!feof(p)) { memset(buf,0,sizeof(buf)); fgets(buf,sizeof(buf),p);//从文件中读取一行记录,字符串最后是以'\n'结尾的 cutereturn(buf);//吸收回车 value = calc_string(buf); memset(buf1,0,sizeof(buf1)); sprintf(buf1,"%s%d\n",buf,value);//将buf和buf计算结果重新组织成一个字符串 fputs(buf1,p1);//将重新组合后的字符串写入新的文件 } fclose(p); fclose(p1); return 0;}14.7 stat函数#include <sys/stat.h>int stat(const char *_Filename, struct stat * _Stat);stat.st_size;//文件大小,单位:字节函数的第一个参数代表文件名,第二个参数是struct stat结构。得到文件的属性,包括文件建立时间,文件大小等信息。Struct stat st = {0};//定义一个结构,名字叫stStat(“D:\\temp\\a.txt”,&st);//调用完stat函数之后,文件相关的信息就保存在了st结构中了14.8 fread和fwrite函数size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);size_t fwrite(const void *ptr, size_t size, size_t nmemb,FILE *stream);注意:这个函数以二进制形式对文件进行操作,不局限于文本文件,可进行二进制的读写和拷贝返回值:返回实际写入的数据块数目size_t res=fread(buf,sizeof(char),sizeof(buf),p);//第一个参数是缓冲区,第二个参数是读取的时候最小单位大小,第三个参数是一次读几个单位,第四个参数是打开的文件指针,fread返回值代表读取了多少记录数fwrite(buf,sizeof(char),res,p1);//从源文件中读取多少字节,那么就往目标文件中写多少字节14.9 fread与feofwhile(!feof(p))//如果没有到达文件结尾,那么循环继续{ memset(buf,0,sizeof(buf));//每次读取文件一行之前都把这个buf清空 fgets(buf,sizeof(buf),p);//从文件中读取一行 fread(&buf, 1, sizeof(buf), p);}14.10 通过fwrite将结构保存到二进制文件中做一个代码例子
    14.11 fseek函数int fseek(FILE *stream, long offset, int whence);函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere为基准,偏移offset(指针偏移量)个字节的位置,函数返回0。如果执行失败则不改变stream指向的位置,函数返回一个非0值。实验得出,超出文件末尾位置,还是返回0。往回偏移超出首位置,还是返回0,请小心使用。第一个参数stream为文件指针;第二个参数offest为偏移量,正数表示正向偏移,负数表示负向偏移;第三个参数whence设定从文件的哪里开始偏移,可能取值为:SEEK_CUR、SEEK_END或SEEK_SET。SEEK_SET:文件开头SEEK_CUR:当前位置SEEK_END:文件结尾fseek(fp, 3, SEEK_SET);
    14.12 ftell函数long ftell(FILE *stream);函数ftell用于得到文件位置指针当前位置相对于文件首的偏移字节数。在随机方式存取文件时,由于文件位置频繁的前后移动,程序不容易确定文件的当前位置。
    long len = ftell(fp);14.13 fflush函数int fflush(FILE *stream);fflush函数可以将缓冲区中任何未写入的数据写入文件中。成功返回0,失败返回EOF。每当程序通过C语言库函数往文件里面写数据,C语言库函数并不是实时的将数据直接写入磁盘,而是放到内存里面,当内存满了或者明确的调用了fclose,才将数据一次性写入磁盘。结合:C语言所有的文件操作函数都是缓冲区函数。
    fflush(p);//fflush将缓冲区的内容立刻写入文件//优势:不会因为停电,或者电脑死机等故障导致缓冲区的内容丢失;//不好:硬盘读写次数增加,导致程序效率低下,同时硬盘寿命变短。修改配置文件的时候,有时候会使用,或者做一些不经常修改的数据,但很重要数据,那么用fflush。
    14.14 remove函数int remove(const char *_Filename);remove函数删除指定文件参数Filename为指定的要删除的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    14.15 rename函数int rename(const char *_OldFilename, const char *_NewFilename);rename函数将指定文件改名参数OldFilename为指定的要修改的文件名,NewFilename为修改后的文件名,如果是windows下文件名与路径可以用反斜杠\分隔,也可以用斜杠/分隔。
    //程序还没有退出的时候,是不能同时打开很多文件的一个程序同时可以打开的文件数是有限的尽量在一个程序中不要同时打开太多的文件,如果确实要操作很多文件,也是一个操作完毕fclose以后,再去操作下一个文件。
    15 基础数据结构与算法15.1 什么是数据结构数据(data)是对客观事物符号表示,在计算机中是指所有能输入的计算机并被计算机程序处理的数据总称。数据元素(data element)是数据的基本单位,在计算机中通常作为一个整体进行处理。数据对象(data object)是性质相同的数据元素的集合,是数据的一个子集。数据结构(data structure)是相互之间存在一种或多种特定关系的数据元素的集合。数据类型(data type)是和数据结构密切关系的一个概念,在计算机语言中,每个变量、常量或者表达式都有一个所属的数据类型。抽象数据类型(abstract data type ADT)是指一个数据模型以及定义在该模型上的一组操作,抽象数据类型的定义仅取决于它的一组逻辑性,与其在计算机内部如何表示以及实现无关。
    15.2 什么是算法算法是对特定问题求解的一种描述,它是指令的有限序列,其每一条指令表示一个或多个操作,算法还有以下特性:

    有穷性一个算法必须总是在执行有限步骤后的结果,而且每一步都可以在有限时间内完成。
    确定性算法中每一个指令都有确切的含义,读者理解时不会产生二义性,在任何条件下,算法只有唯一的一条执行路径,即相同的输入只能得出相同的
    可行性一个算法是可行的,即算法中描述的操作都是可以通过已经实现的基本运算来实现的。
    输入一个算法有零个或者多个输入,这些输入取自与某个特定对象的集合。
    输出一个算法一个或多个输出,这些输出是和输入有某些特定关系的量。

    15.3 排序15.3.1 冒泡排序冒泡排序首先将一个记录的关键字和第二个记录的关键字进行比较,如果为逆序(elem[1]>elem[2]),则两个记录交换之,然后比较第二个记录和第三个记录的关键字,以此类推,直到第n-1个记录和第n个记录的关键字进行过比较为止。上述过程称作第一次冒泡排序,其结果是将关键字最大的记录接安排到最后一个记录的位置上,然后进行第二次冒泡排序,对前n-1个记录进行同样操作,其结果是使关键字第二大记录被安置到第n-1位置上,直到将所有记录都完成冒泡排序为止。
    #include <stdio.h>//冒泡排序void swap(int *a,int *b){ int tmp = *a; *a = *b; *b = tmp;}void bubble(int *array, int n){ int i; int j; for(i = 0; i < n; i++) for(j = 1; j < n-i; j++) { if(array[j - 1] > array[j]) { swap(&array[j - 1],&array[j]); } }}void print(int *array, int n){ int i; for(i = 0; i < n; i++) { printf("%d\n",array[i]); }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; bubble(array,10); print(array,10); return 0;}15.3.2 选择排序选择排序是每一次在n-1+1(i=1,2,3,…n)个记录中选取关键字,最小的记录作为有序序列中第i个记录。通过n-1次关键字间的比较,从n-i+1个记录中选取出关键字最小的记录,并和第i(1<=i<=n)个记录交换之。
    int minkey(int *array, int low, int high)//查找指定范围内的最小值//第一个参数是一个数组,第二个参数是数组的开始下标,第三个参数是数组的终止下标//返回值是最小元素的下标{ int min = low; int key = array[low];//在没有查找最小元素之前,第一个元素是最小的 int i; for(i = low + 1; i < high; i++) { if(key > array[i]) { key = array[i]; min = i; } } return min;}void select(int *array, int n)//选择排序法{ int i; for(i = 0; i < n; i++) { int j = minkey(array, i, n); if(i != j)//范围内的第一个成员不是最小的 { swap(&array[i],&array[j]); } }}int main(void){ int array[10] = {32,45,8,78,21,89,4,15,23,56}; select(array,10); return 0;}15.4 查找15.4.1 顺序查找顺序查找的过程为:从表的最后一个记录开始,逐个进行记录的关键字和给定值比较,如果某个记录的关键字与给定值相等,则查找成功,反之则表明表中没有所查找记录,查找失败。
    int seq(int *array, int low, int high, int key)//顺序查找//在指定范围内寻找和key相同的值,找到返回下标,找不到返回—1{ int i; for(i = low; i < high; i++) { if(array[i] == key) return i; } return -1;}15.4.2 二分查找在一个已经排序的顺序表中查找,可以使用二分查找来实现。二分查找的过程是:先确定待查记录所在的范围(区间),然后逐步缩小查找范围,直到找到或者找不到该记录为止。假设指针low和high分别指示待查找的范围下届和上届,指针mid指示区间的中间值,即mid=(low + high) / 2。
    int bin(int *array, int low, int high, int key)//二分查找{ while(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//如果要找的数大于array[mid],那么就在下半部分继续切刀 low = mid + 1; else//如果要找的数小于array[mid],那么就在上半部分继续切刀 high = mid - 1; } return -1;//没有找到数据}int bin_rec(int *array, int low, int high, int key)//递归法实现二分查找{ if(low <= high) { int mid = (low + high) / 2; if(key == array[mid])//中间切一刀,正好和要查找的数相等 return mid; else if(key > array[mid])//下半部分继续查找 return bin_rec(array,mid + 1,high,key); else return bin_rec(array,low,mid - 1,key);//在上半部分查找 }else return -1;//没有找到数据}15.5 链表15.5.1 单向链表定义对于数组,逻辑关系上相邻的两个元素的物理位置也是相邻的,这种结构的优点是可以随机存储任意位置的元素,但缺点是如果从数组中间删除或插入元素时候,需要大量移动元素,效率不高。
    链式存储结构的特点,元素的存储单元可以是连续的,也可以是不连续的,因此为了表示每个元素a,与其接后的元素a+1之间的关系,对于元素a,除了存储其本身信息外,还需要存储一个指示其接后元素的位置。这两部分数据成为结点(node)。一个结点中存储的数据元素被称为数据域。存储接后存储位置的域叫做指针域。N个结点(ai(1<=i<=n))的存储映像链接成一个链表。
    整个链表必须从头结点开始进行,头结点的指针指向下一个结点的位置,最后一个结点的指针指向NULL;在链表中,通过指向接后结点位置的指针实现将链表中每个结点“链”到一起。链表中第一个结点称之为头结点。N个结点(ai的存储映像)链结成一个链表,即为线性表(a1,a2,…,an)的链式存储结构,因为此链表的每个结点中只包含一个指针域,所以叫做单链表。单链表正是通过每个结点的指针域将线性表的数据元素按其逻辑次序链接在一起。如图所示:
    15.5.2 单向链表数据结构定义struct list{ int data;//数据域 struct list *next;//指针域};15.5.3 单向链表的实现#include <stdio.h>#include <stdlib.h>struct list{ int data;//数据域 struct list *next;//指针域};struct list *create_list()//建立一个节点{ return calloc(sizeof(struct list),1);}struct list *insert_list(struct list *ls, int n, int data)//在指定位置插入元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return NULL;//n的位置大于链表节点数 } struct list *node = create_list();//新建立一个节点 node->data = data; node->next = p->next; p->next = node; return node;}int delete_list(struct list *ls, int n)//删除指定位置元素{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) { return -1;//n的位置不合适 } struct list *tmp = p->next; p->next = p->next->next; free(tmp); return 0;//删除成功}int count_list(struct list *ls)//返回链表元素个数{ struct list *p = ls; int count = 0; while(p) { count ++; p = p->next; } return count;}void clear_list(struct list *ls)//清空链表,只保留首节点{ struct list *p = ls->next; while(p) { struct list *tmp = p->next; free(p); p = tmp; } ls->next = NULL;//只有首节点,那么首节点的next也应该设置为NULL}int empty_list(struct list *ls)//返回链表是否为空{ if(ls->next) return 0; else return -1;}struct list *locale_list(struct list *ls, int n)//返回链表指定位置的节点{ struct list *p = ls; while(p && n--) { p = p->next; } if(p == NULL) return NULL; return p;}struct list *elem_locale(struct list *ls,int data)//返回数据域等于data的节点{ struct list *p = ls; while(p) { if(p->data == data) return p; p = p->next; } return NULL;//没有找到数据域等于data的节点}int elem_pos(struct list *ls, int data)//返回数据域等于data的节点位置{ int index = 0; struct list *p = ls; while(p) { index++; if(p->data == data) return index; p = p->next; } return -1;//没有找到数据域等于data的节点}struct list *last_list(struct list *ls)//得到链表最后一个节点{ struct list *p = ls; while(p->next) { p = p->next; } return p;}void merge_list(struct list *ls1,struct list *ls2)//合并两个链表,结果放入ls1中{ //只合并链表的节点,不合并链条头 last_list(ls1)->next = ls2->next; free(ls2);//链表头不要了}void reverse(struct list *ls)//链表逆置{ if (ls->next == NULL) return;//只有一个首节点,不需要逆置 if (ls->next->next == NULL) return;//也不需要逆置 struct list *last = ls->next;//逆置后ls->next就成了最后一个节点了 struct list *pre = ls;//上一个节点的指针 struct list *cur = ls->next;//当前节点的指针 struct list *next = NULL;//下一个节点的指针 while(cur) { next = cur->next; cur->next = pre; pre = cur; cur = next; } ls->next = pre; last->next = NULL;}void traverse(struct list *ls)//循环遍历链表{ struct list *p = ls; while(p) { printf("%d\n",p->data); p = p->next;//p指向他对应的下一个节点 }}int main(void){ struct list *first = create_list();//在堆中间创建一个节点 struct list *second = create_list();//在堆中间创建一个节点 struct list *third = create_list();//在堆中间创建一个节点 first->next = second; second->next = third; third->next = NULL;//对于链表的最后一个节点,next域一定为NULL first->data = 1; second->data = 2; third->data = 3; insert_list(first, 1, 10); insert_list(first, 1, 20); //delete_list(first, 2); //clear_list(first); traverse(first); printf("--------------\n"); printf("count = %d\n", count_list(first)); printf("%d\n", locale_list(first,3)->data); printf("data = %d\n",last_list(first)->data); printf("--------------\n"); struct list *first1 = create_list(); int i; for(i = 0; i < 10; i++) { insert_list(first1, 0, i); } merge_list(first,first1); printf("--------------\n"); traverse(first); printf("--------------\n"); reverse(first); traverse(first); return 0;}逆置操作

    判断首节点的next是否为NULL;
    判断第二个节点的next是否为NULL;
    逆置后ls->next就成了最后一个节点了
    最后一个节点的next指向NULL。
    1 留言 2019-07-28 16:11:46 奖励12点积分
  • windows下静态使用QxOrm框架并使用泛型编程 (三)

    这篇讲如何整合所有的表并且数据库增加字段升级,首先我们需要一张可以记录版本号的表
    VersionObject 这个类放置在QxObject里 因为字段不会增加所以我们只需要VersionHandler类即可 不需要映射类
    代码如下:VersionObject .h
    #ifndef VERSIONOBJECT_H#define VERSIONOBJECT_H#include "common.h"#include <QxOrm.h>#include <QString>class VersionObject{public: VersionObject(); void init();public: QString name; //long name; long version;};QX_REGISTER_PRIMARY_KEY(VersionObject, QString) //主键不是整数类型的时候使用QX_REGISTER_HPP_IMPORT_DLL(VersionObject, qx::trait::no_base_class_defined, DATABASE_VERSION)#endif // VERSIONOBJECT_HVersionObject .cpp
    #include "VersionObject.h"QX_REGISTER_CPP_IMPORT_DLL(VersionObject)namespace qx{template <> void register_class(QxClass<VersionObject> & t){ t.id(& VersionObject::name, "name"); t.data(& VersionObject::version, "version");}}VersionObject::VersionObject(){}void VersionObject::init(){ this->name= "qxorm"; this->version=DATABASE_VERSION;}VersionHandler.h
    #ifndef VERSIONHANDLER_H#define VERSIONHANDLER_H#include <QxOrm.h>#include "IHandler.h"#include "SQLModule/QxObject/VersionObject.h"namespace VERSION{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="VERSION_CONNECTED";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/version.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}using namespace VERSION;class VersionObject;typedef QSharedPointer<VersionObject> Shared_Version;typedef QList<VersionObject> List_Version; //User类数组typedef qx::QxCollection<int,VersionObject> Collection_Version; //User容器class VersionHandler:public IHandler<Shared_Version,Collection_Version,VersionObject>,public ISqlInterface{public: VersionHandler(); virtual ~VersionHandler();protected: virtual void initSqlconnect(); virtual bool createTable(); virtual void disconnect();public: bool insert(Shared_Version &t); bool update(Shared_Version &t); bool select(Shared_Version &t);private: QSqlDatabase m_SqlDatabase; QMutex m_Mutex;};#endif // VERSIONHANDLER_HVersionHandler.cpp
    #include "VersionHandler.h"VersionHandler::VersionHandler(){ initSqlconnect(); if(createTable()) Shared_Version ersion(new VersionObject()); version->init(); this->insert(version); }}VersionHandler::~VersionHandler(){ disconnect();}void VersionHandler::initSqlconnect(){ QMutexLocker locker(&m_Mutex); if(QSqlDatabase::contains(CONNECT_NAME)) m_SqlDatabase = QSqlDatabase::database(CONNECT_NAME); else m_SqlDatabase= QSqlDatabase::addDatabase(DATABASE_TYPE,CONNECT_NAME); m_SqlDatabase.setDatabaseName(DATABASENAME); m_SqlDatabase.setHostName(HOSTNAME); m_SqlDatabase.setUserName(USERNAME); m_SqlDatabase.setPassword(PASSWORD); m_SqlDatabase.open();}bool VersionHandler::createTable(){ return IHandler<Shared_Version,Collection_Version,VersionObject>::createTable(m_SqlDatabase);}void VersionHandler::disconnect(){ QMutexLocker locker(&m_Mutex); if(m_SqlDatabase.isOpen()) m_SqlDatabase.close(); QSqlDatabase::removeDatabase(CONNECT_NAME);}bool VersionHandler::insert(Shared_Version &t){ return IHandler<Shared_Version,Collection_Version,VersionObject>::insert(t,m_Mutex,m_SqlDatabase);}bool VersionHandler::update(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::update(t,m_Mutex,m_SqlDatabase,list);}bool VersionHandler::select(Shared_Version &t){ QStringList list; return IHandler<Shared_Version,Collection_Version,VersionObject>::select(t,m_Mutex,m_SqlDatabase,list);}为了更好的管理整个数据库的所有表 于是我们需要一个单例类来管理所有的Handler 所以我在SQLModule下新建了一个SQLHelper单例类
    代码如下:
    SqlHelper.h
    #ifndef SQLHELPER_H#define SQLHELPER_H#include <QMutex>#include "SQLModule/QxHandler/UserHandler.h"#include "SQLModule/QxHandler/VersionHandler.h"#include <QSqlDatabase>namespace SQLHELPER //用于版本升级的{const QString DATABASE_TYPE="QSQLITE";const QString CONNECT_NAME="UPDATE_ALLTABLE";const QString DATABASENAME="C:/Users/we/Desktop/workTools/demo/qxorm.db";const QString HOSTNAME="localhost";const QString USERNAME="root";const QString PASSWORD="";}class SqlHelper{public: static SqlHelper * getInstance(); bool init() bool isOldVersion(); //查询当前App 的数据库版本是否是以前的 如果是则调用updateDatabas void updateDatabase(); QSharedPointer<UserHandler> getUser();private: SqlHelper(){} SqlHelper(const SqlHelper&); SqlHelper& operator=(const SqlHelper); class CGarbo //单例自动回收 { public: CGarbo(){} ~CGarbo() if (SqlHelper::m_pSqlHelper) { delete SqlHelper::m_pSqlHelper; } } };private: static QMutex m_sMutex; static SqlHelper *m_pSqlHelper; static CGarbo m_sCGarbo;private: QSharedPointer<UserHandler> m_pUser; QSharedPointer<VersionHandler> m_pVersion;};#endif // SQLHELPER_HSqlHelper.cpp
    #include "SqlHelper.h"SqlHelper * SqlHelper ::m_pSqlHelper = nullptr;QMutex SqlHelper::m_sMutex;SqlHelper::CGarbo SqlHelper::m_sCGarbo;SqlHelper *SqlHelper::getInstance(){ SqlHelper* tmp = m_pSqlHelper; if (tmp == nullptr) { QMutexLocker lock(&m_sMutex); tmp = m_pSqlHelper; if (tmp == nullptr) { tmp = new SqlHelper(); m_pSqlHelper = tmp; } } return m_pSqlHelper;}bool SqlHelper::init(){ QSharedPointer<VersionHandler> tem1(new VersionHandler()); m_pVersion=tem1; QSharedPointer<UserHandler> tem2(new UserHandler()); m_pUser=tem2; return true;}bool SqlHelper::isOldVersion({ if(!m_pVersion.isNull()) { Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); qDebug()<<"dbVersion->version"<<dbVersion->version; qDebug()<<" qApp->property()"<<qApp->property("DataBaseVersion").toInt(); qDebug()<<(dbVersion->version < qApp->property("DataBaseVersion").toInt()); return (dbVersion->version < qApp->property("DataBaseVersion").toInt()); } else { return false; }}void SqlHelper::updateDatabase(){ try { int dbversion=qApp->property("DataBaseVersion").toInt(); Shared_Version dbVersion(new VersionObject()); dbVersion->init(); m_pVersion->select(dbVersion); { QSqlDatabase db; if(QSqlDatabase::contains(SQLHELPER::CONNECT_NAME)) db = QSqlDatabase::database(SQLHELPER::CONNECT_NAME); else db= QSqlDatabase::addDatabase(SQLHELPER::DATABASE_TYPE,SQLHELPER::CONNECT_NAME); db.setDatabaseName(SQLHELPER::DATABASENAME); db.setHostName(SQLHELPER::HOSTNAME); db.setUserName(SQLHELPER::USERNAME); db.setPassword(SQLHELPER::PASSWORD); if(!db.isOpen()) { db.open(); } if (dbVersion->version >= dbversion) { if(db.isOpen()) { qDebug("**************** i come here ********************"); db.close(); } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); return; } QSqlQuery query(db); //获取在QxOrm注册的所有持久化类 qx::QxCollection<QString, qx::IxClass *> * pAllClasses = qx::QxClassX::getAllClasses(); if (! pAllClasses) { qAssert(false); return; } //将所有表获取到数据库中 QStringList tables = db.tables(); for (long k = 0; k < pAllClasses->count(); k++) { qx::IxClass * pClass = pAllClasses->getByIndex(k); if (! pClass) { continue; } // 过滤非persitents类 if (pClass->isKindOf("qx::service::IxParameter") || pClass->isKindOf("qx::service::IxService")) { continue; } // 筛选已经更新的类 if (pClass->getVersion() <= dbVersion->version) { continue; } qDebug()<<"****** pAllClasses->name ******"<<pClass->getName() <<pClass->getVersion(); // 如果表不存在,创建它并设置拥有者 if (! tables.contains(pClass->getName())) { qDebug()<<"***** want to creat table pClass->getName ******:"<<pClass->getName() query.exec("CREATE TABLE " + pClass->getName() + " ( ) WITH (OIDS = FALSE);" "ALTER TABLE " + pClass->getName() + " OWNER TO \"root\";"); //session += query.lastError(); } // 如果不存在列,则将其添加到表中 qx::IxDataMemberX * pDataMemberX = pClass->getDataMemberX(); for (long l = 0; (pDataMemberX && (l < pDataMemberX->count_WithDaoStrategy())); l++) { qx::IxDataMember * p = pDataMemberX->get_WithDaoStrategy(l); if (! p || (p->getVersion() <= dbVersion->version)){ continue; } qDebug()<<"***** add alter pClass->getName *****:"<<pClass->getName() <<p->getName()<<p->getSqlType(); query.exec("ALTER TABLE " + pClass->getName() + " ADD COLUMN " + p->getName() + " " + p->getSqlType() + ";"); //session += query.lastError(); if (p->getIsPrimaryKey()) // PRIMARY KEY { query.exec("ALTER TABLE " + pClass->getName() + " ADD PRIMARY KEY (" + p->getName() + ");"); //session += query.lastError(); } if (p->getAllPropertyBagKeys().contains("INDEX")) // INDEX { query.exec("CREATE INDEX " + pClass->getName() + "_" + p->getName() + "_idx" + " ON " + pClass->getName() + " USING " + p->getPropertyBag("INDEX").toString() + " (" + p->getName() + ");"); //session += query.lastError(); } if (p->getNotNull()) // NOT NULL { query.exec("ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " SET NOT NULL;"); //session += query.lastError(); } if (p->getAutoIncrement()) // AUTO INCREMENT { query.exec("CREATE SEQUENCE " + pClass->getName() + "_" + p->getName() + "_seq" + "; " "ALTER TABLE " + pClass->getName() + "_" + p->getName() + "_seq" + " OWNER TO \"root\"; " "ALTER TABLE " + pClass->getName() + " ALTER COLUMN " + p->getName() + " " + "SET DEFAULT nextval('" + pClass->getName() + "_" + p->getName() + "_seq" + "'::regclass);"); //session += query.lastError(); } if (p->getDescription() != "") // DESCRIPTION { query.exec("COMMENT ON COLUMN " + pClass->getName() + "." + p->getName() + " IS $$" + p->getDescription() + "$$ ;"); //session += query.lastError(); } } } //保存数据库的当前版本 dbVersion->version = dbversion; m_pVersion->update(dbVersion); if(db.isOpen()) { db.close(); } } QSqlDatabase::removeDatabase(SQLHELPER::CONNECT_NAME); } catch(const qx::dao::sql_error & err) { QSqlError sqlError = err.get(); qDebug() << sqlError.databaseText(); qDebug() << sqlError.driverText(); qDebug() << sqlError.number(); qDebug() << sqlError.type(); }}QSharedPointer<UserHandler> SqlHelper::getUser(){ return m_pUser;}是否有发现SQLHelper 里面有两个方法 isOldVersion() 和updateDatabase() 这个是用于校验当前app版本和数据库版本的方法,直接照抄就可以了修改官方并且验证过了。
    还有个common.h
    #ifndef COMMON_H#define COMMON_H#include <QString>const QString DATABASE_APP="DataBaseVersion";const int DATABASE_VERSION=0; //数据库版本控制#endif // COMMON_Hmain.cpp
    #include <QApplication>#include "common.h"int main(int argc, char *argv[]){ QApplication a(argc, argv); a.setProperty("DataBaseVersion",DATABASE_VERSION); return a.exec();}github源码:https://github.com/qq2690351079/ontheway 如果没有 就是我还没整理完还没上传
    后续会发出模板类的MVP 框架QT写的 不过改改C++也应该能直接用。
    数据库记得做DB 每次更新DB以后再update。
    1 留言 2019-07-23 22:48:02 奖励5点积分
  • 机器学习 24 、MF ANN

    前文链接:https://write-bug.com/article/2696.html
    MF(Matrix Factorization)基于矩阵分解的推荐算法-隐语义模型:ALS、LFM、SVD、SVD++
    在15年左右流行
    ALS:-交替最小二乘
    我们之前学习的协同过滤CF:单独考虑user、item
    这里同时考虑User-item两方面:
    原来我们的UI稀疏打分矩阵\<m,n>:

    一般公司用户量可以随意达到上亿,物品量如音乐可以达到几十万,用户量之所以多,是因为可能一个用户有多个账户等等,共同组成一个很大很稀疏的矩阵那么面对这样一个矩阵,我们可以通过矩阵分解来解决:
    将User和Item分别映射到新的空间,分解成两个矩阵,U 和I两个维度都不变



    K值远小于M和N,从而达到降维目的
    无需关注新空间的各个维度,只需假定存在 ,即用向量表示user和item
    新的维度称为Latent Factor(隐因子)

    K的维度相比原来来说很小很小,并且可以人为设定,只需要两个矩阵相乘就能得到UI矩阵,即R’ ≈R
    两个矩阵相似如何界定?误差
    误差表示:RMSE :均方根误差
    目标:真实矩阵,和结果矩阵之间的尽可能逼近
    最小化平方误差作为损失函数:

    考虑矩阵稳定性,引入L2正则项,得到损失函数:

    这里的rui就是上面所说的R,即原矩阵user对item打的分数
    xuyi即分解矩阵后再相乘的预估分数,两个相减就是误差
    未知数:
    Xu:user vectoryi:item vector如何求解最优值:求导=0
    公式1:

    导数为0,可得到:

    同理对yi求导(公式2):

    为什么叫交替二乘法?
    这里的做法是让x和y交替下降:

    随机生成X、Y向量(初始化)
    固定Y,更新X(公式1)
    固定X,更新Y(公式2)
    第2、3步循环,直到满足收敛条件(RMSE)

    ——均方根误差
    那么我们得到这两个小矩阵,可以做什么?
    item-item :IK*KI=IIUser-User:UK*KU=UUuser与item的向量
    LFM思路和ALS算法类似,区别在于,ALS利用坐标下降法,LFM利用梯度下降法
    假设:
    评分矩阵𝑅𝑚,𝑛,m个用户对n个物品评分

    𝑟𝑢,𝑖:用户u对物品i的评分
    𝑅𝑚,𝑛 = 𝑃𝑚,𝐹 ∙ 𝑄𝐹,𝑛:R是两个矩阵的乘积
    P:每一行代表一个用户对各隐因子的喜欢程度 ,即前面的user矩阵
    Q:每一列代表一个物品在各个隐因子上的概率分布,即前面的item矩阵


    在之前的随机梯度下降中,我们更新w(t+1)=w(t) - a*g(梯度)
    那么根据这样的思路,这里把矩阵P与Q的分数当作权重w来更新:

    矩阵展开后相当于对每个元素分数进行偏导:

    随意抽取中间这个元素作为代表,求取偏导:

    由于

    所以后面两个等式相等,之后就有一件很有意思的事情发生了:P的分数是由上一时刻的Q更新的,Q的分数是由上一时刻的P更新的。也就是类似上面的坐标下降法:交替进行。
    代入原式:

    LFM实践:
    class LFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F#维度K self.P = dict() self.Q = dict() self.alpha = alpha#学习率 self.lmbd = lmbd self.max_iter = max_iter#迭代轮数 self.rating_data = rating_data#矩阵 '''随机初始化矩阵P和Q''' for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] for item, _ in rates: if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F))if __name__ == '__main__': '''用户有A B C,物品有a b c d,列表模拟矩阵:''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = LFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 预测计算用户A对各个物品的喜好程度SVDLFM有什么缺点?没有考虑客观的偏置,所以带偏置的LFM称为SVD
    什么是偏置,比如说有的人很极端给一些物品很高或者很低的分数,而有的人给每个物品都很平均的分数,还有包括地区等等都会影响对物品的看法,所以就有一个偏置存在。
    偏置:事件固有的,不受外界影响的属性。

    𝜇:训练集中所有评分的平均值
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    更新:

    SVD实践:
    class BiasLFM(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P和Q''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0#初始化bu cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0#初始化bi self.mu /= cnt#计算μ def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: for item, rui in rates: hat_rui = self.predict(user, item) err_ui = rui - hat_rui #更新两个b self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * (err_ui * self.P[user][f] - self.lmbd * self.Q[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item): '''预测用户user对物品item的评分,加偏置 ''' return sum(self.P[user][f] * self.Q[item][f] for f in xrange(self.F)) + self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = BiasLFM(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item)) # 计算用户A对各个物品的喜好程度SVD++任何用户只要对物品i有过评分,无论评分多少,已经在一定程度上反映了用户对各个隐因子的喜好 程度𝑦𝑖 = (𝑦𝑖1,𝑦𝑖2,…,𝑦𝑖𝐹),y是物品携带的属性,什么意思?比如说A买了3个item,B买了3个item,每个item背后有一系列feature vector,那么我们用A买的3个item背后的fea向量相加(实际计算是学习更新出来的,不一定是相加)代表一个虚拟的物品A,间接表达了这个用户的偏好程度,同理得到向量B,那么对于这个每个人背后的虚拟物品向量,就是y

    所以这个打分是在P上又增加了一个类似于偏置的东西,并做了归一化

    𝑁(𝑢):用户u评价过的物品集合
    𝑏𝑢:用户偏置,代表一个用户评分的平均值
    𝑏𝑖:物品偏置,代表一个物品被评分的平均值


    SVD++实践:
    class SVDPP(object): def __init__(self, rating_data, F, alpha=0.1, lmbd=0.1, max_iter=500): '''rating_data是list<(user,list<(position,rate)>)>类型 ''' self.F = F self.P = dict() self.Q = dict() self.Y = dict() self.bu = dict() self.bi = dict() self.alpha = alpha self.lmbd = lmbd self.max_iter = max_iter self.rating_data = rating_data self.mu = 0.0 '''随机初始化矩阵P、Q、Y''' cnt = 0 for user, rates in self.rating_data: self.P[user] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bu[user] = 0 cnt += len(rates) for item, rate in rates: self.mu += rate if item not in self.Q: self.Q[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] if item not in self.Y:#比之前svd多加了一个y,初始化y self.Y[item] = [random.random() / math.sqrt(self.F) for x in xrange(self.F)] self.bi[item] = 0 self.mu /= cnt def train(self): '''随机梯度下降法训练参数P和Q ''' for step in xrange(self.max_iter): for user, rates in self.rating_data: z = [0.0 for f in xrange(self.F)] for item, _ in rates: for f in xrange(self.F): z[f] += self.Y[item][f]#用户观看过物品的评分集合加和,即虚拟物品向量,即∑Yjf ru = 1.0 / math.sqrt(1.0 * len(rates)) s = [0.0 for f in xrange(self.F)] for item, rui in rates: hat_rui = self.predict(user, item, rates) err_ui = rui - hat_rui self.bu[user] += self.alpha * (err_ui - self.lmbd * self.bu[user]) self.bi[item] += self.alpha * (err_ui - self.lmbd * self.bi[item]) for f in xrange(self.F): s[f] += self.Q[item][f] * err_ui#每个物品的信息和误差相乘的累加 self.P[user][f] += self.alpha * (err_ui * self.Q[item][f] - self.lmbd * self.P[user][f]) self.Q[item][f] += self.alpha * ( err_ui * (self.P[user][f] + z[f] * ru) - self.lmbd * self.Q[item][f]) for item, _ in rates: for f in xrange(self.F): #Y的更新 self.Y[item][f] += self.alpha * (s[f] * ru - self.lmbd * self.Y[item][f]) self.alpha *= 0.9 # 每次迭代步长要逐步缩小 def predict(self, user, item, ratedItems): '''预测用户user对物品item的评分 ''' z = [0.0 for f in xrange(self.F)] for ri, _ in ratedItems: for f in xrange(self.F): z[f] += self.Y[ri][f] return sum( (self.P[user][f] + z[f] / math.sqrt(1.0 * len(ratedItems))) * self.Q[item][f] for f in xrange(self.F)) + \ self.bu[user] + self.bi[item] + self.muif __name__ == '__main__': '''用户有A B C,物品有a b c d''' rating_data = list() rate_A = [('a', 1.0), ('b', 1.0)] rating_data.append(('A', rate_A)) rate_B = [('b', 1.0), ('c', 1.0)] rating_data.append(('B', rate_B)) rate_C = [('c', 1.0), ('d', 1.0)] rating_data.append(('C', rate_C)) lfm = SVDPP(rating_data, 2) lfm.train() for item in ['a', 'b', 'c', 'd']: print(item, lfm.predict('A', item, rate_A)) # 计算用户A对各个物品的喜好程度ANNANN 多维空间检索算法,不完全是算法,是更偏工程的一种方法,时下正在流行的简单方式,从图像检索演化而来,复杂一点的方式一般使用DNN可以达到目的每个用户、物品背后都有自己的向量映射在多为空间的点上,我们的目标就是把这些向量全部映射在一个空间内,求user最近的item点
    稀疏场景适用物品召回:cb倒排(token)、cf倒排(user)
    —召回能力有限
    鲜花和巧克力在情人节的情况下可以关联起来,但是通过cb不能召回,通过cf需要很多很多用户共点击或者共现才会关联
    那么这里如何计算和user距离近的点?之前我们使用cos或jaccard,但是我不能把所有的物品都遍历一遍,计算量过大,所以就引出了ANN的近邻检索annoy
    Annoy目标:建立一个数据结构,在较短的时间内找到任何查询点的最近点,在精度允许的条件下通过牺牲准 确率来换取比暴力搜索要快的多的搜索速度
    先圈出一部分集合,遍历集合取top
    如果推荐的结果不理想,不喜欢,不是这个方法的问题,而是embedding方式的问题
    那么这个集合如何圈呢?
    方案:随机选择两个点,然后根据这两个点之间的连线确定一个可以垂直等分线段的超平面,灰色是两点的连 线,黑色是超平面

    从而由里面的点集合就构成了叶子节点,最终形成整棵树
    建立二叉树结构用于表示空间分布,每一个节点表示一个子空间
    假设,如果两个点在空间彼此靠近,任何超平面不会将他们分开,如果我们搜索空间中的任意一点,和其距离 近的点为推荐候选,通过对二叉树的遍历,可以完成
    重复上页步骤,继续分割 ,过程很像Kmeans要求:每个子节点包含数据不超过K个(10):


    如果落在这个空间内的节点候选太少怎么办?或者本来两个点距离很近,但是被两个空间分割导致不能计算,那么我们就拓宽区域、多建几棵树,每次建立长得样子都不太一样。
    annoy只是多维空间检索中的一种方法,其他还有:

    KD-Tree (KNN-开始不随机,直接找到全局最优,速度慢,建树慢)
    局部敏感哈希LSH
    HNSW
    OPQ等等

    ANN实践:
    annoy安装:pip
    import sysfrom annoy import AnnoyIndexa = AnnoyIndex(3)#建立3颗树a.add_item(0, [1, 0, 0])#添加用户和item点a.add_item(1, [0, 1, 0])a.add_item(2, [0, 0, 1])a.build(-1)print(a.get_nns_by_item(0, 100))#与0这个用户最近的top100集合print(a.get_nns_by_vector([1.0, 0.5, 0.5], 100))#与这个item vector 最近的top100集合下面这个代码是什么意思呢?随着我们候选集合圈的缩小,我们的计算量也在缩小,那么我们目标是让这个候选集合的top和全局的top尽量一致,也就是说要在计算量和召回率之间做一个权衡,那么全局的top我们就只能通过暴力遍历来知道答案了。
    from __future__ import print_functionimport random, timefrom annoy import AnnoyIndextry: xrangeexcept NameError: # Python 3 compat xrange = rangen, f = 100000, 40#初始化10w个点,40颗树t = AnnoyIndex(f)for i in xrange(n): v = [] for z in xrange(f): v.append(random.gauss(0, 1))#高斯分布初始化点 t.add_item(i, v)#添加在树里t.build(2 * f)t.save('test.tree')#保存树limits = [10, 100, 1000, 10000]#圈的大小k = 10#10个候选prec_sum = {}prec_n = 100time_sum = {}for i in xrange(prec_n): j = random.randrange(0, n)#随机选一个点作为用户 #print(j) closest = set(t.get_nns_by_item(j, k, n))#求取与这个用户j最近的全局top10 #print(closest) for limit in limits: t0 = time.time() toplist = t.get_nns_by_item(j, k, limit)#圈内的top10 T = time.time() - t0 found = len(closest.intersection(toplist))#用全局与圈内的top取交集 hitrate = 1.0 * found / k#准确率 #print(hitrate) prec_sum[limit] = prec_sum.get(limit, 0.0) + hitrate time_sum[limit] = time_sum.get(limit, 0.0) + T print(prec_sum[limit])for limit in limits: print('limit: %-9d precision: %6.2f%% avg time: %.6fs' % (limit, 100.0 * prec_sum[limit] / (i + 1), time_sum[limit] / (i + 1)))
    1 留言 2019-06-24 12:17:39 奖励15点积分
  • 基于ASP.NET的小清新风格的新闻发布系统

    项目展示
    项目开发背景
    ASP.NET课设
    主要功能:登录注册,新闻管理,评论回复新闻,个人信息管理

    项目运行环境
    vs2017
    Sqlserver2012

    项目运行安装
    项目包地址:https://download.csdn.net/download/qq_35268841/11865696
    项目数据库位置由于大家所用数据库不一定为2012,在此将数据库的Sql文件放出,地址如下:
    链接: https://pan.baidu.com/s/170lyOlqnE2B_Eu30WOlpMA 提取码: km8k 复制这段内容后打开百度网盘手机App,操作更方便哦

    配置数据库连接字符串(不会获取连接字符串的自行百度)https://blog.csdn.net/qq_35268841/article/details/102563070

    运行项目
    浏览器显示网页主页(网页不是响应式布局,若网页显示不整齐,缩放浏览器比例即可)
    1 留言 2019-12-07 10:30:22 奖励16点积分
  • 基于Java的图书购物商城

    一、功能
    登录用户
    管理员登录和用户登录两种
    注册功能
    有购买书籍功能
    查询功能:按书名查询,按ID名查询
    删除书籍
    显示用户的信息

    二、注册和登录模块的设计与实现2.1 注册和登录界面2.1.1 注册用户在进行注册时,需要通过该界面输入帐号、密码和用户身份,然后点击“点我注册”按钮进行注册,注册界面设计如图1.1所示。

    2.1.2 登录用户在进行系统登录时,需要通过该界面输入帐号、密码和用户身份,然后点击“登录”按钮进行登录,登录界面设计如图1.2所示。

    2.2 该模块涉及到的文件和类通过详细的分析,该模块涉及到的文件和类在工程中的组织如图所示。
    2.2.1 持久层用文件users.txt持久存储用户的信息,文件中以u00001:普通用户:pw00001:张三:0:上海 的形式存储,其中u00001为id,普通用户为用户类型,pw00001为用户密码,张三为用户真实姓名,0代表用户性别为男,上海为用户所在城市。所有用户的信息均以这样的格式存储,且每个用户的信息在文件中占一行。
    为了方便,在类DatabaseConfig中使用静态常量描述了文件users.txt的详细路径。
    2.2.3 文件操作层(Dao层)该层涉及到接口IUserDao和实现该接口的类UserDaoImpl,主要用来完成对文件user.txt的读和写操作。
    public Map<String, User> getUsers();
    该方法从文件中读出用户的信息并使用Map集合返回结果集。Map中key为用户id, value为使用User封装的用户信息。
    public void addUser(User u)
    该方法是将封装到User中的用户信息写入文件。
    类UserDaoImpl的核心代码如下:
    public class UserDaoImpl implements IUserDao{ @Override public void addUser(User u) { //使用缓冲流,一个用户信息占一行 File f = new File(DataBaseConfig.USER_FILE_PATH); FileWriter fw = null; BufferedWriter bw = null; try { fw= new FileWriter(f, true); bw = new BufferedWriter(fw); bw.write(u.toString()); bw.newLine(); } catch (IOException e) { … } finally { … } } @Override public Map<String, User> getUsers() { Map<String, User> users = new HashMap<String, User>(); File f = new File(DataBaseConfig.USER_FILE_PATH); FileReader fr = null; BufferedReader br = null; try { fr = new FileReader(f); br = new BufferedReader(fr); String s; String[] userInfo; User u; while((s=br.readLine()) != null){ userInfo = s.split(":"); u = new User(userInfo); users.put(userInfo[0], u); } } catch (FileNotFoundException e) { … } catch (IOException e) { … } finally { … } return users; }}
    详细请查看源代码
    个人博客(后续有时间的话放在博客上面)
    1 留言 2019-12-18 20:27:03 奖励10点积分
  • 基于Crypto++库的RSA非对称加密实现对数据的加解密

    背景写了一个基于Crypto++加密库中RSA非对称加密算法实现的对数据加密和解密的一个小程序,Crypto++加密库就不详细介绍了,这个库提供了很多知名的加解密算法,直接调用就好了,使用起来还是比较方便的。
    写这篇文章,就是分享自己的学习心得。自己的密码学部分的知识学得不怎么好,还好有Crypto++开源库可以使用,弥补了对加解密部分的不足。现在,向大家分享使用Crypto++中的RSA非对称加密算法实现对数据的加密解密方面的知识。
    程序编译设置注意事项首先,先要下载Crypto++库的开源代码,然后,自己编译得到Crypto++的库文件。下载链接还有具体的编译步骤,可以参考这个平台上其他用户写的分享文章“使用VS2013编译Crypto++加密库”,里面有详细介绍。
    在导入Crypto++的库文件到自己的工程项目的时候,要对自己的工程项目进行编译设置。主要一点就是:项目工程的属性中的“运行库”设置,要与编译Crypto++的库文件时设置的“运行库”选项要对应一致,否则程序会编译不过的。也就是要检查LIB库工程和本测试工程的:属性 —> C/C++ —> 代码生成 —> 运行库 是否统一。
    如果编译出错,报告XX重复定义等错误,同样,要检查LIB库工程和本测试工程的:属性 —> C/C++ —> 代码生成 —> 运行库 是否统一。
    程序设计原理1. 产生公钥和私钥文件原理RSA是非对称加密算法,所以它的加密和解密的密钥是不同的,它有自己的公钥和私钥。在正常的使用过程中,公钥一般是用来加密数据,私钥用来解密数据。反之,也可以。公钥可以公开,但是私钥不可以公开。
    对于Crypto++库产生公私钥的原理如下:

    首先用类RandomPool的方法Put()产生种子seed的byte型伪随机
    RSAES_OAEP_SHA_Decryptor是一个解密的公钥密码系统在文件rsa.h 有如下定义:

    typedef RSAES<OAEP<SHA>>::Decryptor RSAES_OAEP_SHA_Decryptor; 就是在这个类用前面产生的伪随机数和密钥长度keyLength生解密的密钥
    接着,通过类FileSink打开文件szPrivateKeyFileName实行序列化操作,用HexEncoder把它转换为十六进制
    最后,用DEREncode把上面处理好的密码对象写入文件

    这样就得到私钥密码的密钥文件了。产生公钥文件的方法和产生私钥密码文件不同的地方就是使用了RSAES_OAEP_SHA_Encryptor是一个加密的公钥密码系统, 在文件rsa.h 有如下定义:
    typedef RSAES<OAEP<SHA>>::Encryptor RSAES_OAEP_SHA_Encryptor; 是用上面产生的密钥密码系统priv来生成相应公钥。
    2. 使用RSA公钥加密数据实现原理RSA公钥加密可以分成以下两种形式:一种是公钥存储在文件中,加密时,从文件获取密钥;另一种是公钥存储在程序中,直接传递存储密钥的地址。虽然,这两种形式只是公钥传递的形式上的区别,RSA加密的原理还是一样的。但,从编程的角度,把这两个进行区分。
    先介绍公钥存储在文件中的方式,那么RSA公钥加密的实现原理是:

    首先用类RandomPool在种子seed下用方法Put()产生伪随机数,Seed可以任取
    用类FileSource对公钥文件pubFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型
    然后用FileSource的对象pubFile 实例化公钥密码系统RSAES_OAEP_SHA_Encryptor生成对象pub
    用类StringSink 把outstr添加到一个String对象,接着用HexEncoder把这个对象转换为十六进制
    然后用伪随机数randPool、公钥密码系统pub和十六进制的String对象实例化一个公钥密码加密的过滤器,再用这个过滤器对字符串message进行加密把结果放到十六进制的字符串result里,这样就完成了对字符串的加密

    那么,对于另一种公钥存储在程序中的方式,RSA公钥加密的原理和上面的区别,只是在第 2 步的区别,也就是用类StringSource对公钥文件pubFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型。
    3. 使用RSA私钥钥解密数据实现原理同样,对应上述的公钥加密,私钥解密同样区分两种私钥获取的形式。
    先介绍私钥存储在文件中的方式,那么RSA私钥解密的实现原理的基本流程跟加密的基本流程差不多,就使用了几个不同的类,但是这些类跟加密函数的对应类的功能是相对的,很容易理解。

    用类FileSource对私钥文件privFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型
    然后用FileSource的对象privFile 实例化公钥密码系统RSAES_OAEP_SHA_Decryptor生成对象priv
    用类StringSink 把outstr添加到一个String对象,接着用HexEncoder把这个对象转换为十六进制
    然后用伪随机数randPool、私钥密码系统prov和十六进制的String对象实例化一个私钥密码解密的过滤器,再用这个过滤器对字符串message进行解密把结果放到十六进制的字符串result里,这样就完成了对字符串的解密

    和加密一样,对于另一种私钥存储在程序中的方式,RSA私钥解密的原理和上面的区别,只是在第 1 步的区别,也就是用类StringSource对公钥文件provFilename进行一定的转换放入临时缓冲区,并把它从十六进制转换为byte型。
    编程实现1. 导入Crypt++库文件//*************************************************// crypt++加密库的头文件和静态库//*************************************************#include "crypt\\include\\rsa.h"#include "crypt\\include\\randpool.h"#include "crypt\\include\\hex.h"#include "crypt\\include\\files.h"using namespace CryptoPP; // 命名空间#ifdef _DEBUG #ifdef _WIN64 #pragma comment(lib, "crypt\\lib\\x64\\debug\\cryptlib.lib") #else #pragma comment(lib, "crypt\\lib\\x86\\debug\\cryptlib.lib") #endif#else #ifdef _WIN64 #pragma comment(lib, "crypt\\lib\\x64\\release\\cryptlib.lib") #else #pragma comment(lib, "crypt\\lib\\x86\\release\\cryptlib.lib") #endif#endif//*************************************************
    2. RSA产生公私钥实现BOOL GenerateRSAKey(DWORD dwRSAKeyLength, char *pszPrivateKeyFileName, char *pszPublicKeyFileName, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); // 生成RSA私钥 RSAES_OAEP_SHA_Decryptor priv(randPool, dwRSAKeyLength); HexEncoder privFile(new FileSink(pszPrivateKeyFileName)); // 打开文件实行序列化操作 priv.DEREncode(privFile); privFile.MessageEnd(); // 生成RSA公钥 RSAES_OAEP_SHA_Encryptor pub(priv); HexEncoder pubFile(new FileSink(pszPublicKeyFileName)); // 打开文件实行序列化操作 pub.DEREncode(pubFile); // 写密码对象pub到文件对象pubFile里 pubFile.MessageEnd(); return TRUE;}
    3. RSA公钥加密实现1> 公钥存储在文件实现string RSA_Encrypt_ByFile(char *pszOriginaString, char *pszPublicKeyFileName, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); FileSource pubFile(pszPublicKeyFileName, TRUE, new HexDecoder); RSAES_OAEP_SHA_Encryptor pub(pubFile); // 加密 string strEncryptString; StringSource(pszOriginaString, TRUE, new PK_EncryptorFilter(randPool, pub, new HexEncoder(new StringSink(strEncryptString)))); return strEncryptString;}
    2> 公钥存储在程序中string RSA_Encrypt_ByMem(char *pszOriginaString, char *pszMemPublicKey, BYTE *pSeed, DWORD dwSeedLength){ RandomPool randPool; randPool.Put(pSeed, dwSeedLength); StringSource pubStr(pszMemPublicKey, TRUE, new HexDecoder); RSAES_OAEP_SHA_Encryptor pub(pubStr); // 加密 string strEncryptString; StringSource(pszOriginaString, TRUE, new PK_EncryptorFilter(randPool, pub, new HexEncoder(new StringSink(strEncryptString)))); return strEncryptString;}
    4. RSA私钥解密实现1> 公钥存储在文件实现string RSA_Decrypt_ByFile(char *pszEncryptString, char *pszPrivateKeyFileName){ FileSource privFile(pszPrivateKeyFileName, TRUE, new HexDecoder); RSAES_OAEP_SHA_Decryptor priv(privFile); string strDecryptString; StringSource(pszEncryptString, TRUE, new HexDecoder(new PK_DecryptorFilter(GlobalRNG(), priv, new StringSink(strDecryptString)))); return strDecryptString;}
    2> 公钥存储在程序中string RSA_Decrypt_ByMem(char *pszEncryptString, char *pszMemPrivateKey){ StringSource privStr(pszMemPrivateKey, TRUE, new HexDecoder); RSAES_OAEP_SHA_Decryptor priv(privStr); string strDecryptString; StringSource(pszEncryptString, TRUE, new HexDecoder(new PK_DecryptorFilter(GlobalRNG(), priv, new StringSink(strDecryptString)))); return strDecryptString;}
    程序测试在main函数中调用上面封装好的函数进行测试,main函数为:
    char g_szPubKey[] = "30819D300D06092A864886F70D010101050003818B0030818702818100F0CE882D7CCB990323A6DB1B775EBE8F2910BFE75B4B580EF8C5089BB25FEDEEABCE2BBD2AC64A138E47F96A6C39152FE98067C0B4F5DC28F8D9394325ADB12A90A9598FF7A2A7211DEF974FC8A005D0CBCDE059FB8F7F9D214C5BAC2532CEB8EC4041AEAB19E80B8C4020F4A50102F9E738647E2384EA2FCD30C3681559CF6F020111";char g_szPrivKey[] = "30820275020100300D06092A864886F70D01010105000482025F3082025B02010002818100F0CE882D7CCB990323A6DB1B775EBE8F2910BFE75B4B580EF8C5089BB25FEDEEABCE2BBD2AC64A138E47F96A6C39152FE98067C0B4F5DC28F8D9394325ADB12A90A9598FF7A2A7211DEF974FC8A005D0CBCDE059FB8F7F9D214C5BAC2532CEB8EC4041AEAB19E80B8C4020F4A50102F9E738647E2384EA2FCD30C3681559CF6F020111028180210D49E8203005F15F3F0F03C5170B18AB4892CF70EC39434F52426FB91C39C162E0100AE7C0DCFDAA1DF50E9B67351AA7942251AA68051EB8BE7145739A599220030CF5E35ED4DEA41DD6E955722AE46153339FE7417BD00ADF53B368EAB6E71FAE0F7F394A34C91612B0F11AEC5525DB84DD982E6BF10CE74F177FA51ADC51024100F80296900AF134CCC5AC12C58D741C735F5EE9CBDFB8C1B1EB039BF078E37B09322074193B7B0AE5A60B544DDDB9159294E91744404A2C7CDF96287F5483D691024100F8908925066C3ED9AC8EAFE63A59D56FCBEC354A3DD513489DEDA70E42338CD2AEBDEEF685148123B31A55CA27B2A59CA53E2352DA284F30585A5D6B571245FF02410091E367A0066FC4B4B083565616F901AD4728C5C3384E900E4E021F7E653A849BFF5E6269320C24871661046A09F4670AEE2EC264620D8394BFC1BD781398D891024057BA8AC1C608162EB55F896050D46972C0717C38520EF7BF46CC5914175D7CFF107F4547F2BBF157E4DC1E47594E1C55677F57C2E395C19897A76C44009D09A5024100BBB92D3E8776B52FA20303E39FE8AE862637BB75880D82C6580C3217445C4A95BFB6E94120AD62AADC313418A350FF21B0ED861848626CC0F55936F750B44FC4";int _tmain(int argc, _TCHAR* argv[]){ char szPrivateFile[] = "privatefile"; char szPublicFile[] = "publicfile"; char szSeed[] = "WriteBugWriteBug"; char szOriginalString[] = "WRITE-BUG技术共享平台 - 一个专注校园计算机技术交流共享的平台"; /* 密钥在文件方式 */ // 生成RSA公私密钥对 GenerateRSAKey(1024, szPrivateFile, szPublicFile, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA公钥加密字符串 string strEncryptString = RSA_Encrypt_ByFile(szOriginalString, szPublicFile, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA私钥解密字符串 string strDecryptString = RSA_Decrypt_ByFile((char *)strEncryptString.c_str(), szPrivateFile); // 显示 printf("原文字符串:\n[%d]%s\n", ::lstrlen(szOriginalString), szOriginalString); printf("密文字符串:\n[%d]%s\n", strEncryptString.length(), strEncryptString.c_str()); printf("解密明文字符串:\n[%d]%s\n", strDecryptString.length(), strDecryptString.c_str()); printf("\n\n"); /* 密钥在内存方式 */ // RSA公钥加密字符串 string strEncryptString_Mem = RSA_Encrypt_ByMem(szOriginalString, g_szPubKey, (BYTE *)szSeed, ::lstrlen(szSeed)); // RSA私钥解密字符串 string strDecryptString_Mem = RSA_Decrypt_ByMem((char *)strEncryptString_Mem.c_str(), g_szPrivKey); // 显示 printf("原文字符串:\n[%d]%s\n", ::lstrlen(szOriginalString), szOriginalString); printf("密文字符串:\n[%d]%s\n", strEncryptString_Mem.length(), strEncryptString_Mem.c_str()); printf("解密明文字符串:\n[%d]%s\n", strDecryptString_Mem.length(), strDecryptString_Mem.c_str()); system("pause"); return 0;}
    测试结果为:

    根据图片显示,数据成功被加密和解密,两种公钥存储形式测试均成功。
    总结使用Crypto++库的好处之一,就是方便易用,即使你不了解具体的加密算法,但你只要知道要使用的加密算法这个名称,依然可以做出使用相应的加密算法进行数据加解密。
    参考参考自《Windows黑客编程技术详解》一书
    3 留言 2018-12-20 12:25:32 奖励6点积分
显示 60 到 75 ,共 15 条
eject