第6章 结构、共用体与链表
程序设计高级语言允许程序员利用已经存在的数据类型,包括基本数据类型和其他构造数据类型,自行构造新的数据类型。在程序设计过程中程序要处理的对象,往往不是用一种简单的数据类型就可描述的,为了对关系密切但类型不同的数据进行有效的管理,C语言引进了 结构、共用体的概念。结构体后共用体的类型的定义掌握结构体和共用体变量的定义初始化合影用掌握结构体数组和结构体指针的定义及应用,熟悉没几类型,和美即常量的定义初始化和引用了解,用typedf进行数据类型的自定义。链表的操作,包括遍历、插入一个结点和删除一个节点等。
本章演示的实例都是C++类自行定义类型的例子,如怎样定义和使用结构、共用体、结构数组和共用体数组、指向结构的指针,介绍链表实际的使用,这些编程中经常使用,需要反复多实践,加深理解,在实践中应用。
6.1 结构
案例6-1 输出Huffman编码(结构+算法)
【案例描述】
数据结构是组合到同一定义下的一组不同类型的数据,各个数据类型的长度可以不同。一旦被定义,数据结构就成为一个新的有效数据类型的名称,可以像其他基本的数据类型(如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<<"第"<<i<<"个元素=>\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<m1)
{
m2=m1;
r=l;
m1=ht[k].weight;
l=k;
}
else if(ht[k].weight<m2)
{
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<<" "<<ht[i].data<<":";
for(k=hcd[i].start;k<=n;k++)
cout<<hcd[i].cd[k]; //输出叶子节点的Huffman编码值
cout<<endl;
}
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个节点数据结构数组。
注意:结构经常被用来建立数据库,特别是当考虑结构数组的时候。
案例6-2 用C++实现定时器功能
【案例描述】
定时功能在软件开发中应用很广泛,如设定PC机的时间实现自动关机,或定时实现重启、注销。本例是个简单实现定时功能演示,效果如图6-6所示。
【实现过程】
定义时间结构clock;更新定时数据函数update();延时函数delay()输入为毫秒。主函数定义clock结构类型c;用for循环200000,更新定时数据,显示时间。代码如下:
#include<stdio.h>
#include <iostream>
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-3 TCP端口扫描器
【案例描述】
本案例到案例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<endport;i++) //for循环从开始端口到结束端口
{
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