一、所选项目简介
所选项目:c++实现的带GUI的背单词游戏
环境:VS2019 + EasyX
所选项目实现功能:三个不同的背单词游戏
所选项目开源库地址:https://github.com/Wenretium/Word-Games
二、核心类结构——使用PlantUml绘制类图
三、面向对象特征分析
3.1 封装性
3.1.1 基类Ball和Word
分别对图片的位置和单词的语义进行封装,为实现类或者派生类提供底层数据。
基类Ball中的坐标x和y均为protected类型,允许子类进行访问和修改当前图形所处的位置,保证游戏画面中各元素的运动性。
基类Word中的单词拼写和单词语义均为private类型并设置getter()方法进行访问,保证了单词数据的封装性。
3.1.2 派生类的新增属性
LetterBall的Letter字段为自身存储的字母,protect类型。便于子类LetterBallB::print()中直接访问父类LetterBall的letter,获取自身表示的字母。结合源码分析,此时LetterBall::getLetter()方法是冗余的,可以删去。
// 继承Ball,增加私有成员字母,作为Game2中每个浮现的字母 class LetterBall :public Ball { protected: char letter; public: LetterBall(char nletter, int xx, int yy) :Ball(xx, yy), letter(nletter) {} virtual void print()=0; char getLetter() { return letter; } };
WordBurger和WordPiece的两个类中的关于LetterBall的聚合,如vector<LetterBall*>属于类的私有成员变量,外部通过调用类的check_click()、letterballs_empty()来与集合做交互,从而达到对外部隐藏集合,只提供方法接口。
// 用于Game3,每局创建一个WordBurger // 把Word英文单词拆成一组LetterBall,存入letterlayers class WordBurger :public Word { private: list<LetterBall*>letterlayers; public: WordBurger(Word nword); ~WordBurger(); virtual void print() ; bool moveNextLetter(list<LetterBall*>& ls); bool letterlayers_empty(); };
3.2 继承性
基类Ball和Word。Ball负责控制图形坐标和位置移动。Word负责记录单词内容。
游戏1的内容是单词和意思匹配,玩家需要控制Basket篮子类左右移动,来接住不断下落的以小猫图形为背景的单词,所以类WordBall继承类Ball和Word。
游戏2的内容是以字母图标形式出现,按顺序点击字母拼出完整单词。所以类LetterBall表示字母图标,继承Ball,自己增加letter的char字段。类WordPiece表示由多个LetterBall聚合而成的完整单词,其也继承Word类用于记录自身单词内容。
游戏3的内容是使用篮子类Basket通过左右移动来接住汉堡的各个片层代表的字母,从而拼出完成单词。类LetterBallB表示字母的汉堡片层图标,继承LettterBall。类W ordBurger表示由多个LetterBallB聚合而成的完整单词,其继承Word类用于记录自身单词内容。
3.3 多态性
3.3.1 virtual抽象类和重写父类方法实现的多态
抽象类LetterBall中有virtual void print()=0; Word类有virtual void print()const;
在LetterBallA和LetterBallB中都有具体实现,分别用于游戏2和游戏3的不同字母图形实现。如:
void LetterBallB::print() { IMAGE let; loadimage(&let, _T("..\\hamburger\\stuff1.jpg"), LayerWidth, LayerThickness, true); putimage(x, y, &pictures[1], SRCAND); putimage(x, y, &pictures[0], SRCPAINT); setcolor(WHITE); settextstyle(30, 0, _T("Consola")); outtextxy(x+LayerWidth/2, y+LayerThickness/2,letter); }
WordPiece和WordBurger中分别有LetterBallA和LetterBallB的集合,他们重写的打印方法print()为循环调用自身集合中元素的print()。如:
void WordBurger::print() { for (list<LetterBall*>::iterator iter = letterlayers.begin(); iter != letterlayers.end(); iter++) (*iter)->print(); }
WordBall并未重写print()方法,而是在运算符重载()的方法中实现print。目的是便于给for_each()传入对象的静态方法(),取代print()方法。因为需要重载的print()是动态绑定,不是静态方法。此时使用WordBall()即可显示出游戏1 的单词图形。
void WordBall::operator()(WordBall w)//用来替代print成员函数,便于使用for_each算法 { setcolor(BLACK); IMAGE cat; loadimage(&cat, _T("..\\cat1.jpg"), 75, 50, true); putimage(w.x, w.y, &cat); setbkmode(TRANSPARENT); const char* c = (w.word).c_str(); int movesteps = 0; if (w.word.size() <= 6)movesteps = 12; outtextxy(w.x + movesteps, w.y + 25, c); }
3.3.2 运算符重载实现的静态多态
bool Word::operator==(Word a)
{
if (word == a.word&&meaning == a.meaning)
return true;
else return false;
}
bool operator==(WordBall a, WordBall b)
{
if (a.getWord() == b.getWord() && a.getMeaning() == b.getMeaning())
return true;
else return false;
}
- 父类Word和子类WordBall都实现了运算符==的重载,实现了运算符重载的静态多态,用于比较两个元素是否equal。
3.3.3 遵从多态的优化建议
Basket类中的print(string item)方法中存在以下结构:
if (item == "basket") { //… } else (item == "bread"){ //… }
用于实现游戏1和游戏3中用户分别移动控制的“篮子”对象和“面包”对象。此处可以使用多态来优化掉if-else的结构,Basket类和Bread类都继承自Item类,重写各自的print()方法。
四、其他关键语法
4.1 static
- WordBall中有static int count的类的静态成员,用于计数在场的WordBall的球数。在构造函数中++,在析构函数中–。
int WordBall::count = 0;//静态数据成员,控制在场球数
WordBall::WordBall(Word a, int xx, double sspeed) :Word(a.word, a.meaning), Ball(xx, 10), speed(sspeed)
{
count++;
}
class WordBall :public Word, public Ball
{
private:
double speed;
public:
WordBall() :Word(), Ball() {};
WordBall(Word a, int xx, double sspeed);
WordBall(const WordBall& wb) :Word(wb), Ball(wb), speed(wb.speed){ count++; }
~WordBall() { count--; }
static int count;
void falldown(int time) { y += speed * time; }
friend bool operator==(WordBall a, WordBall b);
void operator()(WordBall w);
};
4.2 重写拷贝构造
可以观察到,在operator()、operator==函数中均存在WordBall类型的参数,此处为passed-by-value,则会使用WordBall的拷贝构造。
如果此时不给WordBall重写构造函数,即为浅拷贝,会造成在operator()、operator==函数执行完毕后对函数内创建的WordBall调取析构函数,导致默认拷贝构造时count没有++,但是析构时count–。
所以此时为了维护static count变量,需要重写拷贝构造,让发生拷贝构造时count也++。
重写拷贝构造时,子类拷贝构造函数不会自动调用父类的拷贝构造函数,即需要在
WordBall(const WordBall& wb) :Word(wb), Ball(wb), speed(wb.speed){ count++; }
指明调用父类拷贝构造,否则会执行初始构造。
4.3 运算符重载:静态多态
void WordBall::operator()(WordBall w)//用来替代print成员函数,便于使用for_each算法
{
setcolor(BLACK);
IMAGE cat;
loadimage(&cat, _T("..\\cat1.jpg"), 75, 50, true);
putimage(w.x, w.y, &cat);
setbkmode(TRANSPARENT);
const char* c = (w.word).c_str();
int movesteps = 0;
if (w.word.size() <= 6)movesteps = 12;
outtextxy(w.x + movesteps, w.y + 25, c);
}
此运算符重载的作用是实现print()功能。而选择运算符重载而非重写父类print()方法的原因如下:
在main()函数中调用了
for_each(present_balls.begin(), present_balls.end(), WordBall());
用来遍历打印WordBall。从for_each()函数原型可以看出:
void for_each(T* begin, T* end, const Func& f)
第三个参数需要一个静态方法引用。而重写父类的虚方法print()是动态绑定,需要使用对象信息来完成,不是静态方法。
而运算符重载是静态多态,地址早绑定,在编译阶段确定函数地址,所以可以作为for_each()的第三个静态方法参数。
4.4 new与delete配合使用
WordBurger::WordBurger(Word nword):Word(nword)
{
srand(int(time(0)));
for (int i = 0, n = 1; i < word.size(); i++, n++)
{
if (i == word.size() - 1)n = 0;//bread_end
letterlayers.push_back(new LetterBallB(word[i], rand() % 800, 0, n));
}
}
WordBurger::~WordBurger()
{
for (list<LetterBall*>::iterator iter = letterlayers.begin(); iter != letterlayers.end(); iter++)
delete *iter;
}
- 构造函数中的list<LetterBall*>letterlayers 使用new创建对象,在析构函数中使用迭代器iter进行delete操作。
4.5 深复制
WordPiece::WordPiece(const WordPiece& wp)
{
for (int i = 0; i < letterballs.size(); i++)
letterballs.push_back(new LetterBallA(*dynamic_cast<LetterBallA*>(wp.letterballs[i])));
}
- 由于WordPiece的构造函数中也使用了new作为vector<LetterBall*>letterballs集合,所以重写拷贝构造时需要进行深复制。
4.6 passed-by-reference
void wordmean_separate(string &wordmean, string& nword, string& nmeaning)
{
//...split the line to word and meaning
nword = wordmean.substr(word_begin, word_end - word_begin + 1);
nmeaning = wordmean.substr(meaning_begin, wordmean.size() - meaning_begin + 1);
}
- 把一行有单词和中文的数据separate后返回截取得来的nword和nmeaing,此时需要传入String&引用。