面向懒惰程序员的 C++20 教程(二)
四、鼠标,和if
在这一章中,我们将学习鼠标输入,以及计算机风格的决策艺术。
鼠标功能
示例 4-1 显示了一个程序来检测你点击鼠标的位置并报告结果。很神奇吧。因此,我们介绍三种鼠标功能:SSDL_GetMouseX、SSDL_GetMouseY和SSDL_WaitMouse。
// Program to get a mouse click, and report its location
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char** argv)
{
sout << "Click the mouse and we'll see where you clicked.\n";
// Get the mouse click
SSDL_WaitMouse(); // wait for click...
int xLocation = SSDL_GetMouseX(); // and get its X, Y location
int yLocation = SSDL_GetMouseY();
// Print the mouse click
sout << "The X position of your click was " << xLocation << "\n";
sout << "The Y position of your click was " << yLocation << "\n";
// End the program
sout << "\n\nHit a key to end the program.\n";
SSDL_WaitKey();
return 0;
}
Example 4-1A program to capture and show a mouse click. Excitement!
此时此刻
int xLocation = SSDL_GetMouseX(); // and get its X, Y location
int yLocation = SSDL_GetMouseY();
您的程序分配空间来存储两个整数,xLocation和yLocation,并在每个整数中放置一个值。
此时,程序打印它们(图 4-1 ):
图 4-1
报告鼠标点击。您的点击可能会有所不同
// Print the mouse click
sout << "The X position of your click was " << xLocation << "\n";
sout << "The Y position of your click was " << yLocation << "\n";
在表中,4-1 是新鼠标功能的声明。
表 4-1
SSDL 的基本鼠标功能
| `int``SSDL_GetMouseX` | 返回鼠标指针的 X 位置。 | | `int``SSDL_GetMouseY` | 返回鼠标指针的 Y 位置。 | | `void``SSDL_WaitMouse` | 等待任何鼠标按钮被点击。 |Extra: Where Should You Declare Variables
把它们放在这里
int main (int argc, char** argv)
{
int xLocation; // X and Y location
int yLocation; // of mouse
sout << "Click the mouse and we'll see where you clicked.\n";
// Get the mouse click
SSDL_WaitMouse (); // wait for it...
xLocation = SSDL_GetMouseX (); // and get its X and Y
yLocation = SSDL_GetMouseY (); // location
...
而不是这里
int main (int argc, char** argv)
{
sout << "Click the mouse and we'll see where you clicked.\n";
// Get the mouse click
SSDL_WaitMouse (); // wait for it...
int xLocation = SSDL_GetMouseX (); // and get its X and Y
int yLocation = SSDL_GetMouseY (); // location
...
是一种老式的做事方法,从 C++ 还是普通 C 的时候就开始了。有些人更喜欢它,因为变量总是很容易找到;他们在顶部!我不知道,因为
-
我开始在设置或使用它们的地方寻找它们,而不是在顶部。
-
我更喜欢尽可能将它们初始化为有用的值(在这种情况下,直到调用
SSDL_WaitMouse之后才会发生)。 -
正如这个例子所显示的,这导致了对更多注释的需求。
老方法没有错,但是“尽可能晚地声明”似乎更懒惰。
防错法
-
The numbers reported for the mouse click don’t have anything to do with where you actually clicked. And your code looks like this:
int xLocation = SSDL_GetMouseX (), yLocation = SSDL_GetMouseY (); // Get the X, Y location SSDL_WaitMouse (); // wait for click...Thing is,
SSDL_GetMouseX/SSDL_GetMouseYdon’t get a mouse click location; they just get a location. So here’s what happens:-
程序获得鼠标的 x,y 位置。
-
当程序等待时,你移动鼠标到你想要的地方。
-
你点击。
It gets the location before you move the mouse where it should go. No wonder it’s wrong! Rearrange it thus:
-
当程序等待时,你移动鼠标到你想要的地方。
-
你点击。
-
程序获得鼠标的 x,y 位置。
…如例 4-1 所示。
-
Exercises
-
写一个程序,让你点击两次,在两次鼠标点击之间画一条线。
-
编写一个程序,让你点击一次来设置一个圆的中心,然后再点击一次来设置这个圆的边缘上的一个点;然后它画圆。
if
那么我如何确定鼠标是否在屏幕的特定区域呢?
constexpr int HALF_SCREEN_WIDTH = 320;
if (xLocation < HALF_SCREEN_WIDTH)
sout << It's on the left side of the screen.\n";
else
sout << "It's on the right side of the screen.\n";
如果xLocation小于HALF_SCREEN_WIDTH,程序会告诉我们它在左边;其他在右边。
else部分是可选的。如果xLocation在左边,您可以进行程序报告,如果在右边,您可以什么都不说:
if (xLocation < HALF_SCREEN_WIDTH)
sout << "It's on the left side of the screen.\n";
Note
if语句的一般形式是
if ( <条件> ) *<动作 1>*其他 <动作 2>
其中 <尖括号> 中的东西是你可以用其他东西填充的空白,*【方括号】*中的任何东西都可以省略。这被称为“巴克斯-诺尔形式”(BNF),这是描述编程语言结构的传统方式。
if语句确实像它看起来的那样:如果条件为真,它执行动作 1;否则,执行动作 2 。
自然地,if语句的条件必须是真或假。这通常是表 4-2 中的判断题之一。
表 4-2
在 C++ 中使用比较运算符
|情况
|
意义
|
| --- | --- |
| X < Y | x 小于 y。 |
| X <= Y | x 小于或等于 y。 |
| X > Y | x 大于 y。 |
| X >= Y | x 大于或等于 y。 |
| X == Y | x 等于 Y。(X=Y,使用单个=,表示“将 Y 的值存储在 x 中”。) |
| X != Y | x 不等于 y。 |
您还可以让if部分或else部分包含多个动作:
if (xLocation < HALF_SCREEN_WIDTH)
{
int howFarLeft = HALF_SCREEN_WIDTH - xLocation;
sout << "It's this far left of the middle of the screen: "
<< howFarLeft << "\n.";
}
else
{
int howFarRight = xLocation - HALF_SCREEN_WIDTH;
sout << "It's this far right of the middle of the screen: "
<< howFarRight << "\n.";
}
花括号({})使得编译器将其中的动作捆绑在一起,并将它们视为一件事(if动作或else动作)。如果你在{}中声明一个变量,为什么不呢?–变量只在那些{}中有定义;如果你在它们之外引用它,编译器会告诉你它从未听说过它——“没有在这个作用域中声明howFarLeft”或诸如此类的话。
注意缩进。包含在if中的东西,无论是否在{}中,都是if语句的一部分,因此相对于它缩进——就像包含在main的{}中的东西相对于它缩进一样。不缩进会让其他程序员发疯:
if (xLocation < HALF_SCREEN_WIDTH)
{
int howFarLeft = HALF_SCREEN_WIDTH - xLocation;
sout << "It's this far left of the middle of the screen:";
sout << howFarLeft << ".\n";
}
幸运的是,你的程序员友好的编辑器会为你缩进代码;在行尾按 Enter 键,它会带你到下一行应该开始的地方,除非(比如)你忘了分号,弄混了。
Extra
if有不同风格的布局。这里有一个将if语句串在一起处理排他选项的好方法:
if (x < 0) sign = -1; // it's positive
else if (x > 0) sign = +1; // it's negative
else sign = 0; // it's 0
下面是使用{}的if语句的常见变体:
“埃及括号”得名于何处
if (xLocation < HALF_SCREEN_WIDTH) {
// "Egyptian" brackets, so called
// because they look like where
// the Egyptian's hands are in
// Figure 4-2
// I'd guess the Bangles' song
// "Walk Like an Egyptian" gave
// us this bit of silliness,
// but who knows
int howFarLeft = HALF_SCREEN_WIDTH - xLocation;
sout << "It's this far left of the middle "
<< "of the screen: ";
sout << howFarLeft << ".\n";
}
作者通过将第一个{放在有条件的行上来保存一行。但是现在更难扫描左边距并确保所有的{}都匹配。我不会说这是错误的,但我认为如果你把每个{和}单独放在一行,你会犯更少的错误。
胁迫和if条件(if的肮脏小秘密)
你可能不希望让在if语句的()中使用非真或假条件……但是如果你这样做了呢?
int x;
...
if (x) ...;
碰巧的是,C++ 认为 0 表示假,所有其他整数表示真。所以如果x为 0,if语句失败;否则,它会执行。
如果你真的想这么做,很明显,好吧。但有时它会悄悄靠近我们,就像我们在下面的反欺诈部分看到的那样。
用&&、||、and 组合条件!
我们还可以对条件做些别的事情:组合它们。考虑这些表达式:
-
X>0 && X<10。"&&"读作“与”这意味着X比0多比少10。 -
X<=0 || X>=10。||读作“或”这意味着X要么是0或更少,要么是10或更多。 -
! (X<0)。!读作“不是”这意味着X小于0不是真的。(你需要(),如果你输入! X < 0,C++ 的优先规则会将其解释为(! X) < 0。去想想。)
这些运算符奇怪的外观(为什么是“&&”而不是“&”或“and”)?)是历史文物。你会习惯的。 1
因此,为了适应前面的例子,这里有一种方法来查看存储在xLocation和yLocation中的鼠标点击是否在屏幕的左上角:
if ((xLocation < HALF_SCREEN_WIDTH) && (yLocation < HALF_SCREEN_HEIGHT))
sout << "That's in the upper left quadrant.";
防错法
-
条件总是失败或者总是成功,尽管你确定它不应该,像这样:
// Cinderella must leave the dance at midnight. // Does she have time? int minutesLeftTillMidnight = 32400; // 3 p.m. -- plenty of time! if (minutesLeftTillMidnight = 0) // Warn her if time's up sout << "It's midnight! Cinderella, get home now!\n"; // Print time left sout << "You have " << minutesLeftTillMidnight << " minutes left.\n";
该报告称她还剩 0 分钟,这是错误的!如果她知道,难道不应该警告她回家吗?
问题是条件。minutesLeftTillMidnight = 0我们知道,在minutesLeftTillMidnight中表示存储 0。所以我们改变了不该改变的变量。
但是我们还没完呢!现在if语句必须决定条件是否为真。没问题。0是假的,我们只是在括号之间得到了一个零值,所以if没有启动,灰姑娘尽管失去了所有的时间,也没有得到她的警告。
我们指的是minutesLeftTillMidnight == 0。这就是臭名昭著的double-equals error。它有自己的名字,因为每个人都这样做。
解决办法:尽量不要,做的时候也不要打自己。编译器可能会警告你。注意到编译器警告是一件好事。
-
它执行
if**中的动作,即使条件为假。**问题可能是你在条件:if (2+2==5); sout << "Orwell was right: the Party even controls math!\n";后面加了一个
;
表示你正在做的陈述已经结束。C++ 将前面的代码解释为:如果2+2==5,什么都不做(因为在;之前,什么都不会发生)。在那之后,发表说奥威尔是对的。
解决方法:去掉第一个;。
-
它做了
if中后来的动作,即使条件是假的。if (2+2==5) sout << "Orwell was right: the Party even controls math!\n"; sout << "2+2==5 if I say it does!\n";
该代码将打印出来
2+2==5 if I say it does!
由于没有{},C++ 不会将这两个sout语句捆绑在一起,作为条件成功时要做的事情。它将语句解释为:如果条件成功,打印出奥威尔是对的。然后,不管发生什么,打印2+2==5。解决方案:
if (2+2==5)
{
sout << "Orwell was right: the Party even controls math!\n";
sout << "2+2==5 if I say it does!\n";
}
后两个问题由于正确的缩进而加剧,这使得看起来一切正常。编辑器可以帮助防止这种情况。
Tip
当使用一个程序员友好的编辑器时,如果你发现自己在纠正编辑器的缩进,它可能发现了一个标点符号错误。你可以追溯这个奇怪的缩进问题。
-
你搞不清哪个
if一个else跟搭配。if (TodayIsSaturday) if (IAmAtWork) sout << "I need a life.\n"; else sout << "Life is good.\n";
如果今天不是星期六,我们该怎么办?打印“生活是美好的。”?else和哪个if搭配?缩进对编译器来说无关紧要。编译器需要一个明确的规则,这里就是:else总是和最近的 if在一起。在这种情况下,“生活是美好的。”如果TodayIsSaturday为true但IAmAtWork为假,则打印。如果今天不是星期六,代码什么也不打印。
*这种歧义被称为悬空 else 问题,大多数语言都像 C++ 一样解决这个问题。
- 它仍然给出不正确的结果,看起来像这样:
if (x > y && z) // If x is bigger than both y and z...
这种推理在人类语言中是可行的,但是 C++ 需要&&两边的内容成为你想要评估的真或假条件。C++ 会将该语句理解为“如果x > y为真,z也为真,也就是说,如果z为非零”,这不是我的意思。
这解决了问题:
if (x > y && x > z) // If x is bigger than y
// AND x is bigger than z
Exercises
-
写代码报告某 X 的平方根是否大于 1。
-
给定两个整数,报告它们的顺序(正确顺序、相反顺序或相等)。
-
给定一个分数,0-100,打印它是 A,B,C,D 还是 f。
-
编写一个 if 语句,打印鼠标点击是在屏幕的左上角、右上角、左下角还是右下角。
-
编写代码,打印“超出范围!”如果 X 小于 0 或大于 8,将强制其在范围内(如果 X 太小,则将其更改为 0;如果 X 太大,则将其更改为 8)。
布尔值和变量
如果我们可以在我们的if语句中使用 true 或 false 值,我们是否也可以将它们存储在一个变量中以备后用?当然可以。这里有一个:
bool isRaining = true; // bool means "It's got to be true or false;
// nothing else allowed"
你可以这样使用它:
if (isRaining) sout << "I need an umbrella.\n";。
Tip
我通常以is开始bool变量名,所以很明显值应该是真或假。
布尔变量的可能值是–等待它-true和false。
您也可以在表达式中计算这些值,就像您可以使用int或double或float变量一样:
bool isTooHotForGolf = (temperature > 85);
但是,如果您更喜欢使用if,这也是可行的:
bool isTooHotForGolf;
if (temperature > 85) isTooHotForGolf = true;
else isTooHotForGolf = false;
你为什么想要布尔变量?为了方便和清楚起见。假设你想知道这是不是打高尔夫球的好日子,但你真的很挑剔。你可以说
if (windSpeed < 10 &&
(cloudCover > 50 && (temperature > 75 && temperature < 85) ||
(cloudCover < 50 && (temperature > 60 && temperature < 75)))
// Aaaigh! What does this mean?
或者你可以初始化一些bool变量,然后说
if (isCalm && ((isCloudy && isWarm) || (isSunny && isCool)))
// Cloudy & warm or sunny & cool -- as long as it's calm
我想我已经证明了我的观点。
Extra
乔治·布尔(1815-1864)是现代符号逻辑的创始人:即使用符号作为变量,这些变量可以是真的或假的。
在布尔之前,当然有对逻辑的理解,但人们不确定你是否能在不参考意义的情况下表达逻辑表达式,并且不冒出错的风险。(有些人可能很难相信这一点,比如说,“p 和 q 意味着 p”,但对“如果下雨又冷,那就是下雨了”没有问题。)甚至布尔也很谨慎。在他的书*《逻辑的数学分析》*的序言中,他预料到有人会说他正在做的事情是一个非常糟糕的想法,但他暗示这可能并非完全无用。他是对的;这是一个巨大的成功,你面前的计算机就是它的证明。
Exercises
-
等待鼠标点击,如果 X 大于 y,将一个布尔变量设置为 true。通过在屏幕上打印一条消息来报告是否如此。
-
单击两次鼠标,并设置布尔变量,以确定第二个是否在第一个的右边,以及是否在第一个的下面。然后报告第二个与第一个的关系,如北、东北、东、东南或任何正确的方向。
隐藏物体游戏
在一片岩石中的某处有一块菊石化石(图 4-2 )。我们将制作一个游戏来训练崭露头角的古生物学家:点击屏幕,如果你得到了化石,你就赢了。
图 4-2
要搜索的图像。化石就在那里的某个地方…
我们需要的是一种围绕它的假想的“边界”框。如果我点击盒子,我就赢了;在其他地方,我输了。盒子的坐标太难猜了。也许我可以像示例 4-2 那样在图像上画一个方框,如果看起来不对,就调整一下。我的错误猜测是在图 4-3 中。
图 4-3
看起来菊石的边框需要一些改变。(我调整了线条的粗细,以便更容易看到。)
// Program to draw a box around a fossil, to find its coordinates
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
// Resize window to fit the background image
SSDL_SetWindowSize (500, 375); // image is 500x375
SSDL_SetWindowTitle("A box to enclose the fossil -- hit a key to end");
// Load up the world to find fossil in
const SSDL_Image BACKGROUND = SSDL_LoadImage("media/ammonitePuzzle.jpg");
SSDL_RenderImage (BACKGROUND, 0, 0);
// Draw a box where we think he is. Is it right?
SSDL_SetRenderDrawColor (WHITE); // a white box, for visibility
// arguments below mean: left x, top y, width, height
SSDL_RenderDrawRect (375, 175, 80, 50); // my guess
// End program
SSDL_WaitKey ();
return 0;
}
Example 4-2Drawing a box on the screen, to find the bounding box we want for part of the image
玩了一会儿之后,我得到了一个新的边界框,就是图 4-4 中的那个:
图 4-4
边界框是正确的
SSDL_RenderDrawRect (335, 180, 45, 35); // corrected numbers
所以现在我们将有一个程序(示例 4-3 )来检测鼠标点击是否在那个框内。它使用了几个布尔变量,如果你赢了,它会用红色和白色闪烁边界框。
图 4-5
完整的化石搜寻游戏
// Program to find a fossil in a field of stones
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
// Set up window
constexpr int PICTURE_WIDTH=500, PICTURE_HEIGHT=375;
// size of the picture
constexpr int WINDOW_WIDTH =500, WINDOW_HEIGHT =430;
// size of entire window
// (has extra room for messages)
SSDL_SetWindowTitle ("My fossil hunt: a hidden-object game");
SSDL_SetWindowSize (WINDOW_WIDTH, WINDOW_HEIGHT);
// Load up the world to find the fossil in
const SSDL_Image BACKGROUND
= SSDL_LoadImage ("media/ammonitePuzzle.jpg");
SSDL_RenderImage (BACKGROUND, 0, 0);
// Print instructions to the user
SSDL_SetCursor (0, PICTURE_HEIGHT);
sout << "Where's the ammonite? Click it to win.\n";
// Get that mouse click
SSDL_WaitMouse ();
// See where we clicked, and report if the fossil was found
// I got these numbers by running the 2-searchBox program
constexpr int BOX_LEFT = 335, BOX_TOP = 180;
constexpr int BOX_WIDTH= 45, BOX_HEIGHT= 35;
int x= SSDL_GetMouseX(), y = SSDL_GetMouseY();
// Is X between left side of box and right? Is Y also within bounds?
bool isXInRange = (BOX_LEFT < x && x < BOX_LEFT+BOX_WIDTH );
bool isYInRange = (BOX_TOP < y && y < BOX_TOP +BOX_HEIGHT);
if (isXInRange && isYInRange)
{
sout << "You found the ammonite! Here's your Ph.D.\n";
// Now we'll flash where the fossil was
SSDL_SetRenderDrawColor (RED);
SSDL_RenderDrawRect (BOX_LEFT, BOX_TOP, BOX_WIDTH, BOX_HEIGHT);
SSDL_Delay (250); // 250 msec, or 1/4 sec
SSDL_SetRenderDrawColor (WHITE);
SSDL_RenderDrawRect (BOX_LEFT, BOX_TOP, BOX_WIDTH, BOX_HEIGHT);
SSDL_Delay (250); // 250 msec, or 1/4 sec
SSDL_SetRenderDrawColor (RED);
SSDL_RenderDrawRect (BOX_LEFT, BOX_TOP, BOX_WIDTH, BOX_HEIGHT);
SSDL_Delay (250); // 250 msec, or 1/4 sec
SSDL_SetRenderDrawColor (WHITE);
SSDL_RenderDrawRect (BOX_LEFT, BOX_TOP, BOX_WIDTH, BOX_HEIGHT);
SSDL_Delay (250); // 250 msec, or 1/4 sec
}
else
sout << "You lose.\n";
// End program
sout << "Hit a key to end.";
SSDL_WaitKey ();
return 0;
}
Example 4-3My fossil hunt game: trying to find an object with a mouse. Figure 4-5 shows possible output
Exercises
-
写一个程序,显示一个框,等待鼠标点击,并告诉你是否在框内点击。
为了让它更有趣,贴一个有趣的东西的大致正方形的图像。我喜欢垃圾邮件。特别感谢为这款超现实游戏制作《寻找垃圾邮件》(
www.smalltime.com/findthespam/)的创意人员。 -
制作一个隐藏物体游戏:用户必须点击你提供的物体(有两个或更多)才能获胜。您可以要求用户按顺序执行。点击不是的某个物体,或者按顺序正确的物体,以失败结束游戏。
你可以把每个物体看作屏幕上的一个正方形区域。
-
写一个程序,在你点击的地方画一个泡泡,总是同样大小的泡泡…但是它不会让你在屏幕上放一个部分。如果您单击的位置太靠近边缘,它会将气泡从边缘移开,使其不会越过边缘。创建气泡时,您还可以添加声音效果。
我们也有 single &和 single |作为操作符(见第二十五章)——但那是另一回事。
*
五、循环、输入和char
在这一章中,我们将会看到重复的动作、输入和与角色类型有关的事情。
键盘输入
考虑以下代码:
int ageInYears;
sout << "How old are you? "; ssin >> ageInYears;
这将打印关于年龄的查询,然后等待键盘输入。如果用户输入一个数字,该数字被存储在ageInYears中。(其他任何东西都可能给ageInYears一个 0 值。)ssin 1 在处理输入之前等待你按回车键,所以允许退格。
ssin使用与sout相同的字体和光标;它们都是 SSDL 的一部分。
您可能会注意到<sout,它们从值移动到输出;有了ssin,它们从输入到变量。
这是引入一种新的基本类型的好时机。字符的例子包括'A'和'a'(它们是不同的)'?'、'1'、' '(空格字符)和'\n'。下面是一些使用了char变量的代码:
char answer;
sout << "Are you sure (Y/N)? "; ssin >> answer;
if (answer == 'y')
sout << "Are you *really* sure?\n";
你也可以用>>链接你正在阅读的内容:
图 5-1
侮辱世界,一次一个人
ssin >> firstThingReadIn >> secondThingReadIn;
无论是阅读char s 或数字或其他什么,ssin跳过空白(空格、制表符和回车);所以你可以用空格输入你想要的,它可以处理。
示例 5-1 是一个样本程序,不管你的反应如何,它都会找到侮辱你的方法。图 5-1 显示了一个示例会话。
// Program to insult the user based on input
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
int ageInYears = 0;
sout << "Let's see if you can handle the truth.\n";
sout << "How old are you? "; ssin >> ageInYears;
bool isOlder = (ageInYears >= 20);
// Seriously? Well, 20 *is* old if you're a computer program
if (isOlder) sout << "The truth is you are OLD.\n";
else sout << "You're not old enough. Sorry, kid.\n";
sout << "Hit any key to end.\n";
SSDL_WaitKey ();
return 0;
}
Example 5-1A program using ssin
防错法
-
你得到一串 错误信息 如下: 2
main.cpp: In function 'int main(int, char**)': main.cpp:11:39: error: no match for 'operator<<' (operand types are 'std::istream' {aka 'std::basic_istream<char>'} and 'int') sout << "How old are you? "; ssin << ageInYears; ~~~~~^~~~~~~~~~~~~ main.cpp:11:39: note: candidate: 'operator<<(int, int)' <built-in> main.cpp:11:39: note: no known conversion for argument 1 from 'std::istream' {aka 'std::basic_istream<char>'} to 'int' In file included from /usr/include/c++/8/string:52, from /usr/include/c++/8/bits/locale_classes.h:40, from /usr/include/c++/8/bits/ios_base.h:41, from /usr/include/c++/8/ios:42, from /usr/include/c++/8/istream:38, from /usr/include/c++/8/sstream:38, from ../../external/SSDL/include/SSDL_display.h:26, from ../../external/SSDL/include/SSDL.h:27, from main.cpp:4: /usr/include/c++/8/bits/basic_string.h:6323:5: note: candidate: 'template<class _CharT, class _Traits, class _Alloc> std::basic_ostream<_CharT, _Traits>& std::operator<<(std::basic_ostream<_CharT, _Traits>&, const std::__cxx11::basic_string<_CharT, _Traits, _Alloc>&)' operator<<(basic_ostream<_CharT, _Traits>& __os, ^~~~~~~~
字面上的页数更多。祝你解码顺利。
这一切都源于一个错误:在一条ssin语句中>>走错了路。应该是ssin << ageInYears。编译器有时会感到困惑。
如果你试图ssin >> "\n"或者其他不是变量的东西,你可能会得到另一个错误。
Exercises
|体重不足
|
低于 18.5
| | --- | --- | | 正常重量 | 18.5–25 | | 超重 | 25–30 | | 肥胖的 | 30+ |
-
使用公式厘米= 2.54 *英寸,编写一个将英寸转换为厘米的程序。使其具有交互性,即向用户询问要转换的值。
-
写一个程序来识别你属于哪一代人(
Gen Z、millennial等等)。),基于用户输入的年龄或出生年份。你可以选择范围。 -
身体质量指数(身体质量指数)告诉你是重、瘦还是中等。(这不精确,但如果没有别的,也许我可以说服我的祖母,如果我不吃第二份,我就不会饿死。)
根据维基百科,这些是范围:
所以,写一个程序来计算用户的身体质量指数。公式 isBMI =以千克为单位的重量/(以米为单位的高度) 2
如果你在一个使用英制单位的国家,你也需要这个信息:1 公斤= 2.2 磅,1 米= 39.37 英寸。
-
编写一个程序,要求用户两次(如 1:06 或 12:19)并整齐地打印出差值(如 11:03 或 0:40,但不是 13:0 或-12:70)。你用键盘输入时间——我们不是问计算机现在是什么时间。
-
…但现在我们是了。不是询问用户时间,而是测量用户按回车键的两次,得到当前系统时间,如下所示:
int myTime = time (nullptr);这给出了自 1970 年 1 月 1 日午夜以来的时间(以秒为单位)(在我所知道的系统上)。你需要
#include <ctime>.
while 和 do-while
如果条件为真,程序可以做一些事情*…或者当条件为真时,程序可以做一些事情。*
这里有一个方法可以确定一个数被 10 除多少次才能得到 1。(如果打印的话,这将与数字中的位数相同。)
int digits = 0;
while
(number > 1) // while we haven't reached 1
{
number /= 10; // divide it by 10
digits += 1; // that's one more digit!
}
在巴克斯-诺尔形式(BNF)中,while 语句是
while (<condition>) <action>
只要条件为真,while 循环就会执行动作。当它不再为真时,循环结束,程序继续执行后面的操作。
while 有一个变体,除了在执行 do-while 动作后检查条件之外,它完全相同。它的 BNF 形式是
do <action> while (<condition>)
一个例子是
do
{
sout << "Ready to rumble (Y/N)? "; ssin >> answer;
}
while (answer != 'n' && answer != 'y');
// while answer isn't yes or no, ask again and again
if (answer == 'y')
... // rumble!
这不能作为 while 语句,while (answer != 'n' && answer != 'y') ...,因为你不知道answer是什么,除非你至少问过一次。
底线是 do-while 至少执行一次操作(在测试条件之前),而 while 可能会在执行任何操作之前退出。我们通常使用 while,但有时 do-while 正是我们需要的。因此,我们有了循环的黄金法则。
Golden Rule of Loops (Version 1)
如果希望循环至少执行一次,请使用 do-while。
如果它执行零时间有任何意义,请使用 while。
SSDL 循环
关于 SSDL 有些事我没告诉你。它不会在每次绘图或打印时更新屏幕。为了节省更新时间,它推迟更新,直到有理由等待用户:一个ssin语句或者一个SSDL_WaitKey或者SSDL_WaitMouse。下面的循环将一直显示"Move mouse to right half of screen to end.",直到您向右移动鼠标,但不会显示任何内容:
while (SSDL_GetMouseX() < WHERE_IT_IS)
{
SSDL_RenderClear ();
SSDL_SetCursor (0, 0);
sout << "Move mouse to right half of screen to continue.";
}
SSDL 也不会检查让它退出程序的东西——按下 Escape 或点击 X 来关闭窗口——直到它在等你。所以前面的代码也不会让您退出。
对这两个问题的修复是相同的:函数SSDL_IsQuitMessage。它更新屏幕,检查输入消息(鼠标点击、击键),并返回是否有退出命令:
while (! SSDL_IsQuitMessage () && SSDL_GetMouseX() < WHERE_IT_IS)
{
SSDL_RenderClear ();
SSDL_SetCursor (0, 0);
sout << "Move mouse to right half of screen to continue.";
}
这是之前准备好的 do-while 循环,可以让用户轻松退出。它和前面的 while 循环都在源代码示例ch3/loops-with-SSDL中。
do
{
sout << "Ready to rumble (Y/N)? "; ssin >> answer;
}
while (!SSDL_IsQuitMessage () && (answer != 'n' && answer != 'y'));
Extra
在最后一个 do-while 循环中,我们可以要求用户键入 1 表示“是”,键入 2 表示“否”,如果我们想把自己暴露为 20 世纪 70 年代对用户怀有敌意的倒退,并且永远不再被雇佣的话。(2 跟“不”有什么关系?)用户更容易记住'n'的意思是不。
如果有比是和否更多的选项可供选择——比方说,你的程序操作文件、 o 打开、 s 打开和 r 关闭——用字母(O、S 和 R)而不是数字给出选项仍然是用户友好的。
如何使你的程序易于交互是计算机科学的一个分支:人机交互的主题。
break和continue
break表示立即离开循环。这是之前 while 循环的一个版本,现在使用了break。你决定哪种方式更清晰:
while (SSDL_GetMouseX() < WHERE_IT_IS)
{
if (! SSDL_IsQuitMessage ()) break;
SSDL_RenderClear ();
SSDL_SetCursor (0, 0);
sout << "Move mouse to right half of screen to end.";
}
continue表示跳过循环的剩余部分,返回到顶部。我很少用它。
一些编程风格的专家被break和continue吓坏了。他们认为你应该能够看到循环的继续条件,并立即看到在什么情况下循环可以结束——本质上,这些关键字降低了清晰度。我同意清晰是至关重要的,但我不确定break是问题所在。当然,如果一个循环有 50 行长,检查它的break s 会很乏味。但是我认为解决方案不是让循环有 50 行长。简单就好。
防错法
- **程序不会结束,你连程序都杀不了。**你可能陷入了一个循环,但你如何停止它?首先,试试 Ctrl-C(按住 Ctrl,按 C)。如果这不起作用,请尝试以下操作:
-
Visual Studio:调试➤停止调试或单击窗口顶部附近的红色方块停止。
-
MinGW:用任务管理器干掉它。
-
Unix:如果你连命令提示符都没有,在命令窗口按 Ctrl-Z 就可以得到。
-
有两个命令可以帮助我们。ps列出活动流程:
PID TTY TIME CMD
14972 pts/0 00:00:00 bash
15046 pts/0 00:00:00 bash
15047 pts/0 00:00:01 a.out
15054 pts/0 00:00:00 ps
kill -9 <process-id>的意思是“我试过了,但是我找不到一个好的方法来结束这个过程,所以就杀了它吧。”
a.out是我们想要消灭的,但是如果我们用类似于runx的脚本运行它,我们也希望它消失。这可能是最新的 shell 命令,一些名称中带有“sh”的命令。(拿错了可能会杀了你的终端。哎呀。)这个命令将杀死它和它的依赖进程a.out:
kill -9 15046
-
**循环永远重复,你不能退出。**也许它没有检查退出信息。让你的循环条件看起来像这样
while (! SSDL_IsQuitMessage () && ...whatever else you want to check... ) ...;或者,如果它是一个 do-while,
do { ... } while (! SSDL_IsQuitMessage () && ...); -
循环不断重复,直到你点击退出或者做一些你想做几次的事情。
Consider under what condition you break the loop. It must be that it’s never met:
int rectanglesDrawn = 0; while (!SSDL_IsQuitMessage () && rectanglesDrawn < MAX_RECTANGLES) { SSDL_RenderDrawRect (...); }The loop never incremented
rectanglesDrawn…so no matter how many you draw, the loop doesn’t end. This line should do it:... rectanglesDrawn += 1; } -
The loop repeats forever, or won’t repeat when it should. It’s easy to get confused when the loop has a combination of conditions:
do { sout << "Answer Y or N: "; ssin >> answer; } while (! SSDL_IsQuitMessage () && (answer != 'n' || answer != 'y'));这看起来可能是对的,但它实际上是说继续循环,而没有人说退出,答案不是“是”或不是“否”。嗯,它总是不是“是”或不是“否”!假设是。那么“不是没有”就是真的,所以一直走下去。假设是否定的。那么“不是”是真的,所以继续下去。
The solution is to keep going while it’s not yes and it’s also not no – while it’s a nonsensical answer like
'7'or'X':do { sout << "Answer Y or N: "; ssin >> answer; } while (! SSDL_IsQuitMessage () && (answer != 'n' && answer != 'y'));
Exercises
-
让用户一直输入一个数字,直到他/她猜出你选择的某个数字。
-
…让程序打印出猜了多少次。
-
写一个程序,要求一个(大写)字母,并从
'A'开始计数,直到找到为止。它的输出将类似于'E' is the 5th letter of the alphabet! -
…现在修改程序,让它一直重复,直到你给它一个
'.'结束。 -
写一个程序,在你点击的地方画一个泡泡。气泡的大小应该取决于鼠标最后一次点击的时间。使用互联网了解功能
SDL_GetTicks()。 -
更新上一章末尾的练习 2——隐藏的物体游戏——这样用户可以按任意顺序点击隐藏的物体。
-
制作自己的音乐播放器:在屏幕底部放置标有“音乐打开”和“音乐关闭”的盒子,当用户点击其中一个盒子时,适当地打开或关闭声音。
-
(更用力;需要几何学)考虑一个三角形的周长,这个三角形以一个点为中心,其端点离中心为“R”。
Now consider if it were a square, or a pentagon, or something with N sides (Figure 5-2). What will the perimeter look like when N is large?
图 5-2
练习 4 的多边形
写一个程序,它(a)画一个正 N 边多边形,对于某个大的 N 和某个半径 R,和(b)找到周长。周长除以 2R。根据你在屏幕上看到的形状,你认为这个比例会接近π吗?这是你所期望的吗?
对于循环
for 循环是在一个范围内计数的循环。这里有一个简单的:
for (int count=0; count < 10; count += 1)
sout << ' '; // print these numbers, separated by spaces
下面是它的输出:0 1 2 3 4 5 6 7 8 9
在巴克斯-诺尔形式中,for 循环是
for (<initialization section>; <continuing-condition>; <increment>)
<action>
让我们一点一点来看。
初始化部分–int count=0–在循环开始时完成。如您所见,您可以在其中声明变量。变量只在循环内部可见。
只要继续条件为真,循环就继续。
在每次循环结束时,每次“迭代”,C++ 都会执行递增部分。这可以是任何东西,但是它通常增加一个索引变量(也就是我们用来计数的变量)。
计算机处理这些部分的顺序是
-
做。
-
是真的吗?如果不是,则退出循环。
-
做。
-
做。
-
回到步骤 2。
增量运算符
我们经常发现我们需要给一个变量加 1(或者减 1)。C++ 为此提供了运算符。这里有两个例子:
++y; // adds 1 to y. This is called "increment."
--x; // subtracts 1 from x. This is called "decrement."
大多数计算机都有一个加 1 和减 1 的内置指令——所以我们告诉编译器使用它们。很有效率。
我们经常在 for 循环中这样做,就像这样:
for (int count=0; count < 10; ++count)
sout << count << ' ';
我们也可以使用减量运算符:
for (int count=10; count > 0; --count) // A countdown, 10 to 1...
sout << count << ' ';
sout << "Liftoff!\n";
您可以增加其他数量,尽管这并不常见:
for (int count=2; count <= 8; count += 2) // 2 4 6 8...
sout << count << ' ';
sout << "Who do we appreciate?\n";
还有另一种类型的增量,称为“后增量”,以及相应的后减量。看起来是这样的:count++,不是++count。以下是它们的不同之处:
预增量:Y = ++X表示X = X+1; Y = X。即X加 1;y 得到 X 的新值。
后增量:Y = X++表示Y = X; X = X+1。即X加 1;y 得到X的旧值。
除非你把表达式放在=或的右边作为函数的参数,否则你不会注意到差别。
一个例子:平均数字
假设您想要对用户给出的十个数字列表进行平均。我知道:多刺激啊!但是我们不能一直玩游戏。人们会开始认为编程太有趣,给我们的报酬会更少。这是我的计划:
tell the user what we're doing
total = 0 // so far, nothing in the total...
for ten times
get a number from the user
add that to the total
average = total/10.0 // floating-point division, for a floating-point answer
// better not use int division -- remember
// Example 3-3 (drawing a star), in which our division
// answers kept showing up as zeroes...
print average
示例 5-2 在真实的 C++ 中展示了这一点。
// Program to average numbers
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
constexpr int MAX_NUMBERS = 10;
sout << "Enter " << MAX_NUMBERS
<< " numbers to get an average.\n";
double total = 0.0;
// Get the numbers
for (int i = 0; i < MAX_NUMBERS; ++i)
{
double number;
sout << "Enter the next number: ";
ssin >> number;
total += number;
}
// Print the average
double average = total / MAX_NUMBERS;
sout << "The average is " << average << ".\n";
sout << "Hit any key to end.\n";
SSDL_WaitKey ();
return 0;
}
Example 5-2A program to average numbers, using a for loop
顺便说一下,关键字break和continue在 for 循环中的作用就像在 while 和 do-while 循环中一样。它们是可用的,但以我的经验来看,没什么用。
所以我们现在有三种循环。你知道如何在 while 和 do-while 之间做出决定——在本章前面的循环黄金法则(版本 1)中。for 循环呢?
按照惯例和理由,当我们在一个范围内计数时,我们使用 for 循环——当我们知道我们从什么开始计数,到什么结束计数。因此,我们有了循环的最终黄金法则。
Golden Rule of Loops (Final Version)
如果你事先知道你会做多少次,使用for。否则:
如果希望循环至少执行一次,使用do-while。
如果执行零次有任何意义,就使用while。
防错法
- 后期动作做一次,不多次——通病。这里有一个例子:
// Code to print several powers of 2
int product = 1;
sout << "Here are several successive powers of 2: ";
for (int i = 0; i < 10; ++i)
sout << product << ' ';
product *= 2;
我忘记了{}。我假设代码会这样做:
for i goes from 0 through 9
print product
multiply product by 2
但它实际上是这样做的:
for i goes from 0 through 9
print product
multiply product by 2
为了防止这一点,让你的编辑器为你缩进,从而捕捉错误。
- 不 动作重复进行。
for (int i = 0; i < N; ++i); // This loop prints only one *
sout << '*';
第一行末尾多了一个;。
- 你的循环走得太远了。
for (int i = 0; i <=4; ++i) ...
最后一次通过,++ i,使得i等于 4。但是如果你想要四个条目,你只能得到五个:0,1,2,3,4。解决方案:使用<作为条件。
为了确保您有正确的范围,请在编译之前跟踪代码*。或者总是使用表单*
for (int i = 0; i < howManyTimes; ++i) ...
Tip
对于循环,几乎总是从 0 开始,并使用<,而不是<=,作为继续条件:i < howManyTimes,而不是i <= howManyTimes。
Exercises
-
打印数字 1-10…和它们的平方(1,4,9,16,等等。).
-
现在使用字符,使用 for 循环打印字母 A–z。
-
…向后。
-
编写一个像示例 5-2 中的平均程序一样的程序,但让它提供的不是平均值而是最大值。
-
改编示例 3-3/示例 3-4 (画一颗星),让它询问用户半径、中心和点数——并使用循环而不是重复代码来画线。
-
写一个程序,要求用户输入一个整数和它的幂,并打印结果。当然是用 for 来计算。
-
编写一个程序,让用户猜一个数字(这个数字可以是一个
constexpr),并一直猜下去,直到用户用完了所有的回合——你来决定有多少回合——或者猜对了。然后它报告成功或失败。你需要一个布尔变量isSuccess。 -
(硬)画某个函数的图(正弦就是个好的)。添加 X 和 Y 轴以及适当的标签。
char s 和cctype
示例 5-3 比较两个输入字符,看它们是否按字母顺序排列。
// Program to tell if two letters are in alphabetical order
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char** argv)
{
char char1, char2;
sout << "Give me a letter: "; ssin >> char1;
sout << "Give me another: "; ssin >> char2;
if (char1 < char2)
sout << "You gave me two characters in order.\n";
else if (char1 > char2)
sout << "They are in reverse order.\n";
else
sout << "It's the same letter.\n";
SSDL_WaitKey();
return 0;
}
Example 5-3A program to compare characters
它基本上有效。'a'在'Z'之后,有点奇怪。但计算机就是这么想的:小写字母 a–z 跟在大写字母范围后面。字符的精确排序是在 1967 年决定的,并由美国国家标准协会(ANSI)维护。附录 c 中列出了美国信息交换标准码(ASCII)的完整列表。
我宁愿我的比较忽略大小写。这里有一种方法——将其转换为大写:
char myChar = 'b';
char upperCaseVersion = myChar - 'a' + 'A';
看起来很奇怪,但是…为了得到大写版本的'b',我们这样做:先减去'a'。这当然给了我们一个1的区别。然后我们将这个1加到'A'上,得到'B'。这将适用于任何小写字母。
如果我们不确定它是小写的呢?我们可以使用一个if语句来确定:
if (myChar >= 'a' && myChar <= 'z') // if it's lower case -- fix it
upperCaseVersion = myChar - 'a' + 'A';
else // if not -- leave it alone
upperCaseVersion = myChar;
这是如此有用,我们会想这样做一次又一次。幸运的是,C 和 C++ 的制造者同意这一点。他们给了我们一套函数,一些如表 5-1 所示,用于处理大写和一些其他性质的字符;这些可以在包含文件cctype中找到。有关更多此类功能,请参见附录 f。
表 5-1
一些关于大写的有用函数
| `int``islower` | 返回`ch`是否小写。(非字母字符不小写。) | | `int``isupper` | 返回`ch`是否大写。(非字母字符不大写。) | | `int``tolower` | 返回小写版本的`ch`。如果`ch`不是字母,则返回`ch`。 | | `int``toupper` | 返回`ch`的大写版本。如果`ch`不是字母,则返回`ch`。 |这些函数存在于 C 语言中,C++ 就是从这种语言发展而来的。这解释了一些看起来奇怪的事情:我们正在处理字符,但是类型不是char而是int!嗯,字符在某种程度上像整数,所以这是可以容忍的,如果不是绝对清楚的话。
这些函数的另一个奇怪之处是相似的:islower和isupper返回int。他们不应该返回true或者false吗?是的,但是由于 C++ 将0解释为false,而将其他整数都解释为true,int将起作用,如下面的代码片段所示:
if (isupper (myChar)) sout << "You have an upper-case letter.\n";
示例 5-4 使用toupper来比较字符,不考虑大小写。
// Program to tell if two letters are in alphabetical order,
// regardless of upper or lower case
// -- from _C++20 for Lazy Programmers_
#include <cctype>
#include "SSDL.h"
int main(int argc, char** argv)
{
char char1, char2;
sout << "Give me a letter: "; ssin >> char1;
sout << "Give me another: "; ssin >> char2;
if (toupper(char1) < toupper(char2))
sout << "You gave me two characters in order.\n";
else if (toupper(char1) > toupper(char2))
sout << "They are in reverse order.\n";
else
sout << "It's the same letter.\n";
SSDL_WaitKey();
return 0;
}
Example 5-4Example 5-3, using true alphabetical order rather than simple ASCII order
防错法
-
You try to print a
charwith converted case, and it prints a number instead:sout << "The upper-case version of '" << char1 << "' is ' << toupper (char1) << " '.\n";如果我们运行这个,输出将类似于
The upper-case version of 'a' is '65'.
问题是toupper返回的不是char而是int——所以sout打印的是int。解决办法是:选角。
-
你正在给一个
char赋值,得到类似“不能从const char[2]转换到char”的东西这段代码看起来没错,但不是:
char c = "x"; // wrong!chars 需要单引号,像这样:char c = 'x';记住的方法是:单引号是针对单个
char的,双引号是针对两个(或多个)char的,也就是“引用的文本”
sout << "The upper-case version of '" << char1
<< "' is '" << char (toupper (char1)) << " '.\n";
Extra
到目前为止,我们已经看到了这些类型:
int``double``float``bool
有些可以有修饰语。例如,一个long double比一个常规的double有更多的小数位;多少取决于编译器。int前面可以加上关键词signed、unsigned、short、long或者long long(我猜是取了“humongous”这个词),比如unsigned long int。可以想象,short和long指的是数字可以有多大。int可省略:unsigned short x;或long y;。
如果没有指定,int就是signed。如果一个char没有被指定为signed或unsigned,则由编译器决定是哪一个。这不重要。
wchar_t(“宽字符”)是一种较大的字符类型,在char不够大时使用,即用于国际字符。char8_t、char16_t、char32_t也是国际字符。
文字值上的后缀——如在5.0f或42u中——是用来告诉编译器“这是一个(f)loat,不是一个double,“这是(u)nsigned,等等。后缀可以大写。
如果你想知道int、long int等等到底有多大,你可以使用#include <、climits、>找到答案,它定义了各种类型的最大值和最小值的常量。你可以用sizeof : sizeof (int)或者sizeof (myInt)得到其中一个的大小,以字节 3 表示,其中myInt是一个int。
如果你存储的值太大,它们会换行;使用signed,你将得到一个负数,而不是一个太大的正数。这几乎从来都不是问题。如果是,使用long int或long long int。
有关基本类型的完整列表,请参见附录 d。
Exercises
-
Write a program to determine whether the creature you just saw was a fairy, troll, elf, or some other magical creature, assuming you carried your computer into the Enchanted Forest. You pick the distinguishing features of each type of creature. A session might start like so:
Welcome to the Enchanted Forest. This creature you have seen: Does it have wings (Y/N)? Y ...用户应该能够输入
'y'或'Y'和'n'或'N'来回答。(如果用户输入了没有意义的内容,您可以认为这意味着“不”。)
switch
考虑这个if语句,它打印一个字母是元音、半元音还是辅音:
// Print classification of letters as vowel, semivowel, consonant
if (toupper (letter) == 'A') sout << "vowel";
else if (toupper (letter) == 'E') sout << "vowel";
else if (toupper (letter) == 'I') sout << "vowel";
else if (toupper (letter) == 'O') sout << "vowel";
else if (toupper (letter) == 'U') sout << "vowel";
else if (toupper (letter) == 'Y') sout << "semivowel";
else if (toupper (letter) == 'W') sout << "semivowel";
else sout << "consonant";
会有用的,但是有更简单的方法。
在 BNF 中,一个 switch 语句是
switch (<expression>)
{
case
<value>: <action>*
...
[default
: <action>*]
}
*表示“你想要多少份就有多少份,可能没有。”
这是做什么的:计算括号中的表达式。(它必须是可以计数的东西——整数或字符,没有浮点数,没有双精度数。)如果它与某个特定值相匹配,计算机就转到那个case <值> 并执行随后的任何动作。如果您指定了一个默认操作,那么当表达式不匹配任何内容时就会发生这种情况。
下面是这段代码,使用了一个switch语句:
// Print classification of letters as vowel, semivowel, consonant
switch (toupper (letter))
{
case 'A': // if it's A, keep going...
case 'E': // or if it's E (and so on)...
case 'I':
case 'O':
case 'U': sout << "vowel"; // ...and print "vowel" for all those cases
break;
case 'Y':
case 'W': sout << "semivowel";
break;
default: sout << "consonant";
}
如果letter与'A'匹配,它会在case 'A'之后执行它发现的任何操作,在本例中是sout << "vowel";。它继续前进,直到找到break,和以前一样,它的意思是“离开这个结构”——所以在那一点上,它离开了switch语句。(没有人会因为你这样使用break而抱怨你;switch需要它。)
我通常在一个switch语句中包含一个default,以处理意外值(例如 5-5 )。
sout << "Enter the class of a planet: ";
ssin >> planetaryClassification;
switch (planetaryClassification)
{
case 'J': sout << "Gas giant"; break;
case 'M': sout << "Earthlike world"; break;
case 'Y': sout << "'Demon' planet"; break;
// ...
default: sout << "That's not a valid planetary classification.\n";
sout << "Better watch some more Star Trek!";
}
Example 5-5Using a switch statement’s default to catch bad input
防错法
-
切换
does what you wanted for that value; then it does the options that follow as well.这是switch最常见的错误:忘记了break。解决方案:返回并将break放在您想要的不同选项之间(例如 5-5 、'J'、'M'和'Y')。 -
**编译器报错了一些关于大小写标签和变量的问题。**这段代码有一个问题:
switch (myChar) { case 'P': int turns = MAXTURNS; playGame (turns); break; ... }它不喜欢将变量初始化为
switch的一部分。没问题。我们将把{}放在需要变量的区域周围:switch (myChar) { case 'P': { int turns = MAXTURNS; playGame (turns); break; } ... }
Exercises
这些(当然)都涉及到switch。
-
编写并测试一个函数,在给定一个数字的情况下,打印相关的序数:也就是说,对于 1,打印第一个;对于 2,打印第二个;对于 3,打印 3 号;其他的,打印数字加“th”
-
让用户输入两个一位数并返回总和。这里有个转折:输入的数字是以 16 为基数的(“十六进制”)。以 16 为基数,我们用
'A'代表 10,'B'代表 11,'C'代表 12,以此类推,'F'代表 15。你可以用 10 进制给出结果。 -
菜单是一种由来已久的(老式的)获取用户输入的方式。制作一个菜单,为用户绘制一个圆或一条线,或者其他形状,然后绘制选定的形状。
我本可以称之为sin并等待双关语的开始,但是sin在 C++ 中已经意味着某种东西:正弦函数。“S-in”可能是一个很好的发音方法。
2
这是来自 g++ 编译器。Visual Studio 给了我三行,明确指出<<是问题所在。很好!
3
一个“字节”是一个单独的内存位置,在我听说过的所有系统中足以存储一个char。
六、算法和开发过程
让我们从 C++ 的细节上退一步,从大的方面考虑一些事情:特别是,考虑大的方面的需要。在以后的生活中,规划没有帮助吗?没有计划,你不会盖房子,也不会做饭。(微波炉加热汤不算。)
在编程中,这个计划被称为算法:一系列步骤,按顺序执行,最终达到一个目标。
机器人烹饪历险记
想象一下,如果我们能让电脑制作饼干(蓬松的那种——像烤饼,但不甜)。计算机可以遵循指令,但指令必须清晰。
我拿出一个碗,然后…饼干里放了什么?你抓到我了。面粉应该会有帮助。他们不把鸡蛋放在饼干里吗?牛奶呢?告诉机器人把一些面粉倒在碗里,放入几个鸡蛋和一大杯牛奶,尽可能地搅拌,滚动,然后放进烤箱。
当然,他们会硬得像砖头一样。我们的机器人厨师把鸡蛋放了进去——我祖母会笑的——而且搅拌得太多了。我应该先制定一个可靠的计划。
(左)制作不当的饼干通常对建筑业有用。至少当你试着吃它们的时候你会这么认为。(对)我希望做的是
Tip
把算法写在程序之前,哪怕是简单的任务。
我到底应该告诉它做什么?这里有一个选择:
1 cup/250 mL all-purpose flour
1/2 tsp/3 mL salt
1/8 cup/30 mL cold shortening
1/3 cup/80 mL milk
Heat oven to 450° F/ 230° C
Mix dry ingredients
Mix in shortening just till it's distributed
Mix in milk
Form into balls
Bake in the oven
烤箱加热?检查一下。机器人可以混合干配料。让它取一杯面粉,拌入盐和小苏打。当机器人搅拌它们时,杯子会溢出来。它为什么不把面粉放进碗里?我没有告诉它。
接下来,它会加入起酥油,然后加入牛奶。当然,我们会弄得一团糟。然后它会把这些乱七八糟的东西变成球。多少?我没说。两个可以吗?然后它把渗出的食物放进烤箱,这时食物会从架子上掉到底部……我也没告诉它要用托盘。我也没让它把饼干拿出来!手边有灭火器吗?
步骤不够具体。例如,我告诉它混合,但没有告诉它其中的一个步骤是将东西放入碗中。我们需要更多细节。逐步细化是如何解决这个问题的:写下需要做的事情,然后把这个步骤分解成子步骤,然后把 ?? 的那些步骤分解成子步骤,直到这些步骤简单到连计算机都能处理。
Tip
改进你的算法,直到每一行如何转换成 C++ 变得显而易见。
让我们再试一次:
1 cup/250 mL all-purpose flour
1/2 tsp/3 mL salt
1/8 cup/30 mL cold shortening
1/3 cup/80 mL milk
Heat oven to 450° F/ 230° C
Mix dry ingredients:
Put dry ingredients into a large bowl
Mix them
Mix in shortening just till it's distributed:
Cut shortening into small bits (half-centimeter sized)
Put shortening into the large bowl
Mix just till it's distributed
Mix in milk:
Put milk into the large bowl
Mix till it's distributed
Form into balls:
Get a cookie sheet
While there is dough left
Put flour on your hands so the dough won't stick
Take out dough the size of a baby's fist
Put it on the cookie sheet, not touching any others
Bake in the oven:
Put the cookie sheet in the oven
Wait till the biscuits are golden brown on top
Take the cookie sheet and biscuits out
很好。现在我们结束了。这是一个冗长的详细算法,但如果我们要让计算机理解,我们必须把它分解,直到它变得显而易见——对计算机来说!–这些步骤意味着什么。
写所有细节很费时间吗?不像在编写代码、出错、调试和一次又一次地重新开始的时候去琢磨同样的细节那么耗时。专家一致认为,花时间提前计划减少了总体花费的编程时间。出于这个原因,懒惰的程序员使用以下规则:
Golden Rule of Algorithms
总是写它们。
Extra
阿达·洛芙莱斯女士(1815-1852)和查尔斯·巴贝奇(1791-1871)被认为是世界上第一批计算机科学家。可惜电脑还没有发明出来。
Charles babb age ada lovelace 女士
巴贝奇当然尽力了。他让英国政府资助他的差分机,这本来是一个机械计算器,然后他的分析机,设计成一个机械计算机。(那时候,政府资助研究几乎是闻所未闻的。也许这就是为什么它被称为“发明时代”)当时机器零件不够精细,项目失败了。
洛夫莱斯是诗人拜伦的女儿,她对巴贝奇的机器感兴趣,了解它运行的程序的性质,或者说,如果它存在的话,它会运行的程序的性质。她说:“分析引擎并不自命能创造任何东西。”"它可以做我们知道如何命令它执行的任何事情."这有时被用来反对人工智能的概念。
编写程序,从开始到结束
让我们将这种提前计划的事情应用到一个真实的、虽然很小的编程任务中。
以下是我们为某个目标创建程序可能要经历的步骤:
-
确定需求,也就是对目标进行精确的陈述。
-
写算法。
-
手动追踪算法。
-
将算法转换成有效的 C++ 代码。
-
编译。
-
测试。
如果在此过程中出现错误,我们可以返回到前面的步骤。
需求:我们想做什么?
我想做一系列同心圆,使每个圆的面积是它外面那个圆的一半(图 6-1 )。我们将继续下去,直到他们开始模糊(也许当半径约为 1 个像素)。外圆的半径是,哦,200。
图 6-1
一个制作同心圆的程序,每个圆的面积是下一个更大的圆的一半
准备好开始编码了吗?还没有。首先,我们将制定一个计划,正如上一节所讨论的。
算法:我们怎么做?
那么运行时应该发生什么呢?
draw the first circle
draw the second
keep going until the circle's too small to see -- radius 1, I'd suppose
太明显了?我发现,当我陈述显而易见的事情,把它写下来,并试图提炼它时,编程会变得容易得多。
Tip
陈述显而易见的事情,尤其是在刚开始的时候。
还不够具体。我们不知道如何画圆,因为我们不知道半径。
最外圈的是 200。下一个,如前所述,我希望面积是第一个的一半。记住圆的面积公式:π半径 2 。为了得到减半的面积,我们需要下一个圆的面积=第一个圆的面积/2。这就得出下一个半径是第一个半径/ 。
这是修正后的算法:
draw the first circle, with radius 200...
不,我没说在哪!再试一次:
draw the first circle at center of screen, with radius 200
draw the second circle at center of screen, with radius 200 / √2
draw the third circle at center of screen, with radius 200 / √2 / √2...
太复杂了。
我们可以使用变量。我们将半径的值设为 200,并每次都进行更改:
start radius at 200
draw a circle at center of screen, with this radius
to get a circle with half the area as before【to get a circle with half the area as before
keep going until the circle's too small to see -- radius 1, I'd suppose
“继续”听起来像是我们需要一个循环。我们不知道我们会做多少次,但至少有一次,所以根据循环的黄金法则,这是一次尝试:
start radius at 200
do
draw a circle at center of screen, with this radius
divide radius by √2
while radius > 1 (quit when circle's too small to see)
这已经足够具体了。每次写程序都需要这么麻烦吗?差不多吧。随着时间的推移,你的技能提高,你可以指定更少的细节。但是专家仍然为他们不确定的事情写出步骤。
追踪算法:行得通吗?
我做的另一件事是手动跟踪算法,看看它做了什么,并确认它做了我想要的。
首先,半径设置为 200。面积为π 200 2 。我们用这个半径画一个圆。
接下来,半径被设置为除以的值。面积为π(200/
)2=π2002/2,是第一个面积的一半;这就是我想要的。我们画出新的圆圈。
接下来,半径被设置为除以后的值。面积为π(200/
)2=π2002/4,是第一个面积的四分之一。很好。我们画出新的圆圈。
好像还行。
随着程序变得越来越复杂,检查你的算法将会更加有用。如果一个程序不能完成我们想要的,为什么还要花时间去编译它呢?我懒得做那件事。
编码:把它全部放进 C++(加上:评论懒惰的方式)
为了创建程序,我以通常的方式开始:我在文件的顶部告诉读者我正在做什么。然后我把算法放在main中,作为注释,这样我就可以把它改进成一个工作程序。
在代码被写成代码之前,编译是没有意义的。因此,我将在接下来的几页中完善它,直到我得到看起来可以工作的东西:
// Program to draw 5 concentric circles
// Each circle is twice the area of the one inside it
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
// start radius at 200
// do
// draw a circle at center of screen, with this radius
// divide radius by sqrt (2)
// while radius > 1 (quit when circle's too small to see)
return 0;
}
警察没有因为我把算法放进编辑器而逮捕我,所以我想我会继续下去。
Tip
包括程序中的算法,在//之后,你已经写了大部分的注释。
编辑可以快速地将文本转换成注释。(这对于使有问题的代码停止生成错误也很有用;把它们放在注释里——“把它们注释掉”——直到你准备好处理它们。)
emacs
:突出显示该区域,选择 C++ ➤注释掉区域对其进行注释;如果需要,按 Tab 键缩进。如果您在 emacs 的非图形版本中,通过在区域的一端按 Ctrl-Space,然后将光标移动到另一端来突出显示该区域。Ctrl-C 会把它变成注释,Tab 会缩进。
(还要注意最后一个很酷 emacs 技巧:突出显示一个区域并按 Tab 键,emacs 立刻缩进该区域。)
Visual Studio
:单击注释掉按钮会将突出显示的代码转换为注释。(它看起来像平行的水平线,并在图 6-2 的右上方突出显示。)
图 6-2
“注释掉”按钮突出显示的 Visual Studio 窗口(右上角)
图 6-3 显示了注释。
图 6-3
该算法在注释中
现在,您可以按 Tab 键来缩进该区域。
有时,如果一个编辑器为你做了评论,它将使用我们还没有介绍过的评论风格:将评论放在/*和*/中。那也行得通。
不妨先编写简单的部分:radius的声明和循环;
int main (int argc, char** argv)
{
double radius = 200.0; // start radius at 200
do
{
// draw a circle at center of screen, with this radius
// divide radius by √2
}
while (radius > 1.0); // quit when circle's too small to see
return 0;
}
现在将中间步骤放入代码中,将算法保留为注释:
int main (int argc, char** argv)
{
double radius = 200.0; // start radius at 200
do
{
// draw a circle at center of screen, with this radius
SSDL_RenderDrawCircle (CENTER_X, CENTER_Y, int (radius));
radius /= sqrt (2); // divide radius by √2
}
while (radius > 1.0); // quit when circle's too small to see
return 0;
}
看起来我们需要中心点:
int main (int argc, char** argv)
{
const int CENTER_X = SSDL_GetWindowWidth ()/2;
const int CENTER_Y = SSDL_GetWindowHeight()/2;
double radius = 200.0; // start radius at 200
do
{
// draw a circle at center of screen, with this radius
SSDL_RenderDrawCircle (CENTER_X, CENTER_Y, int (radius));
radius /= sqrt (2); // divide radius by √2
}
while (radius > 1.0); // quit when circle's too small to see
return 0;
}
在程序的开始和我们通常的总结中加入一些友好的元素,我们的程序就完成了,并且已经被评论了(例如 6-1 )。
// Program to draw concentric circles
// Each circle is half the area of the one outside it
// -- from _C++20 for Lazy Programmers_
#include <cmath> // for sqrt
#include "SSDL.h"
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Hit any key to exit.");
const int CENTER_X = SSDL_GetWindowWidth ();
const int CENTER_Y = SSDL_GetWindowHeight();
double radius = 200.0; // start radius at 200
do
{
// draw a circle at center of screen, with this radius
SSDL_RenderDrawCircle (CENTER_X/2, CENTER_Y/2, int (radius));
radius /= sqrt (2); // divide radius by √2
}
while (radius > 1.0); // quit when circle's too small to see
SSDL_WaitKey();
return 0;
}
Example 6-1A program to draw concentric circles, each half the area of the one outside it. Output is in Figure 6-3
注意程序是如何被空行分成算法的主要步骤的。这不是一个要求,但也不是一个坏主意。
Online Extra
“当你找不到开始的方法时”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=UJK4a623D20 找到它。
Exercises
-
写一个算法,求三个数的平均值。
-
为练习 1 编写相应的程序。
-
写一个算法,然后写一个程序,通过画很多半径从 0 到某个半径 r 的圆,画一个填充的圆,不一定要看起来完全填充。
-
为一个程序写一个算法,用你可以用 SSDL 画的形状来画澳大利亚国旗、新西兰国旗、埃塞俄比亚国旗、斯堪的纳维亚半岛国旗或其他一些国旗。专注于一个连贯的子任务或重复的子任务。我们将在后续章节中看到更多的标志问题。