前言
面向对象是十分常见的一种编程思想,本文将通过一个实际项目,带大家走进面向对象,体会面向对象在实际开发中的应用,给我们带来的便捷。
本文将通过对实例进行笔者自己的分析与实现方式,向大家展示面向对象的三大特点,尽可能包含面向对象中的绝大多数知识点,希望对大家有所帮助。
闲话少说,我们这就进入项目实例,开始吧!
一、项目概述
本次开发一个职工管理操作系统,该系统能够实现对职工信息的有序管理,包含增删改查排等常用操作,同时代码应具备较好的完善性、鲁棒性以及可扩展性,内存分配应当严谨且合理,对应户的非法操作及不合理输入作出有效应对,总而言之,应当是一个健壮的管理系统。
项目难点主要在于如何利用面向对象的知识实现题目要求的功能:
首先我们要实现这几个功能函数完成对职工信息容器的对应操作,如增删改查等,相似操作曾在通讯录管理系统中实现过,但是它的处理方式基于结构体数组,因此也存在容量有限、操作复杂等等问题,次数,笔者计划采用C++提供的数组容器存储职工信息,使用迭代器操作数组元素,从而简化代码,提高代码可读性。
其次,我们使用何种数据类型存储职工相关信息,每位职工信息包括姓名、编号、职位以及薪水等信息,要将这些信息综合在一起,我们不难想到使用“类”来实现,但是所谓的“职工类”如何实现?他的相关函数如何实现?如何基于面向对象的思想实现?
整体架构已经明了,那么解决上面两个难题?
二、项目详解
1.职工类 Staff 的实现
笔者计划充分利用面向对象三大特性:封装,继承,多态来实现 Staff 类,那我,我们不妨首先建立一个基类 Worker ,包含职工的基本信息,如姓名、编号、职位,为了程序中职工信息的安全性,我们将这三个数据设置为 protected ,在保证子类继承的同时避免无意识修改。那么,我们如何获取和设置这三个数据的值呢?
这里就要用到面向对象的第一个特性:封装。我们通过成员函数对外提供设置和修改这三个属性的接口,方便操作。
Worker 基类声明如下:
/* Alkaid#3529 */
class worker
{
public:
//对外提供获取与更改私有属性的接口
void set_name(string name);
string get_name();
void set_id(string id);
string get_id();
void set_post(string post);
string get_post();
//纯虚析构,释放子类中开辟在堆区的数据
virtual ~worker() = 0;
protected:
string m_name;
string m_id;
string m_post;
};
Worker 基类实现如下:
/* Alkaid#3529 */
//对外提供获取与更改私有属性的接口
void worker::set_name(string name)
{
this->m_name = name;
}
string worker::get_name()
{
return this->m_name;
}
void worker::set_id(string id)
{
this->m_id = id;
}
string worker::get_id()
{
return this->m_id;
}
void worker::set_post(string post)
{
this->m_post = post;
}
string worker::get_post()
{
return this->m_post;
}
worker::~worker() {}
细心的读者可能发现,我们将 Worker 基类的析构函数设置为了虚函数,这样写有什么用呢?
实现基类后,我们开始编写职工 Staff 派生类,也就是我们最终要使用的类,用它继承已经准备好的 Worker 基类,同时添加一个数据,薪水,特别的是,我们将薪水值开辟在堆区。
Staff 派生类代码声明如下:
class staff :public worker
{
public:
//有参构造,添加默认参数以兼容无参构造
//添加 explicit 关键字,杜绝隐式转换
explicit staff(string name = " ", string id = "000000", string post = "Employee", int salary = 500);
//拷贝构造,即便不使用也必须重写,解决潜在问题
explicit staff(staff& s);
//重写析构函数,正确释放开辟在堆区的数据
~staff();
//对外提供接口获取和修改薪水值
void set_salary(int salary);
int get_salary();
//运算符重载
//左移运算符重载,直接输出工作者的姓名、编号、职位及其薪水
friend ostream& operator<<(ostream& cout, staff& s);
//右移运算符重载,简化后期读入职工信息的代码
friend istream& operator>>(istream& cin, staff& s);
//赋值运算符重载,方便类之间相互赋值
staff& operator=(staff& s);
//关系运算符重载,用于后序排序
bool operator<(staff& s);
private:
//设置整形指针,指向开辟在堆区的整型变量
int* m_salary;
};
首先,我们在 Staff 派生类中声明了他的构造函数,包括有参构造和拷贝构造,同时实现他的虚构函数,此处需要特别声明:因为子类Staff存在开辟在堆区的数据,那么当父类的引用指向子类的对象时,就会导致父类析构函数无法彻底释放子类开辟在堆区中的数据,导致内存泄露,存在安全隐患,因此,我们有必要将父类的析构函数声明为纯虚函数,使得子类必须重写父类的析构函数,保证堆区数据的安全。
其次,就是子类拷贝构造的实现。因为子类存在堆区数据,从而产生了深浅拷贝的问题,普通的拷贝是浅拷贝,在拷贝过程中只是简单的复制数据,容易产生地址浅拷贝,堆区数据重复被释放的问题,因此我们有必要考虑到拷贝构造,解决深浅拷贝的问题。
最后,我们声明了 Staff 派生类的一系列重载运算符,为了简化后续代码,我选择再此处进行运算符重载,从而降低后续读入、输出数据的代码复杂度。
Staff 派生类代码实现如下:
//有参构造,添加默认参数以兼容无参构造
staff::staff(string name, string id, string post, int salary)
{
this->m_name = name;
this->m_id = id;
this->m_post = post;
//在堆区开辟空间存放薪水值,带来了深浅拷贝与析构函数的相关问题
this->m_salary = new int(salary);
}
//拷贝构造,即便不使用也必须重写,解决潜在问题
staff::staff(staff& s)
{
this->m_name = s.m_name;
this->m_id = s.m_id;
this->m_post = s.m_post;
//深拷贝,否则只会将指针地址进行复制
*this->m_salary = *s.m_salary;
}
//重写析构函数,正确释放开辟在堆区的数据
staff::~staff()
{
//确保指针不为空后,再释放
if (m_salary != NULL)
{
delete m_salary;
m_salary = NULL;
}
}
//对外提供接口获取和修改薪水值
void staff::set_salary(int salary)
{
*this->m_salary = salary;
}
int staff::get_salary()
{
return *m_salary;
}
//赋值运算符重载,方便类之间相互赋值
staff& staff::operator=(staff& s)
{
this->set_name(s.m_name);
this->set_id(s.m_id);
this->set_post(s.m_post);
this->set_salary(*s.m_salary);
return *this;
}
//关系运算符重载,用于后序排序
bool staff::operator<(staff& s)
{
if (this->m_id < s.m_id)
{
return 1;
}
else
{
return 0;
}
}
ostream& operator<<(ostream& cout, staff& s)
{
cout << "Name : " << s.m_name;
for (unsigned int i = 0; i < 13 - s.m_name.length(); i++)
{
cout << " ";
}
cout << "ID : " << s.m_id << " Post : " << s.m_post;
return cout;
}
istream& operator>>(istream& cin, staff& s)
{
cout << "请输入职工姓名:";
while (1)
{
cin >> s.m_name;
if (s.m_name.length() > 13)
{
cout << "名称超出指定长度,请重新输入:";
}
else
{
break;
}
}
cout << "请输入职工岗位(Boss, Manager, Employee):";
while (1)
{
cin >> s.m_post;
string str_Boss = "Boss";
string str_Manager = "Manager";
string str_Employee = "Employee";
if (s.m_post == str_Boss || s.m_post == str_Manager || s.m_post == str_Employee)
{
break;
}
else
{
cout << "无该职工岗位,请重新输入:";
}
}
cout << "请输入职工编号(六位数字,如:000001):";
while (1)
{
cin >> s.m_id;
if (s.m_id.length() < 6)
{
cout << "编号长度不足,请重新输入:";
continue;
}
else if (s.m_id.length() > 6)
{
cout << "编号长度过长,请重新输入:";
continue;
}
int i = 0;
while (i < s.m_id.length())
{
if (!(s.m_id[i] >= '0' && s.m_id[i] <= '9'))
{
break;
}
i++;
}
if (i != 6)
{
cout << "编号必须由纯数字组成,不包含特殊字符,请重新输入:";
}
else
{
break;
}
}
return cin;
}
值得一提的是,为了应对用户可能的非法输入,在读入过程中笔者特意添加了错误处理,对用户的错误输入进行提示,保证代码流畅运行。
到此,我们就完成了第一大任务,类的实现,接下来,我们马不停蹄,实现系统整体架构。
2.系统架构实现
系统涉及功能不少,笔者此处选择对每个功能涉及单独的函数实现对应功能,首先我们来看一下整体架构。
架构代码如下:
/* Alkaid#3529 */
// 菜单函数,显示系统菜单,供用户选择,并根据用户选择的功能进入对应的函数
void menu(vector<staff*>& s);
// 根据指定条件定位对应元素所在位置
int Locate_staff(vector<staff*>& s, string clue);
// key == 1 新增职工
void Add_staff(vector<staff*>& s);
// key == 2 显示职工
void Display_staff(vector<staff*>& s);
// key == 3 查找并显示职工信息
void Find_staff(vector<staff*>& s);
// key == 4 修改职工信息
void Revise_staff(vector<staff*>& s);
// key == 5 删除职工信息
void Remove_staff(vector<staff*>& s);
// key == 6 排序职工信息
void Sort_staff(vector<staff*>& s);
// key == 7 清空职工
void Empty_staff(vector<staff*>& s);
// 交换指定位置的两个元素
void swap(vector<staff*>& s, int index1, int index2);
在实现 Staff 子类后,我选择了使用vector容器来存储职工信息,但是大量职工信息在后续功能实现中会导致数据量处理过大,因此我们思考能不能选择一种更高效、便捷的存储方式呢?
没错,就是指针!
将每个Staff对象的指针共同存储在vector容器当中,每个指针维护一个Staff对象,这样,无论是后续交换容器元素还是删除元素都变得极为简单。
首先我们创立整个系统的入口:
/* Alkaid#3529 */
#include"head.h"
int main()
{
vector<staff*>s;
menu(s);
return 0;
}
我们用一个菜单函数作为指引,在菜单函数中实现打印系统界面,功能选择等操作:
void menu(vector<staff*>& s)
{
int key = 0;
while (1)
{
cout << "欢迎使用由 Alkaid#3529 开发的职工管理系统,本系统提供以下功能:" << endl;
cout << "1. 新增职工信息" << endl;
cout << "2. 显示全体职工" << endl;
cout << "3. 查找职工信息" << endl;
cout << "4. 修改职工信息" << endl;
cout << "5. 删除职工信息" << endl;
cout << "6. 进行系统排序" << endl;
cout << "7. 清空管理系统" << endl;
cout << "0. 退出管理系统" << endl;
cout << "请选择您需要的功能 (以数字 0 - 7 表示):";
// 读入 key
while (1)
{
cin >> key;
if (cin.fail() || key < 0 || key>7)
{
cin.clear();
cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
cout << "你的输入有误,请重新输入 (以数字 0 - 7 表示):";
}
else
{
break;
}
}
cout << endl;
if (key == 1)
{
Add_staff(s);
}
else if (key == 2)
{
Display_staff(s);
}
else if (key == 3)
{
Find_staff(s);
}
else if (key == 4)
{
Revise_staff(s);
}
else if (key == 5)
{
Remove_staff(s);
}
else if (key == 6)
{
Sort_staff(s);
}
else if (key == 7)
{
Empty_staff(s);
}
else if (key == 0)
{
cout << endl << "系统已退出" << endl;
break;
}
system("pause");
system("cls");
}
}
循环打印选择界面并读入用户选择的功能,同时对用户的功能选择输入作出处理判断,如果用户正确输入,就进入对应的功能函数。
首先是新增职工函数:
void Add_staff(vector<staff*>& s)
{
staff* s_temp = new (staff);
cin >> *s_temp;
s.push_back(s_temp);
cout << endl << "新增职工信息成功,";
}
值得一提的是,此处我们选择使用new从堆区申请空间来创建职工,那么删除职工时要注意堆区空间的释放。
接下来是展示全体职工信息的功能函数,因为在类中我们已经对右移运算符进行重载,因此直接输出即可:
void Display_staff(vector<staff*>& s)
{
for (int i = 0; i < s.size(); i++)
{
cout << *s[i] << endl;
}
cout << endl;
}
在后续功能实现之前,我们有必要先实现一个功能函数用来查找职工,从而才能为后续一系列功能提供基础,函数功能为根据职工姓名或编号查找对应索引并返回,否则返回 -1:
int Locate_staff(vector<staff*>& s, string clue)
{
for (int i = 0; i < s.size(); i++)
{
if (s[i]->get_name() == clue)
{
return i;
}
}
for (int i = 0; i < s.size(); i++)
{
if (s[i]->get_id() == clue)
{
return i;
}
}
return -1;
}
接下来就是简单的增删改查功能的实现了,并无新奇之处:
void Find_staff(vector<staff*>& s)
{
cout << "请输入您要查找的职工的姓名或编号:";
string clue = "";
cin >> clue;
cout << endl;
int pos = Locate_staff(s, clue);
if (pos == -1)
{
cout << "抱歉,无查找结果" << endl << endl;
}
else
{
cout << *s[pos] << endl << endl;
}
}
void Revise_staff(vector<staff*>& s)
{
cout << "请输入你要修改的职工的姓名或编号:";
string clue = "";
cin >> clue;
cout << endl;
int pos = Locate_staff(s, clue);
if (pos == -1)
{
cout << "抱歉,无查找结果" << endl << endl;
return;
}
cout << *s[pos] << endl << endl;
staff* s_temp = new(staff);
cin >> *s_temp;
delete s[pos];
s[pos] = s_temp;
cout << endl << "修改成功,";
}
void Remove_staff(vector<staff*>& s)
{
cout << "请输入你要删除的职工的姓名或编号:";
string clue = "";
cin >> clue;
cout << endl;
int pos = Locate_staff(s, clue);
if (pos == -1)
{
cout << "抱歉,无查找结果" << endl << endl;
return;
}
delete s[pos];
s.erase(s.begin() + pos);
cout << "删除成功,";
}
对职工进行排序,方便起见笔者选择了选择排序,如果对相关算法感兴趣的话不妨移步笔者的另一篇专栏算法之美,会专门对各种算法做出笔者自己的见解,此处就不赘述了:
void Sort_staff(vector<staff*>& s)
{
cout << "请选择排序规则(0 - 升序,1 - 降序):";
string str_0 = "0";
string str_1 = "1";
string str_k = "0";
while (1)
{
cin >> str_k;
if (str_k != str_0 && str_k != str_1)
{
cout << "输入有误,请重新输入:";
}
else
{
break;
}
}
if (str_k == str_0)
{
for (int i = 0; i < s.size(); i++)
{
int min = i;
for (int j = i + 1; j < s.size(); j++)
{
if (*s[j] < *s[min])
{
min = j;
}
}
if (i != min)
{
swap(s, i, min);
}
}
}
else
{
for (int i = 0; i < s.size(); i++)
{
int max = i;
for (int j = i + 1; j < s.size(); j++)
{
if (*s[max] < *s[j])
{
max = j;
}
}
if (i != max)
{
swap(s, i, max);
}
}
}
cout << endl << "排序完成,";
}
在清空容器时需要注意,不只是清空容器,应当首先释放容器中指针指向的堆区中的空间,如果只是单纯的clear的话会导致内存泄漏的致命问题,后续笔者也会专门对此类相关问题作出详细阐述:
void Empty_staff(vector<staff*>& s)
{
// 逐个释放指针指向空间
for (int i = 0; i < s.size(); i++)
{
delete s[i];
}
// 清空容器存储的数据
s.clear();
}
到此,整个项目的思路就解释完毕了,整个项目笔者尽量融合了面向对象的相关知识,诸如封装、继承、多态,虚函数、纯虚析构、运算符重载等,但笔者能力有限,不足之处,欢迎指正。
总结
面向对象还有很多细节是需要实践才能get到的,本项目也只能作为一个练习供大家参考试用,当然项目设计也有很多弊病有待解决,比这水平有限,但仍追求进步!
后续还会坚持更新新的 C++项目,欢迎大家订阅我的频道,我们下期再见。