第9章 面向对象技术基础
(本资料qq讨论群112133686)
面向对象程序设计通过抽象、封装、继承和多态使程序代码达到最大限度的可重用和可扩展,提高软件的生产能力,控制软件开发和维护的费用。类是面向对象程序设计的核心,利用其可以实现数据的封装、隐蔽,通过其的继承与派生特性,能够实现对问题深入的抽象描述的功能。
本章演示的实例都是C++类基本应用,如怎样定义类,类的继承、多态包括重载运算符、友元和虚函数,本章重点讲解了面向对象的继承、多态及重载,反复举例说明单一继承、多次继承、抽象类、纯虚函数及友元重载运算符概念及应用,这些对于初学者可能一时比较难理解,需要反复多实践,加深理解,在实践中应用。
9.1 面向对象基本概念
本章开始举C++类基本应用的实例。面向对象特点是抽象、封装,把具体对象抽出一类对象的公共性质加以描述,封装成数据与操作数据的函数有机结合成“类”。
案例9-1 营业额
【案例描述】
本例用来计算销售员的销售额,功能是输入销售额后按公式计算销售额,最后显示销售额,注意,用来计算的数学公式是有优先顺序的。本例效果如图9-1所示。
图9-1 营业额
【实现过程】
(1)定义一个类employee,其私有属性有编号、姓名、薪水和静态数据成员编号totalno,类中有构造函数employee()和析构函数~employee(),再定义一个类saleman继承employee,设计计算销售额的成员函数pay()和显示销售额的成员函数display()。其代码如下:
class employee //定义一个类
{
protected: //私有属性
int no;
char *name;
float salary;
static int totalno;
public:
employee() //构造函数
{
char temname[20];
cout<<"执行employee构造函数!"<<endl;
no=totalno++;
cout<<"职工姓名:";
cin>>temname;
name=new char[strlen(temname)+1];
strcpy(name,temname);
salary=0;
}
~employee()
{
cout<<"执行employee析构函数!"<<endl;
delete[] name;
}
void pay(){}
void display(){}
};
int employee::totalno=100; //静态数据成员totalno在类外进行初始化
class saleman:virtual public employee
{
protected:
float commrate; //销售额提纯系数
float sales; //销售额
public:
saleman()
{cout<<"执行saleman构造函数!"<<endl;commrate=0.04f;}
void pay()
{
cout<<name<<"本月销售额:";
cin>>sales;
salary=sales*commrate+600; //600为底薪
}
void display()
{
cout<<"销售员"<<name<<"(编号为"<<no<<")"\
<<")"<<"本月工资:"<<salary<<endl;
}
};
(2)在主函数中,定义一个saleman属性的类s1,然后计算并显示销售员的薪水。代码如下:
void main()
{
saleman s1; //定义类
s1.pay(); //计算工资
s1.display(); //显示工资
system("pause");
return ; //返回语句
}
【案例分析】
(1)计算薪水的表达式salary=sales*commrate+600;,计算的先后顺序通常是从左到右,与数学的优先顺序相同。
(2)对象employee在生成过程中通常需要初始化变量或分配动态内存,构造函数employee通过声明一个与类同名的函数来定义,其作用为当且仅当声明一个新的对象,或给该类的一个对象分配内存的时候,这个构造函数将自动被调用。析构函数~employee完成相反的功能。
案例9-2 命令响应
【案例描述】
下面两个实例采用的是选择结构(switch),目的是对一个表达式检查多个可能的常量值。有点像在上面实例中学习过的把几个if和else if语句连接起来的结构。本例效果如图9-2所示。
图9-2 命令响应
【实现过程】
(1)程序定义一个计算器类,定义一个构造函数初始化的代码a=0;b=0;,定义一个输入一个数的函数newa()和输入两个数的函数newab()。然后定义加、减、乘和除的头函数。其代码如下:
class Calculator //定义一个计算器类
{
double a,b;
public:
Calculator(){a=0;b=0;}; //不能省略
void newa() //输入一个数复制给a
{
double num;
cout<<"请输入数:";
cin>>num;
a=num;
}
void newab() //输入两个数复制给a和b
{
double num1,num2;
cout<<"请输入.输入第一个数:";
cin>>num1;
cout<<"输入第二个数:";
cin>>num2;
a=num1;
b=num2;
}
double Add(Calculator &A);
double Sub(Calculator *A);
double Mul(Calculator &A);
double Div(Calculator &A);
};
double Calculator::Add(Calculator &A) //加
{
return A.a+A.b;
}
double Calculator::Sub(Calculator *A) //减
{
return A->a-A->b;
}
double Calculator::Mul(Calculator &A) //乘
{
return A.a*A.b;
}
double Calculator::Div(Calculator &A) //除
{
if(A.b==0) //除数为0
{cout<<"出错! 程序中断!"<<endl;exit(0);}
return A.a/A.b;
}
(2)主函数显示加、减、乘和除运算提示,选择输入后,进入相应类中的成员函数进行数学计算,返回结果并显示值。其代码如下:
void main()
{
int sel;
Calculator cal;
cout<<"欢迎使用计算器!请选择."<<endl;
cout<<"1:\'+\' 2:\'-\' 3:\'*\' 4:\'/\' "<<endl;
do
{
cout<<"请选择:(0--退出)";
cin>>sel;//输入一个选择
switch (sel)
{
case 0: break;
case 1: cal.newab();
cout<<"计算结果是:"<<cal.Add(cal)<<endl;
break;
case 2: cal.newab();
cout<<"计算结果是:"<<cal.Sub(&cal)<<endl;
break;
case 3: cal.newab();
cout<<"计算结果是:"<<cal.Mul(cal)<<endl;
break;
case 4: cal.newab();
cout<<"计算结果是:"<<cal.Div(cal)<<endl;
break;
default: cout<<"选择错误! 重选!"<<endl;
}
}while(sel!=0); //直到sel=0
}
【案例分析】
(1)switch语句格式是:
switch (expression) {case constant1: block1 break; case constant2:block2 break;... default:default block }
计算表达式expression的值,并检查值是否与常量constant1相等;如果相等,程序执行常量后面的语句块block1,直到碰到关键字break,程序跳转到switch选择结构的结尾处。
(2)代码中定义了类Calculator,把取得输入的数据和定义加、减、乘和除数学运行的功能封装起来,提供函数接口供调用,可读性好。除法中加入了被除数不能为0的判断。
注意:switch只能被用来比较表达式和不同常量的值,不能够把变量或范围放在case之后。
案例9-3 动态口令(系统时间+口令表或算法)
【案例描述】
加密时候生成口令很重要。口令最好是随时间变化,这样不容易破解,如根据日期和MD5摘要生成口令。本例效果如图9-3所示。
图9-3 动态口令(系统时间+口令表或算法)
【实现过程】
本例先创建一个md5类,然后定义md5类的变量mg,获取消息值,初始化消息,对明文进行适当处理,md5摘要提取,把密码结果打印。其代码如下:
class md5
{
public:
md5(); //构造函数
virtual ~md5(); //析构函数
bool LoadMessage(); //获取消息值
void InitMessage(); //初始化消息
void md5Transform( char *, int ); //md5摘要提取
void Decode(unsigned int *Mj, char *buf, int len64 );//将512bit字符串转换为16个32位整数
void Encode( unsigned int a, unsigned int b, unsigned int c, unsigned int d );//将abcd四个整数级联成摘要
void printMD5(); //输出结果
private:
char message[100]; //消息值,明文
char MD5[17]; //生成密文
};
#define LENTH 100 //定义消息缓冲区长度
typedef unsigned int UINT4; //数据类型定义
//链接变量
#define A 0x01234567
#define B 0x89abcdef
#define C 0xfedcba98
#define D 0x76543210
//代码略
int main()
{
md5 mg; //定义md5类
mg.LoadMessage(); //获取消息值
mg.InitMessage(); //初始化消息
mg.printMD5(); //输出结果
system("pause");
return 0;
}
【案例分析】
(1)在实例8-6中,讨论过MD5,本实例用类方法实现比较易懂。代码中(>>)表示右移、(<<)表示左移,如(unsigned char)((d >> 16) & 0xff)表示右移16位再‘与’运算。
(2)MD5报文摘要算法,是对输入的任意长度的信息进行计算,产生一个128位长度的“指纹”或“报文摘要”。
9.2 C++类的定义
案例9-4 汽车行驶里程
【案例描述】
在C++面向对象的程序设计中,定义的类中成员可以用函数实现。定义成员函数方法和定义普通函数大部分相同。通常,主函数以外的函数大多是被封装在类中的,主函数或其他函数可以通过类对象调用类中的函数。本实例举卡车类中定义私有成员和函数方法,效果如图9-4所示。
图9-4 汽车行驶里程
【实现过程】
定义卡车类Truck,其私有成员为wage、distance和price,两个成员函数Truck和sum,一个友元函数sum(Truck &T);输入数据,计算出运费。代码如下:
#include<iostream>
using namespace std;
class Truck //定义卡车类
{
public:
Truck(float w,float d,float p) //成员函数
{
weight=w;distance=d;price=p;
}
static double sum(Truck &T) //静态成员函数,某一辆卡车的运费
{
return T.weight*T.distance*T.price;
}
friend double sum(Truck &T); //友元函数
private:
float weight; //卡车的载货量(单位:吨)
float distance; //行驶的里程(单位:里)
float price; //运费单价(单位:元/每吨每里)
};
double sum(Truck &T) //某一辆卡车的运费
{
return T.weight*T.distance*T.price;
}
void main()
{
float weight; //卡车的载货量(单位:吨)
float distance; //行驶的里程(单位:里)
float price; //运费单价(单位:元/每吨每里)
cout<<"输入数据:卡车的载货量、行驶的里程和运费单价"<<endl;
cin>>weight>>distance>>price;
Truck a(weight,distance,price);
cout<<"某一辆卡车的运费:"<<endl;
cout<<Truck::sum(a)<<endl; //某一辆卡车的运费
//或cout<<a.sum(a)<<endl;
system("pause");
}
【案例分析】
(1)定义一个卡车类Truck,其私有成员为wage、distance和price,分别表示卡车的载货量、行驶的里程和运费单价。用友元函数sum(Truck &T)和静态成员函数sum(Truck &T)计算某一辆卡车的运费。
(2)类中定义两个成员函数Truck()和sum()。friend double sum(Truck &T);是友元函数,在第10章实例会详细描述。友元函数是一种定义在类外部的普通函数,其特点是能够访问友元类中的私有成员和保护成员,即友元类的访问权限的限制对其不起作用。
注意:return语句可以是一个表达式,函数先计算表达式值后再返回值。return语句还可以终止函数,并将控制返回到主调函数。
案例9-5 产量统计
【案例描述】
本实例定义个农作物类,把需要计算产量的输入变量和计算产量函数封装,来计算各种产量。本例效果如图9-5所示。
图9-5 产量统计
【实现过程】
定义农作物类crops,私有成员output、area、total1为单产、种植面积和产量,定义构造函数和析构函数crops()、~crops(),输出各类农作物的产量函数print()。代码如下:
#include <iostream>
#include <cstring>
using namespace std;
class crops //定义农作物类
{
private:
int output; //单产(公斤)
float area; //种植面积(亩)
float total1,total2,total3,total4; //定义各类农作物的产量
public:
//构造函数
crops(int output1,float area1,int output2,float area2,int output3,float area3,int output4,float area4)
{
total1=output1*area1; //计算小麦总产
total2=output2*area2;
total3=output3*area3;
total4=output4*area4;
}
~crops() //析构函数
{
cout<<"清理现场"<<endl;
}
void print() //信息输出
{
cout<<"小麦总产:"<<total1<<endl;
cout<<"大麦总产:"<<total2<<endl;
cout<<"玉米总产:"<<total3<<endl;
cout<<"油菜总产:"<<total4<<endl;
}
};
int main()
{
//调用构造函数声明crops变量comp
crops comp(196,35.6,106,7.4,650,10.04,91,13.01);
comp.print(); //信息输出
return 0;
}
【案例分析】
(1)代码crops()为构造函数(constructor),通过声明一个与类同名的函数来定义。当且仅当要生成一个类的新的实例的时候,也就是当且仅当声明一个新的对象,或给该类的一个对象分配内存的时候,这个构造函数才自动被调用。代码~crops()为析构函数(destructor),完成相反的功能。其是在对象从内存中释放的时候自动调用,释放可能性是因为其存在的范围已经结束了。
(2)类是对象的抽象,而对象是类的具体实例。在面向过程的结构化程序设计中,程序用公式来表述为∶ 程序=算法+数据结构。面向对象的程序的公式为:对象 = 算法 + 数据结构,程序=(对象+对象+对象+……)+ 消息,消息的作用就是对对象的控制。程序设计的关键是设计好每一个对象以及确定向这些对象发出的命令,使各对象完成相应的操作。
注意:在C++中对象的类型称为类,类代表了某一批对象的共性和特征。类是对象的抽象,而对象是类的具体实例。
案例9-6 走出迷宫(类+算法)
【案例描述】
现实生活中的很多有趣的游戏可以定义个类,本实例把地图定义个类,后形成迷宫,玩家需要在地图中寻找出口,很有趣味性,充分享受编程带来的乐趣。本例效果如图9-6所示。
图9-6 走出迷宫 (类+算法)
【实现过程】
定义个地图类graph,定义路径的数组arcs,主函数中进行路径数组初始化,把路程赋予路径。代码实现如下:
#include"iostream.h"
#include"stdio.h"
#define max 32767
class graph //定义个地图类
{
public:
int arcs[20][20]; //定义路径数组,当前路径
int a[20][20]; //定义路径数组
int lu[20][20]; //定义路径数组,下一个路径
void floyd(graph &t, int n,int e);
};
void graph::floyd(graph &t,int n,int e)
{
char ch;
cout<<"ㄨ输入除s的任意键开始ㄨ"<<endl;
while(ch=getchar()!='s') //直到输入's'
{
int r,c;
cout<<"您设置的入口是端点:";
cin>>r; //取得输入数据
cout<<"您设置的出口是端点:";
cin>>c;
int i,j,k; //取得输入数据
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{
t.a[i][j]=t.arcs[i][j]; //路径数组
if((i!=j)&&(t.a[i][j]<max))
t.lu[i][j]=i;
else t.lu[i][j]=0;
}
for(k=1;k<=n;k++)
{
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
if((t.a[i][k]+t.a[k][j])<t.a[i][j])
{
t.a[i][j]=t.a[i][k]+t.a[k][j];
t.lu[i][j]=t.lu[k][j];
}
}
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{
if(i!=j)
{
if(t.a[i][j]==max) //判断,最大路径
cout<<"很糟糕,您迷失在迷宫里,无路可走";
else
if(i==r&&j==c)
{
cout<<"+ω+"<<endl;
cout<<"您应该选择的路线为:"<<endl;
cout<<"路程为:"<<t.a[i][j]<<endl<<endl<<"方向为:";
int next=t.lu[i][j]; //下一个路径
cout<<j;
while(next!=i)
{
cout<<"<---"<<next;
next=t.lu[i][next];
}
cout<<"<---"<<i<<endl;
}
}
}
cout<<"输入s键退出,其他任意键继续游戏"<<endl;
}
}
【案例分析】
程序的实现主要是for循环,如代码中for(i=1;i<=n;i++)、for(j=1;j<=n;j++),这样很容易实现对数值的赋值。代码t.a[i][j]=t.arcs[i][j];为路径的数组。
9.3 C++类的实现
案例9-7 求圆的面积和周长
【案例描述】
对于一个类来说,就是把特征封装起来,实现具体功能,如本实例举的是圆周方面的例子,用成员函数实现计算圆面积和圆周长。本例效果如图9-7所示。
【实现过程】
定义圆周类Circle,私有成员radius为半径,定义成员函数Area ()和length ()为返回圆面积和圆周长,主函数中定义Circle类c,初始化输入半径,把类返回的值赋给结构circle1。代码如下:
#include <iostream.h>
#include <fstream>
using namespace std;
const double PI = 3.14159;
class Circle //声明类Circle 及其数据和方法
{
private:
float radius; //半径
public:
Circle(float r) //构造函数
{radius=r;}
double Area() //圆面积
{ return PI * radius * radius; }
double length() //圆周长
{ return 2*PI * radius; }
};
void main ()
{ float radius;
cout<<"输入圆的半径: ";
cin>>radius; //取得输入半径
Circle c(radius); //初始化圆
struct{
float area; //面积
float length; //周长
}circle1; //定义圆结构
circle1.area=c.Area(); //面积赋值
circle1.length=c.length(); //周长赋值
cout<<"圆面积:"<<circle1.area<<endl;
cout<<"圆周长:"<<circle1.length<<endl;
system("pause");
}
【案例分析】
(1)类的定义格式:
class 类名{private :成员数据;public :成员数据;protected: 成员数据};
priviate称为私有成员,public称为公有成员,protected称为保护成员。
(2)构造函数和析构函数是在类体中定义的两种特殊的成员函数。构造函数是在创建对象时,使用给定的值来将对象初始化。析构函数的功能正好相反,是在系统释放对象前,对对象做一些善后处理的工作,如内存释放。
案例9-8 主动连接型木马框架
【案例描述】
木马是黑客主要攻击手段之一,它通过渗透对方主机系统,实现对目标主机的远程操作攻击目的。现在发展到的第五代木马,通过与病毒紧密结合,利用操作系统漏洞,从而实现感染传播病毒的目的,而不必像以前的木马那样需要欺骗用户主动激活。例如类似冲击波病毒的木马。本实例举木马简单编程演示,代码见案例9-9。本例效果如图9-8所示。
图9-8 主动连接型木马框架
【实现过程】
定义一个简单木马类C*Horse,其公有函数成员为IfShell、CopyFileaddr、ShellFile、SetAutoRun和ShutDown。代码如下:
# include<Afx.h>
# include<windows.h>
#include<afxwin.h>
#include<tlhelp32.h>
class C*Horse
{
public: //增加代码
public: //增加代码
C*Horse();
~C*Horse();
protected: //增加代码
public:
BOOL IfShell(CString BeKissPrcName);
BOOL CopyFileaddr(CString m_CopyFile);
void ShellFile(CString m_ShellFile);
BOOL SetAutoRun(CString strPath); //设置自动运行
void ShutDown();
private: //增加代码
};
C*Horse::C*Horse() { //增加代码 }
C*Horse::~C*Horse() { //增加代码 }
BOOL C*Horse::IfShell(CString BeKissPrcName) //判断程序是否在运行
{
CString str,a,prcnum;
//给系统内所有的进程拍个快照
HANDLE SnapShot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
SHFILEINFO shSmall; //文件的信息
PROCESSENTRY32 ProcessInfo; //声明进程信息变量
ProcessInfo.dwSize=sizeof(ProcessInfo); //设置ProcessInfo的大小
//返回系统中第一个进程的信息
BOOL Status=Process32First(SnapShot,&ProcessInfo);
int m_nProcess=0;
int num=0;
while(Status)
{
num++;
m_nProcess++;
ZeroMemory(&shSmall,sizeof(shSmall)); //获取进程文件信息
SHGetFileInfo(ProcessInfo.szExeFile,0,&shSmall,sizeof(shSmall),SHGFI_ICON| SHGFI_SMALLICON); //取图标索引号等信息
str=ProcessInfo.szExeFile;
if(str==BeKissPrcName)
{
AfxMessageBox("找到进程成功!"+BeKissPrcName);
return true;
}
//获取下一个进程的信息
Status=Process32Next(SnapShot,&ProcessInfo);
}
AfxMessageBox("失败!");
return false;
}
BOOL C*Horse::CopyFileaddr(CString m_CopyFile)//复制文件 { //代码略 }
void C*Horse::ShellFile(CString m_ShellFile) //执行所要的程序
{
ShellExecute(NULL,"open",m_ShellFile,NULL,NULL,SW_SHOWNORMAL); //运行一个外部程序
}
BOOL C*Horse::SetAutoRun(CString strPath) //修改注册表 { //代码略 }
void C*Horse::ShutDown() //重新启动计算机 { //代码略 }
int main()
{
C*Horse horse; //定义类
horse.IfShell("197.exe");
return 0;
//代码略 }
【案例分析】
(1)木马可以说是基于网络的一种特殊的远程控制软件。主动式连接方式的木马以冰河为代表。冰河功能强,实现技术基于TCP/IP协议,可以说基本上黑客都认识这个木马攻击软件。这种木马的连接技术就是主动连接,即服务端在运行后监听指定端口,而在连接时,客户端会主动发送连接命令给服务端进行连接。
(2)CreateToolhelp32Snapshot()实现枚举进程的功能。
案例9-9 反弹式穿墙木马框架
【案例描述】
本实例继续上面实例演示。“反弹式木马”——是利用防火墙对内部发起的连接请求无条件信任的特点,假冒自己是系统的合法网络请求,来取得攻击计算机对外的端口;再通过某些方式连接到木马的客户端,从而窃取入侵计算机的资料同时遥控入侵计算机。本例演示修改注册表和重新启动计算机,效果如图9-9所示。
图9-9 反弹式穿墙木马框架
【实现过程】
定义2个函数SetAutoRun()和ShutDown(),分别实现修改注册表和重新启动计算机,函数中涉及到Windows的API函数RegSetValueEx()和ExitWindowsEx()。代码如下:
BOOL C*Horse::SetAutoRun(CString strPath) //修改注册表
{
CString str;
HKEY hRegKey;
BOOL bResult;
str=_T("Software\\Microsoft\\Windows\\CurrentVersion\\Run");
//打开给定键
if(RegOpenKey(HKEY_LOCAL_MACHINE, str, &hRegKey) != ERROR_SUCCESS)
bResult=FALSE;
else
{
//分解出路径、文件名、扩展名
_splitpath(strPath.GetBuffer(0),NULL,NULL,str.GetBufferSetLength(MAX_PATH+1),NULL);
strPath.ReleaseBuffer(); //内存已经使用完毕,进行封口
str.ReleaseBuffer();
if(::RegSetValueEx( hRegKey,
str,
0,
REG_SZ,
(CONST BYTE *)strPath.GetBuffer(0),
strPath.GetLength() ) != ERROR_SUCCESS)
bResult=FALSE; //修改注册表
else
bResult=TRUE;
strPath.ReleaseBuffer(); //内存已经使用完毕,进行封口
}
return bResult; //返回修改结果
}
void C*Horse::ShutDown() //重新启动计算机
{
if (IDYES == MessageBox(NULL,"是否现在重新启动计算机?", "注册表提示", MB_YESNO))
{
OSVERSIONINFO OsVerInfo; //保存系统版本信息的数据结构
OsVerInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx(&OsVerInfo); //取得系统的版本信息
CString str1 = "", str2 = "";
str1.Format("你的系统信息\n版本为:%d.%d\n", OsVerInfo.dwMajorVersion,
OsVerInfo.dwMinorVersion);
str2.Format("型号:%d\n", OsVerInfo.dwBuildNumber);
str1 += str2;
AfxMessageBox(str1);
if(OsVerInfo.dwPlatformId == VER_PLATFORM_WIN32_NT)
ExitWindowsEx(EWX_REBOOT | EWX_SHUTDOWN, 0); //重新启动计算机
}
}
【案例分析】
(1)普通木马是驻留在用户计算机里的一段特殊的服务程序,攻击者控制的则是相应的客户端程序。服务程序通过特定的端口,打开用户计算机的连接资源。一旦攻击者所掌握的客户端程序发出请求,木马便和它连接起来,将用户的信息窃取出去。因此此类木马的最大弱点,在于攻击者必须和用户主机建立连接,木马才能起作用。采用对外部连接审查严格的防火墙,可使木马很难工作起来。
(2)反弹式木马在工作原理上和常见的木马不同。反弹式木马使用的是系统信任的端口,系统会认为木马是普通应用程序,而不对其连接进行检查。防火墙在处理内部发出的连接时,也就信任了反弹式木马。上述代码:dwOSVersionInfoSize:指定该数据结构的字节大小;RegOpenKey,函数打开给定键;HKEY hKey,要打开键的句柄;LPCTSTR lpSubKey,要打开子键的名字的地址;PHKEY phkResult,要打开键的句柄的地址;dwMajorVersion:操作系统的主版本号;dwMinorVersion:操作系统的副版本号;dwBuildNumber:操作系统的创建号。
注意:对付木马可用木马清除软件,如360、金山。
9.4 C++类的使用
案例9-10 实现类自动化管理内存
【案例描述】
和一个简单变量创建在动态存储区一样,可以用new和delete操作符为对象分配动态存储区,本实例演示了如何为对象和对象数组动态分配内存,把功能封装成类。本例效果如图9-10所示。
图9-10 实现类自动化管理内存
【实现过程】
TestClass为单个的类重载 new[ ]和delete[ ],内存的请求被定向到全局的new[ ]和delete[ ] 的操作符,这些内存来自于系统堆。代码如下:
# include<iostream.h>
# include<stdlib.h>
# include<malloc.h>
class TestClass {
private: //私有数据成员列表
int x;
int y;
public:
void * operator new[ ](size_t size);
void operator delete[ ](void *p);
TestClass(int xp=0,int yp=0) //构造函数,带缺省参数值
{
x=xp;
y=yp;
cout<<"构造函数被调用"<<endl;
}
~TestClass() //析构函数
{
cout<<"析构函数被调用"<<endl;
}
void print() //成员函数,类内部实现
{
cout<<"x: "<<x<<", y: "<<y<<endl;
}
void Set(int xp,int yp) //成员函数,类内部实现,用来修改成员x和y
{
x=xp;
y=yp;
}
};
//重载new[ ]操作符
void *TestClass::operator new[ ](size_t size)
{
void *p = new int[size];//malloc(size);
return (p);
}
//重载delete[ ]操作符
void TestClass::operator delete[ ](void *p)
{
delete[]p;
// free(p);
}
int main()
{
TestClass *p = new TestClass[2]; //申请一块动态内存,连续存放两个TestClass对象,将首地址赋值给TestClass指针p
p[0].print(); //可以将指针当成数组名,使用下标运算符访问对应对象,等价性
p[1].Set(4,5); //调用数据元素(对象)的成员函数Set
p[1].print();
delete[ ] p; //释放申请的动态内存
p=NULL; //养成好的习惯,防止野指针
system("pause");
return 0;
}
【案例分析】
(1)使用new为对象数组分配动态空间时,不能显式调用对象的构造函数。因此,对象要么没有定义任何形式的构造函数(由编译器默认提供),要么显式定义一个(且只能有一个)所有参数都有默认值的构造函数(包括无参构造函数)。
(2)上述代码TestClass *p = new TestClass[2];,开辟了一个大小为2的对象数组,TestClass类中定义的所有参数都有默认值的构造函数被调用,根据指针和数组名的等价性,通过诸如p[0]和p[1]的形式对数组中的对象进行访问。delete[ ] p;,释放了数组所占的内存空间,new和delete激活了数组中每个对象的构造函数和析构函数。
注意:用数组名的形式(如p[0]、p[1])对其中的对象访问,如果使用p->.print(); p++等指针形式,一旦p的值不同于内存申请时返回的指针,delete的操作会引发错误。
案例9-11 定义一个ping类
【案例描述】
在网络管理中经常用ping。ping的作用是测试到某一个ip之间的网络是否通畅。根据ping不同的ip就可以知道需要了解的各种的网络状况。本例定义一个类,封装ping功能,效果如图9-11所示。
【实现过程】
定义ping类CPing,成员函数开始ping函数Ping(),解析头数据DecodeICMPHeader()。其代码如下:
图9-11 定义一个ping类
class CPing
{
public:
void usage(char* progname); //使用说明
void ValidateArgs(int argc, char** argv); //解析输入main()参数
void DecodeIPOptions(char *buf, int bytes);
void Cleanup(); //结尾清除释放
void Ping(int timeout =1000); //开始ping
SOCKET m_hSocket;
IpOptionHeader m_ipopt;
SOCKADDR_IN m_addrDest;
SOCKADDR_IN m_addrFrom;
char *icmp_data; //发送数据
char *recvbuf; //接收数据
USHORT seq_no ; //顺序号
char *lpdest; //目标IP
int datasize; //数据包大小
BOOL m_bRecordRout; //记录路由选择
CPing(); //构造函数
virtual ~CPing(); //析构函数
private:
void DecodeICMPHeader(char *buf, int bytes, SOCKADDR_IN* from); //解析头数据
USHORT checksum(USHORT *buffer, int size); //ip校验和
void FillICMPData(char *icmp_data, int datasize); //赋值
};
……
// 描述:
//显示使用帮助
int main(int argc, char* argv[])
{
if (argc != 2)
{
printf("You must specify who you want to ping on the command line\n");
return -1;
}
CPing ping;
ping.ValidateArgs(argc, argv); //解析输入main()参数
ping.Ping();
Sleep(1000);
printf("Ping end!\n");
ping.Cleanup(); //结尾清除释放
getchar();
return 0;
}
【案例分析】
ping在Windows系统下自带的一个可执行命令。使用它可以检查网络是否能够连通,熟悉掌握后可以很好地帮助分析判定网络故障。应用格式:ping ip地址。ValidateArgs()解析输入main()参数,解析argv输入的参数。代码ping.Ping()开始执行ping操作。
案例9-12 利用ICMP协议实现ping
【案例描述】
继续9-11实例话题,代码见实例9-11。ping是日常经常用到的网络工具,所用到的协议是ICMP,通过套接口机制,只要记录下数据发送到接收的时间,就等于就知道网络通讯情况。本例效果如图9-12所示。
图9-12 利用ICMP协议实现ping
【实现过程】
定义CPing成员函数Ping,输入是超时时间,利用套接口函数编程实现。部分代码实现如下:
void CPing::Ping(int timeout)
{ //创建一个与指定传送服务提供者捆绑的套接口,可选择地创建或加入一个套接口组
m_hSocket = WSASocket (AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
WSA_FLAG_OVERLAPPED);
if (m_hSocket == INVALID_SOCKET) //失败
{
printf("WSASocket() failed: %d\n", WSAGetLastError());
return ;
}
if (m_bRecordRout)
{
//每个ICMP数据包结束设置IP头
ZeroMemory(&m_ipopt, sizeof(m_ipopt));
m_ipopt.code = IP_RECORD_ROUTE; //记录路由选项
m_ipopt.ptr = 4; //指向第一个地址偏移量
m_ipopt.len = 39; //选项报头长度
int ret = setsockopt(m_hSocket, IPPROTO_IP, IP_OPTIONS,//设置套接口的选项
(char *)&m_ipopt, sizeof(m_ipopt));
if (ret == SOCKET_ERROR) //失败
{
printf("setsockopt(IP_OPTIONS) failed: %d\n",
WSAGetLastError());
}
}
//设置发送、接收超时值
int bread = setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO,
(char*)&timeout, sizeof(timeout));
if(bread == SOCKET_ERROR)
{
printf("setsockopt(SO_RCVTIMEO) failed: %d\n",
WSAGetLastError());
return ;
}
timeout = 1000; //超时
//设置套接口的选项
bread = setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)&timeout, sizeof(timeout));
if (bread == SOCKET_ERROR)
{
printf("setsockopt(SO_SNDTIMEO) failed: %d\n", WSAGetLastError());
return ;
}
//将m_addrDest中前n个字节用0替换并返回m_addrDest
memset(&m_addrDest, 0, sizeof(m_addrDest));
//如需要重命名终端
m_addrDest.sin_family = AF_INET;
if ((m_addrDest.sin_addr.s_addr = inet_addr(lpdest)) == INADDR_NONE)//目标ip
{
struct hostent *hp = NULL;
//返回对应于给定主机名的包含主机名字和地址信息的hostent结构指针
if ((hp = gethostbyname(lpdest)) != NULL)
{
memcpy(&(m_addrDest.sin_addr), hp->h_addr, hp->h_length);//拷贝字符串
m_addrDest.sin_family = hp->h_addrtype; //地址家族
printf("m_addrDest.sin_addr = %s\n", inet_ntoa(m_addrDest.sin_addr));
}
else
{
printf("gethostbyname() failed: %d\n",
WSAGetLastError()); //错误状态
return ;
}
}
// 产生ICMP包
datasize += sizeof(IcmpHeader);
icmp_data =(char*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
MAX_PACKET);
recvbuf =(char*) HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
MAX_PACKET);
if (!icmp_data)
{
printf("HeapAlloc() failed: %d\n", GetLastError());
return ;
}
memset(icmp_data,0,MAX_PACKET);
FillICMPData(icmp_data,datasize); //赋值
//开始发送/接收ICMP包
while(1)
{
static int nCount = 0;
int bwrote;
if (nCount++ == 4)
break;
((IcmpHeader*)icmp_data)->i_cksum = 0;
((IcmpHeader*)icmp_data)->timestamp = GetTickCount();
((IcmpHeader*)icmp_data)->i_seq = seq_no++; //顺序号
((IcmpHeader*)icmp_data)->i_cksum =
checksum((USHORT*)icmp_data, datasize); //校验和
bwrote = sendto(m_hSocket, icmp_data, datasize, 0,
(struct sockaddr*)&m_addrDest, sizeof(m_addrDest)); //发送数据
if (bwrote == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("timed out\n");
continue;
}
printf("sendto() failed: %d\n", WSAGetLastError()); //显示发送失败错误信息
return ;
}
if (bwrote < datasize)
{
printf("Wrote %d bytes\n", bwrote); //显示写入字节数
}
int fromlen = sizeof(m_addrFrom);
bread = recvfrom(m_hSocket, recvbuf, MAX_PACKET, 0,
(struct sockaddr*)&m_addrFrom, &fromlen); //接收数据
if (bread == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("timed out\n"); //显示超时
continue;
}
printf("recvfrom() failed: %d\n", WSAGetLastError());//显示接收失败错误信息
return ;
}
DecodeICMPHeader(recvbuf, bread, &m_addrFrom); //解析头数据
}
}
【案例分析】
(1)ICMP的全称是Internet Control Message Protocol,即互联网控制消息协议。ICMP就是实现一个“错误侦测与回报机制”的共性;目的就是能够检测网路的连线状况,也能确保连线的准确性。实现侦测远端主机是否存在,建立及维护路由资料,重导资料传送路径的功能。
(2)为了实现发送、监听ICMP报文,必须建立SOCK_RAW(原始套接口)。首先需要定义一个IP首部:IpHeader;然后定义一个ICMP首部,可以通过WSASocket()建立一个原始套接口SockRaw为WSASocket;随后可以使用fill_icmp_data()子程序填充ICMP报文段,再使用CheckSum()子程序计算ICMP校验和CheckSum();随后就可以通过sendto()函数发送ICMP_ECHOREPLY报文。
注意:如要编写服务端的监听程序,基本的操作相同,使用recvfrm()函数接收ICMP_ECHOREPLY报文,并用decoder()函数将接收来的报文解码为数据和命令。
案例9-13 相亲速配系统
【案例描述】
MySQL是一个开源精巧的SQL数据库管理系统,具体功能强大、灵活性、丰富的应用编程接口(API)以及精巧的系统结构的特点。它应用很广泛,可以说它是一个真正的多用户、多线程SQL数据库服务器。本例演示MySQL数据库利用SQL(结构化查询语言)存储数据,效果如图9-13所示。
图9-13 相亲速配系统
【实现过程】
函数select()中,MySQL数据库引擎建立连接,利用select *语句查询所有的记录,得到查询结果。主函数根据想要匹配的星座,输入相应的星座,得到星座详细特征信息。部分代码如下:
#define _CRT_SECURE_NO_DEPRECATE
#pragma once
#include <winsock2.h>
#include "mysql.h"
#pragma comment(lib,"libmysql")
#include<string>
#include<iostream>
using namespace std;
class Date //日期类
{
int month, day;
public:
Date(int m, int d)
{ set(m, d); }
void set(int m, int d);
void constell();
};
void Date::set(int m, int d) {
//在此最好检查日期是否输入正确
month=m, day=d;
}
void Date::constell()
{
string c[12][2]={ {"魔蝎座", "水瓶座"}, {"水瓶座", "双鱼座"}, {"双鱼座", "白羊座"}, {"白羊座", "金牛座"}, {"金牛座", "双子座"}, {"双子座", "巨蟹座"}, {"巨蟹座", "狮子座"}, {"狮子座", "处女座"}, {"处女座", "天秤座"}, {"天秤座", "天蝎座"}, {"天蝎座", "射手座"}, {"射手座", "魔蝎座"} };
string d[12]={ //代码略 } };
int x=month-1;
int y=day/21;
cout<<"该日子的星座是:"<<c[x][y]<<"\n";
int z=(x+y)%12;
cout<<"该星座的性格特征是:"<<d[z];
}
int select()
{
//MYSQL;
MYSQL_RES *query_result;
MYSQL_ROW row;
MYSQL *db_handle, mysql;
int query_error;
mysql_init(&mysql);//初始化MySQL
//尝试与运行在主机上的MySQL数据库引擎建立连接
db_handle=mysql_real_connect(&mysql, "127.0.0.1", "root", "888888", "mysql", 3306, 0, 0);//db_handle=mysql_real_connect(&mysql, "yourdbip", "test", "test", "yourtable", 3306, 0, 0);
mysql_set_character_set(&mysql,"GBK"); //当前连接设置默认的字符集
if(db_handle==NULL)
{
cout<<mysql_error(&mysql); //函数返回上一个 MySQL 操作产生的文本错误信息
}
//查询所有的记录;
string mq1("select * from Marriage_matching ");
string mq2;
query_error=mysql_query(db_handle, mq1.c_str()); //发送一条 MySQL 查询
if(query_error!=0)
{
cout<<mysql_error(db_handle);
return 1;
}
query_result=mysql_store_result(db_handle); //查询结果
while((row=mysql_fetch_row(query_result))!=NULL) //检索一个结果集合的下一行
{
string mysqls1(row[0] ? row[0]:"NULL");
string mysqls2(row[1] ? row[1]:"NULL");
string mysqls3(row[2] ? row[2]:"NULL");
string mysqls4(row[3] ? row[3]:"NULL");
cout<<mysqls1<<" "<<mysqls2<<" "<<mysqls3<<" "<<mysqls4<<endl;
}
mysql_free_result(query_result); //函数释放结果内存
mysql_close(db_handle); //关闭 MySQL 连接
return 0;
}
【案例分析】
(1)SQL称为结构化查询语言,是目前最流行和标准化的数据库语言。MySQL实现的是一个客户机/服务器结构,它由一个服务器守护程序mysqld和很多不同的客户程序和库组成。MySQL主要目标是快速、健壮和易用。其环境超过40个数据库,包含10,000个表。
(2)SQL是一种标准化的语言,它使得存储、更新和存取信息更容易。select *用于从表中选取数据。
注意:能用SQL语言为一个网站检索产品信息及存储顾客信息,同时MySQL也足够快和灵活以方便存储记录文件和图像。
案例9-14 注册帐号数据库(服务器数据存储技术)
【案例描述】
C++编写的网络程序,需要处理大量数据,然后把这些数据存储在服务器的数据库中,本例继续上面实例的演示,讨论SQL语言使用,插入记录、查询记录。效果如图9-14所示。
图9-14 注册帐号数据库(服务器数据存储技术)
【实现过程】
初始化MySQL,尝试与运行在主机上的MySQL数据库引擎建立连接,插入带有账号、密码、EMAIL记录,发送一条带有ID的MySQL查询语句,返回查询结果。其代码如下:
int main()
{
while (true)
{
string username1,password1,email1,salt1,uid="1";
cout<<"请输入账号、密码、EMAIL:"<<endl;
cin>>username1>>password1>>email1;
//获取SALT;
salt1=salt();
cout<<"随机函数生成密钥:"<<salt1<<endl;
//计算MD5;
string password2;
password2=BBSmd5(password1,salt1);
cout<<"加密后的口令:"<<password2<<endl;
system("pause");
//MYSQL;
MYSQL_RES *query_result;
MYSQL_ROW row;
MYSQL *db_handle, mysql;
int query_error;
mysql_init(&mysql); //初始化MySQL
//尝试与运行在主机上的MySQL数据库引擎建立连接
db_handle=mysql_real_connect(&mysql, "127.0.0.1", "root", "888888", "mysql", 3306, 0, 0);//db_handle=mysql_real_connect(&mysql, "yourdbip", "test", "test", "yourtable", 3306, 0, 0);
mysql_set_character_set(&mysql,"GBK"); //当前连接设置默认的字符集
if(db_handle==NULL)
{
//函数返回上一个 MySQL 操作产生的文本错误信息
cout<<mysql_error(&mysql);
}
//插入记录;
string mqcr1("insert into pre_ucenter_members values('"+uid+"','"+username1+ "','"+password2+"','"+email1+"','"+salt1+"')");
if (!mysql_query(&mysql,mqcr1.c_str())) //发送一条 MySQL 查询
{
cout<<"注册列表插入成功!"<<endl;
}
else
{
cout<<"注册列表插入失败!"<<endl;
}
//查询记录;
string mq1("select * from pre_ucenter_members where uid='");
string mq2;
cout<<"请输入要查找的ID"<<endl;
cin>>mq2;
string mq3(mq1+mq2+"'");
query_error=mysql_query(db_handle, mq3.c_str()); //发送一条 MySQL 查询
if(query_error!=0)
{
cout<<mysql_error(db_handle);
return 1;
}
query_result=mysql_store_result(db_handle); //查询结果
while((row=mysql_fetch_row(query_result))!=NULL) //检索一个结果集合的下一行
{
string mysqls1(row[0] ? row[0]:"NULL");
string mysqls2(row[1] ? row[1]:"NULL");
string mysqls3(row[2] ? row[2]:"NULL");
string mysqls4(row[3] ? row[3]:"NULL");
cout<<mysqls1<<" "<<mysqls2<<" "<<mysqls3<<" "<<mysqls4<<endl;
}
mysql_free_result(query_result); //函数释放结果内存
mysql_close(db_handle); //关闭 MySQL 连接
}
string end1;
cin>>end1;
system("pause");
return 0;
}
【案例分析】
上述代码insert into,用于向表格中插入新的行,格式为:INSERT INTO 表名称 VALUES (值1, 值2,....)。也可以插入数据的指定所要的列,格式为:INSERT INTO table_name (列1, 列2,...) VALUES (值1, 值2,....),功能为插入新的列。
案例9-15 网吧管理系统
【案例描述】
本实例模拟一个简单的网吧管理系统,编写输入、查询、删除、充值函数进行网吧管理。这些功能具有代表性,在很多信息管理功能中都需要这样功能。本例效果如图9-15所示。
图9-15 网吧管理系统(综合)
【实现过程】
struct tm*ptr; time_t m[100]使用时间函数进行时间记录;建立用户类Yong;输入用户信息,实现查询、删除、充值、显示所有用户的功能,直充和通过合并其他用户充值就能相互交换值。其代码如下:
struct tm*ptr; time_t m[100]; //时间记录
class Yong {
public: Yong(string a="0",string b="0",int c=1){aa=a;bb=b;cc=(float)c;}
string xianshiming(){return aa;}
void xianshiyong(){cout<<"姓名"<<aa<<setw(10)<<"身份证号"<<bb<<setw(10) <<"余额/原有"<<cc<<endl;}
Yong operator + (const Yong &c2) const ;
float cc;
private: string aa,bb;
};
Yong Yong::operator + (const Yong &c2)const {
return Yong (aa,bb,cc+c2.cc);
}
void chaxun (Yong p[100],int &i){ //查询
++i;
string a; int j,c; float d;
cout<<"进入查询系统,请输入用户姓名"<<endl;
cin>>a;
for(j=0;j<=i;j++)
if(a==p[j].xianshiming())
{p[j].xianshiyong(); break;} //显示所有用户信息
m[i]=time(0);
c=m[i]-m[j];
d=(float)200/3600*c;
cout<<"余额"<<p[j].cc-d<<endl;
--i;
}
void shanchu(Yong p[100],int &i,int &n){ //结账 //代码略 }
void chongqian (Yong p[100],int &i){ //充值 //代码略 }
int main(){
string a,b;
Yong p[100];
int c=1,i=0,d,n=0,t;
xinxin:
huahua:
cout<<"输入0 0 0时进入其他程序"<<endl;
while(c!=0)
{
cin>>a>>b>>c;
if(c<=0) cout<<"进入其他系统/钱不够无法建立用户,谢谢合作"<<endl;
m[i]=time(0);
p[i]=Yong(a,b,c);
if(c!=0)
++i;
}
cout<<"输入0 进入查询系统"<<endl;
cout<<"输入1 进入充值系统"<<endl;
cout<<"输入2 进入结账系统"<<endl;
cout<<"输入3 进入 返回"<<endl;
cout<<"输入4 显示所有用户信息"<<endl;
cout<<"输入数字大于等于5 结束"<<endl;
cin>>d;
switch (d){
case 0: chaxun(p,i); goto xinxin ; break; //查询
case 1: chongqian(p,i);goto xinxin; break; //充值
case 2: shanchu(p,i,n); goto huahua; break; //结账
case 3: c=1; goto huahua; break; //进入、返回
case 4:i=i-1;for(t=0;t<=i;t++)
p[t].xianshiyong(); //显示所有用户信息
i=i+1; goto huahua ;break;
case 5: return 0;
}
return 0;
}
【案例分析】
(1)开启计算器功能,显示默认开启十进制,A、B、C、D、E、F按键不可用。选择进制或制度,计算器相应开启相关按键。由于使用函数UpdateData(0);,所以输入框不允许键盘输入,但出错处理函数的判断比较简单。
(2)程序采用的关键(难点)技术:
— 使用时间函数对各个用户的余额进行实时监控;
— 通过使用重载函数进行用户合并。
注意:计算器的功能还有许多不完善的地方,如为了能给出错处理提供更准确地分析,只允许计算器按键输入,不允许键盘输入,因此还需要更多的设计来完善对于表达式的处理。
案例9-16 网络购物系统
【案例描述】
网络购物方便快捷,改变大众生活,如今越来越多人喜欢在淘宝上购买自己喜爱的东西,本例以淘宝购物为例,通过定义订单信息类,实现了一个简单的网上购物系统,效果如图9-16所示。
图9-16 网络购物系统
【实现过程】
实现功能有:储存、查询、修改宝贝信息,主要信息为宝贝编号、买家姓名、发货时间、收货地址、预定详情。代码实现如下:
class Yage //订单信息类
{
public:
void getdetail(); //获取信息
void print(); //输出单个订单信息
void display(); //显示初始化化信息
char*getnumber(); //获取作为号使用指针
void setorder(); //修改订单信息
void setdelete(); //删除订单信息
static void statistics();
private:
char number[20]; //宝贝编号
char name[40]; //购买者姓名
char time[20]; //发货时间
char address[40]; //收货地点
char statue; //预定详情
static int sum;
};
int Yage::sum=0;
void Yage::statistics()
{
cout<<"本次操作需要交易数目:"<<sum<<endl;
}
void Yage::getdetail() //接受预定信息
{
cout<<"请输入您要预定的宝贝编号:";
cin.ignore();
cin.getline(number,20,'\n'); //宝贝编号
cout<<"请输入您的姓名:";
cin.getline(name,40,'\n');
cout<<"请输入您要发货时间: ";
cin.getline(time,20,'\n'); //发货时间
cout<<"请输入收货地点:";
cin.getline(address,40,'\n');
cout<<"您确定要预定吗?(f/t):";
cin>>statue;
if(statue=='t') sum++;
}
void Yage::display() //显示初始化化信息
{
cout<<"宝贝编号"<<"\t"<<"购买者姓名"<<"\t"<<"发货时间"<<"\t"<<"收获地点"<<"\t"<<"预定详情"<<endl;
}
void Yage::print() //输出预定信息
{
cout<<number<<"\t\t"<<name<<"\t\t"<<time<<"\t\t"<<address<<"\t\t"<<statue<<endl;
}
char *Yage::getnumber() //获取作为号使用指针
{
return number;
}
void Yage::setorder() //取消订单信息
{ //代码略 }
void Yage::setdelete()
{ //代码略 }
void main()
{
int choice;
while(true)
{ Yage baobei;
//代码略
cin>>choice;
switch(choice)
{ //代码略 }
【案例分析】
本实例功能:
1、我要淘宝信息,逐步显示淘宝信息。如宝贝编号、购买者姓名、发货时间、收获地址、预订详情,各数据间用“\t”格式分隔。
2、输出购买信息是用print()函数,逐步输出宝贝编号、购买信息等。
3、想要修改宝贝信息是调用setorder()函数,即重新把已经存储的数据进行动态修改。
4、使用setdelete()函数进行数据删除操作。
5、退出系统exit(0)。
9.5 对象的创建和撤销
案例9-17 利用多媒体接口函数取得音量值
【案例描述】
这是个多媒体方面的实例。实例定义音量控制类,利用多媒体应用程序接口winmm.lib提供的函数实现音量的各种控制如设置音量大小、静音。本例效果如图9-17所示。
图9-17 利用多媒体接口函数取得音量值
【实现过程】
定义个音量控制类SysVolume,功能进行音量的各种控制;函数GetVolume()得到设备的音量。其代码如下:
#pragma comment(lib,"winmm.lib")
class SysVolume
{
public:
BOOL IsMute(unsigned int dev=0); //检查设备是否静音
BOOL SetMute(bool vol,unsigned int dev=0 ); //设置设备静音
BOOL SetVolume(unsigned int vol,unsigned int dev=0); //设置设备的音量
//得到设备的音量dev=0主音量,1波形,2MIDI,3cd 唱机,4后端输入
unsigned int GetVolume(unsigned int dev=0);
SysVolume(); //构造函数
virtual ~SysVolume(); //析构函数
private:
//下面是输出函数,可以自己调用
//设置音量
BOOL SetVolumeValue(HMIXER hmixer ,MIXERCONTROL *mxc, long volume);
BOOL SetMuteValue(HMIXER hmixer ,MIXERCONTROL *mxc, BOOL mute);
//取得音量
unsigned int GetVolumeValue(HMIXER hmixer ,MIXERCONTROL *mxc);
BOOL GetMuteValue(HMIXER hmixer ,MIXERCONTROL *mxc);
//取得音量控件
BOOL GetVolumeControl(HMIXER hmixer ,long componentType,long ctrlType,MIXERCONTROL* mxc);
};
#endif // !defined(AFX_SYSVOLUME_H__9535AFB9_FC5B_48D6_8FCF_5F8695E183C0__INCLUDED_)
SysVolume::SysVolume()
{
}
SysVolume::~SysVolume()
{
}
//得到设备的音量dev=0主音量,1波形,2MIDI
unsigned int SysVolume::GetVolume(unsigned int dev)
{
long device;
unsigned rt=0;
MIXERCONTROL volCtrl;
HMIXER hmixer;
switch (dev) {
case 1:
device=MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT; break; //Wave对应的值
case 2:
device=MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER; break; //Midi对应的值
default:
device=MIXERLINE_COMPONENTTYPE_DST_SPEAKERS; }
if(mixerOpen(&hmixer, 0, 0, 0, 0)) //打开混音器设备
return rt;
if(!GetVolumeControl(hmixer,device,MIXERCONTROL_CONTROLTYPE_VOLUME,&volCtrl))
return rt;
rt=GetVolumeValue(hmixer,&volCtrl)*100/volCtrl.Bounds.lMaximum;
mixerClose(hmixer); //关闭混音器设备
return rt;
}
【案例分析】
对音频线路的操作流程如下:
(1)通过GetLineInfo()获取指定音频线路的信息,返回一个MIXERLINE结构。
(2)然后通过GetLineControl()获取音频线路相关的控制的通用信息,通过MIXERCONTROL结构返回。
(3)通过GetConrolDetails()获取指定控制的属性值。
(4)通过SetControlDetails()设置指定控制的属性值。
对于每个线路设备,mixer都用一个类型值来表示,比如:Volume对应的值MIXERLINE_ COMPONENTTYPE_DST_SPEAKERS,Midi对应的值为MIXERLINE_COMPONENTTYPE_ SRC_SYNTHESIZER,Wave对应的值为MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT。
案例9-18 利用多媒体接口函数设置音量值
【案例描述】
继续实例9-17的演示代码见实例9-17。介绍设置音量和设置设备静音的具体实现,这两个功能都是利用类的成员函数,通过多媒体接口提供的函数实现,本例效果如图9-18所示。
图9-18 利用多媒体接口函数设置音量值
【实现过程】
定义个类的成员函数SetVolume()和SetMute(),在函数中调用多媒体接口函数。其代码如下:
BOOL SysVolume::SetVolume(unsigned int vol,unsigned int dev)
{
//dev=0,1,2分别表示主音量,波形MIDI和LINE IN
//vol=0-100 表示音量的大小,设置返回的音量的值用的是百分比,即音量从0-100,而不是设备的绝对值
long device;
BOOL rc=FALSE;
MIXERCONTROL volCtrl;
HMIXER hmixer;
switch (dev) {
case 1:
device=MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT; break; //Wave对应的值
case 2:
device=MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER; break; //Midi对应的值
default:
device=MIXERLINE_COMPONENTTYPE_DST_SPEAKERS; }
if(mixerOpen(&hmixer, 0, 0, 0, 0)) //打开混音器设备
return rc;
if(GetVolumeControl(hmixer,device,MIXERCONTROL_CONTROLTYPE_VOLUME,&volCtrl)){
vol=vol*volCtrl.Bounds.lMaximum/100;
if(SetVolumeValue(hmixer,&volCtrl,vol))
rc=TRUE;
}
mixerClose(hmixer);
//如返回false表示设置音量的大小的操作不成功,true表示设置音量的大小的操作成功
return rc;
}
BOOL SysVolume::SetMute(bool vol,unsigned int dev) //设置设备静音
{
//dev=0,1,2分别表示主音量,波形MIDI和LINE IN
//vol=0,1 分别表示取消静音,设置静音
long device;
BOOL rc=FALSE;
MIXERCONTROL volCtrl;
HMIXER hmixer;
switch (dev) {
case 1:
device=MIXERLINE_COMPONENTTYPE_SRC_WAVEOUT; break; //Wave对应的值
case 2:
device=MIXERLINE_COMPONENTTYPE_SRC_SYNTHESIZER; break; //Midi对应的值
default:device=MIXERLINE_COMPONENTTYPE_DST_SPEAKERS; }
if(mixerOpen(&hmixer, 0, 0, 0, 0)) //打开混音器设备
return 0;
if(GetVolumeControl(hmixer,device,MIXERCONTROL_CONTROLTYPE_MUTE,&volCtrl)){
if(SetMuteValue(hmixer,&volCtrl,(BOOL)vol))
//返回值:fals,表示设置音量的大小的操作不成功;true,表示设置音量的大小的操作成功
rc=TRUE;
}
mixerClose(hmixer); //关闭混音器设备
return rc;
}
【案例分析】
上述代码用简单的几个Windows的API函数就实现了控制音频的输入和输出设备,详细的函数介绍可以参见MSDN。mixerOpen()和mixerClose()函数,用来打开和关闭混音器设备,mixerGetNumDevs()可以确定系统中有多少混音器设备。mixerGetDevCaps()函数可以确定混音器设备的能力,mixerGetLineInfo()可以检索指定音频线路的信息,mixerGetLineControls()用于检索一个或者多个与音频线路相关的控制的通用信息,mixerGetControlDetails()用于检索与某个音频线路相关的一个控制的属性,mixerSetControlDetails()用于设置制定控制的属性。
注意:其实有关这几个函数的定义可以在C:\Program Files\Microsoft Visual Studio\VC98\Include\mmsystem.h文件中找到。
9.6 复制构造函数
深拷贝指的就是当拷贝对象中有对其他资源(如堆、文件、系统等)的引用时(引用可以是指针或引用),对象的另开辟一块新的资源,而不再对拷贝对象中有对其他资源的引用的指针或引用进行单纯的赋值拷贝。和浅拷贝的区别是在对象状态中包含其他对象的引用的时候,当拷贝一个对象时,如果需要拷贝这个对象引用的对象,则是深拷贝,否则是浅拷贝。
案例9-19 入学登记系统
【案例描述】
默认的拷贝构造函数把初始值对象的每个成员的值都复制到新建立的对象中。实际上并不是合适的,因为它完成的是浅拷贝,因将两个对象简单复制后,两个指针指向的是同一内存地址,并没有形成真正的副本,运行会出现错误。本实例演示定义学生类,利用资源复制函数输出学生信息,效果如图9-19所示。
图9-19 入学登记系统
【实现过程】
程序有学生类student,私有成员id、name、sex、age为学号、姓名、性别和年龄,构造函数实现赋值姓名、性别和年龄,资源复制函数copy(student & s),输出学生信息函数disp()。其代码如下:
#include <string.h>
#include <iostream.h>
#include <iostream>
class student
{
public:
//构造函数
student(char * pName="no name",int ssId=0,char pSex='M',unsigned int pAge=12)
{
id=ssId; //赋值操作
name=new char[strlen(pName)+1];
strcpy(name,pName);
sex=pSex;
age=pAge;
cout<<"新建一个学生资料 "<<pName<<endl;
}
//错误!不能如此编写copy函数,因为实现的是前浅拷贝
/* void copy(student & s)
{
cout<<"construct copy of"<<s.name<<endl;
strcpy(name,s.name);
id=s.id; sex=s.sex; age=s.age;
}*/
void copy(student & s) //资源复制函数
{
if(this==&s)
{
cout<<"错误:不能将一个对象复制到自己!"<<endl;
return;
}
else
{
name=new char[strlen(s.name)+1]; //①分配新的堆内存
strcpy(name,s.name); //②完成值的复制
id=s.id; sex=s.sex; age=s.age;
cout<<"资源复制函数被调用!"<<endl;
}
}
void disp() //输出信息
{
cout<<"姓名:"<<name<<" 学号:"<<id<<
" 性别:"<<sex<<" 年龄:"<<age<<endl;
}
~student()
{
cout<<"Destruct "<<name<<endl;
delete name;
}
private:
int id; //学号
char * name; //姓名
char sex; //性别
unsigned int age; //年龄
};
void main()
{
// 调用普通的构造函数
student a("LiMing",12,'M',13),b("Tom",23,'W',14);
a.disp();//
b.disp();
a.copy(a); //显示对象a的信息
b.copy(a); //调用资源复制函数
a.disp(); //输出信息
b.disp();
system("pause");
}
【案例分析】
资源复制函数copy中代码name=new char[strlen(s.name)+1];,为分配新的堆内存属深拷贝。如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
注意:如果实行浅拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也同时指向同一块内存。这就出现了问题:当B把内存释放了(如析构),这时A内的指针就是野指针了,出现运行错误。
9.7 特殊数据成员
案例9-20 一个类的对象作为另一个类的成员
【案例描述】
一个类的对象作为另一个类的数据成员,如果一个类A的对象作为另一个类B的数据成员,则在类B的对象创建过程中,调用其构造函数的过程中,数据成员(类A的对象)会自动调用类A的构造函数,效果如图9-20所示。
图9-20 一个类的对象作为另一个类的成员
【实现过程】
定义的类Line,在对类Line的对象进行初始化时,必须首先初始化其中的子对象Point,即必须首先调用这些子对象的构造函数。因此,类Line的构造函数的定义格式应为:Line::Line(参数表0):成员1(参数表1),成员2(参数表2),…,成员n(参数表n){ ……}。其代码如下:
#include <iostream>
using namespace std;
class Point{
int a,b;
public:
void Set(int m,int n){
a=m;
b=n;
}
int GetA(){
return a;
}
int getB(){
return b;
}
};
class Line{
Point P;
int H,W;
public:
void Set(int m,int n,int h,int w);
Point * GetP();
int GetHeight(){
return H;
}
int GetWidth(){
return W;
}
};
void Line::Set(int m,int n,int h,int w){
P.Set(m,n);
H=h;
W=w;
}
Point * Line::GetP()
{
return &P;
}
int main(int argc, char** argv) {
Line p1;
p1.Set(10,2,25,20);
cout<<p1.GetHeight()<<","<<p1.GetWidth()<<endl;
Point *p=p1.GetP();
cout<<p->GetA()<<","<<p->getB()<<endl;
return 0;
}
【案例分析】
(1)如果类A的构造函数为有参函数时,则在程序中必须在类B的构造函数的括号后面加一“:”和被调用的类A的构造函数,且调用类A的构造函数时的实参值必须来自类B的形参表中的形参。这种方法称为初始化表的方式调用构造函数。
(2)在构造新类的对象过程中,系统首先调用其子对象的构造函数,初始化子对象;然后才执行类X自己的构造函数,初始化类中的非对象成员。
(3)对于同一类中的不同子对象,系统按照它们在类中的说明顺序调用相应的构造函数进行初始化,而不是按照初始化表的顺序。
9.8 特殊函数成员
案例9-21 类的静态成员
【案例描述】
静态成员是属于整个类的而不是某个对象,静态成员变量只存储一份供所有对象共用。类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象,效果如图9-21所示。
图9-21 类的静态成员
【实现过程】
建立一个点类Point,类中定义一个私有静态整型变量m_nPointCount,构造函数m_nPointCount加1,析构函数m_nPointCount减去1,,用一个静态成员变量输出m_nPointCount的值。其代码如下:
#include <stdio.h>
class Point
{
public:
Point()
{
m_nPointCount++;
}
~Point()
{
m_nPointCount--;
}
static void output()
{
printf("%d\n", m_nPointCount);
}
private:
static int m_nPointCount;
};
int Point::m_nPointCount = 0;
void main()
{
Point pt;
pt.output();
}
【案例分析】
(1)静态成员函数中不能调用非静态成员。
(2)非静态成员函数中可以调用静态成员。因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的。
(3)非静态成员函数中可以调用静态成员。因为静态成员属于类本身,在类的对象产生之前就已经存在了,所以在非静态成员函数中是可以调用静态成员的。
9.9 对象的组织
案例9-22 对象数组
【案例描述】
类是对象的抽象,可以使用一个类来定义很多的对象,然后每个对象都有自己的属性。当我们使用类来定义很多相同结构的对象的时候,我们可以采取对象数组的方法,效果如图9-22所示。
图9-22 对象数组
【实现过程】
建立一个对象数组,内放5个学生的数据(学号,成绩),设立一个函数max,在max函数中找出5个学生中成绩最高者,在建立对象数组时,分别调用构造函数,对每个元素初始化。每一个元素的实参分别用括号括起来,对应构造函数的一组形参。其代码如下:
#include <strstream>
#include <iostream>
using namespace std;
int main()
{
char cStr[50] = "12 34 65 -23 -32 33 61 99 321 32";
int nArray[10];
cout << "Old cStr: " << cStr << endl;
// 以字符串cStr作为输入流,建立strin与cStr的关联,缓冲区大小为字符串cStr的大小
istrstream strin(cStr, sizeof(cStr));
int i, temp;
for (i = 0; i < 10; i++)
strin >> nArray[i];
// 输出读入的数组
cout << "nArray: ";
for (i = 0; i < 10; i++)
cout << nArray[i] << " ";
cout << endl;
for (i = 0; i < 5; i++) // 数组旋转
{
temp = nArray[i];
nArray[i] = nArray[9 - i];;
nArray[9-i] = temp;
}
ostrstream strout(cStr, sizeof(cStr));
for (i = 0; i < 10; i++)
strout << nArray[i] << " ";
strout << endl; // 加入'\0'
cout << "New cStr: " << cStr << endl; // 输出最新的cStr
return 0;
}
【案例分析】
案例中,一个班有50个学生,先声明了一个学生类,该类的学生具有相同的数据成员和成员函数。在建立数组时,同样要调用构造函数。如果有50个元素,就需要调用50次构造函数。在需要的时候,可以在定义数组时提供实参以实现初始化。如果构造函数只有一个参数可以这样初始化Studet stud[3]={60,70,80};如果构造函数有多个参数时Student stud[3]={ Student(10,20,30),Student(40,50,60),Student(70,80,90)};。
9.7 为对象动态分配内存
案例9-23 通讯录
【案例描述】
实际编程中经常要管理信息数据,实现输入、编辑、修改数据等功能。下面简单介绍一个通讯录的例子,首先定义一个通讯录的数据结构,然后输入数据,最后显示通讯录的内容。本例效果如图9-23所示。
【实现过程】
(1)建立结构struct friends,结构中再嵌套struct date结构,代码如下:
图9-23 通讯录
struct date
{
int year; int month; int day; //定义一个出生日期的结构
};
struct friends
{ //定义名字、性别、电话、出生日期和指向下一个链表
char name[10]; char sex;char tel[12]; date birthday;friends* next;
};
int n; //动态分配结构friends的个数
(2)创建结构struct friends* head,*p1,*p2; ,然后输入通讯录的数据,代码如下:
struct friends* create() //新建链表,此函数返回一个指向链表头的指针
{
struct friends* head,*p1,*p2;
n=0;
p1=p2=new friends; //新建链表结构
cout<<"Input my friends'name ,with'#' to end:"<<endl;
cin>>p1->name;
cout<<"Continue to input my friends'sex(F/M),tel and birthday:"<<endl;
cin>>p1->sex;
cin>>p1->tel;
cin>>p1->birthday.year>>p1->birthday.month>>p1->birthday.day;
head=NULL;
while (strcmp(p1->name,"#")!=0) //如果输入名字字符串为#,表示输入结束
{
n++;
if(n==1)
head=p1;
else
p2->next=p1;
p2=p1;
p1=new friends; //新建结构
cout<<"Input my friends'name ,with'#' to end:"<<endl;
cin>>p1->name;
if(strcmp(p1->name,"#")!=0) //如果输入一个名字并以#结束
{
cout<<"Continue to input my friends'sex(F/M),tel and birthday:"<<endl;
cin>>p1->sex;
cin>>p1->tel;
cin>>p1->birthday.year>>p1->birthday.month>>p1->birthday.day;
}
}
p2->next=NULL;
return head;
}
void print(struct friends* head) //输出链表
{
struct friends* p;
p=head;
cout<<setw(11)<<"name"<<setw(5)<<"sex"<<setw(12)<<"telNO."
<<setw(16)<<"birthday"<<endl;
while(p!=NULL) //输出链表数据,直到为空,下列代码固定输出结构
{
cout<<setw(11)<<p->name<<setw(4)<<p->sex<<setw(16)<<p->tel<<" ";
cout<<setw(5)<<p->birthday.year;
cout<<setw(3)<<p->birthday.month<<" ";
cout<<setw(2)<<p->birthday.day<<endl;
p=p->next; //链表指向下一个
}
}
(3)下面是主程序,创建friends*s,先输入数据,然后显示数据,代码如下:
void main()
{
friends*s; //定义结构
s=create();
system("pause");
print(s);
system("pause");
}
【案例分析】
(1)该实例先定义好一个数据结构,然后输入数据。truct friends* create()用于新建链表,此函数返回一个指向链表头的指针。int n;的作用是动态分配结构friends的个数。
(2)void print定义输出链表,读出内存并显示。因输出是一个链表,故采取循环读取原理,指针指向内存的头,直到读出为NULL。函数setw()输出字符宽度,如setw(11)表示输出11个字符的宽度。
注意:动态申请内存时,在实际编程中可以灵活地使用内存空间,不过申请使用完后要注意释放内存。
9.7 本章练习