1052 Quiz#3 節省系統資源的物件架構

 
C++ 實習測試: 節省系統資源的物件架構

時間: 70分鐘 (106/05/18 09:20 ~ 10:30 上傳時間截止)

你可以看自己的資料, 可以查網路上的說明或是範例, 但是請不要和同學討論

 

先下載執行一下這個 Marquee 範例程式, 解壓縮以後裡面有三個程式

Marquee0

Marquee1

Marquee2

第一個應用程式是靜態的顯示不同大小、不同顏色的字元符號; 第二個應用程式讓它們有一些動作, 三排的文字第一及第三排以同樣速度向左移動, 第二排每個字母翻轉後向右移動, 第一排向左移動就移出畫面而已, 第二排移出去以後會由第一排的右邊移入, 第三排的動作類似第二排; 第三個應用程式只有一排, 變成簡單的跑馬燈動畫, 當然一般的 Video game 隨著主角移動時場景的改變也是很像這樣的效果

上面主要是展示一下 SFML 的貼圖, 給我們這裡要製作的程式一個應用的環境: 在貼出一張圖片的時候, 圖片本身對照到一張影像圖, 這張影像需要載入繪圖卡, 是比較重要的資源, 我們如果要在視窗中貼出同一張圖片時, 影像應該只要載入繪圖卡一次, 不要重複地載入相同的圖片...

原本的影像如下

在程式裡需要載入整張影像, 將個別字母影像, 在不同的位置上以不同的大小, 不同的角度, 不同的著色繪出, 例如上面第二張圖中左上角的 L 是藍色, 右下角的 L 是黃色, 他們的大小和位置是不一樣的, 但是對照到的基本影像是一樣的, 你可以想像如果應用程式裡要重複繪製某個字母圖片很多次, 而且每一個顯示的字母都對應到不同的繪圖物件的話, 會很浪費記憶體資源。(另一個類似的狀況是瀏覽器裡顯示下載的影像, 如果某一個網頁裡有多張重複的圖片, 瀏覽器需要重複下載相同的圖片嗎? 還是每一張圖片只下載一次, 但是繪製很多次?)

我們可以設計一下物件的架構, 使每一個字母的繪圖物件都可以重複使用多次, 主要的影像只存放一次, 位置、縮放大小、角度等等參數則每次使用這個物件的時候調整。如果像上面這張圖裡的字型影像還有很多很多組, 應用程式允許使用者指定任一字母以某一字型、某一位置、某一大小、某一顏色來顯示,基本上程式只需要把用到的影像載入, 需要用到的字元才產生繪圖物件, 很多沒有用到的字型影像完全不需要載入, 載入的字型中如果沒有使用到的字母也不需要產生繪圖物件。

在 SFML 環境中我們可以考慮下圖中的架構:

上圖中的 sf::Spritesf::Texture 是 SFML 中提供的類別, sf::Texture 是用來載入影像圖片的, sf::Sprite 則是可以指定位置、大小、顏色、角度的繪圖物件, 不過大家還不熟悉 SFML, 而且真的放繪圖物件進來以後上面的類別功能解釋起來繁瑣多了, 所以我們先簡化一下, 先考慮只有 CharSpriteFactory 和 CharSprite 兩個類別, 你先假設建構 CharSprite 類別的物件是很花資源的, 所以這個設計的精神在於: 「節省資源, 整個系統中不同的 font 和 symbol 的 CharSprite 物件只保留一個」, 請根據下面簡化過的類別圖、類別的功能解釋還有部份的程式碼來完成相關的設計

首先下面的 main() 函式就是上圖中的 Client, 也就是運用 CharSpriteFactory 和 CharSprite 兩個類別的客戶端

struct Character
{
    string font;
    char symbol;
};

int main()
{
    Character documentData[] = 
                               {{"Doodle", 'R'}, {"Doodle" , 'A'}, 
                                {"Doodle", 'C'}, {"Cartoon", 'E'},
                                {"Tatoo" , 'C'}, {"Tatoo"  , 'A'}, 
                                {"Tatoo" , 'R'}, {"Tatoo"  , 'M'},
                                {"Doodle", 'A'}, {"Tatoo"  , 'D'}, 
                                {"Doodle", 'A'}, {"Tatoo"  , 'M'}};
    vector<Character> document(documentData, documentData+12);

    // 上面這個 document vector 物件裡面就是要顯示的文件, 其中有 12 個文字, 
    // "RACECARMADAM", 每個文字還有指定顯示的字型, 目前假設只有 Doodle 
    // 和 Tatoo 兩種字型

    // 下面的 factory 物件是用來產生管理所有 CharSprite 物件的, 客戶端使用
    // getSprite() 介面來取得指定 font 和 symbol 的 CharSprite 物件的指標, 
    // factory 物件需要檢查參數 font 和 symbol, 看看是不是已經產生過了, 
    // 如果是的話就直接回傳那個 CharSprite 物件的指標, 否則就用指定的 font 
    // 和 symbol 產生一個新的 CharSprite 物件, 再傳回新產生物件的指標

    CharSpriteFactory* factory = new CharSpriteFactory;

    // 下面的迴圈中, 客戶端程式就是運用這個 factory 傳回需要的 CharSprite
    // 物件的指標 s, 然後運用 CharSprite::draw() 介面來以指定的縮放比例和位置
    // 繪出那個字, 因為我們已經把 Sprite 和 Texture 拿掉了, 所以 draw 介面
    // 裡面不要真的繪製圖片了, 只要把 font, symbol, scale, xPosition, yPosition
    // 印出來就好了

    float scale = 0.9f;
    int xPosition = 0, yPosition = 0;
    for (int i=0; i<document.size(); i++)
    {
        CharSprite *s = factory->getSprite(document[i].font, document[i].symbol);
        s->draw(scale, xPosition, yPosition);
        scale*=1.01;
        xPosition+=30;
        yPosition+=40;
    }
    
    delete factory;

  return 0;
}

程式輸出

(Doodle,R,0.9,0,0)
(Doodle,A,0.909,30,40)
(Doodle,C,0.91809,60,80)
(Cartoon,E,0.927271,90,120)
(Tatoo,C,0.936544,120,160)
(Tatoo,A,0.945909,150,200)
(Tatoo,R,0.955368,180,240)
(Tatoo,M,0.964922,210,280)
(Doodle,A,0.974571,240,320)
(Tatoo,D,0.984317,270,360)
(Doodle,A,0.99416,300,400)
(Tatoo,M,1.0041,330,440)

上面這段程式碼還缺 CharSprite 還有 CharSpriteFactory 兩個類別還沒定義, 所以還沒有辦法編譯, 讓我們加上最少的程式碼來編譯這一段程式

class CharSprite
{
public:
    void draw(float scale, int xPosition, int yPosition) 
    { 
        cout << '(' << scale << ')' << endl; 
    }
};

class CharSpriteFactory
{
public:
    CharSprite* getSprite(string font, char symbol) { return &tmp; }
private:
    CharSprite tmp;
};

上面這段程式只要加入需要的 #include 敘述就可以編譯執行了, 其中藍色的部份只是暫時的, 只是為了符合介面的型態, 同時讓程式可以暫時執行而已, 下面的步驟裡你應該會把他改掉。

請完成 CharSprite 類別的設計

  1. 根據上面的類別圖需要設計兩個資料成員 m_font 和 m_symbol

  2. 需要設計建構元函式 CharSprite(string font, char symbol);

  3. 需要完成 void draw(float scale, int xPosition, int yPosition); 函式 (只需要印出 font, symbol, scale, xPosition, yPosition 就好了)

程式應該還是可以編譯執行的, 會印出多一點東西了, 建構元函式只需要把傳入的資料記錄下來就可以, 這個函式是給 CharSpriteFactory 物件使用的, 並不是給客戶端使用的

請完成 CharSpriteFactory 類別的設計

目標: CharSpriteFactory 產生出來的 factory 物件是用來產生管理所有 CharSprite 物件的, 客戶端使用 CharSpriteFactory::getSprite() 介面來取得指定 font 和 symbol 的 CharSprite 物件的指標, factory 物件需要檢查參數 font 和 symbol, 看看指定的 CharSprite 物件是不是已經產生過了, 如果是的話就直接回傳那個 CharSprite 物件的指標, 否則就用指定的 font 和 symbol 產生一個新的 CharSprite 物件, 紀錄在資料容器裡面, 再傳回新產生物件的指標, 客戶端並不擁有這些 CharSprite 物件, 所以客戶端用完以後不要刪除這些物件, 要留給 factory 物件來刪除

  1. 根據類別圖請設計 m_sprites 這個容器, 需要紀錄產生出來的 CharSprite 物件的指標, 需要能夠查詢指定的 font 和 symbol 的 CharSprite 物件是不是存在於這個容器裡面

    你可以定義你需要的結構, 可以用你熟悉的容器來實作, 甚至可以用陣列來實作

    當然如果你用過 STL 裡面的 map 的話, 請你使用 map 來實作, 因為 map 是一個 binary search tree, 查詢的時候最迅速, 而且它的介面很簡單

    不管你打算用哪一種容器實作, 因為查詢的關鍵字有兩個欄位 font 和 symbol, 請定義下面的結構來輔助

    struct FontSymbol
    {
        FontSymbol(string f, char s);
        string font;
        char symbol;
    };

    請定義並且實作成員函式來比對 font 和 symbol 兩個不同型態的關鍵字

    bool operator<(const FontSymbol &rhs) const;

    所以可以用 map<FontSymbol, CharSprite*> m_sprites; 來實作


  2. 要查詢 map 物件裡面存不存在某一個鍵值, 你不能用 if (m_sprites[FontSymbol(font, symbol)] == ???) 來測試, 這樣子做的話當不存在的時候會直接放進去一個新的資料, 你需要用 if (m_sprites.find(FontSymbol(font, symbol) == m_sprites.end()) 來測試, 請完成 getSprite() 介面函式, 在指定的物件不存在時, 動態地產生 CharSprite 物件

  3. 請運用 map<FontSymbol, CharSprite*>::iterator 來完成 CharSpriteFactory 類別的解構元

到這裡應該可以得到需要的輸出了

考試時間: 70分鐘 (09:20 ~ 10:30 上傳時間截止)

將所完成的 project (只需保留 .cpp, .h, .sln 以及 .vcxproj 檔案即可; 刪除掉 .suo, .sdf, .filters, .users, debug\ 資料匣, 以及 ipch\ 資料匣下的所有內容) 以 zip/rar/7zip 程式將整個資料匣壓縮起來, 在「考試作業」繳交區選擇 Labtest3 上傳

後續:

  1. 在 SFML 的環境中, 你可以運用 sf::Texture 類別來載入影像, sf::Sprite 類別來作出每一個字母的繪圖物件, 你可以用最前面下載的 Marquee 應用程式裡面的影像檔案 graffitiAlphabetFacialEmotion.png 和資料檔案 graffitiAlphabetFacialEmotion.txt 來設計, 資料檔案裡面每一個字母有一列來描述, 例如 A 9 11 49 93 24, 其中 (9,11) 是左上角, (49,93) 是寬度和高度, 24 是字型基準線以下的高度

  2. 上面這個是 Flyweight 樣版的一個特例, 一般化的類別圖如下


    設計 Factory  類別除了管理所有共享的 Flyweight 物件之外, 是為了移除 Client 和真正 ConcreteFlyweight 類別的產生程式碼之間的相依性; 設計 Flyweight 抽象類別是為了完全隔絕 Factory 和 Client 以及 ConcreteFlyweight 類別的相依性; Flyweight 類別裡面的 intrinsicState 是那些固定不動的資料, 以上面例子來說就是影像的資料, extrinsicState 是那些需要調整的資料, 以上面的例子來說就是圖片的顯示位置、以及縮放大小。

C++ 物件導向程式設計課程 首頁

製作日期: 05/18/2017 by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw TEL: 02 24622192x6615
海洋大學 電機資訊學院 資訊工程學系 Lagoon