實習目標 |
|
---|---|
步驟一 |
先下載原來的 3 bags 程式,
這個程式的類別圖 (Class Diagram) 如下:
|
步驟二 | 我們希望把原來的程式修改成有四個袋子, 每個袋子裡有三個球, 分別是 (紅, 紅, 紅), (紅, 紅, 白), (紅, 白, 白), (白, 白, 白), 我們希望去算出 "任選一個袋子, 如果前兩個抽出的球是紅球的話, 第三個球是紅球的機率" 是多少 |
步驟三 |
我們由底層的類別來開始修改, 不過 Ball 類別似乎已經符合要求,
不需要修改了
Bag 類別需要由原來放兩個球改為放三個球, 所以類別定義裡指標陣列 Ball *m_balls[2]; 的元素個數需要改為 3, 建構元 Bag::Bag(...) 函式的參數要改為三個, 初始化串列 : m_numberOfBalls(2) 也要更改數值為 3, 函式內容需要多增加一個 new 的記憶體配置敘述, 解構元 Bag::~Bag() 裡要釋放的記憶體也相對地增加一個 Ball *Bag::getABall() 函式裡需要增加袋中有三個球的可能性, 如果你有時間的話, 邏輯可以改一下, 讓程式看起來簡潔一點 void Bag::putBallsBack(); 函式很容易改, 在設計這個 Bag 類別的時候所有產生的 Ball 物件都由它來管理, 所以在 Bag 物件銷毀的時候必須負責把所有管理的 Ball 物件都刪除掉。 另外你也可以思考一下: getABall() 和 putBallsBack() 原來在實作時雖然會透過 getABall() 回傳一個 Ball 物件的指標, 但是基本上所有的 Ball 物件還是由 Bag 物件來管理, 它們的指標還是記錄在 Bag::m_balls 陣列中, 這樣的邏輯和實際實驗時差距比較大, 實際上每次由 Bag 中拿出一個球的時候, 這個 Ball 物件應該要由 Bag 物件中移掉, 而由 main() 函式來管理, 直到 putBallsBack() 被呼叫時, 先前抽出的球再透過這個函式的參數傳回 Bag 物件中, 這樣的運作模式比較直覺。 |
步驟四 |
Game 類別需要由原來三個袋子改為四個袋子, 所以類別定義裡指標陣列 Bag *m_bags[3]; 的元素需要改為 4, 同時我們也練習一下替這個類別增加一個常數資料成員, const int m_numberOfBags; 建構元 Game::Game() 函式必須使用初始化串列來設定 m_numberOfBags 整數常數的數值: Game::Game():m_numberOfBags(3)函式內容需要多增加一個 new 的記憶體配置敘述, 記得 Bag 的建構元已經修改過了, 所以 new Bag(Ball::White, Ball::White) 的參數是錯的, 解構元 Game::~Game() 裡要釋放的記憶體也相對地增加一個, 請盡量利用 m_numberOfBags 這個變數來改寫程式, 讓程式裡出現的常數數字 (literal) 越少越好 Game *Game::getABag() 函式裡需要由三個袋子改為四個袋子 邏輯可能要改一下 |
步驟五 |
最後要修改 main 函式,
原來是抽出一顆球, 檢查是不是紅球, 是的話再抽一顆球,
現在抽出第二個球的時候要再檢查一次, 如果是紅球才再抽第三個球,
然後累計結果
範例執行程式(請下載後執行) |
步驟六 | 進一步修改程式, 我們覺得現在放在 main() 函式裡的實驗步驟其實應該寫到 Game 類別裡成為一個成員函式,
這樣子的話, Game 可以有好多個不同的實驗, 比方說還可以有另外一個實驗試看看如果抽第一個球是紅球, 第二個球是白球的機率有多少...
在修改程式之前我們有一些準備的工作: 我們知道如果在 srand() 函式中傳入一個固定的數字, 例如 srand(0), 不管什麼時候執行它, 產生的亂數序列應該是完全一樣的。 在這個步驟中打算作的修改基本上不會改變程式的結果, 所以在修改程式前, 請先在原來主程式結束前加上 assert 的敘述, 例如 assert(thirdIsAlsoRed == 2477); 其中 2477 這個數字要在程式還沒修改前利用原來的程式列印出來, 加上這個敘述的目的是要保證修改過的程式還能夠得到一樣的結果。 接下來就可以進行程式的修改了, 把 main() 中實驗的部份抽出來到一個 Game 類別的成員函式中, 傳回一個機率值到主程式來列印。 這樣子的修改方法是防止你在更動程式架構時不小心造成結果的錯誤, 物件導向程式中有很多程式架構的彈性, 例如說某一個成員函式可以放在某一個類別中或是另外一個類別中, 某一個資料成員可以放在這個類別或是另外一個類別中, 這些都是可以一步一步調整的, 很可能各有各的好處, 需要視實際狀況來調整, 所以常常我們會更改必要的程式, 但是不需要更改的部份一定完全不動, 而需要更改的程式會藉由一些 assert 敘述來保證它沒有被改錯 |
步驟七 |
接下來我們要再增加這個程式一點功能,
我們先來做一個骰子 Dice 的類別,
這個骰子不見得是公平的,
我們可以視實際需要來設定這個骰子各個點數出現的機率,
例如: {1/4, 1/6, 1/12, 1/12, 1/6, 1/4} 代表 1 點和 6
點出現的機率最大各為 1/4, 其次是 2 點和 5 點, 機率各為
1/6, 出現最少的是 3 點和 4 點, 機率各是 1/12。
也就是說這個類別需要有一個建構元 Dice::Dice(double probability[]) { ... } 來設定六個機率值, 傳進去一個六個元素的浮點陣列, 記得用 assert 敘述檢查一下總和是不是 1.0 (目前還沒有學到使用 exception, 所以如果檢查失敗了, 還沒有辦法處理) 我們可以在任何時候丟這個骰子, 來看到它出現的點數, 所以這個類別會有一個公開的成員界面, int Dice::randomThrow();每次呼叫都會根據這個骰子預設的機率值 傳一個點數回來 這個 randomThrow() 函式裡應該要實作類似丟銅板決定正反面的程式 int x; x = rand(); if (x < RAND_MAX/2) return 0; // 正面 else return 1; // 反面 只是現在有六個可能的輸出數值 {1, 2, 3, 4, 5, 6}, 而且決定的邊界不見得是均勻地平分 RAND_MAX, 而是根據建構這個骰子物件時設定的機率 probability[6] 來設定邊界, 例如 int i; int boundary[6]; double cumulativeProbability = 0; for (i=0; i<6; i++) { cumulativeProbability += probability[i]; boundary[i] = cumulativeProbability * RAND_MAX; } 在 randomThrow() 函式中決定骰子點數的程式片段如下: int x, i; x = rand(): for (i=0; i<6; i++) if (x < boundary[i]) return i; |
步驟八 |
在我們的 Game 的類別裡現在需要有兩顆骰子,
一顆是公平的 {1/6, 1/6, 1/6, 1/6, 1/6, 1/6},
另外一顆骰子是不公平的 {1/4, 1/6, 1/6, 1/6, 1/6, 1/12}
初始化及銷毀其中一個骰子的程式可以如下 Dice *dice[2]; double prob1[]={1.0/6, 1.0/6, 1.0/6, 1.0/6, 1.0/6, 1.0/6}; dice[0] = new Dice(prob1); ... delete dice[0]; 現在請再寫一個 Game 的成員函式來做一個新的實驗, 1. 請由四個袋子中任意挑選一袋 2. 請由袋子中選一顆球出來, 如果是紅球的話, 把球放回去, 重新再來 3. 如果在 2 中挑到白球, 請隨便挑一顆骰子丟, 如果是奇數的話, 不做任何事, 如果是偶數的話再挑一顆球, 把球留在外面 4. 最後再由袋子裡挑一顆球,請計算它是白球的機率請計算執行第四步驟時白球出現的機率是多少? (第四步驟可能出現紅球或是白球,兩個機率的和是 1) 範例執行程式(請下載後執行) |
步驟九 | 請嘗試修改上面的類別圖, 加上新的類別, 畫上它和其他類別的關係 |
步驟十 | 請助教檢查後, 將所完成的 project (去掉 debug/ 資料匣下的所有內容) 壓縮起來, 選擇 Lab6-1 上傳, 後面的實習課程可能需要使用這裡所完成的程式 |
回
C++ 物件導向程式設計課程
首頁
製作日期: 04/11/2011
by 丁培毅 (Pei-yih Ting)
E-mail: pyting@mail.ntou.edu.tw
TEL: 02 24622192x6615
海洋大學
電機資訊學院
資訊工程系
Lagoon