编写通用的功能模块,处理多种对象类型。关于这种用法,上面已经讨论许多,这里作简要总结。
q 处理的多种对象应该有共性,可以封装在一个类家族中。这个类家族的理想结构是多向二层次的(如图2-4所示),或者是单向多层次的(如图2-5所示)。
图2-4 多向二层次类家族 图2-5 单向多层次类家族
q 在基类中定义若干公共虚拟函数,派生类对其部分或全部重载,但重载的虚函数应该与基类的声明一致,即参数列表一致(当然可以省掉virtual关键字)。
q 每个类都封装一种处理对象的数据集和操作集。其中,几个处理对象的共性部分封装在相对的基类中;每个处理对象的特性操作封装在重载的虚函数中。
q 定义通用函数,其形参之一是基类的指针或引用。
以上是一般的应用模式,随着需求的不同会有一定差异。
2.4.1 不改变父类的代码,可以改变父类的行为
下面举例说明这一用法。示例2.2定义了一个Rectangle类,用于计算矩形面积、输出矩形信息。
示例清单2.2
#include "stdio.h"
class Rectangle
{
public:
Rectangle(){ m_cx=0;m_cy=0;};
Rectangle(float cx,float cy)
{
m_cx=cx;
m_cy=cy;
}
//声明为保护,为了给子类继承
protected:
//m_cx是水平宽度,m_cy是垂直高度
float m_cx;
float m_cy;
public:
//加const关键字,该函数不能直接或间接更改类成员
//在简单并且使用率高的成员函数前加inline关键字,可以提高效率
inline float GetCx() const
{return m_cx;}
inline float GetCy() const
{return m_cy;}
inline void SetCx(float cx)
{ m_cx=cx;}
inline void SetCy(float cy)
{ m_cy=cy;}
void DispRectInfo();
protected:
//因为该虚函数只在类的内部使用,所以声明为保护。在本例中也可以声明为私有
virtual float CalculateArea();
};
void Rectangle::DispRectInfo()
{
printf("the cx=%f\n",GetCx());
printf("the cy=%f\n",GetCy());
printf("the area=%f\n",CalculateArea());
}
float Rectangle::CalculateArea()
{ //该虚函数用于计算矩形面积
return m_cx*m_cy;
}
int main(float argc, char* argv[])
{
Rectangle rect(20,30);
rect.DispRectInfo();
return 0;
}
程序输出:
the cx=20.000
the cy=30.000
the area=600.000
如果现在要修改示例2.2的功能,增加对平行四边形的处理,可以定义一个Rectangle的派生类Parallelogram。其代码如下:
class Parallelogram :public Rectangle
{
public:
Parallelogram(float cx,float cy,float height):Rectangle(cx,cy)
{ m_height=height;}
Parallelogram(){ m_height=0;}
protected:
float m_height;
public:
inline float GetHeight()const
{return m_height;}
inline void SetHeight(float height){m_height=height;}
//定义用于输出平行四边形信息的函数
void DispParalleInfo();
protected:
//重载虚函数,计算平行四边形面积,而不是矩形面积
float CalculateArea();
};
void Parallelogram::DispParalleInfo()
{
printf("the height=%f\n",m_height);
//调用基类的成员函数,输出边和面积信息
DispRectInfo();
}
float Parallelogram::CalculateArea()
{
//计算平行四边形面积
return m_cx*m_height;
}
程序段
{
Parallelogram para(20,30,10);
para.DispParalleInfo();
}
的输出结果:
the height=10.000
the cx=20.000
the cy=30.000
the area=200.000
由输出结果可知,Rectangle::DispRectInfo()并没有调用Rectangle::CalculateArea(),而是调用了Parallelogram::CalculateArea()。边长同样是20和30,但输出结果不同。是的,父类的行为被改变了。
2.4.2 不知道对象的类型,可以执行对象的特定操作
如果正在设计一个数据库管理系统,那就必然要做数据录入的工作。面对各种数据类型如整形、浮点、特殊格式的字符串等,我们往往使用CEdit类进行处理。使用CEdit类的明显缺点是,在录入时要逐个进行专门校验,提交数据时,要记住每个CEdit控件关联的数据类型。
对于以上问题,可以从CEdit派生一个类,假设名为CCustEdit,在类中定义若干虚函数。然后针对每种数据类型,从CCustEdit派生出子类进行封装。子类通过重载虚函数实现录入校验、提交处理等工作。例如处理数据提交时,可以从控件父窗体逐一取得每个控件的CCustEdit*型的指针,调用其相应的虚函数,得到正确格式的数据。
示例2.3是生成insert SQL语句的演示程序。假设insert语句中,数值类型不加单引号,字符串类型加单引号。例如:
insert into user_database ( name, age, stature, weight) values(‘XinChangAn’,27,1.65,125.22)
本例程序定义了一个CCustEdit基类,由其派生出3个类CIntEdit、CFloatEdit、CVcharEdit分别封装对整型、浮点型、字符串型数据的处理。CCustEdit::BuildFields()函数用于构造insert语句的字段列表部分;子类重载的虚拟函数BuildValues()用于构造值列表部分。这两部分最后在main()中被连接起来,形成一个完整的SQL语句。
示例清单2.3
/C123.H
//insert语句字段列表以前的长度
#define MAXSIZE_SQL_FIELDS 200
//insert语句值列表以后的长度
#define MAXSIZE_SQL_VALUES 200
//name字段的最大长度
#define MAXSIZE_NAME_FIELD 20
//字段名称最大长度
#define MAXSIZE_FIELDNAME 20
//语句中最多字段数
#define MAX_FIELDSCOUNT 50
//基类CCustEdit的定义
class CCustEdit
{
public:
CCustEdit();
CCustEdit(const char* FieldName);
virtual ~CCustEdit(){};
public:
void BuildFields(char * Statement);
//基类定义虚函数,为子类提供重载的形式
void virtual BuildValues(char * Statement);
void SetFieldName(const char * FieldName);
protected:
//用于存储控件关联的字段名称
char m_FieldName[MAXSIZE_FIELDNAME];
};
//子类CIntEdit的定义
class CIntEdit:public CCustEdit
{
public:
CIntEdit();
CIntEdit(const char * FieldName,int number);
virtual ~CIntEdit(){};
public:
void virtual BuildValues(char * Statement);
CIntEdit& operator =(int iValue);
void SetValue(int iValue);
int GetValue()const;
private:
//用于封装整型字段值
int m_data;
};
//子类CFoatEdit的定义
class CFloatEdit:public CCustEdit
{
public:
CFloatEdit();
CFloatEdit(const char* FieldName,float number);
virtual ~CFloatEdit(){};
public:
void virtual BuildValues(char * Statement);
CFloatEdit& operator =(float fValue);
void SetValue(float fValue);
float GetValue()const;
private:
//用于封装浮点型字段值
float m_data;
};
//子类CVcharEdit的定义
class CVcharEdit:public CCustEdit
{
public:
CVcharEdit();
CVcharEdit(const char * FieldName,const char * str);
virtual ~CVcharEdit(){};
public:
void virtual BuildValues(char * Statement);
CVcharEdit& operator =(const char* sValue);
void SetValue(const char * sValue);
void GetValue(char * sValue,unsigned int MaxCount)const;
private:
//用于封装字符串型字段值
char m_data[MAXSIZE_NAME_FIELD];
};
///C123.CPP
#include "stdio.h"
#include "string.h"
#include "stdlib.h"
#include "C123.h"
//基类CCustEdit的实现
CCustEdit::CCustEdit()
{
memset(m_FieldName,0,MAXSIZE_FIELDNAME);
}
CCustEdit::CCustEdit(const char* FieldName)
{
//形参FieldName用于初始化m_FieldName
memset(m_FieldName,0,MAXSIZE_FIELDNAME);
if(NULL==FieldName)
return;
if(strlen(FieldName)>=MAXSIZE_FIELDNAME)
{ printf("the field name %s is too long,truncated it",FieldName);
strncpy(m_FieldName,FieldName,MAXSIZE_FIELDNAME-1);
}
else
strcpy(m_FieldName,FieldName);
}
void CCustEdit::BuildValues(char * Statement)
{
printf("insert statement is %s\n",Statement);
}
void CCustEdit::BuildFields(char * Statement)
{
//构造语句的字段列表部分,即将字段名m_FieldName和一个','字符追加到实参Statement中
if(NULL==Statement)
return;
if(strlen(Statement)>MAXSIZE_SQL_FIELDS-strlen(m_FieldName)-2)
printf("out of buffer when build statement with fields\n");
else
sprintf(Statement+strlen(Statement),"%s,",m_FieldName);
}
void CCustEdit::SetFieldName(const char * FieldName)
{
//形参FieldName用于设置m_FieldName
if(NULL==FieldName)
return;
memset(m_FieldName,0,MAXSIZE_FIELDNAME);
if(strlen(FieldName)>=MAXSIZE_FIELDNAME)
{ printf("the field name %s is too long,truncated it",FieldName);
strncpy(m_FieldName,FieldName,MAXSIZE_FIELDNAME-1);
}
else
strcpy(m_FieldName,FieldName);
}
子类CIntEdit的实现
CIntEdit::CIntEdit(const char * FieldName,int number):CCustEdit(FieldName)
{
m_data=number;
}
CIntEdit& CIntEdit::operator =(int iValue)
{
m_data=iValue;
return *this;
}
void CIntEdit::SetValue(int iValue)
{
*this=iValue;
}
int CIntEdit::GetValue()const
{
return m_data;
}
void CIntEdit::BuildValues(char * Statement)
{
//构造语句的值列表部分,即将m_data和一个','字符追加到实参Statement中
if(NULL==Statement)
return;
char sTemp[20];
_itoa(m_data,sTemp,10);
if(strlen(Statement)>MAXSIZE_SQL_VALUES-strlen(sTemp)-2)
printf("out of buffer when build statement with values\n");
else
sprintf(Statement+strlen(Statement),"%d,",m_data);
}
//子类CFoatEdit的实现
CFloatEdit::CFloatEdit()
{
m_data=0;
}
CFloatEdit::CFloatEdit(const char* FieldName,float number):CCustEdit(FieldName)
{
m_data=number;
}
CFloatEdit& CFloatEdit::operator =(float fValue)
{
m_data=fValue;
return *this;
}
void CFloatEdit::SetValue(float fValue)
{
*this=fValue;
}
float CFloatEdit::GetValue()const
{
return m_data;
}
void CFloatEdit::BuildValues(char * Statement)
{
//构造语句的值列表部分,即将m_data和一个','字符追加到实参Statement中
if(NULL==Statement)
return;
if(strlen(Statement)>MAXSIZE_SQL_VALUES-14)
printf("out of buffer when build statement with values\n");
else
sprintf(Statement+strlen(Statement),"%8.3f,",m_data);
}
///子类CVcharEdit的实现
CVcharEdit::CVcharEdit()
{
memset(m_data,0,MAXSIZE_NAME_FIELD);
}
CVcharEdit::CVcharEdit(const char * FieldName,const char * str):CCustEdit(FieldName)
{
//形参str用于初始化m_data
memset(m_data,0,MAXSIZE_NAME_FIELD);
if(NULL==str)
return;
if(strlen(str)<MAXSIZE_NAME_FIELD)
strcpy(m_data,str);
else
strncpy(m_data,str,MAXSIZE_NAME_FIELD-1);
}
void CVcharEdit::BuildValues(char * Statement)
{
//构造语句的值列表部分,即将m_data和一个','字符追加到实参Statement中
if(NULL==Statement)
return;
if(strlen(Statement)>MAXSIZE_SQL_VALUES-strlen(m_data)-4)
printf("out of buffer when build statement with values\n");
else
sprintf(Statement+strlen(Statement),"\'%s\',",m_data);
}
CVcharEdit& CVcharEdit::operator =(const char* sValue)
{
//形参str用于设置m_data
if(NULL==sValue)
return *this;
memset(m_data,0,MAXSIZE_NAME_FIELD);
if(strlen(sValue)<MAXSIZE_NAME_FIELD)
strcpy(m_data,sValue);
else
strncpy(m_data,sValue,MAXSIZE_NAME_FIELD-1);
return *this;
}
void CVcharEdit::SetValue(const char * sValue)
{
*this=sValue;
}
void CVcharEdit::GetValue(char * sValue,unsigned int MaxCount)const
{
if(NULL==sValue)
return ;
if(strlen(m_data)>=MaxCount)
strncpy(sValue,m_data,MaxCount-1);
else
strcpy(sValue,m_data);
}
/*******************************************************************
全局的构造SQL语句帮助器函数
EditList 封装字段名和字段值的控件的列表。在这里不必关心每个控件的类型,利用每个
控件的虚函数,可以方便地构造出SQL语句
InsStaFields 用于保存insert语句字段列表以前部分
InsStaValues 用于保存insert语句值列表以后部分
********************************************************************/
void gBuildHelper(CCustEdit** EditList,char*InsStaFields,char * InsStaValues)
{
for(int i=0;i<MAX_FIELDSCOUNT&&EditList[i]!=NULL;i++)
{
EditList[i]->BuildFields(InsStaFields);
EditList[i]->BuildValues(InsStaValues);
}
}
int main(int argc, char* argv[])
{
char InsertStatement[MAXSIZE_SQL_FIELDS+MAXSIZE_SQL_VALUES+2];
char InsStaFields[MAXSIZE_SQL_FIELDS];
char InsStaValues[MAXSIZE_SQL_VALUES];
CVcharEdit Name("name","XinChangAn");
CIntEdit Age("age",27);
CFloatEdit Stature("stature",(float)1.65);
CFloatEdit Weight("weight",(float)125.22);
CCustEdit **EditList;
EditList=new CCustEdit*[5];
EditList[0]=&Name;
EditList[1]=&Age;
EditList[2]=&Stature;
EditList[3]=&Weight;
EditList[4]=NULL;
strcpy(InsStaFields,"insert into user_database(");
strcpy(InsStaValues,"values(");
//使用控件列表构造SQL语句
gBuildHelper(EditList, InsStaFields,InsStaValues);
//将最后一个字符','改为')'
InsStaFields[strlen(InsStaFields)-1]=')';
InsStaValues[strlen(InsStaValues)-1]=')';
//连接两段语句到InsertStatement中
strcpy(InsertStatement,InsStaFields);
strcat(InsertStatement,InsStaValues);
//输出完整的SQL语句
printf("%s\n",InsertStatement);
delete[] EditList;
return 0;
}
程序输出的结果:
insert into user_database(name,age,stature,weight)values('XinChangAn',27, 1.650, 125.220)
以上讨论了虚函数的两种应用情况。现在读者可以得出这样的结论:每一种应用情况都是从不同的应用角度考虑问题的结果,其实质都是利用虚函数的动态联编,编写相对通用的模块。
2.4.3 如果类包含虚拟成员函数,则将此类的析构函数也定义为虚拟函数
这是很必要的,因为派生类对象往往由基类的指针引用,如果使用new操作符在堆中构造派生类对象,并将其地址赋给基类指针,那么最后要使用delete操作符删除这个基类指针(释放对象占用的堆栈)。这时如果析构函数不是虚拟的,派生类的析构函数不会被调用。例如示例2.4的程序将产生内存泄漏。
示例清单2.4
#include "stdio.h"
#include "string.h"
class Ca
{
public:
Ca(){m_Style=0;}
Ca(int style){m_Style=style;}
~Ca(){}
virtual void OutputValue()
{
printf("%d\n",m_Style);
}
private:
int m_Style;
};
class Cb:public Ca
{
public:
Cb(){ m_pTitle=NULL;}
Cb(const char* Title);
~Cb()
{
if(NULL!=m_pTitle)
delete[] m_pTitle;
}
virtual void OutputValue()
{
if(NULL!=m_pTitle)
printf("%s\n",m_pTitle);
}
private:
char *m_pTitle;
};
Cb::Cb(const char* Title)
{
if(NULL==Title)
m_pTitle=NULL;
else
{ m_pTitle=new char[strlen(Title)+1];
strcpy(m_pTitle,Title);
}
}
int main(int argc, char* argv[])
{ //在堆栈中构造派生类对象,由基类指针引用
Ca* pa=new Cb("bill_server");
pa->OutputValue();
/*删除堆栈中的对象,释放内存。但派生类的析构函数没有被调用,
其中的释放内存操作也就无法完成,即Cb::m_pTitle没有得到释放。*/
delete pa; //虽然可以改写为delete (Cb*)pa; 但必须预先知道转换的类型
return 0;
}
要解决这一问题很简单,只需在基类的析构函数前加virtual关键字即可,不必惊动派生类的代码。