Week 3 & 4 - Bigger Challenge and A First Look at Object-Oriented Programming

课后疑难解答

Q:Card类怎么用?怎么把牌加入vector<card>

A:这里给一个例子:

Card c1("A"), c2("2"), c3("3");  // 定义三个Card类的实例

// 大于号小于号怎么用?
if (c1 > c2) cout << "true" << endl;
else cout << "false" << endl;  // 这里会输出false
if (c3 < c2) cout << "true" << endl;  // 这里会输出true
else cout << "false" << endl;

// vector<Card> 怎么用?
vector<Card> cards;  // 初始化一个空的vector,类似数组,但初始长度是0
cards.push_back(c1);  // c1被加入cards的末尾,现在数组长度为1
cards.push_back(Card("JOKER");  // cards中又加入了一张大王,现在数组长度为2
cout << cards[0] << endl;  // 输出第一张牌,此时是A
cout << cards[1] << endl;  // 输出第一张牌,此时是JOKER
cout << cards[2] << endl;  // 报错!你下标越界了。因为cards里只有两张牌

sort(cards.begin(), cards.end(), greater<Card>());  // 将cards中的牌从大到小排序。如果你要排序的vector<Card>名为hand,就把cards改成hand

// 输出cards里面的所有牌,你可以这样写:
// 注:你总是可以用xx.size()得到vector的大小
for (int i = 0; i < cards.size(); ++i) 
    cout << cards[i] << " ";
// 也可以这样写:
// 注:如果你好奇这种写法,可以搜索“迭代器”的相关资料;如果你弄不明白,可以跳过
for (vector<Card>::iterator i = cards.begin(); i != cards.end(); ++i) 
    cout << *i << " ";

Q:我不懂继承和类,成员函数应该怎么调用?例如showHand()

A:showHand()全称是DDZPlayer::showHand(),是DDZPlayer成员函数

  1. 如果你现在在实现一个DDZPlayer的其他成员函数,例如你在实现DDZPlayer::legal(...),想要输出当前对象的手牌,你可以直接调用——直接写
showHand();
  1. 如果你不在DDZPlayer的成员函数里,例如,你在调试run()函数,想在run()函数中输出看看某个玩家的手牌,你可以这样调用:
// run()函数中,players[0]、players[1]、players[2]分别是三个玩家的指针。
players[0]->showHand();  // 输出players[0]的手牌
  1. 如果你在实现DDZHumanPlayer的成员函数,你会发现,虽然没有实现过DDZHumanPlayer::showHand(),但是它还是可以被调用(参见DDZHumanPlayer::play()的实现)。这是由于DDZHumanPlayer继承自DDZPlayer,所以DDZPlayer的成员函数它可以直接用。不仅是showHand()函数,所有别的函数和变量都能直接用。
  2. 既然DDZPlayer的所有函数和变量都能直接用,为什么重写了一个DDZHumanPlayer::play()?因为我希望DDZHumanPlayer::play()完成的功能与DDZPlayer::play()不一样。

Q:我没有学过面向对象,不懂继承,重载,这次作业太难了我写不了!

A:不,你能写得了。需要你完成的部分不涉及面向对象的特性、继承、重载等等内容,你只需要用提供好的变量和函数拼装成要求你完成的部分就行了。你还需要阅读一下代码,尝试理解一点面向对象的工作机制。如果哪里读不懂,请直接咨询助教,我们会帮你解答。

Q:我能用自己的类实现吗?不用代码框架?

A:题目:我们提供了一辆汽车,缺一个轮子,大家看一下,体会一下这个汽车是怎么设计的,并自己把这个轮子装上,把车开起来。

同学1:老师,你这个汽车设计太过时了,是我小学的时候玩的,我能从头搞一辆坦克出来,不用你的破车了行不?

老师:可以,鼓励。

同学2:老师,你这个汽车的结构我看不懂,我有辆自行车也能跑起来,不用你的车行不?

老师:……不建议。

题目 3 - 面向对象重构

题目要求

上周的题目虽然没有困难的知识点,但是如果不能良好地组织代码,实现起来将会比较麻烦。

本周的题目旨在提供一个面向对象程序设计的第一印象,让大家体会面向对象程序设计中的类的设计和功能的划分。因此,这里提供一个代码框架,请大家基于这个代码框架,将上周的第二题以面向对象的方式完成。

通过合理的设计,程序的逻辑可以十分清晰。程序的main()函数作为顶层的流程,实现只需要三行:

int main() {
    DDZPlayer p1("Alice"), p2("Bob"), p3("Charlie");
    DDZGame g(&p1, &p2, &p3);
    g.run();
}

DDZGame类打包了游戏运行的玩家和状态信息,并具有控制游戏流程的功能。其成员变量和函数声明如下(一些辅助函数和变量没有列出):

class DDZGame {
private:
    vector<DDZPlayer*> players;  // 保存三个玩家的指针
    
public:
    DDZGame(DDZPlayer *p1, DDZPlayer *p2, DDZPlayer *p3);  // 构造函数
    void run();  // 执行游戏流程
};

DDZGame::run()函数控制了游戏的整个流程。纵观游戏的流程,可以发现DDZPlayer类需要和DDZGame类有一些交互(下图红字标出),也就是DDZPlayer类需要实现的函数功能。

因此,我们依次可以设计出DDZPlayer类的成员变量和函数(一些辅助函数和变量没有列出):

class DDZPlayer {
protected:
    string name;  // 玩家名
    int position;  // 你的位置编号,0为地主,1为地主下家,2为地主上家
    vector<Card> hand;  // 手牌

public:
    DDZPlayer(string name);  // 构造函数,初始化玩家名
    virtual void draw(Card card);  // 将cards中的牌加入手牌
    virtual void draw(vector<Card> cards);  // 将cards中的牌加入手牌
    virtual void setPosition(int pos);  // 初始化用,决定地主后设置
    virtual void observed(int pos, vector<Card> cards);  // 观测到玩家出牌
    virtual vector<Card> play();  // 轮到自己时决定出什么牌
    bool leftNoCard();  // 返回是否打完了牌?
};

此外,框架中还实现了一个Card类,继承自std::string。你可以把它当作string使用,但是Card类的成员变量可以使用>/<来比较牌面的大小。

class Card : public string {
public:
    Card(const char* str) :string(str) {}; 
    Card() :string() {};                   
    Card(string str) :string(str) {};

    static vector<Card> get_new_deck();

    // 重载操作符,使得牌面可以比较大小
    bool operator <(const Card &other) const;
    bool operator >(const Card &other) const;
};

实现要求

这道题目的目标有两个:

  1. 阅读代码,感受面向对象的设计和功能划分;
  2. 把上节课的程序修改,完成DDZPlayer::play()函数。如果正确完成,程序将执行模拟牌局的功能。

对于阅读代码,建议你按照下面的流程进行:

  1. main函数开始,读懂main函数。
  2. main函数中调用的DDZGame::run()是主要流程,读懂这个函数的大致流程
  3. 重读DDZGame::run()函数,当看到里面调用的DDZPlayer类的成员函数时,跳到相应的函数阅读。
  4. 在你开始填写play()函数之前,你应该已经大致弄懂了CardDDZPlayerDDZGame三个类。

在完成代码的过程中,请注意:

  1. 你可以写自己需要的辅助函数。
  2. 请不要改写已有的函数接口(参数和返回值)。如果你有100分的自信把类设计的比现在好,你可以从头写一个你的版本。
  3. 体会不同的数据和功能如何被集合到不同的类;同一个类中的数据具有强关联(高内聚);不同类之间有较少的相互关系,不需要在一个类中过多深究其他类的细节(低耦合)。

题目 4 - 单机斗地主游戏

题目要求

借助面向对象程序设计的特性,前面的程序可以轻易地扩展出不同种类的玩家类,使游戏变得丰富。例如:

程序框架中DDZHumanPlayer已经基本实现好了,但是判断出牌合法性的DDZPlayer::legal(...)函数还没有完成。注意legal(...)函数虽然是DDZPlayer类的成员函数,当其正确实现后,继承自DDZPlayer类的DDZHumanPlayer也可以直接使用这个函数。当你正确完成这个函数后,将main2()函数中的内容写到主函数中,一个单机版的斗地主游戏就可以运行了。

int main() {
    string name;
    cout << "Please input your name:" << endl;
    getline(cin, name);

    DDZPlayer p1("Alice"), p2("Bob");
    DDZHumanPlayer p3(name);
    DDZGame g(&p1, &p2, &p3);
    g.run();
}

实现要求

这道题目实现的具体要求如下:

  1. 完成DDZPlayer::legal(cards)函数,判断出牌的正确性。
  2. main2函数的内容填到main函数中,程序就变成了单机斗地主(弱智版)。把程序调试正确。
  3. 体会DDZHumanPlayerDDZPlayer的继承,弄明白哪些函数和变量由于继承,被重用而没有多写一遍
  4. 体会由于DDZHumanPlayerDDZPlayer的派生类,所以可以直接传给DDZGame类作为玩家而不用写特殊判断;在DDZGame类调用play()函数时又实质上执行了不同的出牌策略(多态)。