- 论坛徽章:
- 0
|
6.4.43 实现类的属性和行为
完成上面的分析与设计之后,小陈深深地伸了个懒腰。他知道,只要完成了程序中类的属性和行为的分析和设计,整个程序就相当于完成了一大半。接下来的工作,不过就是依葫芦画瓢,用C++这种开发语言将之前的设计表达出来,形成具体的程序而已。
按照之前的设计与分析,小陈决定首先实现最基础的Employee类:
// SalarySys.cpp
#include <ctime> // 使用其中的时间函数
#include <string> // 使用字符串对象
using namespace std;
// 枚举员工的级别
enum EmpLevel
{
enumOfficer = 1, // 高级员工
enumStaff = 2 // 一般员工
};
// 员工类
class Employee
{
public:
// 构造函数,根据员工的姓名和入职年份构造对象
Employee(string strName, int nY)
:m_strName(strName),m_nYear(nY) // 设定员工的姓名和入职年份
{}
// Employee类的行为,这些行为都是供外界调用的,
// 所以将其访问级别设定为public
public:
// 获得员工姓名
string GetName()
{
return m_strName;
}
// 获得员工入职年份
int GetYear()
{
return m_nYear;
}
// 获得员工级别
EmpLevel GetLevel()
{
return m_nLevel;
}
// 获得员工工资,因为这个行为同具体的员工类相关,
// 不同的派生类有不同的行为(计算方法),所以在基类Employee中只是
// 用纯虚函数表示接口,具体内容由其派生类实现
virtual int GetSalary() = 0;
// GetWorkTime()只是供自身和自己的派生类似用,所以将其
// 访问级别设定为protected
protected:
// 获得在职时间,也就是现在年份减去入职年份
// 因为这个行为只是供其派生类计算工资时使用,
// 所以将其访问级别设定为protected
int GetWorkTime()
{
// 获得现在年份
time_t t = time(NULL);
struct tm* now = localtime(&t);
// time()函数获得的时间是以1900年为起点,所以这里需要
// 加上1900。同时,不满一年按照一年计算,所以最后要加1
return now->tm_year + 1900 - m_nYear + 1;
}
// Employee类的属性
// 因为这些属性也同样应当是其派生类具有的,需要由基类遗传给
// 它的派生类,所以这里使用protected访问控制,允许其派生类继承这些属性
protected:
string m_strName; // 姓名
int m_nYear; // 入职年份
EmpLevel m_nLevel; // 级别
};
完成Employee类的实现后,就好像造房子打好了地基,小陈接着在其基础上,派生出具体的员工类Officer和Staff,分别完成具体的工资计算:
// 高级员工类
// 因为高级员工也是员工的一种,所以它可以从Employee员工类派生
class Officer : public Employee
{
public:
// 构造函数
// 调用基类Employee的构造函数,完成相同部分的构建
Officer(string strName, int nY)
:Employee(strName,nY)
{
// 进行派生类独有的构建,设定员工的级别
m_nLevel = enumOfficer;
}
public:
// 对基类的纯虚函数进行定义,具体实现计算工资的行为
virtual int GetSalary()
{
return GetWorkTime()*5000;
}
};
// 普通员工类
class Staff : public Employee
{
public:
Staff(string strName, int nY)
:Employee(strName,nY)
{
m_nLevel = enumStaff;
}
public:
// 不同的派生类对相同的行为有不同的实现,
// 这就是类的多态机制的体现
virtual int GetSalary()
{
return GetWorkTime()*1000;
}
};
在员工类及其派生类的实现中,全面体现了面向对象的三大特征。首先,我们将所有员工,包括高级员工和普通员工的共有属性和行为封装成员工类Employee这个基类,这里体现的是类对属性和行为的封装;然后使用面向对象的继承机制从员工类Employee中派生出高级员工类Officer和普通员工类Staff,这样使得这两个派生类可以复用基类的代码,无须重复实现基类已有的属性和行为,例如员工的姓名和入职时间等共有属性,以及供外界访问的GetName()等接口函数。派生类所要做的,只是实现自己特有的属性和行为。例如,两个派生类各自对工资的计算方式不同,所以利用面向对象多态的机制,它们对基类提供的纯虚函数进行自定义,各自完成自己的GetSalary()这一函数的定义。
完成了具体的员工类的实现,接下来就是对这些员工对象进行管理的,最核心的SalarySys类。按照前面的设计,小陈用一个数组来保存这些员工对象的指针,同时又分别实现了SalarySys类的各个行为,对这些员工对象进行输入、查询和输出:
// 引入需要的头文件
#include <iostream> // 屏幕输入输出
#include <fstream> // 文件输入输出
// 定义SalarySys中数组的最大数据量,
// 也就是SalarySys最多能处理多少个数据
const int MAX = 100000;
// 工资管理类SalarySys
class SalarySys
{
public:
// 构造函数,对属性进行初始化
SalarySys()
:m_nCount(0), // 设定当前数据量为0
m_strFileName("SalaryData.txt") // 设定员工数据文件名
{
// 对数组进行初始化
for(long i = 0; i<MAX; ++i)
{
m_arrEmp[i] = NULL;
}
// 读取员工数据文件
Read();
}
// 析构函数
~SalarySys()
{
// 将员工数据写入文件
Write();
// 释放已经创建的员工对象
for(long i = 0; i<m_nCount; ++i)
{
delete m_arrEmp[i]; // 释放对象
m_arrEmp[i] = NULL; // 将指针设置为NULL
}
}
// SalarySys的公有行为
public:
// 从员工数据文件读取已经输入的数据
int Read()
{
// 用于文件读取的中间变量
string strName = "";
int nLevel = 0;
int nYear = 0;
// 读取的数据个数
int i = 0;
// 打开数据文件
ifstream in(m_strFileName);
if(in.is_open())
{
// 如果打开文件成功,构造无限循环进行读取
while(true)
{
// 分别读取姓名、级别和入职年份
in>>strName>>nLevel>>nYear;
// 判断是否读取正确,如果读取错误,
// 例如读取到达文件末尾,则结束读取
if(!in)
break;
// 根据读取的员工级别,分别创建不同的员工对象,
// 并保存到m_arrEmp数组进行管理
if( enumOfficer == nLevel)
{
// 根据员工姓名和入职年份,创建高级员工对象
m_arrEmp[i] = new Officer(strName,nYear);
}
else if ( enumStaff == nLevel)
{
m_arrEmp[i] = new Staff(strName,nYear);
}
++i; // 记录已经读取的数据数量
// 如果读取的数量大于数组容量,则结束读取,否则开始下一次读取
if(i>=MAX)
break;
}
// 输入完毕,关闭文件
in.close();
}
// 输出读取结果并返回读取的数据个数
cout<<"读取"<<i<<"个员工数据"<<endl;
m_nCount = i;
return i;
}
// 将员工数据写入文件
void Write()
{
// 打开数据文件作为输出
ofstream o(m_strFileName);
if(o.is_open())
{
// 如果成功打开文件,则利用for循环逐个输出数组中保存的数据
for(int i = 0;i < m_nCount;++i)
{
Employee* p = m_arrEmp[i];
o<<p->GetName()<<"\t" // 名字
<<p->GetLevel()<<"\t" // 级别
<<p->GetYear()<<endl; // 入职年份
}
// 输出完毕,关闭文件
o.close();
}
}
// 手工输入员工数据
int Input()
{
// 新输入的数据保存在数组已有数据之后,
// 所以这里将已有数据个数m_nCount作为输入起点
int i = m_nCount;
// 提示输入
cout<<"请输入员工信息(名字 等级 入职年份),例如:Jiawei 1 1983"<<endl;
cout<<"-1表示输入结束"<<endl;
for(; i < MAX; ++i) // 初始化语句留空
{
// 利用for循环逐个输入
cout<<"请输入"<<i<<"号员工的信息:"<<endl;
// 根据输入的数据创建具体的员工对象,并保存到数组
string strName = "";
int nL = 0;
int nY = 0;
// 获取用户输入
cin>>strName>>nL>>nY;
// 对输入情况进行判断处理
if(!cin) // 如果输入错误,则重新输入
{
cout<<"输入错误,请重新输入"<<endl;
cin.clear(); // 清理输入标志位
cin.sync(); // 清空键盘缓冲区
--i; // 本次输入作废,不计算在内
continue; // 开始下一次输入循环
}
else // 输入正确
{
// 检查是否输入结束
if("-1" == strName)
{
break; // 结束输入循环
}
// 根据输入的数据,创建具体的员工对象并保存到数组
if(enumOfficer == nL)
m_arrEmp[i] = new Officer(strName,nY);
else if(enumStaff == nL)
m_arrEmp[i] = new Staff(strName,nY);
}
}
// 输入完毕,调整当前数组中的数据量
m_nCount = i;
// 返回本次输入的数据个数
return i;
}
// 获得最高工资的员工对象
Employee* GetMax()
{
// 表示结果的指针,初始值为NULL
Employee* pMax = NULL;
// 设定一个假想的当前最大值
int nMax = INT_MIN;
// 用for循环遍历数组中的每一个对象
for(int i = 0;i < m_nCount; ++i)
{
// 如果当前对象的工资高于当前最大值nMax,则将当前最大值
// 替换为当前对象的工资,并将当前对象的指针作为结果保存
// 这里使用的是基类Employeed 的指针,调用基类的公有接口函数 // GetSalry()。但是因为这个接口函数是虚函数,所以实际上,它们
// 将调用这个指针所指向的实际对象的相应函数。例如,这里通过指针
// pMax对GetSalary() 函数的调用,编译器将根据pMax这个基类
// 指针所指向的具体对象是Office还是Staff来决定到底是调用
// Officer类中的GetSalary()实现还是Staff类中的GetSalary()
// 实现。这就是面向对象中多态的体现——基类的指针,调用的却是
// 它所指向的具体派生类对象的函数。
if(m_arrEmp[i]->GetSalary()>nMax)
{
// 则将当前对象记录为结果对象
pMax = m_arrEmp[i];
// 并将当前对象的工资记录为当前最大值
nMax = pMax->GetSalary();
}
}
// 返回指向拥有最高工资的员工对象的指针
return pMax;
}
// 查询员工工资
void Find()
{
// 构造无限循环进行查询
while(true)
{
// 查询的姓名
string strName = "";
// 输入提示
cout<<"请输入要查询的员工名字(-1表示结束查询):"<<endl;
// 获取用户输入的员工序号并保存到n
cin>>strName;
// 对用户输入进行检查
if(!cin) // 如果输入错误,提示重新输入
{
cout<<"输入错误,请重新输入"<<endl;
cin.clear();
cin.sync();
continue;
}
else if("-1" == strName) // 如果查询结束
{
// 查询结束,用break结束循环
cout<<"查询完毕,感谢使用!"<<endl;
break;
}
// 记录是否找到查询的员工
bool bFind = false;
// 用for循环遍历所有员工对象,这个进行比对查找
for(int i = 0;i<m_nCount;++i)
{
Employee* p = m_arrEmp[i];
if(strName == p->GetName())
{
// 输出符合查询条件的员工信息
cout<<"员工姓名:"<<p->GetName()<<endl;
cout<<"员工工资:"<<p->GetSalary()<<endl;
bFind = true; // 获得查询结果
break; // 结束循环
}
}
// 如果没有找到,则提示用户重新输入
if(!bFind)
{
cout<<"无法找到名字为"<<strName<<"的员工。"<<endl;
cout<<"请核对姓名,重新输入"<<endl;
}
}
}
// SlarySys类的属性
// 因为这个属性都只是供SalarySys类访问,
// 所以其访问级别设定为private
private:
// 数据文件名,因为不可以修改,所以使用const关键字修饰
const string m_strFileName;
Employee* m_arrEmp[MAX]; // 保存员工对象指针的数组
int m_nCount; // 当前员工数
};
完成了工资系统类SalarySys之后,实际上就是万事俱备,只欠东风了。接下来就只需要在主函数中运用这些类来完成需求设计中的用例,那就是大功告成了:
int _tmain(int argc, TCHAR* argv[])
{
// 创建一个SalarySys对象
// 在构造函数中,它会首先去读取数据文件中的员工数据,
// 完成““从文件读取”这一用例
SalarySys sys;
// 让用户输入数据,完成“手工输入”用例
sys.Input();
// 调用SalarySys的GetMax()函数,
// 完成“计算最大值”用例
Employee* pMax = sys.GetMax();
if(NULL != pMax)
{
cout<<"工资最高的员工是:"<<endl;
cout<<"名字:"<<pMax->GetName()<<endl;
cout<<"工资:"<<pMax->GetSalary()<<endl;
}
// 调用SalarySys类的Find()函数,完成“查询工资”用例
sys.Find();
// 最后,当sys对象析构的时候,会调用自己的Write()函数,
// 完成“输出数据到文件”用例
return 0;
}
有了面向对象思想和类的帮助,短短的几百行代码,小陈就完成了一个功能齐备的工资程序。从这里小陈也体会到,用面向对象思想进行分析与设计,更加接近我们思考问题、解决问题的思维模式,这使得工资程序的设计更加直观、更加自然、更加贴近现实的需求,其实现也更加思路清晰、更加容易。封装,可以让函数和她所操作的数据成为对象,可以起到很好的数据保护的作用;继承,可以复用共同的属性和行为,起到代码复用的作用。同时还可以很方便地对其进行扩展,从而支持更多更新的需求;多态,让我们可以以一致的调用方式,实现不同的操作行为。从而使得我们在设计中考虑得更多的是接口问题,而不用担心后面的实现问题。
当小陈将改写后的工资程序拿给老板使用以后,老板更是赞不绝口:
“不错不错,不仅能动态地计算各种员工的工资,并且时间变化以后,工资也会跟着变化。干得不错,啊哈哈……,下个月,涨工资。”
当再次听到老板的“涨工资”时,小陈已经没有先前那么激动了,他反问了一句:
“真的?”
“当然是真的,”老板立刻掩饰说,“我什么时候说话算数啊!”
听到这话,小陈也不去戳穿老板的伪装。现在在他看来,学好C++比涨工资更加重要,现在他已经越来越感受到C++的魅力,他已经开始爱上C++了。
|
|