面向懒惰程序员的 C++20 教程(三)
七、函数
在这一章中,我们得到了不会迷失在一页页的代码中直到你的眼睛变得模糊的最好方法:函数。
返回值的函数
想一想糖果厂的工作方式。它有制造我们想要的东西的机器。每台机器都有它需要的东西通过管道输入,它生产的东西通过管道输出。想要块糖吗?启动机器并给它输入,它将提供结果(见图 7-1 )。
图 7-1
makeCandyBar机器的结构
C++ 也有“机器”(称为“函数”):例如,SSDL_CreateColor、SSDL_WaitKey、sin和cos。例如SSDL_CreateColor(图 7-2 ,取三个int,返回一个SSDL_Color。
图 7-2
SSDL_CreateColor函数的结构
函数的 BNF 为
<返回类型> <名称> ( <参数,用逗号分隔>**//【表头】
{
*<要做的事情——变量声明,动作,随便什么> **
}
其中一个*<>参数是一个<>*类型加一个 <名称> : int red例如。
最上面一行是函数头;剩下的就是函数体。我们经常复制最上面一行,并在末尾加上一个;,以精确描述我们如何与函数(它的输入和输出)交互:SSDL_Color SSDL_CreateColor (int red, int green, int blue);。这是一个函数声明或原型,在之前描述库函数的章节中出现过。它不仅对程序员学习有用,对编译器也有用。
现在来做我们自己的。图 7-3 显示了一个平均三个int的函数。
图 7-3
int average (int a, int b, int c)函数
声明只是在最上面一行加上一个;:
int average (int a, int b, int c); // function declaration
函数是我们做的,所以让我们使用它。使用它意味着将它的值存储在一个变量中,打印它,将它发送给另一个函数(参见接下来的例子)…并用结果做某事;否则,调用这个函数是没有意义的:
int myAverage = average (1, 2, 12); // A "function call"
好的,这很有效。我们可以给它变量,而不是文本
int i = 1, j = 2, k = 12;
int myAverage = average (i, j, k); //parameter a gets i's value, b gets j's...
…或常量或表达式–任何具有适当值的内容:
int otherAverage = average (DAYS_PER_WEEK, 14/2, sqrt(144));
我们不能像这样声明括号中的变量:
int a = 1, b = 2, c = 12;
int myAverage = average (int a, int b, int c); // NO -- won't compile!
C++ 读取形式为 *<函数名> ( <参数列表,用逗号分隔>)的东西;*心想,我知道那是什么——是宣言!它可能会感到困惑(就像这里),为什么你要设置一个等于函数声明的int。它可能只是说,“好的,我看到声明了”,然后继续。但是有一点是肯定的:它不会调用函数。
Tip
当调用一个函数时,把类型信息放在()之外。类型信息是用于声明的。
Extra
一些纯粹主义者更喜欢每个函数只有一个 return 语句——而不是
if (condition)
return this;
else
return that;
but
if (condition)
result = this;
else
result = that;
return result;
这涉及到简单地跟踪函数和验证正确性。当我们在接下来的章节中浏览例子时,你可以看到你的想法。
这里有一个函数,可以将给定颜色的灰度等效为红色、绿色和蓝色分量。它将通过对红色、绿色和蓝色进行平均并将平均值应用于每个组件来实现这一点,从而创建并返回一个SSDL_Color:
// Gets a greyscale color for a given r, g, b
SSDL_Color greyscale (int r, int g, int b)
{
int rgbAverage = average (r, g, b);
SSDL_Color result = SSDL_CreateColor (rgbAverage, rgbAverage, rgbAverage);
return result;
}
我使用了之前的函数average。*这是好事。*代码重用是如何避免重复做同样的工作,每次都犯新的错误。
Golden Rule of Code Reuse
如果你已经写了代码来做某事,不要再写了。把它放在一个函数中并调用那个函数。
一如既往,宣言是最上面的一行,结尾是;:
SSDL_Color greyscale (int r, int g, int b);
示例 7-1 显示了一个利用我们所做的程序(输出如图 7-4 )。请注意,程序的结构变得有点复杂。以前是
// initial comments
#include "SSDL.h"
main
but now it’s
// initial comments
#include "SSDL.h"
function declarations1
main
function bodies
编译器在到达任何可能有函数调用的代码之前读取函数声明,因此可以确保调用是正确的(拼写正确、参数正确、返回值使用正确)。
图 7-4
几种亮色,通过SSDL_Color greyscale转换成单色(int r, int g, int b);
// Program to change some colors to greyscale
// -- from _C++20 for Lazy Programmers
#include "SSDL.h"
//Function declarations go here
// Averages 3 ints
int average(int, int, int);2
// Gets a greyscale color for a given r, g, b
SSDL_Color greyscale(int r, int g, int b);
int main (int argc, char** argv)
{
sout << "Some colors you know turned to black-and-white. "
<< "Hit any key to end.\n";
// By now the compiler knows that greyscale
// takes 3 ints and returns an SSDL_Color, but doesn't
// know how to do the greyscale...
SSDL_SetRenderDrawColor (greyscale (255, 255, 255));
sout << "WHITE\n";
SSDL_SetRenderDrawColor (greyscale (255, 0, 0));
sout << "RED\n";
SSDL_SetRenderDrawColor (greyscale ( 0, 255, 0));
sout << "GREEN\n";
SSDL_SetRenderDrawColor (greyscale ( 0, 0, 255));
sout << "BLUE\n";
SSDL_SetRenderDrawColor (greyscale (181, 125, 41));
sout << "MARIGOLD\n";
SSDL_SetRenderDrawColor (greyscale ( 50, 205, 50));
sout << "LIME GREEN\n";
SSDL_WaitKey ();
return 0;
}
// Function bodies come after main, by convention
// Averages 3 ints
int average(int a, int b, int c)
{
return (a + b + c) / 3;
}
// Gets a greyscale color for a given r, g, b
SSDL_Color greyscale(int r, int g, int b)
{
int rgbAverage = average (r, g, b);
SSDL_Color result
= SSDL_CreateColor (rgbAverage, rgbAverage, rgbAverage);
return result;
}
//...and now the compiler has all the information
// it needs about greyscale (and anything else)
Example 7-1A program to make and use grayscale colors
Exercises
-
编写并测试一个函数来获得屏幕的纵横比,即宽度除以高度。
-
编写并测试一个函数来返回下一个字母:例如,给它一个
'A',它将返回'B'。是的,就是这么简单。 -
编写一个算法,然后编写并测试距离公式:
不返回任何值的函数
有些函数不返回值,而是做其他事情——画图、打印文本或其他任何事情。
考虑一个函数,不画矩形或圆形,就像我们已经有的那样,而是画一个十字。由于没有原创性,我们将把它命名为drawCross。
它需要什么样的输入才能开始?它需要知道在哪里画十字,所以这是一个 x 和一个 y。它还需要知道大小,从中心到两端的距离。这将起作用:
void drawCross (int x, int y, int distanceToEnds)
{
...
}
返回类型是void,意思是“我不返回任何东西。”参数名的含义非常明显,这是一件好事。
示例 7-2 显示了使用drawCross函数的示例程序。输出如图 7-5 所示。
图 7-5
示例 7-2 的输出
// Program to draw a cross on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
void drawCross (int x, int y, int distToEnds);
int main(int argc, char** argv)
{
int crossX = 40, crossY = 25, size = 20;
drawCross (crossX, crossY, size); //draw a cross
SSDL_WaitKey();
return 0;
}
// draw a cross centered at x, y, with a distance to ends as given
void drawCross (int x, int y, int distToEnds)
{
SSDL_RenderDrawLine (x-distToEnds, y, x+distToEnds, y);
// draw horizontal
SSDL_RenderDrawLine (x, y-distToEnds, x, y+distToEnds);
// draw vertical
}
Example 7-2A program that uses a function to draw a cross. The order of arguments sent in determines the order received: crossX is sent to x, crossY to Y, and size to distToEnd
s
在使用函数时,我发现画出哪些函数是活动的以及它们有哪些参数和变量的图表很有帮助。
首先,C++ 创建主函数的一个实例(图 7-6 )。
图 7-6
主,在示例中 7-2
当main到这一行:drawCross (crossX, crossY, size);...C++ 创建了一个drawCross的副本,带有它的参数(以及它拥有的任何其他变量),并复制了(图 7-7 )中的值。这就是为什么main传入的参数和drawCross的参数是否同名并不重要。每个函数都使用自己的一组名称。
图 7-7
main,调用drawCross并复制值
当我们对drawCross的调用结束时,它被删除,我们回到main(图 7-8 )。
图 7-8
离开drawCross并返回main
我们可以随心所欲地经常重用drawCross,就像我们可以重用SSDL_RenderDrawPoint、SSDL_RenderDrawCircle等等(例如 7-3 ,输出如图 7-3 )。
图 7-9
示例 7-3 的输出
// Program to draw a cross on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
void drawCross(int x, int y, int distToEnds);
int main(int argc, char** argv)
{
drawCross( 40, 40, 20); //draw three crosses
drawCross( 80, 30, 15);
drawCross(110, 50, 40);
SSDL_WaitKey();
return 0;
}
// draw a cross centered at x, y, with a distance to ends as given
void drawCross(int x, int y, int distToEnds)
{
SSDL_RenderDrawLine (x - distToEnds, y, x + distToEnds, y);
// draw horizontal
SSDL_RenderDrawLine (x, y - distToEnds, x, y + distToEnds);
// draw vertical
}
Example 7-3Calling function drawCross multiple times
全局变量
有些人找到了这种变通方法:他们不传入参数,而是将变量设为全局(意思是“不在任何人的{}内”),而不是局部(在main的{}或drawCross的或某人的内)。
// Program to draw a cross on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
// GLOBAL VARIABLES: THE EIGHTH? NINTH? DEADLY SIN
int x = 40, y = 40;
int distanceToEnds = 20;
// Function declarations
void drawCross ();
int main (int argc, char** argv)
{
// draw three crosses
drawCross();
x = 80; y = 30; distanceToEnds = 15; drawCross();
x = 110; y = 50; distanceToEnds = 40; drawCross();
SSDL_WaitKey();
return 0;
}
// draw a cross centered at x, y, with distance to ends, all global
void drawCross ()
{
SSDL_RenderDrawLine (x-distanceToEnds, y, x+distanceToEnds, y);
SSDL_RenderDrawLine (x, y-distanceToEnds, x, y+distanceToEnds);
}
Example 7-4What not to do: use global variables
简单吧。
不完全是。有三个缺点:
-
读写很难。
drawCross会画一个十字,但是在哪里?你必须在身体内部寻找答案:它在(x,y)处画出它。什么是x和y?看上面;他们是(40,40)。然后在main中回头看看它们是如何变化的。希望不要有其他函数也使用x和y做其他事情并改变它们的值。可以肯定的是,你必须浏览所有的代码。试试 500 页的程序。啊! -
调试是魔鬼。查看整个程序,找出是什么搞糟了一个变量是一项艰巨的工作。我们试图将程序分成相对独立的部分(函数),用参数列表清楚地说明这些部分如何相互作用。这缩小了查找错误的范围。这也有助于团队项目:不同的程序员可以处理不同的函数,彼此的工作干扰最小。这叫做模块化**。**
-
不得不维护你的代码的程序员会讨厌你。不是你冷落了他们,而是你向他们的汽车扔鸡蛋,侮辱了他们的母亲。他们不想继承调试灾难。
-
我听说,因为某些过错,圣诞老人不会给你带任何礼物。更糟糕的是,圣诞老人会拿走你的礼物。这是后一种。
Golden Rule of Global Variables
直接说不。
防错法
-
**这不是调用函数。**你可能放了类型信息进去,所以编译器认为是函数声明。取出类型信息。
-
你调用了一个返回值的函数,但是没有效果。参见第三章,“内置函数和类型转换”,反欺诈小节——同样的问题,同样的解决方案。
-
**您会得到一个错误,类似于“不允许本地函数定义”**如果某些东西缺少结束
},编译器可能会认为当你启动另一个函数时,你还在这个函数中。确保{}是平衡的——这是避免埃及括号的好理由(见第四章)。为了防止这种情况,当启动一个函数体时,同时将两个{}放在适当的位置。如果你这样做了,函数可能会编译,即使它还没有做任何事情。这样的空函数被称为存根。在未完成的程序中出现它们是很常见的。
-
It skips the latter part of the function, as here:
int value (char letter) // score letters in a word game. // Q, K are best { return 1; // default score is 1 if (toupper (letter) == 'Q' || toupper (letter) == 'K') return 5; }这总是返回 1。原因是 first
return不仅建立了返回值,还停止了函数;它不会继续运行if。解决方案:将
return视为函数做的最后一件事。 -
无论你给什么参数,函数总是做同样的事情。确保它们没有在函数内部被重置(参见下一节“如何用四个简单的步骤编写一个函数(并在一个步骤中调用)”。
-
通过让函数调用自身来重复函数。这不是错误,但也不是最佳实践。假设你想多次玩一个游戏:
void playGame () { ... // now let's play again: playGame (); }
从上一节中的图的角度来考虑……当你调用一个函数时,C++ 会创建一个副本,并一直保存到函数完成。当你在这里打了无数场比赛后会发生什么?你得到了如图 7-10 所示的函数的无数副本,每一个都需要内存。如果“无数”变得非常大,它会使程序崩溃。
图 7-10
一个函数的多个“递归”(自调用)副本
更好的解决方案:使用循环。
Exercises
还没有很多练习:我真的希望你在开始写你自己的函数之前看看下一节。但是这里有两个。
-
编写一个算法,然后编写并测试一个函数,在参数指定的位置画一个三角形。
-
写一个函数画一个圈起来的数字,像一些限速标志。它需要什么参数?
如何用四个简单的步骤编写一个函数(并在一个步骤中调用它)
我强烈建议对你写的每一个函数都使用这些步骤,直到你确定你已经确定了函数。
-
将它放在
main之后,使用您自己的函数名和注释:<return type> greaterNumber () // Returns the greater of two numbers { <return type> result; return result; } -
它返回什么样的值?不管它是什么,使用它作为返回类型:
double greaterNumber () // Returns the greater of two numbers { double result; return result; }如果函数不返回任何东西,跳过所有返回的东西,使它的类型无效:
void drawCross () // Draws a cross { } -
启动该函数需要哪些信息?
说你是
greaterNumber函数,我是main。我对你说:“给我更大的数字!”你说,“我不能,我需要更多信息!”你需要什么信息?You need the numbers. They go in the
()’s. You need to specify their types (int,double,char, etc.):double greaterNumber (double num1, double num2) // Returns greater of 2 numbers { double result; return result; } -
该函数是如何工作的?
-
将问题描述作为注释放入函数中。然后将它细化到一个足够具体的算法,如第六章所述。跳过这一步是个坏主意,除非你真的知道你在做什么。
double greaterNumber (double number1, double number2) // Returns the greater of two numbers { double result; //if number1 is bigger, that's the result; //if not, it's number2 return result; } -
编写有效的 C++ 来完成任务:
-
-
使用该函数:
-
复制最上面一行,放在
main上面,以分号结束(如下)。 -
调用函数,并(如果不是
void)存储结果或使用它。double greaterNumber (double number1, double number2); // Returns the greater of two numbers int main (int argc, char** argv) { ... bigNum = greaterNumber (20, 30);
-
double greaterNumber (double number1, double number2)
// Returns the greater of two numbers
{
double result;
// let result be the bigger of number1, number2
if (number1 > number2)
result = number1;
else
result = number2;
return result;
}
现在,关于什么会出错的一些注意事项。
在步骤 3 中,参数的值来自哪里?它们是在我们调用函数时由main提供的,例如 7-1 、 7-2 和 7-3 。因为它们是从main发送的,所以我们将而不是这样做:
double greaterNumber (double number1, double number2)
// Returns the greater of two numbers
{
double result;
sout << "Enter two numbers: ";
ssin >> number1; // WRONG. It erases the numbers main gave us!
ssin >> number2;
...
return result;
}
或者这个:
double greaterNumber (double number1, double number2)
// Returns the greater of two numbers
{
double result;
number1 = 12; // WRONG. It erases the numbers main gave us!
number2 = 25;
...
return result;
}
另一件我们不需要的东西:印刷术。我们几乎从来没有函数打印东西(除非它被命名为“printThis”或“outputSomething”之类的)。我们返回值并让main决定如何处理它。(想想看,如果在示例 3-3 、sin和cos中绘制星星的程序中,每次调用它们时都打印“这个函数的结果是……”并用文本掩盖你的星星。啊!)
double greaterNumber (double number1, double number2)
// Returns the greater of two numbers
{
double result;
...
sout << "The bigger number is " << result;
// WRONG. We're supposed to *return*, not *print*
return result;
}
Tip
函数不应该打印,除非名字说明了输出(如在printDialog中),也不应该读取任何内容,除非名字包含了输入(如在getUserResponse中)。将不同的任务分开。
现在我敦促你写出你写的每一个函数,每一个前面的步骤。复制
<return type> greaterNumber () // Returns the greater of two numbers
{
<return type> result;
return result;
}
进入你的编辑器,用你自己的东西替换greaterNumber和评论(这是第一步);将返回类型替换为您自己的类型(这是第 2 步);等等。
在第七章的源代码文件夹中有一个该流程的表格。请按照它写你自己的函数,直到你有信心!
Exercises
对于所有的练习,使用四个简单的步骤。
图 7-11
澳大利亚国旗,为练习 5 简化
-
为幂函数写一个算法,然后写并测试它。
myPow (a, b)应该返回 a b 。为了简单起见,假设指数只有整数值:可以计算myPow (3, 2),但不能计算myPow (3, 2.1)。 -
编写一个算法,然后编写并测试一个函数,该函数在给定一个正整数的情况下,返回该整数之前所有数字的和。例如,给定一个 5,它应该返回 1 + 2 + 3 + 4 + 5 = 15。
-
编写一个算法,然后编写并测试一个函数
log,给定一个正整数和一个整数基数,返回 log base (数字)。对数基数(数字)被定义为你可以用数字除以基数多少次才能得到 1。比如 8/2 得 4,4/2 得 2,2/2 得 1;那是三个师;所以 log 2 8 是 3。我们不会担心小数部分:log 2 15 也是 3,因为(用整数除法)15/2 是 7,7/2 是 3,3/2 是 1。 -
写一个算法,然后写一个显示希腊国旗的程序。您至少需要这两个函数:
drawCanton(左上角)和drawStripes。 -
为一个绘制澳大利亚国旗的函数写一个算法(如第六章最后一个练习)。然后,适当地使用函数,编写程序。您将需要一个函数
drawStar来绘制您在旗帜上看到的任何星星,这意味着它应该能够处理五角星或七角星。你不会填星星,只是做一个大概的轮廓(见图 7-11 ),除非你能想到一个窍门。Wikipedia 是旗帜规范的一个很好的来源。
为什么要有函数呢?
到目前为止,我们都是从函数开始,然后利用它。现在让我们来看看如何编写一个更大的程序,并推断出我们需要什么函数——这是更常用的方法。
考虑一下我们如何编写一个程序来一帧一帧地展示一幅漫画。为了方便绘图,我们将使用会说话但不会动的简笔画。我们将有四个画面,一次显示一个。
我们开始我们的算法:
write the dialog for the left character
draw the line from the left character's head to the dialog
draw the left character's head
draw the left character's body
draw the left character's left arm
draw the left character's right arm
draw the left character's left leg
draw the left character's right leg
write the dialog for the right character
write the line from the right character's head to the dialog
...
啊!写了很多。这不是给想要腕管综合症的程序员准备的 C++20。当我们开始编码时,我们会发现额外的打字是我们最不担心的。两倍的代码意味着五倍的出错机会。(这可能在数学上不合理,但根据经验,这是保守的。)
如果你这样做…就像上一章所述,你编写你的算法,仔细检查,编写程序,并运行它。然后你发现一个错误,比方说,对话框在错误的位置,并修复它——在一个帧的中。其他的框架,你忘了修理。更多错误。
更好的方法是遵循前面提到的代码重用的黄金法则:将对话框绘制代码放在一个函数中,并在需要时调用。
因此,我们将把算法(以及后来的代码)捆绑到函数中,以实现代码重用。 3 下面是main可能的样子:
main program:
give the window a title
draw frame 1; wait for user to hit a key
draw frame 2; wait for user to hit a key
draw frame 3; wait for user to hit a key
draw frame 4; wait for user to hit a key
我作弊了吗?我实际上并没有说如何做任何事情。嗯,这不完全正确。我说怎么做一切!只是不详细。只要我在另一个函数中给出这个细节——可能是一个名为draw frame的函数——那就没有错:
draw frame:
clear the screen
draw left character and its dialog
draw right character and its dialog
我又一次推迟了大部分工作!(你对一个懒惰的程序员有什么期待?)不过还好;draw frame是一项连贯的任务。只要draw character和draw dialog管用,我们就没事:
draw character:
draw head as a circle
draw body, a line
draw arms, two lines
draw legs, two lines
draw dialog:
draw line
draw the text
我们经历的过程——从主程序开始,编写它的子任务,然后是子任务的子任务,依此类推,直到我们知道如何用 C++ 编写东西(画线、画圆和写文本)——被称为自顶向下设计 、,这就是我们编写程序的方式。(指出存在其他软件工程技术的纯粹主义者是对的,但是你必须从某个地方开始。)
我们仍然需要细节,但我现在会把它留在这里,因为我想谈谈我们如何决定哪些代码应该被做成函数。
首先,如果代码可能被重复调用,我应该把代码做成一个函数,就像draw character一样。将它变成一个函数意味着它可以重复(正如我们在主程序中看到的)。
前面的例子(draw character和其他)也是连贯的任务——就像我们已经看到的函数,比如sqrt、sin、SSDL_RenderText等等。为什么没有一个函数SSDL_RenderPrintSin来“找出角度的正弦值并打印在屏幕上”?它需要更长的时间来描述,这是一个提示,它通常不太有用。(多久你要打印一次正弦?)最好把它分成几个函数,每个函数做一件事情。
另一个标准是一个函数应该足够短,以便理解。如果它太大了以至于在屏幕上看不到,那么当你编写和调试它的时候,它也太大了以至于不能理解它在做什么。一旦超过一屏,就把它分成子任务。
Golden Rule of Functions
如果代码可能被多次调用,则将代码放入函数中;或者形成一个连贯的任务;或者它是另一个函数的一部分,这个函数将会变得超过一个屏幕的长度。 4
Extra
心理学家测量了大脑同时意识到多件事情的能力,并确定一个人可以同时想到大约七件事情。 5
5 7 16 19 28 29 32
现在试着用这个数列来做。有一点小麻烦?
5 7 16 19 28 29 32 3 8 12 26 32 14 19 7 50 2 19 18 33 25 11 36 41 1
关键是你不能一次在脑子里保存任意长的数据集。跨越数百页(甚至一页)的主程序版本太长,难以理解。
我们有算法了。既然它有函数,让我们用“如何用四个简单的步骤编写一个函数”中的第三个问题来决定每个新函数的参数:这个函数需要什么信息来开始?
需要对话框,所以我们将它传入。它还需要知道把它放在哪里(左边角色的区域还是右边的区域):
draw dialog (x, y, dialog):
draw line
draw the text
除了位置之外都是一样的,所以它只需要:
draw character (x, y):
draw head as a circle
draw body, a line
draw arms, two lines
draw legs, two lines
draw frame绘制一个框架;它需要对话。有两个部分:左边角色的对话和右边角色的对话。因为简笔画不会移动,所以应该是这样:
draw frame (left char's dialog, right char's dialog):
draw character (left x, left y);
draw dialog (left x, left y, left char's dialog)
draw character (right x, right y);
draw dialog (right x, right y, right char's dialog)
这里是main,显示了它调用的函数的参数:
main:
give the window a title
draw frame 1 (left char's dialog, right char's dialog)
wait for user to hit a key
draw frame 2 (left char's dialog, right char's dialog)
wait for user to hit a key
draw frame 3 (left char's dialog, right char's dialog)
wait for user to hit a key
draw frame 4 (left char's dialog, right char's dialog)
wait for user to hit a key
我用来帮我画的图纸表示在图 7-12 中。
图 7-12
绘图纸画出一个卡通画框
该程序在示例 7-5 中;其输出如图 7-13 所示。
原来wait for user to hit a key不仅仅是对SSDL_WaitKey()的裸呼;为了用户友好,我需要一个提示,而且我希望它放在合适的位置。任务是重复的,编写起来很繁琐,所以遵循代码重用的黄金法则,它得到了自己的函数。
图 7-13
四格卡通节目的第一格。除了有趣得多之外,其他画面都很相似
// Program to display a 4-panel comic strip with stick figures.
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
// Function declarations
void drawFrame (const char* leftDialog, const char* rightDialog);6
void drawCharacter(int x, int y);
void drawDialog (int x, int y, const char* dialog);
void hitEnterToContinue (); //wait for user to hit Enter
int main (int argc, char** argv)
{
// Set up: window title and font
SSDL_SetWindowTitle ("My own 4-panel comic");
const SSDL_Font COMIC_FONT = SSDL_OpenSystemFont("comic.ttf",18);
SSDL_SetFont (COMIC_FONT);
// Now the four frames
drawFrame ("Somebody said something really nasty\nto me "
"on Internet.\nSo I put him in his place.",7
"Maybe it's not a him.\nMaybe it's a her. "
"You never know.");
hitEnterToContinue()
;
drawFrame ("OK, her. Whatever. She kept saying\nall this "
"stuff about how superior\nshe was. I found "
"a spelling error and\ntold her she can't even "
"spell so she\nshould just shut up.",
"If it's a her. It *might* be a him.\nThe point "
"is we just don't know.");
hitEnterToContinue();
drawFrame ("The *point* is, he went on a rant about\nhow you "
"can spell things like \"b4\"\nand so on in l33t, "
"and I told him l33t\nis for lusers -- with a u, "
"you know.\nThen he told me I misspelled \"loser.\"",
"If it's a him. It could be both.\nSometimes "
"married people\nshare accounts.");
hitEnterToContinue();
drawFrame ("You're making me crazy!",
"Can I have the URL for that forum?\nI'm not "
"done yet.");
hitEnterToContinue ();
return 0;
}
// draw a cartoon's frame, given dialog for each of two characters
void drawFrame (const char* leftDialog, const char* rightDialog)
{
constexpr int LEFT_X = 0, LEFT_Y = 20;
constexpr int RIGHT_X = 320, RIGHT_Y = 40;
// right character is drawn a little lower
// it doesn't look so much like a mirror image
SSDL_RenderClear (); // clear background to black
drawCharacter (LEFT_X, LEFT_Y);
drawDialog (LEFT_X, LEFT_Y, leftDialog);
drawCharacter (RIGHT_X, RIGHT_Y);
drawDialog (RIGHT_X, RIGHT_Y, rightDialog);
}
// draw a stick-figure character, with its dialog at the top.
// The upper-left corner of it all is x, y
void drawCharacter (int x, int y)
{
constexpr int HEAD_RADIUS = 45;
SSDL_RenderDrawCircle (x+140, y+195, HEAD_RADIUS); // draw head
SSDL_RenderDrawLine (x+142, y+240, x+140, y+340); // draw body,
// slightly angled
SSDL_RenderDrawLine (x+142, y+260, x+115, y+340); // draw arms
SSDL_RenderDrawLine (x+142, y+260, x+165, y+342);
SSDL_RenderDrawLine (x+140, y+340, x+100, y+420); // draw legs
SSDL_RenderDrawLine (x+140, y+340, x+157, y+420);
}
// Draw the dialog for a character, with a line connecting
// it to the character. x, y is the upper-left corner of
// the whole set (dialog plus character)
void drawDialog (int x, int y, const char* dialog)
{
// line linking character to dialog
SSDL_RenderDrawLine (x+90, y+100, x+112, y+130);
// dialog itself
SSDL_RenderText (dialog, x+20, y);
}
void hitEnterToContinue()
{
// How far up to put the "Hit a key" message
constexpr int BOTTOM_LINE_HEIGHT = 25;
// More succinct than "Hit any key to continue but not
// Escape because that ends the program"
SSDL_RenderTextCentered("Hit Enter to continue",
SSDL_GetWindowWidth() / 2,
SSDL_GetWindowHeight() - BOTTOM_LINE_HEIGHT);
SSDL_WaitKey();
}
Example 7-5A program to do a four-panel cartoon
概述
函数是组织代码的一种基本方式,所以你和那些阅读你的程序的人不会迷失在其中。它们对于代码重用也是必不可少的。
出于这两个目的,函数名必须清晰,信息必须以正确的方式传递(通过参数列表,在()之间)。返回值通过return语句输出。
对于你编写的每一个函数,在下一章中,直到你熟悉为止,请使用“如何编写函数”的步骤——它们对于有效使用这个强大的语言特性至关重要。
Exercises
对于每个函数,使用“如何编写函数”步骤。
图 7-14
练习 2 的坦扎克
-
编写你自己的多面板卡通:首先是算法(你肯定会需要它!),然后是函数。
-
在日本的七夕,人们会在垂直的纸条上写下愿望,然后绑在竹子上来庆祝。编写一个程序,在屏幕上做出几个潭柘寺,文字垂直书写,如图 7-14 。你需要什么函数?
我们也可以把函数体放在这里……但是人们喜欢把main放在前面,这样很容易很快看到程序mainly是关于什么的。
2
只要不损害清晰度,可以在声明中省略参数名。
3
提取大量信息并标记为可重用是抽象:在大型编程项目中保持理智的一种基本方式。
4
什么是满屏?C++ 的创始人和 C++ 核心指南( isocpp.github.io/CppCoreGuidelines/ )的主编比雅尼·斯特劳斯特鲁普建议,“试试 60 行乘以 140 个字符。”但是当然没有这么精确!只使用你的显示器上的作品;大概就可以了。
5
乔治·米勒。“神奇的数字七,正负二。”《心理评论》,1956 年,第 63 卷,第 81-97 页。将此应用于不是数字序列的事物——就像我现在正在做的——被指责为城市传说材料,理由是人们可以记住的准确项目数量因认知任务的类型而异( www.knosof.co.uk/cbook/misart.pdf ,在撰写本文时)。的确如此——但即使有所不同,也是有限度的。
6
我们已经在与文本相关的 SSDL 函数声明中看到了类型为const char*的参数(例如void SSDL_SetWindowTitle (const char* text))。我们将在第十四章了解const char*的真正含义。现在,就把它想成是“文本”
7
叫做“字符串文字串联”;如果你把”quoted” ”things”混在一起,中间只有空格,C++ 会把它们解释为一个”quoted thing”。这有助于我们在任何我们喜欢的地方整齐地换行。很好!
这对打印时如何换行没有影响;为此我们使用\n。
八、函数·续
关于函数的更多信息:随机数函数、布尔函数、变化的参数和“作用域”——也就是说,变量在哪里可以被看到和使用取决于它在哪里被声明。
随机数
当然,任何考虑用算术方法产生随机数的人都是有罪的。
FORTRAN 语言的发明者约翰·冯·诺依曼(Goldstine,1972 年)
随机数不只是对游戏有用。它们对模拟很有用——预测一些系统的平均行为——对科学计算也很有用。你可以告诉人们这就是你研究他们的原因。它不是用来玩的。诚实。
但是计算机是有序的机器。正如冯·诺依曼所知,他们不能真的制造随机数。我想你可以像扔硬币一样扔一个,看看它是怎么落地的,但是你怎么在它上面玩纸牌呢?
制作随机数生成器
所以我们要做的是制造一个数字序列,在人类观察者看来看起来是随机的——但是如果你知道计算机是如何做到的,它们实际上是完全可以预测的。
回想一下第三章中的%操作符,叫做“模数”,意思是“除取余数”例如,36%10给我们 6,因为如果你把 36 除以 10,余数是 6。
还要记住A += B的意思是A = A+B;*=和%=的定义相似:
int rand () // Return a pseudo-random integer
{
static unsigned int seed = 76;
static constexpr unsigned int INCREMENT = 51138;
static constexpr unsigned int MULTIPLIER= 21503;
static constexpr unsigned int MODULUS = 32767;
seed += INCREMENT;
seed *= MULTIPLIER;
seed %= MODULUS;
return seed;
}
这里的static关键字意味着这些变量将被创建一次,即第一次调用函数时,并且只要程序在运行,这些变量将一直存在。通常,每次调用函数时,都会重新创建函数中的变量。但是我们希望seed不时被记住,这样我们就可以根据之前发生的事情,为每个呼叫得到不同的答案。我们还将使constexpr s static不需要在每次调用函数时重新初始化。
假设 seed 从,哦,76 开始。加上那个INCREMENT;乘以MULTIPLIER;然后除以MODULUS,取余数。新的价值是什么?你不能在脑子里这么做吗?
其他人也不能。如果你一次又一次地调用这个函数,你会得到一个不经过计算就无法预测的大数序列。看起来是随机的;不是:21306 20152 10309 31100…
通常我们不想要这么大的数字。没问题。我们可以很容易地将它们降低到一个可管理的范围,就像这样:
int numberLessThanTen = rand()% 10;
这给了我们一个 0-9 之间的数,因为除以 10 后的最大余数是 9。
int oneThroughTen = rand()%10 + 1;
现在我们有 1–10:7 3 10 1 6…
就是这么做的。
我们还需要一样东西。我们总是想从同一个数字 76 开始吗?如果我们这样做,我们将得到一个明显随机的数字序列…但它将永远是相同的序列!如果我们在做一个纸牌游戏,我们选择的那些牌将总是相同的,顺序相同的。
你可能见过向要种子的游戏。如果其中一个选项是“选择游戏”,你给它一个数字,你所做的就是初始化随机数生成器的种子。示例 8-1 修改了代码以支持这一点。
unsigned long int seed; // Current random number seed
void srand (unsigned int what) // Start the random number generator
{
seed = what;
}
int rand () // Return a pseudo-random integer
{
static constexpr unsigned int INCREMENT = 51138;
static constexpr unsigned int MULTIPLIER= 21503;
static constexpr unsigned int MODULUS = 32767;
seed += INCREMENT;
seed *= MULTIPLIER;
seed %= MODULUS;
return seed;
}
Example 8-1A complete random number generator
你叫srand(“s”代表“种子”?为了“开始”?任一个为我工作)开始你的序列。对rand的后续调用获得序列中的下一个数字,然后是下一个,依此类推。
现在,这有一个严重的缺点:变量seed被声明在任何函数之外。这意味着整个程序中的任何函数都可能把它弄糟。我们尽可能避免使用全局变量——但是在这种不寻常的情况下,没有其他好的方法。srand和rand都需要访问。
我现在肯定在淘气名单上了。抱歉,圣诞老人。
使用内置的随机数生成器
好消息:除了我们,其他程序员都想要伪随机数!所以示例 8-1 中显示的函数是你的编译器自带的。以下是如何使用它们:
#include <cstdlib> // for srand, rand
int main (int argc, char** argv)
{
srand (someNumber); // start random number generator
int num = rand()%10 + 1; // pick a random # 1..10
...
cstdlib代表“C 标准库”cstdlib赋予我们rand、srand等函数。
我对那件事不太满意。它是从哪里来的?我们总是可以让用户通过输入种子来选择游戏,但是这需要用户做更多的工作。
更好:从电脑上获取号码。但是我们怎么能确定它每次都给我们不同的答案呢?
咨询时钟。
每次重启程序,时间都不一样。如果我们能给srand一个基于时间的数字,我们每次都会得到不同的序列。
以下是如何:
#include <cstdlib> // for srand, rand
#include <ctime> // for time
int main (int argc, char** argv)
{
srand
((unsigned int) (time (nullptr)));
...
ctime包含一个函数time,该函数返回自格林威治标准时间 1970 年 1 月 1 日午夜以来的秒数。我们不在乎起点,但我们在乎答案每一秒都不一样——所以我们会得到不同的游戏。
它以一个time_t的形式返回时间,这是某种类型的int。srand要unsigned int;我们转换它,这样编译器就不会给我们一个警告。
不用担心nullptr是什么意思;我们稍后会谈到这一点。
的黄金法则srand
**称之为一次,从而srand (time (nullptr));。
如果你多次调用它,你将多次重置“随机”数字序列——你得到的前几个“随机”数字(直到第二个改变)将是相同的。我试过这个(见例 8-2 )得到这个结果:
for (int i = 0; i < 100; ++i) // Print a bunch of random #'s
{
srand ((unsigned int) (time(nullptr))); // WRONG!
sout << rand() % 10 << ' ';
}
Example 8-2Repeating srand and what it gets you
8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8...
调用srand一次;这就是你所需要的。
作为正确做事的一个例子,让我们尝试一个程序,它像在双骰子游戏中一样掷出几个骰子,并告诉你怎么做。在第一次尝试中,如果你得到 2、3 或 12,你就输了。7 或 11 胜。任何其他数字都是您进行更多下注的“点数”。
算法:
main:
start things up with srand
roll 2 dice
print what you rolled
print what happens to your bet
wait for user to hit a key
我们如何掷骰子?一个合理的问题。
roll die:
pick a random number 1 to 6.
我该怎么做?如前所述,除以范围,取余数,然后加上 1:
roll die:
return rand () % 6 + 1
程序在示例 8-3 中,输出在图 8-1 中。
图 8-1
骰子程序的可能输出,示例 8-3
// One step in a game of craps
// -- from _C++20 for Lazy Programmers_
#include <ctime> // for time function
#include <cstdlib> // for srand, rand
#include "SSDL.h"
constexpr int SIDES_PER_DIE = 6;1
int rollDie (); // roll a 6-sided die
int main (int argc, char** argv)
{
srand ((unsigned int) (time (nullptr)));
// This starts the random # generator
// It gets called once per program
SSDL_SetWindowTitle ("Craps roll");
sout << "Ready to roll? Hit a key to continue.\n";
SSDL_WaitKey ();
int roll1 = rollDie (), roll2 = rollDie ();
sout << "You rolled a " << roll1 << " and a " << roll2;
switch (roll1 + roll2)
{
case 2:
case 3:
case 12: sout << " -- craps. You lose the pass line bet.\n";
break;
case 7:
case 11: sout << " -- natural. You win the pass line bet.\n";
break;
default: sout <<", so " << roll1 + roll2 << " is your point.\n";
}
sout << "Hit a key to end.\n";
SSDL_WaitKey();
return 0;
}
int rollDie ()
{
int result = rand() % SIDES_PER_DIE + 1;
return result;
}
Example 8-3A program to do a craps roll, illustrating srand and rand
防错法
调试时,你可能每次都要相同序列的伪随机数;如果事情出错了,你希望每次都以同样的方式出错,这样你就可以解决它。为了实现这一点,用srand (someInteger)替换srand (time (nullptr)),直到它被调试完毕。
现在,有些事情可能会出错:
-
你一遍又一遍地得到同一个随机数。见
srand的黄金法则。Exercises For these exercises, please use the “How to write a function” steps from Chapter 7.
-
On paper, write what you think a sequence of 20 coin flips might be. Then write a program that flips coins and tells the user what the results are. Output might look like this:
How many coins do you want to flip? 20 Here are the results: HTTHTHHHTTHHTTTTTHHT That's 9 heads and 11 tails.序列看起来像你期望的那样吗?
-
掷出 6 之前,你需要掷出多少次六面骰子?编写一个算法,然后编写并测试一个程序,直到它滚动并报告它需要的次数。
现在做一千次,报告平均值。
-
用一个骰子与电脑或另一个玩家对弈,直到有人出局。你可以在维基百科或其他地方找到这些规则。
-
欢迎来到价格合适,快下来吧!我们这次的任务是找到经典的“蒙蒂霍尔”问题的正确答案。
你可以在 1 号门、2 号门和 3 号门之间进行选择。一辆后面是保时捷和一个南太平洋岛屿的清晰所有权;在另外两个后面是一周前的比萨饼、一只山羊和盒装通心粉和奶酪。虽然拒绝芝士通心粉很难,但你的心已经放在了保时捷和小岛上。
你选一扇门。然后主人打开另一扇门,一扇后面没有奖品的门。(如果你选错了,他会打开另一扇没有奖品的门。如果你选对了,他会随机选择一扇没有奖品的门。)他给你提供了一个交换的机会。
你应该吗?
写一个程序来模拟整个过程,并做很多次。(什么是合理的大数?你决定。)确保做好每一部分,一直到识别玩家切换到的门,并将其与后面有奖品的门进行比较。(简化问题可能是正确的,但简化可能会让你犯错。此外,找到一种识别被切换到的门的方法也很有趣。)
如果你换了,你赢的几率是多少?如果是 50%或者接近 50%,那一定没关系。
这重要吗?
Extra
蒙蒂霍尔问题是一个经典的概率问题,曾因《游行》杂志“问玛丽莲”专栏的高智商作者玛丽莲·沃斯·莎凡特而流行开来。她给出了正确的答案…然后随着人们不断地写进来,写了更多的专栏。为了找到答案,一个小学班级尝试运行这个场景几次(大概是在没有电脑的情况下)。几个数学教授,给出了他们的名字和所属单位,写信要求她放弃,并评论道“你搞砸了!”和“你是山羊!”多尴尬啊——对他们来说。
-
布尔函数
我们以前有函数返回 true 或 false 值:isupper,例如,在第五章。示例 8-4 展示了我会如何写它。
bool isupper (char ch) // returns whether ch is an upper-case letter
{
bool result;
if (ch >= 'A' && ch <= 'Z')
result = true;
else
result = false;
return result;
}
Example 8-4My own isupper, a Boolean function
所以如果ch在大写范围内,它返回true;否则返回false。另一方面,在下一个版本中…如果ch在大写范围内,则返回true;否则false。他们做着完全相同的事情:
bool isupper (char ch) // returns whether ch is an upper-case letter
{
return ch >= 'A' && ch <= 'Z';
}
我喜欢短的那个。挑一个你觉得更清晰的。
Exercises
-
写一个函数
inRange,给定一个数字和一个上下界,告诉我们这个数字是否在上下界之间。通常情况下,它不会打印任何内容,但会返回答案。main可以做打印。 -
写一个算法,然后写一个函数,这个函数在屏幕上显示“是”和“否”的方框;另一个等待鼠标单击,如果单击了“是”框,则返回 true,如果没有单击,则返回 false,如果单击在两个框之外,则继续等待。然后编写一个程序演示这些函数的用法。
-
将前面练习中的函数添加到第五章“
chars 和cctype”部分练习 1 中的魔法生物分类器程序中,这样用户可以点击而不是键入他/她的回答。
&参数
如果您希望函数提供多个值,该怎么办?改变一个变量。一个函数只能返回一个东西。 2
打个糖果制造机的比方,你只能吐出一种产品。我们需要一种不同的机器:一种能接收一种或多种糖果并对其进行改变(烘烤、冷冻等等)的机器。
我想要的函数是一个可以交换值的函数:swap (x, y)应该使x成为y的样子,y成为x的样子。这是我的第一次尝试:
void swap (int arg1, int arg2)
{
arg1 = arg2; arg2 = arg1;
}
追踪这个。图 8-2 显示了我们在这个过程中这些变量的状态。假设这些值最初是 5 和 10。
图 8-2
swap发生了什么:第一次尝试
变量就像一个盒子,里面有一个值,但只有一个值。如果你想交换你手中的东西,你会怎么做?你会找到一个地方来放置其中一个物体——一个临时存放区。如果计算机要交换,它也需要第三个位置:一个临时变量。这应该可行(参见图 8-3 ):
void swap (int arg1, int arg2)
{
int temp = arg1; arg1 = arg2; arg2 = temp;
}
图 8-3
第二次尝试
现在让我们看看当我们调用它时会发生什么:
int main (int argc, char** argv)
{
int x=5, y=10;
swap (x, y);
...
}
我们从main(图 8-4 )开始。
图 8-4
main在调用swap之前
然后我们叫swap。编译器创建了一个swap函数的实例——它包含的变量和它需要知道的任何其他东西,将来自main的变量复制到参数中(图 8-5 )。
图 8-5
swap开始
我们经历与之前相同的过程,成功地交换了arg1和arg2(图 8-6 )。
图 8-6
swap完成
现在我们已经完成了swap,所以它可以消失了(图 8-7 )。
图 8-7
swap消失了
我想那很有趣,但是……我们不是应该在main中改变x和y吗?
相反,我们改变了swap的局部变量arg1和arg2。当 swap 离开时,他们也离开了。
解决的办法是把 & 放在参数中的类型后面。这使得arg1和arg2不是传递进来的副本,而是临时别名:arg1 就是 x,只要我们在调用swap。一个表示与号,一个表示别名。
void swap (int& arg1, int& arg2)
{
int temp = arg1; arg1 = arg2; arg2 = temp;
}
这里是另一个演练,从图 8-8 开始。
图 8-8
使用&参数调用swap
既然arg1 是x``arg2是 y,那么我们对arg1和arg2所做的,就是真正对x和y所做的。所以x和y真的变了(图 8-9 )。
图 8-9
swap现在居然互换了!
函数完成并消失(图 8-10 ),交换x和y。
图 8-10
swap完整(且正确)
一般来说,什么时候应该用&从函数中获取一个值,什么时候使用return语句?现在,如果你正好有一个值要返回,使用return。如果你有多个值,你需要一个带&的参数表。
Golden Rule of Function Parameters and
return (第一版)
如果函数没有向调用函数提供任何信息,那么它的返回类型是void。
如果它提供了一条信息,那么它的返回类型就是这条信息的类型。
如果它提供了多个片段,那么它的返回类型是void,这些片段是通过使用&的参数列表提供的。
防错法
- **该函数似乎改变了它的参数;但是当你离开这个函数时,它们是不变的。**这是因为忘记了
&——一个常见且令人抓狂的错误。
Exercises
-
编写算法,编写一个函数,在屏幕上生成一个随机位置,并提供给调用它的函数。然后用它在随机位置用星星(点)填充窗口。运行几次,以确保您不会总是得到相同的模式。
-
写一个函数使颜色变深。方法如下:将红色、绿色和蓝色各切一半。这意味着你必须对红色、绿色和蓝色的
int值进行处理,而不是对SSDL_CreateColor提供的SSDL_Color进行处理。然后使用此函数制作一系列逐渐变暗的点。询问用户初始值。 -
写一个函数解二次公式。解决方案是
和
(平方根前的符号是差)。这相当于两个解,或者一个解(如果两个解相同),或者零个解(如果我们取 b24ac 的平方根的东西是负的)。所以给主程序提供解和一个参数,表示有多少个解。(如果有 0,则解决方案的内容无关紧要。)
标识符范围
标识符的范围(变量名、函数名或一些其他定义的名称)是它有意义的区域。
考虑一下swap的例子。在图中,我们看到x和y在main里面(因为它们是);我们看到arg1、arg2和temp在swap里面(因为他们是)。函数内部的变量不能被其他函数看到或干扰。这意味着外部代码不能搞乱它们。这就是模块化:将不同的东西分开,主要是为了安全。
为了进一步了解作用域,再次考虑例子 8-3 :一个掷骰子的程序。它有两个函数,main和rollDie。这里是一个回顾:
// One step in a game of craps
// -- from _C++20 for Lazy Programmers_
...
constexpr int SIDES_PER_DIE = 6;
int rollDie (); // roll a 6-sided die
int main (int argc, char** argv)
{
...
int roll1 = rollDie (), roll2 = rollDie ();
sout << "You rolled a " << roll1 << " and a " << roll2;
...
return 0;
}
int rollDie ()
{
int result = rand() % SIDES_PER_DIE + 1;
return result;
}
定义可以进成对的花括号,但是不能出来(见图 8-11 )。这就像一个鸭百叶窗:如果你在百叶窗里,你可以看到外面的东西,但他们看不到你。每个人都可以看到SIDES_PER_DIE,因为它在一切之外,定义总是可以进去;rollDie可以用,main也可以。除了它的主人rollDie,没有人能看到result,因为它不能离开声明它的{}。同样,除了main外,没有人能看到roll1和roll2。
图 8-11
标识符范围。在{}之外的定义可以在它们内部看到;里面的定义外面看不到。试图引用它们会得到类似“未找到标识符”或“未声明”的结果
这是有道理的。SIDES_PER_DIE是每个人的事。roll1和roll2是main的事,不关别人的事。result在rollDie建设期间,是rollDie的事(虽然结束后会向main汇报)。
那么函数是如何共享信息的呢?通过参数表和return语句。
Golden Rule of Identifier Scope
在声明标识符的{}之外看不到标识符。
关于算法的最后一点说明
通过我们的练习,我们继续为我们在第六章中建议的任何事情编写算法。我不会一直把提醒放进去,但我现在会做一个总括声明:最好养成习惯。跳过这一步感觉很懒,但总体来说工作量更大,所以懒的地方在于:解决算法编写这一步怎么做的问题。那么编码就可以相对容易。
Footnotes 1全局变量,没有;全局常量,是的,因为没有什么可以把它们弄乱。我们通常把它们放在靠近顶部容易找到的地方。
2
直到第二十章。
**
九、使用调试器
调试器可以让你一行一行或者一个函数一个函数地单步调试程序,查看变量值,这样你就可以判断出哪里出了问题。好主意,对吧?
我想是的。为了涵盖有用的调试器命令,让我们使用调试器来修复示例 9-1 中有缺陷的程序。它打算画一面美国国旗:一种时髦的手工制作的版本,有空心的星星,如图所示。(为了做得更好,我们将使用文件中的图像——但是现在我想调试一个写星函数。)旗帜的设计如图 9-1 所示。
图 9-1
设计美国国旗
// Program to draw Old Glory on the screen
// -- from _C++20 for Lazy Programmers_
#include <cmath> // for sin, cos
#include "SSDL.h"
constexpr double PI = 3.14159;
// Dimensions1
constexpr int HOIST = 400; // My pick for flag width
// Called "A" in Fig. 9-1
constexpr int FLY = int (HOIST * 1.9), // B
UNION_HOIST = int (HOIST * 0.5385), // C
UNION_FLY = int (HOIST * 0.76); // D
constexpr int UNION_VERTICAL_MARGIN = int (HOIST * 0.054), // E & F
UNION_HORIZONTAL_MARGIN = int (HOIST * 0.063); // G & H
constexpr int STAR_DIAMETER = int (HOIST * 0.0616); // K
constexpr int STRIPE_WIDTH = HOIST/13; // L
Example 9-1A buggy program to draw the US flag. Output is in Figure 9-2
// Colors2
const SSDL_Color RED_FOR_US_FLAG = SSDL_CreateColor (179, 25, 66);
const SSDL_Color BLUE_FOR_US_FLAG = SSDL_CreateColor ( 10, 49, 97);
void drawStripes (); // the white and red stripes
void drawUnion (); // the blue square
void drawStar (int x, int y); // draw a star centered at x, y
// draw a row of howMany stars, starting with the x, y position,
// using UNION_HORIZONTAL_MARGIN to go to the right as you draw
void drawRowOfStars (int howMany, int x, int y);
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Old Glory");
SSDL_SetWindowSize (FLY, HOIST);
drawStripes ();
drawUnion (); // draw the union (blue square)
SSDL_WaitKey();
return 0;
}
void drawStripes ()
{
SSDL_SetRenderDrawColor (RED_FOR_US_FLAG);
SSDL_RenderFillRect (0, 0, FLY, HOIST); // first, a big red square
// Starting with stripe 1, draw every other stripe WHITE
SSDL_SetRenderDrawColor (WHITE);
for (int stripe = 1; stripe < 13; stripe += 2)
SSDL_RenderFillRect (0, stripe*STRIPE_WIDTH,
FLY, STRIPE_WIDTH);
}
void drawRowOfStars (int howMany, int x, int y)
// draw a row of howMany stars, starting with the x, y position,
// using UNION_HORIZONTAL_MARGIN to go to the right as you draw
{
for (int i = 0; i < howMany; ++i)
{
drawStar (x, y); x += 2*UNION_HORIZONTAL_MARGIN;
}
}
void drawUnion ()
{
// draw the blue box
SSDL_SetRenderDrawColor (BLUE_FOR_US_FLAG);
SSDL_RenderFillRect (0, 0, UNION_FLY, UNION_HOIST);
SSDL_SetRenderDrawColor (WHITE);
int row = 1; // What's the current row of stars?
for (int i = 0; i < 4; ++i) // Need 4 pairs of 6- and 5-star rows
{
drawRowOfStars (6, UNION_HORIZONTAL_MARGIN,
row*UNION_VERTICAL_MARGIN);
++row;
// Every 2nd row is staggered right slightly
drawRowOfStars (5, 2*UNION_HORIZONTAL_MARGIN,
row*UNION_VERTICAL_MARGIN);
++row;
}
// ...and one final 6-star row
drawRowOfStars (6, UNION_HORIZONTAL_MARGIN,
row*UNION_VERTICAL_MARGIN);
}
void drawStar (int centerX, int centerY)
{
constexpr int RADIUS = STAR_DIAMETER/2,
POINTS_ON_STAR = 5;
int x1, y1, x2, y2;
double angle = PI/2; // 90 degrees: straight up vertically
// 90 degrees is PI/2 radians
x1 = int (RADIUS * cos (angle)); // Find x, y point at this angle
y1 = int (RADIUS * sin (angle)); // relative to center
for (int i = 0; i < POINTS_ON_STAR; ++i)
{
angle += (2 * PI / 360) / POINTS_ON_STAR;
// go to next point on star
x2 = int (RADIUS * cos (angle));// Calculate its x,y point
y2 = int (RADIUS * sin (angle));// relative to center
SSDL_RenderDrawLine (centerX+x1, centerY+y1,
centerX+x2, centerY+y2);
x1 = x2; // Remember the new point
y1 = y2; // for the next line
}
}
图 9-2 向我们展示了:本来可以做得更好。星星是几乎看不见的点。甚至条纹也脱落了:注意蓝色的联合广场与中间的红色条纹不一致,底部的条纹太大。
图 9-2
有问题的美国国旗
您将使用什么调试器?如果你用的是微软的 Visual Studio,它是内置的。对于 Unix,我推荐使用ddd,一个友好的、免费的图形界面给gdb调试器。MinGW 在这一点上不支持ddd,所以我推荐gdb本身:基于文本但标准且免费。
当你阅读接下来的章节时……如果你自己做,最容易记住事情,所以我强烈建议你从本书的示例代码(ch9/1-flag)中加载这个程序,然后跟着做,做和书中一样的事情。
断点和监视的变量
让我们从检查尺寸开始,看看为什么条纹不对齐。什么维度?STRIPE_WIDTH似乎相关!蓝色方块的高度UNION_HOIST和整个物体的高度HOIST也是如此。
ddd
要在 Unix 中用ddd调试程序a.out,请转到它的文件夹并键入./dddx。如果没有dddx,从你一直使用的basicSSDLProject文件夹中复制。
高亮显示main。(如果您没有看到任何代码,请查看反欺诈部分。)在控件的顶行,找到标有“Break”的停止标志图标;点击那个。一个停止标志出现在这条线上,意味着程序运行到这里就停止了。您应该会看到类似图 9-3 的内容。
在带有(gdb)提示符的底部窗口中,应该会出现命令break main。ddd是一个训练轮子界面,总是告诉你刚刚选择了什么gdb命令。这样你可以边走边学gdb。
图 9-3
gdb调试器的ddd接口
要运行,单击右侧菜单上的 run 或在(gdb)提示符下键入run。print STRIPE_WIDTH等等,得到STRIPE_WIDTH、HOIST和UNION_HOIST的值。
单击断点可能会将其删除。如果没有,delete <breakpoint number>。断点编号见gdb窗口。要退出,输入quit。
gdb
转到程序的文件夹,键入./gdbx (Unix)或bash gdbw (MinGW)。
为了让程序停在第一行,我输入break main (Unix)或break SDL_main (MinGW)。当我运行这个程序时,它会在这里中断,我可以检查这些值。
要启动程序,请键入run。要查看这些值,请键入print : print STRIPE_WIDTH、print HOIST和print UNION_HOIST。
要结束gdb,请键入quit。
可视化工作室
确保在调试模式下编译。您应该在菜单栏下方的顶部附近看到 Debug,而不是 Release(参见图 9-4 )。否则,调试器命令将不起作用。
点击main左侧的灰白色条;一种红色停车标志出现,如图 9-4 所示。(好吧,是红点。但是“停车标志”更容易记住。)
图 9-4
在 Microsoft Visual Studio 中设置断点
像往常一样启动程序。
Visual Studio 不喜欢我的断点在哪里,所以它把它向下移动了一行(图 9-5 )。没问题。黄色箭头表示“这一行是下一个。”它即将开始main,所以它已经完成了初始常量声明。让我们看看它制造了什么。
图 9-5
在 Microsoft Visual Studio 中启动调试器会话
在 Visual Studio 窗口的左下角,您可能会看到一个带有汽车、局部变量等选项卡的窗口。(如果没有,请尝试使用菜单栏上的窗口➤重置窗口布局。)
汽车是 Visual Studio 认为您可能想看到的东西。这次错了。我不担心argv和argc。
局部变量是局部变量;我们没有。
Watch 1 是一个我们可以观察变量值的地方。单击该选项卡。你现在可以点击“名称”并给出你想看的东西的名称。试试STRIPE_WIDTH,然后是UNION_HOIST和HOIST。
这些是我们需要的数字。如果您愿意,可以再次单击断点将其删除,我们将继续。
修复条纹
现在我们有了数字;让我们来理解它们。
一个条纹应该是HOIST的十三分之一,也就是 400。400/13 是 30.76 几;作为一名 ??,他只有 30 岁。那个UNION_HOIST应该有七条条纹覆盖;条纹覆盖 7 * 30 = 210 像素,但是UNION_HOIST是HOIST * 7/13 = 215。
问题是我们在做整数除法,丢失了小数位。
让我们不要 400,而是能被 13 整除的数。STRIPE_WIDTH为 30;13 * 30 = 390.我们将相应地改变HOIST的初始化
constexpr int HOIST = 390; // My pick for flag width
再跑一次。条纹问题修复!
进入函数
明星问题需要进一步挖掘。因此,在main处恢复断点,并再次启动调试器。
ddd
在右边的“DDD”菜单中,下一步将带您进入下一行,并单步执行一个函数。左边的箭头显示了将要执行的行。使用下一步转到drawUnion。到了那里,进入那个函数。
使用 Next 和 Step,进入drawRowOfStars,然后进入drawStar,直到到达 for 循环。
此时,找出变量是什么是有意义的。在数据菜单下,选择显示局部变量。您可能需要使数据区域可见:查看➤查看数据窗口。图 9-6 显示了结果。
图 9-6
在ddd中显示局部变量
看起来没有明显的问题。我们接着来看SSDL_RenderDrawLine。难道angle不应该改变更多吗?print (2 * PI/360)/POINTS_ON_STAR在gdb提示符下,看看你会得到什么。
gdb
要在程序中更进一步,您可以键入next(或n)转到下一行,键入step(或s)进入一个函数。(回车重复最后一个命令。)随着你的进展,它会打印当前行,这样你就知道你在哪里了。使用这些命令进入drawUnion,然后通过drawStar,直到到达 for 循环。
你可能想放一个断点,以防你需要回到这一行。break将在当前行放置一个。break drawStar将在函数开始时中断。只是为了咧嘴笑,现在试试,然后输入run。然后continue,或cont或c,返回断点。
delete <number of breakpoint>删除断点;delete全部删除。
要打印本地变量,输入info locals。
看起来没有明显的问题。我们接着来看SSDL_RenderDrawLine。难道angle不应该改变更多吗?print (2 * PI/360)/POINTS_ON_STAR看看你得到了什么。
可视化工作室
查看“调试”菜单,您可以通过选择“单步调试”或在某些键盘上按下 F10-功能键-F10 来单步调试(执行)一行。当您这样做时,黄色箭头向下移动一行,执行该行。
当你下降到drawUnion时,你想让进入(F11/函数 F-11)那个函数。
使用 F10 和 F11,进入drawRowOfStars,然后进入drawStar,直到到达 for 循环,如图 9-7 。右下方的调用栈显示了您所在的函数(上面一行)以及您是如何到达那里的(下面一行)。
图 9-7
Microsoft Visual Studio 中的局部变量窗口
如果没有看到“局部变量”窗口,请单击“局部变量”选项卡。
看起来没有明显的问题。我们接着来看SSDL_RenderDrawLine。角度不应该改变更多吗?在 Watch 1 窗口中(见图 9-8 ,输入或粘贴(2 * PI/360)/POINTS_ON_STAR。调试器将为您计算它。
图 9-8
Microsoft Visual Studio 中的 Watch 1 窗口
修理星星
本该带我们去星球上的下一个地方。圆的五分之一不是大于 0.00349 吗?应该是一个圆除以 5。那是 360 /5,或者用弧度表示,2π/5,但公式不是这么说的。看起来我把角度和弧度混淆了。这就是我们的问题,所以这里是我们的解决方案:
angle += (2 * PI)/POINTS_ON_STAR; // go to next point on star
当我们重新编译并运行时,我们得到一个标有五角星的旗帜。至少他们有五个面!
该程序告诉计算机绕圆走五步,每次画一条线,覆盖五分之一的距离。这不就是五边形,而不是星星的作用吗?
要画一颗星,不要画到圆的五分之一处。走五分之二的路。让我们试试:
angle += 2*(2 * PI)/POINTS_ON_STAR; // go to next point on star,
// 2/5 way around circle
现在星星在那里,但是颠倒了。我认为 90 度是垂直向上的,但是对于 SDL,我们有越来越大的 Y 向下的方向。这就是问题所在吗?我试着从-90 度开始,看看会发生什么:
float angle = -PI/2; // -90 degrees -- straight up vertically
结果如图 9-9 所示。好多了。
图 9-9
有真正星星的旗子
总结
有关常见调试器命令的摘要,请参见附录 g。
防错法
-
(ddd/gdb)没有文件。也许你忘了
make吧!或者你是为 Unix 做的,但正在用 MinGW 调试,或者相反。 -
**(MinGW)上面说没有行号信息,所以不能说“下一个”**确保你一开始说的是
break SDL_main**,**而不是break main。 -
它只是呆在那里,没有任何提示 **。**它可能正在等待输入。点击程序的窗口,给它所需要的。
-
你正在看一些不是你写的文件。是编译器的代码或者库的。
Visual Studio:跳出(Shift-F11)您所在的函数,返回到您自己的代码。或者在代码中设置断点并继续(F5)。
ddd/gdb:up带你上“调用栈”(被调用函数列表,从当前一个到main);这样做足够多,看看你在你的代码的哪一部分。然后在你喜欢的地方设置一个断点,和continue。
Extra
GNU(“guh-NOO”)自由软件基金会( www.gnu.org )成立于 1984 年,为 Unix 操作系统提供自由软件。从那以后,它的使命扩大了,当人们想要自由分享他们的作品时,他们可以使用 GNU 公共许可证作为许可协议。
这就是我们如何不仅得到ddd和gdb而且得到g++、GIMP 和其他酷的东西。
GNU 经常给事物起一些搞笑的名字,GNU 本身也不例外。GNU 是首字母缩略词;它代表“GNU 不是 Unix”
自下而上测试
我们在第七章中看到了自顶向下的设计:从main开始,然后编写它调用的函数,然后是它们调用的函数*,直到完成。*
自底向上的测试是一个自然的推论。有时一次测试整个程序太难了。假设你正在用一个有许多函数的程序进行经济预测,如图 9-10 所示,其中main调用getRevenues、getBorrowing和getSpending,这些函数调用其他函数,一直到wildGuess、crossFingers等等。
图 9-10
一堆复杂的东西需要调试。main调用了三个函数getRevenues、getBorrowing和getSpending,它们还调用了许多其他函数
你运行这个程序,它会告诉你
The nation will be bankrupt in -2 years.
那不可能是对的。但是哪个功能有问题呢?main?consumerPriceIndex?wildGuess?你不能从一个空的“-2!”
你需要知道你可以信任每一个函数。所以你拿最下面的(wildGuess和crossFingers),不叫别人但是被叫的,测试一下,直到你有信心为止。
然后测试调用它们的和调用它们的…一直到main。
更多关于反欺诈的信息
这里有更多关于获得工作程序的提示。
-
做好充足的备份 。如果这是一个大的或者困难的项目,留下一条线索,这样如果有什么事情搞砸了,你可以回溯。
-
保持函数模块化(无全局变量)。
-
**显示您需要的信息。**在前面的例子中,直到某个事件发生的年份的答案-2 显然是错误的,但在其他方面没有提供信息。
The biggest problem in testing often is you just don’t know what values are in your variables. Here are two common fixes:
-
使用调试器。
-
使用大量的打印语句。如果一个变量有一个值,那么就在调试的时候,打印它——清楚地标记:not
sout << growthRate; // so 0.9 gets printed.// What does it mean?但是
sout << "growthRate is " << growthRate << ".\n";那就更长了,但总比努力记住数字的含义要好。盲目地去修复一个你无法识别的错误是一件很麻烦的事情。最好看到问题,这样你就可以解决它。
-
-
不要让它夺走你的 进取心 。在《禅宗与摩托车保养艺术》一书中,作者罗伯特·皮尔西格警告说,有些事情会消耗你的“进取心”,即你解决甚至专注于自行车问题的能力。或者你的论文。或者别的什么。
在编程中,你也会失去进取心。你刚刚发现了一个你以为已经解决的 bug,而这个程序在你修复它之前毫无用处,所以现在你太沮丧了,除了闷闷不乐什么也做不了。我去过那里。最近。
有一次,我花了两个工作日的时间去寻找被证明是放错了位置的括号。那就是 it ?小括号?它们很小,但却是个大问题,因为在它们修好之前我无法继续。
我只是说了声“咻!”然后进入下一个问题。
如果你能在出现错误时暂停自我评估,回到问题上来,你就成功了。
-
如果你犯了愚蠢的错误…参见上面的“进取心”。愚蠢的错误是最好的类型,因为它们很容易修复。难的是微妙的。每个人都会犯愚蠢的错误。
-
如果你不擅长这个 …每个人一开始都会有这种感觉。我说了。你不会期望在几周的学习后就能流利地掌握一门新语言,C++ 比任何口头语言都酷。
-
快点做。想把事情做对,大概要先把事情做错。所以尽管做错吧。修复一个坏掉的程序比盯着屏幕直到有所启发要快得多。
Exercises
还没有练习,只是确保在后续章节中使用您选择的调试器!
Footnotes 1尺寸由美国政府提供,从 4 USC 1 开始,可在 uscode.house.gov 获得。
2
来自美国国务院的颜色:ECA . State . gov/files/bureau/State _ Department _ u . s _ flag _ style _ guide . pdf