本次教程的内容:
- 类的构造函数
- 类属性的可见性
- 多个源代码文件
- 代码重构
抽象思维,就是从看似不同的具体事物中找出共性。
佛说,离一切相,即一切法。
初建成功的地图工作组成员认为精灵大人对自己感到很满意,毕竟现在主程序的代码已经那么短了。几乎所有具体工作都由地图工作组来完成了,精灵大人只须打几个电话指挥一下就好。
谁知当记者去采访的时候发现,其实精灵大人一点都不满意:后面的工作还有很多等着本精灵去做,我还得打那么多电话去指挥你一个具体工作组的工作,又是初始化,又是显示英雄,又是开始监听。我希望叫到它们的时候就一个电话:干活。
既然如此,让我们来进一步优化一下地图工作组的代码。
我们在上节课看到,当精灵创建了地图工作组后,它首先做的事情就是调用Init()函数对它进行初始化。几乎所有的对象,在工作前都会有一个初始化的需求。于是c++语言为所有的类,设计了一个共同的初始化函数:构造函数。
构造函数的名字和类名相同,没有返回值,在创建对象时自动调用。
我们把初始化的工作放在这里,这样,调用Init()初始化的步骤就可以省略了。
Map(){
mapInfo[0]= "■■■■■■■■■■■■";
mapInfo[1]= " ■■ ■";
mapInfo[2]= "■ ■ ■■ ■■ ■";
mapInfo[3]= "■ ■■ ■ ";
mapInfo[4]= "■■■■■■■■■■■■";
x= 0;
y= 1;
}
然后我们增加一个work()函数
void work(){
showMaop();
showHero();
listen();
}
把工作展开的过程也封装起来了。
这样主程序的代码精简到了只有三行,其中与地图工作组直接相关的只有两行。
一行创建对象,一行开始工作。
int main(){
HideCursor();
Map map1;
map1.work();
}
这次,精灵大人感觉怎么样呢?记者又一次采访了精灵。
记者:请问您对地图工作组最近的工作还满意么?
精灵:满意?这么小的一张地图翻来覆去走了这么多天,你问我满意不满意?
记者:哦,我相信更多的地图已经在研制中了。请问对它们其他方面的工作情况还有什么意见么?
精灵:其他方面,你去看看它们的listen函数,足足有40行代码,怎么就不能再优化一点?
记者:咳咳,我相信地图工作组的代码优化工作仍在持续地进行中。我听说地图工作组经过努力把外部的调用代码减到了最少?
精灵:我的主程序固然是精简一些了。但是打开文件后,整个代码仍然很长,看起来很乱。你看看控制台工作组做了那么多事情,它在我的代码中只多占了一行的位置include <conio.h>。
记者:我想我弄明白您的意思了。让我们共同期待地图工作组进一步改进它们的工作。
看来想得到精灵大人的肯定还没那么容易。
当我们开始设计一个具有一定复杂度的软件,首先必须掌握的就是化解复杂性的方法。
一个常见的方法就是模块化。
把一个复杂的工作拆分成很多相对比较小的模块化工作。先构建起较小的模块,然后用这些小模块逐渐搭建起更大的模块。直到建设成一个完整的系统。
在整个过程中,我们应当一直对每个模块的复杂性有一个平衡度的把握。如果感觉到一个模块可能过于复杂了,我们就应当建立起一种方法,将它做进一步的拆分。
比如现在的主程序,里面明显包含了两部分内容,一个是主程序,一个是地图工作组。如果能把它们分开,代码会显得更加清晰。
include语句正是用来做这件事的。
我们前面见的include语句,都是后面跟一个用一对尖括号<>包围的名字,这些往往对应着系统所提供的标志库(工作组)。
现在我们自己也开发了一个工作组,也就是建立一个自己的库。这种库也可以用include来引用,一般都用""双引号来标志。
现在我们把现有的代码一分为二,
- 一个是从main函数开始下面的部分,保存文件名为rpg.cpp。
- 另一个是main函数以上的所有部分,保存文件名为map.cpp。
然后在rpg.cpp中加一句引用
#include "map.cpp"
至于class Map中的函数listen,确实有点过长了。
函数复杂性的一个标准,就是代码的行数。这里并没有硬性的标准,有个一般的建议是一个函数的长度最好不超过一个屏幕。这样当我们编辑和修改这个函数时,可以不用到滚动条。
怎样缩短一个函数呢?
一个简单的方法就是通过把一些功能直接剥离出来形成新的函数,比如main函数的缩短。
另一种高级一些的方法是找到相似的结构,把它们提炼成函数,避免重复。比如listen函数就是这样。
处理四个方向的移动都有一个共同的逻辑结构:
- 明确新的目标位置
- 判断目标位置是否允许移动
- 如果可以
- 隐藏英雄
- 把英雄的当前位置改为新的目标位置
- 显示英雄
现在的代码中,这套逻辑重复了4遍,如果我们把它提炼为一个函数,可以更加精炼。
完整源代码如下:
map.cpp
# include <iostream>
# include <conio.h>
# include <windows.h>
using namespace std;
void HideCursor(){
CONSOLE_CURSOR_INFO cursor_info = {1, 0};
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}
void gotoxy(unsigned char x,unsigned char y){
COORD cor;
HANDLE hout;
cor.X = x;
cor.Y = y;
hout = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hout, cor);
}
class Map{
string mapInfo[5];
int x, y;
void showMap(){
for (int i=0; i< 5; i++){
cout << mapInfo[i]<< endl;
}
}
void showHero(){
gotoxy(x,y);
cout << "♀";
}
void hideHero(){
gotoxy(x,y);
cout << " ";
}
void tryMove(int ax, int ay){
hideHero();
if (mapInfo[ay][ax]== ' ') {
x= ax;
y= ay;
}
showHero();
}
void listen(){
int a;
while(1){
a= getch();
if (a==72) {
// 向上
tryMove(x, y-1);
}
if (a==80) {
// 向下
tryMove(x, y+1);
}
if (a==75) {
// 向左
tryMove(x- 2, y);
}
if (a==77) {
// 向右
tryMove(x+ 2, y);
}
}
}
public:
Map(){
mapInfo[0]= "■■■■■■■■■■■■";
mapInfo[1]= " ■■ ■";
mapInfo[2]= "■ ■ ■■ ■■ ■";
mapInfo[3]= "■ ■■ ■ ";
mapInfo[4]= "■■■■■■■■■■■■";
x= 0;
y= 1;
}
void work(){
showMap();
showHero();
listen();
}
};
我们看一下,增加了一个tryMove函数,不但使得listen函数更加简短,而且让它的代码含义也更加清晰了。另外,充分利用了已经定义的显示英雄函数showHero,以及模仿showHero创造了hideHero函数,都有助于让代码的含义更加清晰。
后面我们会看到,还有手段让代码的含义更加清晰。
对于一个复杂项目来说,在代码可读性上的努力终究会物有所值的。
最后把变量定义和所有函数都放到了public: 标记的前面。保证只有构造函数Map和work函数能够被外界看到。
rpg.cpp
//# include <iostream>
//# include <conio.h>
//# include <windows.h>
# include "map.cpp"
using namespace std;
int main(){
HideCursor();
Map map1;
map1.work();
}
主程序极大地精炼到了。由于现在几乎所有的工作都来自map.cpp中的定义,甚至连原有的几个include都可以取消了。但是考虑到我们将来或许还会用到,所以暂时把它们注释起来,以备后用。这也是注释这一功能的常见用法之一。
后来记者又一次采访了精灵。
记者:您看现在的代码怎样?
精灵:非常好!这么简短的源代码,真是让人干劲十足啊!
记者:听说地图工作组还优化了listen函数的实现方法。
精灵:什么listen函数?我不清楚,也不关心,那是它们自己的事情。
记者:嗯,听说它们还在准备引入更多、更大、更好看的地图!
精灵:想法很好,不过设计地图不适合它们来做,我已经另有人选了。它们当前的重点工作是业务逻辑还差得太远。
记者:哦哦,您对地图组还有什么建议么?
精灵:每次修改,保存文件后,到主程序这边来运行。
记者:明白了,无论代码有多少,主程序的地位仍不能取代。感谢您接受采访。
课程小结:
本节课讲了类的构造函数和类函数的可见性,介绍了一些代码优化的方法,以及多源文件协作的方法。