C/C++ Tutorial, Goal-Oriented Style (3) C++ Object

目標

本章最後會寫一個回合制的 RPG 遊戲,怪物是個物件,玩家是個物件,整個世界地圖也是個物件。

物件 (Object)

物件 (Object) 說白了就是把資料 (data) 和函式 (function / method) 包在一起的概念。請參考下面的程式:

counter.hpp

#ifndef COUNTER_HEADER
#define COUNTER_HEADER
class Counter
{
public:
    Counter()
    {
        count_ = 0;
    }

    Counter( int init_count )
    {
        count_ = init_count;
    }
    
    ~Counter() { }
    
    void inc() 
    {
        count_ ++;
    }
    
    int count_;
};
#endif

main.cpp

#include <iostream>
#include "counter.hpp"
int main()
{
    Counter counter_1, counter_2( 5 ); // constructor
    
    Counter *counter_3;
    counter_3 = new Counter(); // constructor
    counter_1.inc();
    counter_2.inc();
    counter_3->inc();  // convenient operator "->"
    (*counter_3).inc(); // same meaning as above
    
    std::cout << counter_1.count_ << std::endl;
    std::cout << counter_2.count_ << std::endl;
    std::cout << counter_3->count_ << std::endl;
    
    delete counter_3; // destructor
    // counter_1/counter_2's destructor called here
}
  • public 指的是 method 和 data 都開放給別人隨便使用
    • 因為我們不希望 counter_ 被別人隨便改,所以應該改為 private
    • 約定俗成的看法是所有的 data 都要宣告為 private
  • Constructor (建構子) 在物件被建立時會被呼叫 (stack vs heap)
  • Destructor (解構子) 在物件消失時會被呼叫 (stack vs heap)
  • 便利的 operator ->

Namespace

為了怕撞名,C++有命名空間 (namespace) 的語法,拿 counter 這個物件當例子,我們給他一個命名空間叫 superlib。

#ifndef COUNTER_HEADER
#define COUNTER_HEADER

namespace superlib
{
    class Counter;
}

class superlib::Counter
{
public:
    Counter() { count_ = 0; }
    ~Counter() { }    
    inc() { count_ ++; }

private:    
    int count_;
};
#endif

在使用時,原來叫 Counter 的就改叫 superlib::Counter就好。命名空間也可以直接把整個宣告用 namespace 框起來,下一小節的例子就是。

這小節的目的只為了是說明 std:: 的字頭代表的意義,std 是 C++ 標準庫的 namespace。但是在單人作業的小型的 project 中用 namespace 只是找自己麻煩,會用並不代表需要使用,程式精簡易讀比看起來酷炫帥氣重要。:-)

Header for Class

物件的宣告和實作也可以分成二個檔案來處理。

counter.hpp

#ifndef COUNTER_HEADER
#define COUNTER_HEADER

namespace superlib
{
    class Counter
    {
    public:
        Counter();
        ~Counter();
        void inc();

    private:    
        int count_;
    };
}
#endif

counter.cpp

#include "counter.hpp"

superlib::Counter::Counter()
{
    counter_ = 0;
}

superlib::Counter::Counter( int init )
{
    counter_ = init;
}

superlib::Counter::~Counter()
{
}

void superlib::Counter::inc()
{
    counter_++;
}

main.cpp

#include "counter.hpp"

int main()
{
    superlib::Counter counter;
    counter.inc();
    return 0;
}

Standard C++ Library

標準的 C++ Library 就是已經寫好的物件,或叫函式庫。這些標準的函示庫都是在 std namespace 下。我們介紹二個這章會用的標準函式庫。

C++ String

不同於 C String 只是個 char array,C++ String 是一個包括了資料和字串操作的物件。我們直接把這章會用的功能寫成一個範例。

#include <iostream>
#include <string>

int main()
{
    std::string str1, str2( "my str2" ), str3;
    std::string substr;
    str1 = "hello";
    str3 = str1 + str2;
    substr = str2.substr(3, 4);
    std::cout << "str1.size(): " << str1.size() << std::endl;
    std::cout << "str2[1]: " << str2[1] << std::endl;
    std::cout << "str2.substr(3, 4): " << substr << std::endl;
    return 0;
}
  • 我們用了 substr 的 method 取得並複製了一部分的 str2 的字串
  • std::string 有多種 construtor,我們用了二種
  • std::string 有複寫了 =,[],+ 等多種的 operator,這些進階的功能這邊不會提及要如何做到,但是如果想要,我們也可以讓前面的 Class Counter 也能做到 counter3 = count1+ count2 或是 count1[4] = 5 之類操作,但複寫這些 operator 對 Class Counter 有沒有意義就是另一回事了。

C++ Vector

C++ 有一整套跟資料結構相關的函式庫,有個專有名詞叫 STL (Standard Template Library) ,裡面用到了 C++ 很獨特的 Template 語法。老實說,Template 不易寫,很複雜,出錯也很難 debug,複雜度的問題一直都是 C++ 被人攻擊的點。Template 的優點是速度很快,因為它是直接在 compile 時依照資料類型產生代碼,而非 runtime 來處理不同類型的資料的 casting。

資料結構就是存放資料的方法,我們介紹最常用的 Vector。

Vector 是一個會自己長大的陣列,舉例來說,一開始先預留 10 個整數,如果使用者一直用,用到了超過 10 的位置,Vector 就會自動從 heap 再要一個更大的記憶體,然後把舊的只有 10 個整數的陣列複製過去,然後繼續使用。所以使用 Vector 時我們就不用怕陣列太小不夠用了。

Vector 的優點是存取中間某個資料的速度較快,缺點是要刪除中間的資料要花更多時間,和另一種叫 List 的資料結構正好相反。

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> int_array;
    std::vector<int>::iterator it_int;
    
    // change capacity
    std::cout << "capacity:" << int_array.capacity() << std::endl;    
    int_array.reserve( 10 );
    std::cout << "capacity:" << int_array.capacity() << std::endl;

    // change size    
    int_array.resize( 10 );
    
    // assign 10 value to int_array[0~9]
    for ( int i = 0; i < 10; i ++ )
    {
        int_array[i] = i;
    }
    
    std::cout << "size:" << int_array.size() << std::endl;

    // find and delete
    iterator = int_array.begin(); // iterator points to array[0]
    iterator ++; // iterator points to array[1]

    int_array.erase( iterator ); // delete array[1]
    std::cout << "size: " << int_array.size() << std::endl;

    // traversal    
    for (auto it = int_array.begin(); it != int_array.end(); it++) 
    {
        std::cout << *it << std::endl;
    }

    return 0;
}
  • 了解 vector / link / map 的大致概念
  • 了解 STL 的 iterator 設定 (用法不是很直覺,需要記一下)
  • 了解 C++11 的 auto iterator (convenient syntax)

繼承 (Inheritance)

我們直接用怪物和玩家的物件來說明繼承是什麼。

在地圖上,每回合玩家會動,怪物也會動。對遊戲來說,玩家和怪物在同樣 “會動” 的概念下是同類型。差別是玩家怎麼動是靠玩家自己輸入的,怪物怎麼動是遊戲邏輯決定的。

我們先寫一個叫 Moveable 的物件,裡面有個 virtual function 叫 next_move,可以回傳 1~4 分別代表 N\E\S\W。virtual 的意思是告訴 compiler 這個 function 是等等會被 (或說被允許) 複寫掉的。

然後再寫二個物件,一個叫 monster,它繼承了 moveable,複寫掉 next_move 並用 override 的贅詞告訴 compiler 複寫這件事 (主要目的只是防呆除錯)。一個叫 player,它也繼承同樣的 moveable,然後用 std::cin 取得下一步要去哪的指令。

mob.hpp

#ifndef MOB_HPP
#define MOB_HPP
#include <iostream>

class Moveable
{
public:
    virtual int type() { return 0; }
    virtual int next_move() { return 0; }
};

class Monster : public Moveable
{
public:
    int type() override { return 10; }
    int next_move() override
    {
        return 1; // not-very-smart monster that can only go north
    }
};

class Player : public Moveable
{
public:
    int type() override { return 20; }
    int next_move() override
    {
        int move;
        std::cout << "your next move? (1~4) ";
        std::cin >> move;
        return move;
    }
};
#endif

然後我們看一下 main.cpp,我們建了一個 Moveable* 的 vector 陣列,裡面同時放了 Monster 和 Player。因為 Monster 和 Player 都是 Moveable 型態的,所以不會有 compile error。然後因為 Monster 和 Player 都有 type() 和 next_move() 的 function,所以可以無違和的當同樣的物件來使用。

#include <iostream>
#include <vector>
#include "mob.hpp"

int main()
{
    std::vector<Moveable*> mobs;
    
    mobs.push_back( new Monster() );
    mobs.push_back( new Monster() );
    mobs.push_back( new Monster() );
    mobs.push_back( new Monster() );
    mobs.push_back( new Player() );
    
    while ( true )
    {        
        for ( auto it = mobs.begin(); it != mobs.end(); it ++ )
        {
            int move = (*it)->next_move();
            std::cout << "mob type:" << (*it)->type() << " goes " << move << std::endl;
        }
    }
    
    // DON'T FORGET TO RELEASE THE HEAP!
    for ( auto it = mobs.begin(); it != mobs.end(); it++ )
    {
        delete (*it);
    }
}

為什麼 vector 要用 Moveable* 而非 Moveable 呢? 記得 pass-by-reference 嗎? 如果寫成

    std::vector mobs;
    Monster mob_1, mob_2, mob_3;
        
    mobs.push_back( mob_1 );
    mobs.push_back( mob_2 );
    mobs.push_back( mob_3 );

它會試著複製 mob_1,mob_2,mob_3 到 vector 裡面,可是這不像複製個整數 5 到 function stack 的 data 空間一樣單純,我們沒有告訴 compiler 怎麼複製 Monster 這個物件,也就是沒為 Monster 寫過 operator = 或同等的複製方法。因為不知道怎麼正確複製 Monster 到 function stack 的 data,compiler 常常會出現預期以外的行為,是種危險的寫法。

Static Function and Static Variable

思考一下 monster 和 player 的戰鬥,戰鬥的邏輯不該單獨存在於 Monster/Player 裡面,因為那是二個物件之間對等且雙方都會有影響的互動。在 C++ Programming Style 裏,所有的 function 都該寫在某個物件裏,我這邊的選擇是寫成 World 裡面的某個 static function。

參考下面的程式

#include <iostream>

#include "mob.hpp"

class World
{
public:
    static const int world_version_ = 10422;
    
public:
    static void attack( Moveable* attacker, Moveable* attackee)
    {
        // ATTACK!!!
    }
};

int main()
{
    Monster* monster = new Monster();
    Player* player = new Player();
    
    std::cout << "Version: " << World::world_version_ << std::endl;
    World::attack( monster, player );
    
    delete monster; // DON'T FORGET TO RELEASE RESOURCE!
    delete player;
    
    return 0;
}

static function 和 static variable 不需要建立實體的物件就能直接使用,是用時也不是用 . 或是 ->,而是 ::。對 static variable 來說,其實就跟 C 風格的 global variable 是一樣的意思,是個全域且共用的變數。

My First Design Pattern – “Singleton”

類似我們在上一小節思考在哪裡放 fight function,在寫大程式時,會出現很多可以用同時用多種方法解決的問題,但哪一種是對的? 哪一種未來要增減功能時痛苦指數最低? 哪一種最符合人腦的直覺想法? 哪一種最後不會讓你想把自己的名字從作者欄中刪掉? 這些程式設計上的選擇就是 Design Pattern。

現在,我們來使用一個常見的 Design Pattern, Singleton, 來處理 World 這個物件。

Singleton 指的是有一個物件,在全程式中是單一,共用,且一直存在不會消失的,也就是 C 的 global variable 的概念。對 RPG 來說,世界物件,包括了地圖或是遊戲參數,這些都該是單一且共用的東西。遊戲任何時候都不該建立二個相同的 “世界” 物件,”世界” 物件需要一直存在,除非程式結束 “世界” 永遠不該被消滅。這些特性都是 Singleton 的使用情境。

架構如下:

class World
{
public:
    static World& get_instance()
    {
        static World instance;
        return instance;
    }
public:
    World(const World& arg) = delete; // copy constructor
    World& operator=(const World& arg) = delete; // assignment operator

private:
    World() {}
public:    
    void tick() 
    {
    }
};

int main()
{
    World::get_instance().tick();
    
    return 0;
}
  • 永遠都只有一個的物件,絕對不能有 = 號的出現,因為 = 就代表了複製,但 compiler 會自動產生一個,所以要告知刪除以防萬一
  • 永遠都只有一個的物件,絕對不能讓外界使用它的 constructor 來複製,但 compiler 會自動產生一個,所以要告知刪除以防萬一
  • 預設的 Constructor 依然是必要的,放在 Private 讓使用者不能用。
  • 唯一的實體存在於 class 宣告內的 static World instance,取用時是取得它的 reference (也就是可以直接使用,且不會被更改的 address)。
  • 使用裡面的 function tick() 的方法如程式所述。

RPG!

作業: 下面是個離職員工寫的糟糕程式,是個半成品沒辦法 compile,想辦法完成它吧。看你要打掉重寫或是修改都行!

class Moveable
{
public:
    Moveable( int hp )
    {
        hp_ = hp;
        attacking_ = NULL;
    }
    
    virtual int next_move();
    
    // accessor
    void x( int x ) { x_ = x; }
    void y( int y ) { y_ = y; }
    void hp( int hp ) { hp_ = hp; }
    
    int  x()        { return x_; }
    int  y()        { return y_; }
    int  hp()       { return hp_; }
    
private:
    int x_, y_, hp_;    
    Moveable *attacking_;
};

class Monster : public Moveable
{
public:
    int next_move() override
    {
        return 1; // not-very-smart monster that can only go north
    }
};

class Player : public Moveable
{
public:
    int next_move() override
    {
        int move;
        std::cout << "your next move? (1~4) "; std::cin >> move;
        return move;
    }
};

class World
{
public:
    static World& get_instance()
    {
        static World instance;
        return instance;
    }

public:
    World(const World& arg) = delete; // copy constructor
    World& operator=(const World& arg) = delete; // assignment operator
    
public:
    void tick()
    {
        if ( player_ is not fighting )
        {
            // show room description
            std::cout << desc( player_.x(), player_.y() ) << std::endl; 
            
            // show exit, ex: "You can see two exit(s) [north, south]" 
            // show monster information, ex: "One monster is here."
        }

        for all monster in mobs_ 
        { 
            if ( monster is in the same room with player_ )
            {
                if ( monster is not fighting ) 
                { 
                    set monster start attacking player_ 
                    show "monster start attacking you!" 
                    
                    if ( player_ is not fighting ) 
                        set player_ start attacking monster 
                 } 
            } 

            if ( monster is not fighting ) 
            { 
                int direction = monster->next_move();
            
                if ( direction is valid )
                {
                    monster->x( new x );
                    monster->y( new y );
                    
                    if ( new x and new y is where the player is )
                    {
                        show "monster came from [n/e/w/s], combat is starting!"
                        set monster start attacking player
                        if player is not fighting
                            set player start attacking that monster
                    }
                }
            }
            else // monster is fighting
            {
                attack( monster, player ); // shows battle information in this function
            }
        }
        
        // player's move
        if ( player is not fighting )
        {
            int direction = player->next_move();
            if ( direction is valid )
            {
                player->x( new x );
                player->y( new y );
            }
        }
        else // player is fighting
        {
            attack( monster, player ); // shows battle information in this function
        }
        
        if ( player died )
        {
            // Good Game 🙂
        }
        
        // traversal all mobs
        for all monster in mobs_
        {
            if ( monster dead )
            {
                1. remove from attacking target for player
                2. change player's target to other mob if anyone is attacking him/her
            }
        }
    }

private:
    World()
    {
        // create map, this '{}' is c++11 syntax
        map_ = new int[5*5]
        {
            1, 1, 1, 1, 1,
            1, 0, 0, 0, 0,
            1, 0, 0, 1, 0,
            1, 1, 0, 1, 0,
            1, 0, 0, 0, 0            
        };
        
        
        // summon mobs
        // create player
    }
    
    ~World()
    {
        // delete player
        // delete mobs
        // delete map
        delete [] map_;
    }

    bool create_monster( int hp, int x, int y )
    {
        return false;
    }
    
    bool create_player( int hp, int x, int y )
    {
        return false;
    }
    
    std::string desc( int x, int y )
    {
        std::ostringstream stringStream;
        stringStream << "This is a empty room with a white board floating on the air. On the board, it wrote ("
                     << x << "," << y << ")";
        return stringStream.str();
    }
    
    void attack( Moveable* attacker, Moveable* attackee ) 
    {
    }
    
private:
    int* map_;
    std::vector<Monster*> mobs_;
    Player* player_;
};

int main()
{
    while (true)
    {
        World::get_instance().tick();
    }    
    return 0;
}
*** 遊戲開始 ***
你走在一個峽谷裏,左右是高山,只有前後有路。
你看到可以走的出口是 [north(1), south(3)]
[HP 10] >> 1

你往北走
你走在一個峽谷裏,前方沒有路,只能往後走。
你看到可以走的出口是 [south(3)]
你看到前面有一隻怪物
你看到前面有一隻怪物
[HP 10] >>
怪物開始攻擊你
怪物開始攻擊你

怪物傷害你 1 HP [HP 9]
怪物傷害你 1 HP [HP 8]
你攻擊怪物 1 HP

南方來了一隻怪物
怪物開始攻擊你