5.4.3 工资程序成长记:函数
自从上次小陈“程序员”的工资程序得到老板的夸奖,口头许诺给他涨工资以后,老板再也没有找过他,涨工资的事自然也就没有下文了。这天,老板又突然召他去办公室。这下可把小陈高兴坏了,心想盼星星盼月亮终于盼来涨工资这一天了。于是赶紧到了老板的办公室。可他刚进门就发现情况有点不对,只见老板阴沉着脸坐在他那张硕大的老板椅上,满头大汗,手指还在不停地敲击着键盘输入着什么。一见到小陈进来,就好像见到仇人似的,劈头盖脸地来了一句:
“小陈啊,你这个工资程序怎么搞的,怎么每次都要重新输入工资数据啊?你是不是想累死我啊?”
老板的话如同一个晴天霹雳,让小陈的好心情一下子从山顶跌落到了谷底,心想,这下涨工资的事情肯定泡汤了。听老板这一抱怨,他才想起来原来的工资程序没法读取已有的工资数据,也无法将已经输入的工资数据保存成文件以备下次使用。所以这个工资程序是“一次性”的,每次使用都要重新输入全部数据。小陈想想那成千上万的工资数据,心中暗想,这都没把这个可恶的老板累死,真是算他走运了。不过转念又想,要是把他累死了谁给我涨工资啊?于是,赶紧解释说:
“老板,这可能是工资程序存在的一个缺陷,也就是我们通常所说的Bug(臭虫)。您也是知道的,程序中出现Bug是在所难免的。让我拿回去修改修改,马上就好,马上就好。”
就这样,小陈没有听到涨工资的好消息,反倒是拿了一个有问题的工资程序回来修改。好在他这几周还算勤快,在C++方面进步了不少,他已经学到了函数,懂得了如何对一个问题“自顶向下、逐步求精”地进行分析,划分模块并用C++的函数加以解决。所以,对于解决老板遇到的这个问题,他还是很有信心的。
拿到问题后,小陈对这个问题进行了简单的分析。整体而言,这是一个典型的对数据进行处理的程序,按照业务流程,它主要可以分为数据输入、数据处理和数据输出三个比较大的模块。而根据输入方式的不同,数据输入模块又可以细分成手工输入和文件读入,而数据输出部分,除了屏幕显示结果之外,相应地还应该包括将工资数据输出到文件。在最重要的数据处理模块,为了保证函数的职责单一而明确,需要将原来混杂在数据输入时进行的最大值最小值以及平均值的计算,移到数据处理模块中成为独立的子模块。经过这样的简单分析,小陈很快地在白板上画出了工资程序的模块图:
图5-11 工资程序模块图
有了程序的模块图,我们只要用函数将每个模块实现,然后在主函数中按照业务流程对各个子模块加以调用,就可以解决整个问题了。在模块图的指引下,小陈很快就用函数实现了每个模块,并将它们组装成了新的工资程序:
// 工资程序V2.0 #include <iostream> // 为了读写文件,引入文件流对象头文件 #include <fstream> #include <string> #include <climits> // 为了使用int类型的最大值和最小值 using namespace std; // 全局的工资数据文件名,使用一个不可修改的常量字符串表示 const string strFileName = "SalaryData.txt"; // 从数据文件读取工资数据到arrSalary数组 int Read(int* arrSalary, int nCount) { int i = 0; // 当前工资序号 // 打开工资数据文件SalaryData.txt用于读入数据 // 这个文件应该在exe文件所在的相同目录下 ifstream in(strFileName); if(in.is_open()) // 如果成功打开数据文件 { // 构造一个while循环读取文件中的工资数据 // 如果读取的数据个数i小于数组的容量nCount,则继续读取 while(i < nCount) { // 将读取的数据保存到arrSalary[i] in>>arrSalary[i]; // 对读取结果进行判断,看是否读取到了文件结束 // 如果到达文件结尾,则用break关键字结束读取循环 if(!(in)) { break; } ++i; // 尚未读取完毕,开始下一次读取 } // 读取完毕,关闭文件 in.close(); } // 输出读取结果,返回读取的数据个数 cout<<"读取"<<i<<"个工资数据"<<endl; return i; } // 将arrSalary数组中的工资数据写入数据文件 void Write(int* arrSalary, int nCount) { // 创建或打开工资数据文件SalaryData.txt用于输出 // 输出完成后,这个文件将出现在exe文件所在的目录 ofstream o(strFileName); if(o.is_open()) // 如果成功打开数据文件 { // 利用for循环,将数组中的数据输出到文件 for(int i = 0;i < nCount;++i) { o<<arrSalary[i]<<endl; // 每一行一个数据 } // 输出完毕,关闭文件 o.close(); } } // 获取工资数组中的最大值 int GetMax(int* arrSalary, int nCount) { int nMax = INT_MIN; // 初始值为int类型的最小值 // 利用for循环遍历数组中所有数据元素,逐个进行比较 for(int i = 0;i < nCount; ++i) { if(arrSalary[i] > nMax) nMax = arrSalary[i]; } // 返回找到的最大值 return nMax; } // 获取数组中的最小值(请依最大值函数的葫芦自行画出最小值函数的瓢)… // 计算数组中所有数据的平均值 float GetAver(int* arrSalary, int nCount) { // 计算总和 int nTotal = 0; for(int i = 0;i < nCount; ++i) { nTotal += arrSalary[i]; } // 计算平均值并返回 if(0 != nCount) // 判断总数是否为0 return (float)nTotal/nCount; else return 0.0f; // 特殊情况返回0 } // 手工输入数据 int Input(int* arrSalary, // 工资数组首地址 int nMax, // 数组能够容纳的数据的个数 // int nIndex) // 数组中已有的数据的个数 { // 用数组中已有数据的个数作为输入的起点 int i = nIndex; // 在for循环之前初始化i,应为i在for循环之后还需要用到 for(; i < nMax; ++i) // i已经初始化,初始化语句留空 { // 提示输入 cout<<"请输入"<<i<<"号员工的工资(-1表示输入结束):"<<endl; // 将输入的数据保存到数组的arrSalary[i]数据元素 int n = 0; cin>>n; // 检查输入是否合法 if(cin) { arrSalary[i] = n; } else// 如果输入不合法,例如输入了英文字符,则提示用户重新输入 { cout<<"输入错误,请重新输入"<<endl; // 清理cin的输入标志位以重新输入 cin.clear(); // 清空输入缓冲区 cin.sync(); --i; // 将输入序号退后一个 continue; // 直接开始下一次循环 } // 检查是否输入结束 if(-1 == arrSalary[i]) { break; // 结束输入循环 } } // 返回当前数组中共有的数据个数 return i; } // 查询工资数据 void Find(int* arrSalary,int nCount) { while(true) // 构造无限循环进行工资查询 { int n = 0; // 提示用户输入要查询的员工序号 cout<<"请输入要查询的员工序号(0-"<<nCount-1 <<",-1表示结束查询):"<<endl; // 获取用户输入的员工序号并保存到n cin>>n; // 对用户输入进行检查 if(!cin) // 如果用户输入不合法 { cout<<"输入错误,请重新输入"<<endl; // 清理cin的输入标志位以重新输入 cin.clear(); // 清空输入缓冲区 cin.sync(); continue; // 开始下一次查询 } else if(-1 == n) // 检查查询是否结束 { // 查询结束,用break结束循环 cout<<"查询完毕,感谢使用!"<<endl; break; } else if(n<0||n>=nCount) // 检查输入是否超出序号范围 { // 输入序号超出范围,用continue开始下一次循环 cout<<"输入的序号"<<n<<"超出了序号范围0-" <<nCount-1<<",请重新输入。"<<endl; // 开始下一次查询 continue; } // 输入合法,输出用户查询的员工工资 cout<<"员工序号:"<<n<<endl; cout<<"员工工资:"<<arrSalary[n]<<endl; } } int main() { // 定义保存员工数据的超大数组 const int MAX = 100000; int arrSalary[MAX] = {0}; // 首先从数据文件读取已经保存的数据 int nCount = Read(arrSalary,MAX); // 然后用手工继续输入工资数据 nCount = Input(arrSalary,MAX,nCount); // 对输入的工资数据进行统计 cout<<"输入完毕。一共有"<<nCount<<"个工资数据"<<endl; cout<<"最大值:"<<GetMax(arrSalary,nCount)<<endl; cout<<"最小值:"<<GetMin(arrSalary,nCount)<<endl; cout<<"平均值:"<<GetAver(arrSalary,nCount)<<endl; // 对工资数据进行查询 Find(arrSalary,nCount); // 查询结束,将工资数据保存到数据文件,以备下次使用 Write(arrSalary,nCount); return 0; }
在改写的过程中,小陈按照面向过程中“自顶向下,逐步求精”的设计思路,首先将整个问题分解成了手工输入、文件输入、获取最大值等多个模块,然后利用多个函数分别将这些模块一一实现,最后在主函数中将这些函数按照业务流程组织起来,最终解决了整个大问题。当小陈将这个改写后的工资程序拿给老板用过之后,老板笑得合不拢嘴,一直夸奖说:
“不错不错,现在的工资程序不仅可以将输入的数据保存下来,下次可以接着用,再也不用我每次都重新输入数据了。同时还对用户的输入进行了检查,很好地防止了输入错误的发生。干的不错,下个月,涨工资,啊哈哈哈……”
老板的一句“涨工资”,让小陈又重新燃起了希望,心中想,这次的改写,让我见识了函数所体现出来的这种“将一个大问题分解成多个小问题,然后各个击破”的解决问题的思路,以后遇到再大的问题也不用担心了。同时他也暗下决心,C++真是个好东西,只要自己好好学,下个月一定能够涨工资。