簡介
前幾天我讀了“roachmaster”寫得非常好的文章,並認為這是一個學習 C++ 的優秀入門專案。但隨後我想到,也許它也可以成為一箇中級專案的好指南。它實現了與“roachmaster”模擬器相同的規則,但在設計和實現上有所不同。它更廣泛地使用了面向物件概念和 STL。
C++
C++ 是一種非常通用的語言,一旦你開始利用它所提供的一切。STL 對初學者來說非常棒。Stroustrup(C++ 的發明者)的目標之一是讓 std::vector 類至少和 C 風格陣列一樣快。他還曾表示,C 風格陣列很糟糕,它們甚至不知道自己包含多少個元素。C++ 的另一個重要方面是物件概念。物件是具有與其他物件關係的自定義型別。這意味著,如果你有一個計算平方釐米面積的函式,你可以建立一個釐米物件傳遞給函式,而不是一個裸露的 double 值。這樣你就不會不小心將英寸物件傳遞給該函式。它會在編譯時生成錯誤,並且程式碼在未編譯的情況下無法釋出。這也被稱為型別安全。
本專案
本專案面向已經掌握了基礎 C++ 語法和詞法的程式設計師,他們希望擴充套件面向物件程式設計的設計思路。我們要解決的問題很簡單,因此我們可以更多地關注程式碼及其背後的概念。
此程式碼還有很多內容可以新增才能使其完整,但類可以編譯並已準備好放入專案中。
因此,當前的問題是繪製隨機彩票號碼,為玩家建立帶有隨機號碼的彩票,並檢查玩家是否中獎。
規則
1. 彩票隨機抽取 6 個球,5 個白球和 1 個紅球。
2. 你可以購買任意數量的彩票,每張彩票有 6 個隨機號碼。
3. 如果抽取的號碼與你彩票上的號碼匹配,你就中獎。
4. 如果匹配到紅球,則獲得獎金。
球
當我開始設計新版本時,我想到不同顏色的球可以完美地展示物件繼承。所以我建立了一個基類 Ball,它實現了任何球都通用的所有程式碼。也就是說,每個球都有一個號碼,每個球都有一個顏色。球也可以與其他球進行比較,並且由於所有球都有號碼,它們也可以與任何整數進行比較。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
class Ball
{
public:
Ball(int i) { number = i; }
int getNumber() { return number; }
bool isRed() { return Red; }
const bool operator == (int i) { return number == i; }
const bool operator == (Ball &rhs) { return number == rhs.number; }
const bool operator < (Ball &rhs) { return number < rhs.number; }
const bool operator > (Ball &rhs) { return number > rhs.number; }
protected:
int number;
bool Red;
};
|
現在我們需要區分不同種類的球。在這個彩票遊戲中,有白球和紅球。唯一的真正區別是顏色。我們設定一個標誌來指示球是否是紅色的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
class WhiteBall : public Ball
{
public:
WhiteBall(int i) : Ball(i)
{
Red = false;
}
};
class RedBall : public Ball
{
public:
RedBall(int i) : Ball(i)
{
Red = true;
}
};
|
隨機數
沒有隨機數,彩票就不叫彩票了。RandomNumber 類建立隨機數。建構函式接受 3 個整數引數:最小值、最大值和數量。最小值是要生成的最低值,最大值是要生成的最高值,數量是要建立的號碼數量。我們使用新的 C++11 庫 <random> 來建立我們的隨機數。首先,我們建立一個 random_device 物件。該物件在 GNU/Linux 上使用 /dev/urandom 來為隨機生成器生成隨機種子。我不知道 Windows 如何實現這個物件,但它仍然有效。我們使用預設的隨機引擎(寫作時是 mersenne_twister_engine)來生成我們的數字。
我們這裡也看到了一個新的 C++11 結構:新的 for 迴圈。它使遍歷陣列和其他帶迭代器的物件更加容易。有重複的球是不行的。如果我們得到的號碼在我們的彩票上,我們應該算作兩次匹配還是一次匹配?如果我們檢測到重複,就重新生成號碼並將新號碼放入向量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
class RandomNumbers
{
public:
RandomNumbers(int min, int max, int amount)
{
std::random_device rd;
std::default_random_engine re(rd());
std::uniform_int_distribution<int> uid(min, max);
for (int i = 0; i < amount; i++)
{
int num = uid(re);
for (int n : numbers)
{
if (n == num)
num = uid(re);
}
numbers.push_back(num);
}
}
std::vector<int> getNumbers() { return numbers; }
private:
std::vector<int> numbers;
};
|
彩票
接下來我決定建立 ticket 類。它非常簡單。它只為我們儲存 6 個 1 到 58 之間的隨機號碼。它還包含顯示我們彩票的邏輯。這個類中最有趣的是我們正在使用一個我們自己建立的物件,即 RandomNumbers 物件。我們還實現了 std::sort 函式,該函式可以對包含基本型別(例如 int、char、double、float)的陣列、向量和其他容器進行排序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
class Ticket
{
public:
Ticket()
{
RandomNumbers ticketNums(1, 58, 6);
numbers = ticketNums.getNumbers();
std::sort(numbers.begin(), numbers.end());
}
void display()
{
std::cout << "Ticket: ";
for (int i : numbers)
{
std::cout << std::setw(2) << i << " ";
}
std::cout << std::endl;
}
std::vector<int> getNumbers() { return numbers; }
private:
std::vector<int> numbers;
};
|
彩票
現在我們有了除彩票本身之外的所有東西。有趣的是 vector balls 的使用。它包含指向基類 Ball 的指標,但我們用 5 個 WhiteBalls 和一個 RedBall 來填充它。我們可以這樣做,因為它們都是 Ball 型別。由於我們使用 new 關鍵字建立球,我們還需要使用 delete 來釋放球分配的記憶體。這是如何使用類的建構函式和解構函式的典型示例。建構函式分配物件所需的記憶體,解構函式在物件超出範圍時釋放記憶體。
我們在這裡看到了 C++11 的另一個用法:對我們的球進行排序。Std::sort 尚不知道如何對球進行排序,但它允許我們提交一個自定義函式來展示我們想要如何對它們進行排序。正如我們在 ball 類中所看到的,一個球知道如何與另一個球進行比較。但是,只寫一個函式來測試一個球是否比另一個球的值低,感覺很麻煩。C++11 提供瞭解決方案:Lambda 表示式。Lambda 表示式是一小段程式碼,它像函式一樣工作,但可以很好地嵌入到一行程式碼中,並用作接受函式作為其引數之一的函式的引數。它對於我們不太可能重複使用但仍然需要的功能也很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
class Lottery
{
public:
Lottery()
{
RandomNumbers RandomWhite(1, 58, 5);
RandomNumbers RandomRed(1, 34, 1);
for (int n : RandomWhite.getNumbers())
{
balls.push_back(new WhiteBall(n));
}
std::sort(balls.begin(), balls.end(), [](Ball* a, Ball* b){ return *a < *b; });
balls.push_back(new RedBall(RandomRed.getNumbers()[0]));
}
~Lottery()
{
for (auto ball : balls)
{
delete ball;
}
}
void display()
{
std::cout << "Lottery: ";
for (auto ball : balls)
{
if (ball->isRed())
std::cout << "Red number: " << ball->getNumber() << std::endl;
else
std::cout << std::setw(2) << ball->getNumber() << " ";
}
}
std::vector<Ball*> getBalls() { return balls; }
private:
std::vector<Ball*> balls;
};
|
獲勝條件
好的,我們有了球、彩票和彩票系統。現在我們需要看看玩家是否贏得了任何東西。即使程式碼看起來有些混亂,邏輯也相當直接。如果你使用一個好的 IDE,它會幫助你突出顯示大括號之間的內容。我使用 Microsoft Visual Studio Express 2013,但還有許多其他非常有能力的 IDE,請務必選擇一個適合你需求的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
|
class Winning
{
public:
Winning(std::vector<Ticket*> tickets, std::vector<Ball*> balls)
{
for (auto ticket : tickets)
{
int matches = 0;
bool hasRed = false;
for (int number : ticket->getNumbers())
{
for (auto ball : balls)
{
if (*ball == number)
{
matches++;
if (ball->isRed())
hasRed = true;
}
}
}
winnsPerTicket.push_back(matches);
hasRedTicket.push_back(hasRed);
}
}
int getWinnings()
{
for (size_t i = 0; i < winnsPerTicket.size(); i++)
{
std::cout << "Got " << winnsPerTicket[i] << " matches.";
if (hasRedTicket[i])
std::cout << " And has got the red ball!" << std::endl;
else
std::cout << " But has not got the red ball." << std::endl;
}
return 0;
}
private:
std::vector<int> winnsPerTicket;
std::vector<bool> hasRedTicket;
};
|
遊戲
我們現在快完成了。唯一剩下的就是建立一個購買彩票的選單並實現玩遊戲的邏輯。同樣,我們需要確保在解構函式中刪除我們用 new 建立的物件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
|
class Game
{
public:
Game() {};
~Game()
{
for (auto ticket : tickets)
{
delete ticket;
}
}
void Menu()
{
int numTic = 0;
std::cout << "Welcome to the PowerBall Lottery!" << std::endl;
std::cout << "To play you need to purchase a ticket at $2. More tickets increase the odds to win." << std::endl;
std::cout << "How many tickets would you like? " << std::endl;
do
{
std::cout << "Enter amount of tickets you would like to purchase: ";
std::cin >> numTic;
std::cin.sync();
if ((numTic < 1) || (numTic > 100))
{
std::cout << "Input invalid. Needs to be a number between 1 and 100. Please try again" << std::endl;
}
} while ((numTic < 1) || (numTic > 100));
createTickets(numTic);
std::cout << "Your tickets are registered. Thank you for playing the PowerBall lottery!" << std::endl;
}
void Play()
{
std::cout << "Let\'s see this weeks PowerBall lottery numbers!" << std::endl;
lotto.display();
for (auto ticket : tickets)
{
ticket->display();
}
Winning w(tickets, lotto.getBalls());
w.getWinnings();
}
private:
std::vector<Ticket*> tickets;
Lottery lotto;
void createTickets(int numTic)
{
for (int i = 0; i < numTic; i++)
{
tickets.push_back(new Ticket);
}
}
};
|
後記
就是這樣!差不多了。我留了一些東西給你實現,但程式碼可以編譯,並且使用本教程中的程式碼可以玩彩票。你可能想新增一些函式來跟蹤輸贏,也許給玩家一個錢包物件來存放錢。也可以考慮如何儲存遊戲狀態以在會話之間保留統計資料。但正如我已經說過的,這些都留給你實現。
哦,在我忘記之前。你需要包含一些標頭檔案才能使此程式碼正常工作。
1 2 3 4 5
|
#include <iostream> // For std::cout, std::cin
#include <iomanip> // For std::setw
#include <random> // For all random generation stuff
#include <algorithm> // For std::sort
#include <vector> // For std::vector
|
本教程中的所有程式碼都採用 copyleft 許可。也就是說,你可以隨意使用此程式碼。如果對任何函式或方法不確定,請務必檢視 http://www.cplusplus.com/ 獲取更多資訊。如果您有任何問題或建議,請透過私信或電子郵件與我聯絡。
“roachmaster”的文章
http://www.cplusplus.com/articles/4yhv0pDG/作者 Tomas Landberg (tomas.landberg (at) gmail.com)
祝你好運!
附件: [PowerBallLotteryRevamped.pdf]