简介
前几天我读了“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]