Musel's blog

BUAA-C++与C#-项目分析报告

字数统计: 2.1k阅读时长: 9 min
2022/03/22

一、所选项目简介

所选项目: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&引用。
CATALOG
  1. 1. 一、所选项目简介
  2. 2. 二、核心类结构——使用PlantUml绘制类图
  3. 3. 三、面向对象特征分析
    1. 3.1. 3.1 封装性
      1. 3.1.1. 3.1.1 基类Ball和Word
      2. 3.1.2. 3.1.2 派生类的新增属性
    2. 3.2. 3.2 继承性
    3. 3.3. 3.3 多态性
      1. 3.3.1. 3.3.1 virtual抽象类和重写父类方法实现的多态
      2. 3.3.2. 3.3.2 运算符重载实现的静态多态
      3. 3.3.3. 3.3.3 遵从多态的优化建议
  4. 4. 四、其他关键语法
    1. 4.1. 4.1 static
    2. 4.2. 4.2 重写拷贝构造
    3. 4.3. 4.3 运算符重载:静态多态
    4. 4.4. 4.4 new与delete配合使用
    5. 4.5. 4.5 深复制
    6. 4.6. 4.6 passed-by-reference