面向懒惰程序员的-C--20-教程-二-

111 阅读28分钟

面向懒惰程序员的 C++20 教程(二)

原文:C++20 for Lazy Programmers

协议:CC BY-NC-SA 4.0

四、鼠标,和if

在这一章中,我们将学习鼠标输入,以及计算机风格的决策艺术。

鼠标功能

示例 4-1 显示了一个程序来检测你点击鼠标的位置并报告结果。很神奇吧。因此,我们介绍三种鼠标功能:SSDL_GetMouseXSSDL_GetMouseYSSDL_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();

您的程序分配空间来存储两个整数,xLocationyLocation,并在每个整数中放置一个值。

此时,程序打印它们(图 4-1 ):

img/477913_2_En_4_Fig1_HTML.jpg

图 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_GetMouseY don’t get a mouse click location; they just get a location. So here’s what happens:

    1. 程序获得鼠标的 x,y 位置。

    2. 当程序等待时,你移动鼠标到你想要的地方。

    3. 你点击。

       It gets the location before you move the mouse where it should go. No wonder it’s wrong! Rearrange it thus:

    4. 当程序等待时,你移动鼠标到你想要的地方。

    5. 你点击。

    6. 程序获得鼠标的 x,y 位置。

    …如例 4-1 所示。

Exercises

  1. 写一个程序,让你点击两次,在两次鼠标点击之间画一条线。

  2. 编写一个程序,让你点击一次来设置一个圆的中心,然后再点击一次来设置这个圆的边缘上的一个点;然后它画圆。

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语句的常见变体:

img/477913_2_En_4_Fig2_HTML.jpg

“埃及括号”得名于何处

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"&&"读作“与”这意味着X010

  • X<=0 || X>=10||读作“或”这意味着X要么是0或更少,要么是10或更多。

  • ! (X<0)!读作“不是”这意味着X小于0不是真的。(你需要(),如果你输入! X < 0,C++ 的优先规则会将其解释为(! X) < 0。去想想。)

这些运算符奇怪的外观(为什么是“&&”而不是“&”或“and”)?)是历史文物。你会习惯的。 1

因此,为了适应前面的例子,这里有一种方法来查看存储在xLocationyLocation中的鼠标点击是否在屏幕的左上角:

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在一起。在这种情况下,“生活是美好的。”如果TodayIsSaturdaytrueIAmAtWork为假,则打印。如果今天不是星期六,代码什么也不打印。

*这种歧义被称为悬空 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

  1. 写代码报告某 X 的平方根是否大于 1。

  2. 给定两个整数,报告它们的顺序(正确顺序、相反顺序或相等)。

  3. 给定一个分数,0-100,打印它是 A,B,C,D 还是 f。

  4. 编写一个 if 语句,打印鼠标点击是在屏幕的左上角、右上角、左下角还是右下角。

  5. 编写代码,打印“超出范围!”如果 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变量名,所以很明显值应该是真或假。

布尔变量的可能值是–等待它-truefalse

您也可以在表达式中计算这些值,就像您可以使用intdoublefloat变量一样:

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

  1. 等待鼠标点击,如果 X 大于 y,将一个布尔变量设置为 true。通过在屏幕上打印一条消息来报告是否如此。

  2. 单击两次鼠标,并设置布尔变量,以确定第二个是否在第一个的右边,以及是否在第一个的下面。然后报告第二个与第一个的关系,如北、东北、东、东南或任何正确的方向。

隐藏物体游戏

在一片岩石中的某处有一块菊石化石(图 4-2 )。我们将制作一个游戏来训练崭露头角的古生物学家:点击屏幕,如果你得到了化石,你就赢了。

img/477913_2_En_4_Fig3_HTML.jpg

图 4-2

要搜索的图像。化石就在那里的某个地方…

我们需要的是一种围绕它的假想的“边界”框。如果我点击盒子,我就赢了;在其他地方,我输了。盒子的坐标太难猜了。也许我可以像示例 4-2 那样在图像上画一个方框,如果看起来不对,就调整一下。我的错误猜测是在图 4-3 中。

img/477913_2_En_4_Fig4_HTML.jpg

图 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 中的那个:

img/477913_2_En_4_Fig5_HTML.jpg

图 4-4

边界框是正确的

  SSDL_RenderDrawRect (335, 180, 45, 35); // corrected numbers

所以现在我们将有一个程序(示例 4-3 )来检测鼠标点击是否在那个框内。它使用了几个布尔变量,如果你赢了,它会用红色和白色闪烁边界框。

img/477913_2_En_4_Fig6_HTML.jpg

图 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

  1. 写一个程序,显示一个框,等待鼠标点击,并告诉你是否在框内点击。

    为了让它更有趣,贴一个有趣的东西的大致正方形的图像。我喜欢垃圾邮件。特别感谢为这款超现实游戏制作《寻找垃圾邮件》( www.smalltime.com/findthespam/ )的创意人员。

  2. 制作一个隐藏物体游戏:用户必须点击你提供的物体(有两个或更多)才能获胜。您可以要求用户按顺序执行。点击不是的某个物体,或者按顺序正确的物体,以失败结束游戏。

    你可以把每个物体看作屏幕上的一个正方形区域。

  3. 写一个程序,在你点击的地方画一个泡泡,总是同样大小的泡泡…但是它不会让你在屏幕上放一个部分。如果您单击的位置太靠近边缘,它会将气泡从边缘移开,使其不会越过边缘。创建气泡时,您还可以添加声音效果。

Footnotes 1

我们也有 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";

你也可以用>>链接你正在阅读的内容:

img/477913_2_En_5_Fig1_HTML.jpg

图 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+ |

  1. 使用公式厘米= 2.54 *英寸,编写一个将英寸转换为厘米的程序。使其具有交互性,即向用户询问要转换的值。

  2. 写一个程序来识别你属于哪一代人(Gen Zmillennial等等)。),基于用户输入的年龄或出生年份。你可以选择范围。

  3. 身体质量指数(身体质量指数)告诉你是重、瘦还是中等。(这不精确,但如果没有别的,也许我可以说服我的祖母,如果我不吃第二份,我就不会饿死。)

    根据维基百科,这些是范围:

所以,写一个程序来计算用户的身体质量指数。公式 isBMI =以千克为单位的重量/(以米为单位的高度) 2

如果你在一个使用英制单位的国家,你也需要这个信息:1 公斤= 2.2 磅,1 米= 39.37 英寸。

  1. 编写一个程序,要求用户两次(如 1:06 或 12:19)并整齐地打印出差值(如 11:03 或 0:40,但不是 13:0 或-12:70)。你用键盘输入时间——我们不是问计算机现在是什么时间。

  2. …但现在我们是了。不是询问用户时间,而是测量用户按回车键的两次,得到当前系统时间,如下所示:

    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)而不是数字给出选项仍然是用户友好的。

如何使你的程序易于交互是计算机科学的一个分支:人机交互的主题。

breakcontinue

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表示跳过循环的剩余部分,返回到顶部。我很少用它。

一些编程风格的专家被breakcontinue吓坏了。他们认为你应该能够看到循环的继续条件,并立即看到在什么情况下循环可以结束——本质上,这些关键字降低了清晰度。我同意清晰是至关重要的,但我不确定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

  1. 让用户一直输入一个数字,直到他/她猜出你选择的某个数字。

  2. …让程序打印出猜了多少次。

  3. 写一个程序,要求一个(大写)字母,并从'A'开始计数,直到找到为止。它的输出将类似于'E' is the 5th letter of the alphabet!

  4. …现在修改程序,让它一直重复,直到你给它一个'.'结束。

  5. 写一个程序,在你点击的地方画一个泡泡。气泡的大小应该取决于鼠标最后一次点击的时间。使用互联网了解功能SDL_GetTicks()

  6. 更新上一章末尾的练习 2——隐藏的物体游戏——这样用户可以按任意顺序点击隐藏的物体。

  7. 制作自己的音乐播放器:在屏幕底部放置标有“音乐打开”和“音乐关闭”的盒子,当用户点击其中一个盒子时,适当地打开或关闭声音。

  8. (更用力;需要几何学)考虑一个三角形的周长,这个三角形以一个点为中心,其端点离中心为“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?

    img/477913_2_En_5_Fig2_HTML.png

    图 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++ 都会执行递增部分。这可以是任何东西,但是它通常增加一个索引变量(也就是我们用来计数的变量)。

计算机处理这些部分的顺序是

  1. 做。

  2. 是真的吗?如果不是,则退出循环。

  3. 做。

  4. 做。

  5. 回到步骤 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

顺便说一下,关键字breakcontinue在 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. 打印数字 1-10…和它们的平方(1,4,9,16,等等。).

  2. 现在使用字符,使用 for 循环打印字母 A–z。

  3. …向后。

  4. 编写一个像示例 5-2 中的平均程序一样的程序,但让它提供的不是平均值而是最大值。

  5. 改编示例 3-3/示例 3-4 (画一颗星),让它询问用户半径、中心和点数——并使用循环而不是重复代码来画线。

  6. 写一个程序,要求用户输入一个整数和它的幂,并打印结果。当然是用 for 来计算。

  7. 编写一个程序,让用户猜一个数字(这个数字可以是一个constexpr),并一直猜下去,直到用户用完了所有的回合——你来决定有多少回合——或者猜对了。然后它报告成功或失败。你需要一个布尔变量isSuccess

  8. (硬)画某个函数的图(正弦就是个好的)。添加 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!嗯,字符在某种程度上像整数,所以这是可以容忍的,如果不是绝对清楚的话。

这些函数的另一个奇怪之处是相似的:islowerisupper返回int。他们不应该返回true或者false吗?是的,但是由于 C++ 将0解释为false,而将其他整数都解释为trueint将起作用,如下面的代码片段所示:

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 char with 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!

    char s 需要单引号,像这样: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前面可以加上关键词signedunsignedshortlong或者long long(我猜是取了“humongous”这个词),比如unsigned long int。可以想象,shortlong指的是数字可以有多大。int可省略:unsigned short x;long y;

如果没有指定,int就是signed。如果一个char没有被指定为signedunsigned,则由编译器决定是哪一个。这不重要。

wchar_t(“宽字符”)是一种较大的字符类型,在char不够大时使用,即用于国际字符。char8_tchar16_tchar32_t也是国际字符。

文字值上的后缀——如在5.0f42u中——是用来告诉编译器“这是一个(f)loat,不是一个double,“这是(u)nsigned,等等。后缀可以大写。

如果你想知道intlong int等等到底有多大,你可以使用#include <climits>找到答案,它定义了各种类型的最大值和最小值的常量。你可以用sizeof : sizeof (int)或者sizeof (myInt)得到其中一个的大小,以字节 3 表示,其中myInt是一个int

如果你存储的值太大,它们会换行;使用signed,你将得到一个负数,而不是一个太大的正数。这几乎从来都不是问题。如果是,使用long intlong long int

有关基本类型的完整列表,请参见附录 d。

Exercises

  1. 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. 编写并测试一个函数,在给定一个数字的情况下,打印相关的序数:也就是说,对于 1,打印第一个;对于 2,打印第二个;对于 3,打印 3 号;其他的,打印数字加“th”

  2. 让用户输入两个一位数并返回总和。这里有个转折:输入的数字是以 16 为基数的(“十六进制”)。以 16 为基数,我们用'A'代表 10,'B'代表 11,'C'代表 12,以此类推,'F'代表 15。你可以用 10 进制给出结果。

  3. 菜单是一种由来已久的(老式的)获取用户输入的方式。制作一个菜单,为用户绘制一个圆或一条线,或者其他形状,然后绘制选定的形状。

Footnotes 1

我本可以称之为sin并等待双关语的开始,但是sin在 C++ 中已经意味着某种东西:正弦函数。“S-in”可能是一个很好的发音方法。

  2

这是来自 g++ 编译器。Visual Studio 给了我三行,明确指出<<是问题所在。很好!

  3

一个“字节”是一个单独的内存位置,在我听说过的所有系统中足以存储一个char

 

六、算法和开发过程

让我们从 C++ 的细节上退一步,从大的方面考虑一些事情:特别是,考虑大的方面的需要。在以后的生活中,规划没有帮助吗?没有计划,你不会盖房子,也不会做饭。(微波炉加热汤不算。)

在编程中,这个计划被称为算法:一系列步骤,按顺序执行,最终达到一个目标。

机器人烹饪历险记

想象一下,如果我们能让电脑制作饼干(蓬松的那种——像烤饼,但不甜)。计算机可以遵循指令,但指令必须清晰。

我拿出一个碗,然后…饼干里放了什么?你抓到我了。面粉应该会有帮助。他们不把鸡蛋放在饼干里吗?牛奶呢?告诉机器人把一些面粉倒在碗里,放入几个鸡蛋和一大杯牛奶,尽可能地搅拌,滚动,然后放进烤箱。

当然,他们会硬得像砖头一样。我们的机器人厨师把鸡蛋放了进去——我祖母会笑的——而且搅拌得太多了。我应该先制定一个可靠的计划。

img/477913_2_En_6_Fig1_HTML.jpg

(左)制作不当的饼干通常对建筑业有用。至少当你试着吃它们的时候你会这么认为。(对)我希望做的是

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)被认为是世界上第一批计算机科学家。可惜电脑还没有发明出来。

img/477913_2_En_6_Fig2_HTML.jpg

Charles babb age ada lovelace 女士

巴贝奇当然尽力了。他让英国政府资助他的差分机,这本来是一个机械计算器,然后他的分析机,设计成一个机械计算机。(那时候,政府资助研究几乎是闻所未闻的。也许这就是为什么它被称为“发明时代”)当时机器零件不够精细,项目失败了。

洛夫莱斯是诗人拜伦的女儿,她对巴贝奇的机器感兴趣,了解它运行的程序的性质,或者说,如果它存在的话,它会运行的程序的性质。她说:“分析引擎并不自命能创造任何东西。”"它可以做我们知道如何命令它执行的任何事情."这有时被用来反对人工智能的概念。

编写程序,从开始到结束

让我们将这种提前计划的事情应用到一个真实的、虽然很小的编程任务中。

以下是我们为某个目标创建程序可能要经历的步骤:

  • 确定需求,也就是对目标进行精确的陈述。

  • 写算法。

  • 手动追踪算法。

  • 将算法转换成有效的 C++ 代码。

  • 编译。

  • 测试。

如果在此过程中出现错误,我们可以返回到前面的步骤。

需求:我们想做什么?

我想做一系列同心圆,使每个圆的面积是它外面那个圆的一半(图 6-1 )。我们将继续下去,直到他们开始模糊(也许当半径约为 1 个像素)。外圆的半径是,哦,200。

img/477913_2_En_6_Fig3_HTML.jpg

图 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。这就得出下一个半径是第一个半径/ \sqrt{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 beforeto 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 。我们用这个半径画一个圆。

接下来,半径被设置为除以\sqrt{2}的值。面积为π(200/\sqrt{2})2=π2002/2,是第一个面积的一半;这就是我想要的。我们画出新的圆圈。

接下来,半径被设置为除以\sqrt{2}后的值。面积为π(200/\sqrt{2}/\sqrt{2})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 的右上方突出显示。)

img/477913_2_En_6_Fig4_HTML.jpg

图 6-2

“注释掉”按钮突出显示的 Visual Studio 窗口(右上角)

图 6-3 显示了注释。

img/477913_2_En_6_Fig5_HTML.jpg

图 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. 写一个算法,求三个数的平均值。

  2. 为练习 1 编写相应的程序。

  3. 写一个算法,然后写一个程序,通过画很多半径从 0 到某个半径 r 的圆,画一个填充的圆,不一定要看起来完全填充。

  4. 为一个程序写一个算法,用你可以用 SSDL 画的形状来画澳大利亚国旗、新西兰国旗、埃塞俄比亚国旗、斯堪的纳维亚半岛国旗或其他一些国旗。专注于一个连贯的子任务或重复的子任务。我们将在后续章节中看到更多的标志问题。