面向懒惰程序员的-C--20-教程-全-
面向懒惰程序员的 C++20 教程(全)
一、入门指南
本书前半部分的大多数程序使用了 SDL 和 SSDL 的图形和游戏库, 1 理论上,观看彩色图形在屏幕上移动并互相射击比打印文本更有趣。别担心。当你完成后,你将能够用这个库或不用这个库来编写程序——如果我对此有什么要说的,你会从中得到乐趣。
如果你已经选择了你的平台,很好。如果没有,我的建议是:
如果你只是想在一个简单易管理的平台上学习 C++,微软 Visual Studio 很棒。
如果您是一名 Unix 系统管理员,或者有很好的访问权限,并且希望使用这个流行且功能强大的平台,那么就去使用它吧。
要学习 g++ 并在 Windows 中用相对简单的设置制作来自 Unix 世界的强大工具,请使用 MinGW。
不同平台之间的编程不会有太大差异。但是系统设置可能是一个问题。
初始设置
首先,你需要教材的源代码。您可以通过位于 www.apress.com/9781484263051
的下载源代码按钮访问代码。
然后拉开拉链。在 Unix 中,unzip
命令应该可以工作;在 Windows 中,您通常可以双击它或右键单击并选择“提取”或“全部提取”。
…在 Unix 中
在 Unix 中漫游不在本书的讨论范围之内,但是不用担心。复制文件、移动文件等基础知识很容易掌握。 2
Unix 系统管理的方式超出了本书的范围。 3 但是安装 SSDL 很容易。在你刚刚解压的文件夹里
进入
external/SSDL/unix
,输入make
。这将 SSDL 构建在一个源代码中的程序知道在哪里可以找到它的地方。进入
ch1/test-setup
。cp Makefile.unix Makefile
make
./runx
你应该看到(听到)图 1-1 中所示的程序。(如果没有,一定是遗漏了什么——请参见附录 a。)您可能需要一点时间来尝试来自ch1
的另一个程序,比如1-hello
。像你在test-setup
一样经营它。
图 1-1
test-
的输出setup
…在 MinGW 中
你可以在 sourceforge.net 和其他地方找到 MinGW。试试在网上搜索“MinGW 下载”
一旦安装完成,让它添加 C++ 的基础知识;启动 MinGW 安装管理器(mingw-get.exe
),至少安装mingw32-gcc-g++-bin
、mingw32-gdb-bin
和msys-make-bin
。
你不会需要安装 SDL 或者 SSDL;它们在你解压的源代码里。
所以让我们试试吧。打开 Windows 命令提示符(单击开始菜单并键入cmd
)并转到源代码的ch1/test-setup
文件夹。这里有一个简单的方法:在该文件夹的窗口中,单击地址栏左边的文件夹图标,该部分显示类似于... > ch1 > test-setup
的内容。它将被一个高亮显示的路径代替,如图 1-2 所示。按 Ctrl-C 复制它。
图 1-2
在 Windows 中获取用于命令提示符的路径
在命令窗口中,输入您复制的路径的前两个字符(在我的例子中是C:
);然后输入cd
,粘贴路径(Ctrl-V),再按回车键(见图 1-3 )。
图 1-3
在命令提示符下找到正确的文件夹
然后
copy Makefile.mingW Makefile
make
bash runw
你应该看到(听到)图 1-1 中所示的程序。(如果没有,请参见附录 a。)您可能需要一点时间来尝试来自ch1
的另一个程序,比如1-hello
。像你在test-setup
一样经营它。
…在 Microsoft Visual Studio 中
目前,Visual Studio 完全免费。进入微软的下载页面(目前是visual studio . Microsoft . com/downloads/)下载社区版。
安装要花很长时间。确保用 C++ 进行桌面开发(图 1-4 ,右上角)——否则,你会有 Visual Studio,没错,但它不知道 C++。
图 1-4
安装 Visual Studio 的 C++ 部分
安装完成后,进入该书的源代码文件夹,进入
ch1
子文件夹;双击解决方案文件,ch1.sln
或ch1
。(如果它要求您登录,而您现在还没有准备好,请注意“现在不行,以后再说”这一行。)
现在,在解决方案浏览器窗口中(见图 1-5 ,你应该在底部看到一个名为测试设置的项目。右键单击它,并选择调试➤启动新实例。
图 1-5
Visual Studio 中的ch1
解决方案,突出显示了test-setup
项目
你应该看到和听到图 1-1 中的节目。(如果没有,请参见附录 a。)您可能需要一点时间来尝试来自ch1
的另一个程序,比如1-hello
。像你在test-setup
一样经营它。
简单的程序
从小处着手是明智的。更少的事情会出错。
所以我们将从一个简单的程序开始,它写着“你好,世界!”在屏幕上。我们会一行一行的看里面有什么。(在下一节中,我们将编译并运行它。现在,坐着别动。)
// Hello, world! program, for _C++ for Lazy Programmers_
// Your name goes here
// Then the date4
// It prints "Hello, world!" on the screen.
// Quite an accomplishment, huh?
#include "SSDL.h"
int main (int argc, char** argv)
{
sout << "Hello, world! (Press any key to quit.)\n";
SSDL_WaitKey (); // Wait for user to hit any key
return 0;
}
Example 1-1“Hello, world!” is a classic program to start a new language with. (I think it’s a law somewhere.) This program is in source code, in the ch1 folder, as 1-hello
第一组行是注释。评论看起来是这样的——//Something on a line after two slashes
——它们仅仅是为你或者是为后来试图理解你的程序的人准备的。最好善待你自己和你的维护者——帮助他们容易地知道程序在做什么,而不必去搜索和弄清楚。
接下来,我们有一个include
文件。一些语言特性内置于 C++ 编译器本身,比如注释标记//
和#include
。其他仅在需要时加载。在这种情况下,我们需要知道如何使用 SSDL 库在屏幕上打印东西,所以我们包含了文件SSDL.h
。
接下来,我们有了main
函数。main ()
特殊;它告诉编译器,“这就是我们在程序中要做的;从这里开始。”现在我将推迟解释这个奇怪的顶行——我们将在第二十五章的“命令行参数”一节中讨论它——只是说,现在,我们总是用同样的方式写它。否则,C++ 之神会用无法理解的错误消息来惩罚我们。
在这种情况下,main ()
只做两件事:
首先,它使用sout
对象打印"Hello, world!"
消息,读作“S-out”\
n
的意思是“继续下一行”
第二,它调用SSDL_WaitKey ()
,在它结束程序之前等待你按一个键。否则,程序会在您有机会看到它的消息之前关闭。
我们return 0
是因为main ()
必须return
一些东西,很大程度上是因为历史原因。实际上,我们几乎从不关心main
返回什么。
花括号{}
告诉main ()
从哪里开始采取行动,在哪里结束;无论你想让程序做什么,都在花括号之间。
编译器对你输入的东西非常挑剔。去掉一个;
,程序就不会编译。改变某些东西的大小写,C++ 不会识别它。
如果你想知道没有 SSDL 这样一个简单的程序会是什么样子,请看第二十九章。它不适合初学者,但以后应该会有意义。
Extra
“你好,世界!”通常是初学者用新语言编写的第一个程序。虽然它最初是 C 语言中的一个简单例子——c++ 是这种语言的起源——但是将它作为第一个程序来编写的做法已经传播开来。这里是“你好,世界!”在 BASIC 中:
10 PRINT "Hello, world!"
不错吧。
这是它在 APL 中的样子。APL ( A P 编程 L 语言)被描述为“只写”语言,因为据说你不能阅读自己写的程序。APL 需要符号,如□
、∇和ρ:
□t0□
尽管这些看起来比 C++ 的版本简单,但 C++ 的版本既不是最长的也不是最难的。为了节约资源,我就不赘述了(一个例子是 Redcode 语言有 158 行,这可能是你从未听说过 Redcode 的原因),但这里有一个很难的例子,来自一种有时被称为 BF 的故意困难的语言:
++++++++++++++++[>++++>++++++>+++++++>+++>++<<<<<-]>++++++++.>+++++.+++++++..+++.>>----.>.<<+++++++.<.>-----.<---.--------.>>>+.
更多“你好,世界!”在撰写本文时,可在hello world collection . de/
找到相关示例。
间隔
编译器不关心间距。只要你不在单词里面放空格,你可以把它放在你喜欢的任何地方。你可以选择换行或者不换行;它不会在意,只要你不弄坏一个//comment
或者一个"quotation"
。
// Hello, world! program, for _C++ for Lazy Programmers_
// It prints "Hello, world!" on the screen.
// Quite an accomplishment, huh?
#include "SSDL.h"
int main (int argc, char** argv) {
sout <<
"Hello, world! (Press any key to quit.)\n";
SSDL_WaitKey (); // Wait for user to hit any key
return 0;
}
Example 1-2A blatant instance of evil and rude5 in programming
编译器不会在意间距——但是必须理解你的 500 页程序的可怜灵魂会在意!示例 1-2 的间距对于后来维护你的代码的人来说是一件残酷的事情。
可读性是一件好事。努力理解你的意思的程序员很可能就是写完它几天后的你。软件开发的大部分费用是程序员的时间;你不会想浪费你的时间去破译你自己的代码。说清楚。
Tip
在你写代码的时候,而不是之后*,让你的代码变得清晰。可读代码有助于开发,而不仅仅是未来的维护。*
为了更加清晰,我在示例 1-1 中使用了一些东西,比如初始注释、#include
和main ()
,用空行隔开。这有点像在英语论文中写段落;每一节都是它自己的“段落”空行增加可读性。
我也以一种使程序易于阅读的方式缩进。默认的缩进是左边距。但是如果一个东西包含在另一个东西中——就像主函数中包含的sout
语句——它会缩进几个空格。
这类似于论文的大纲格式或目录的布局(图 1-6 )。包含在其他内容中的内容会稍微缩进。
图 1-6
就像英文论文大纲一样,C++ 程序是缩进的,子部分相对于它们所属的部分缩进
继续读下去,你会看到很多清晰缩进的例子。
Golden Rule of Indenting
当某个东西是之前的一部分时,它应该缩进(像这样)。
当它独立时,它保持相同的缩进级别。
创建 SSDL 项目
…使用 g++ (Unix 或 MinGW)
要创建自己的项目,进入newWork
目录,将basicSSDLProject
复制到一个有适当名称的新目录中——类似于cp -R basicSSDLProject myNewProject
。
然后将Makefile.unix
复制到Makefile
(如果你用的是 Unix)或者将Makefile.mingw
复制到Makefile
(如果你用的是 MinGW)。Makefile
告诉系统如何编译,在哪里找到库,诸如此类的事情。
你还需要打开你的文本编辑器。在 Unix 上,你可能会使用 vi/vim(我觉得这很难,但也许你不会)、emacs、 7 或者其他一些编辑器。在 Windows 上,Notepad++ 是一个不错的选择。根据需要熟悉一下,打开main.cpp
进行编辑。
这个程序是有效的,但是它没有做任何有趣的事情,所以你需要给它一些内容。现在,你可以输入 Hello,world!来自示例 1-1 的程序。要进行编译,请在命令提示符下键入make
。
也许你会犯一些错别字。如果是这样,make
会给你一个错误信息列表。有时信息的意思很清楚,有时又不清楚。这里有一个典型的:我忘了一个;
。
main.cpp:11:53: error: expected ';' before 'SSDL_WaitKey'
随着时间的推移,您会更加理解模糊错误消息的含义。现在,将您键入的程序与示例 1-1 进行比较,并解决任何差异,直到您获得图 1-7 中的成功结果。(该程序实际上是在黑色上打印白色,不像显示的那样。书,大块的黑色墨水,不是一个好的组合。)
图 1-7
你好,世界!运转
你创建的文件
在新文件夹中,根据提示键入ls
或dir
。您可能会看到一些文件:
a.out main.cpp main.cpp~ main.o #and a bunch of other stuff.
a.out
是可执行程序。main.cpp
是你写的让它产生的代码。main.cpp
~
是你的编辑可能对你的.cpp
文件做的备份文件。main.o
是一个“对象”文件,g++ 可以在创建程序的过程中构建它。如果你看到它,你可能看不到,删除它是绝对安全的:
rm
main.o
要删除此处列出的您不需要的东西,请键入make clean
。
很酷的命令行技巧
重复一个命令 : 经常在命令提示符下,你可以按向上箭头重复上一个命令,或者多次重复一个更早的命令。如果这样不起作用,
!
后面跟着命令的前几个字母可能会重复它的最后一个实例。在目录名中使用通配符:
cd partialname*
通常可以节省时间。cd partialname
后面跟着 Tab 键也可能起作用。
Extra:
tar
Files for Unix (MinGW 用户,参见下面的“Extra:zip
Files”)
想要将该目录放入一个文件中进行邮寄或存储吗?在删除任何你不想要的大文件之后(make clean
),进入一个目录(cd ..
)并tar
它:
tar -czvf project1.tar.gz project1
#for a directory named project1
您现在应该有一个文件project1.tar.gz
,适合作为您最喜欢的邮件程序的附件发送。
要解包,把它放在你想放的地方(确保那里还没有一个project1
目录,以防止覆盖),然后说
tar -xzvf project1.tar.gz
Unix 安装各不相同;您可能需要稍微修改命令——但是这在许多机器上都是可行的。
防错法
在“反欺诈”部分,我们考虑可能出错的事情以及如何修复或防止它们。例如:
你运行程序,它永远不会停止。它可能在等待一些输入(比如按下一个键继续),或者它可能已经永远进入啦啦啦状态。可以用 Ctrl-C 杀死它(按住 Ctrl,按 C)。
它以信息
Segmentation fault
: core dumped
停止。这或多或少意味着,“发生了不好的事情。”现在,只需删除核心文件(rm core
)并在程序中查找问题。
你可能想现在跳到“如何不痛苦(无论你的平台是什么)”这一小节
…在 Microsoft Visual Studio 中
最简单的开始方式如下:
在源代码的
newWork
文件夹中,复制basicSSDLProject
子文件夹,将你的副本保存在相同的位置,这样它就可以找到 SDL 和 SSDL。适当改名(
hello
,也许?).打开它的解决方案文件
SSDL_Project.sln
。您应该会看到类似图 1-8 的内容。 8
图 1-8
一个 SSDL 项目。在解决方案资源管理器窗口中(我的在左边;你的可能在别处),点击 SSDL 项目旁边的三角箭头,然后源文件;然后双击main.cpp
查看主程序的(不完整)内容
如果你想从头开始,请参阅附录 a 中的说明。
编译你的程序
你的程序还没有做任何事情,所以你要给它一些内容。现在,你可以输入 Hello,world!来自示例 1-1 的程序。
也许你会犯一些错别字。
如果是这样的话,编辑器可能会在它反对的内容下面划一条弯弯曲曲的红线来警告你(图 1-9 )。将鼠标指针放在有问题的部分,它会给出一个提示,告诉你哪里出错了(尽管这个提示可能并不总是很清楚)。
图 1-9
Visual Studio 突出显示并正确识别错误
尽管这很有帮助,但你不能确定编辑是正确的。直到你试着编译并运行时,你才能确定。
要编译您的程序,请转到构建➤构建解决方案。要运行它,请转到调试➤不调试启动。或者,单击标签为“本地 Windows 调试器”的窗口顶部附近的绿色箭头或三角形
如果你的程序不能编译,它会给出一个错误列表。有时信息的意思很清楚,有时又不清楚。这里有一个典型的,用“…”来使它更简短:我忘了一个;
。
c:\...\main.cpp(13): error C2146: syntax error: missing ';' before identifier 'SSDL_WaitKey'
随着时间的推移,您会更加理解模糊错误信息的含义。现在,将您键入的程序与示例 1-1 进行比较,并解决任何差异,直到您获得这个成功的结果:一个显示消息Hello, world! (press any key to quit)
的窗口。当它运行时,按任意键结束它。
Extra
在 Visual Studio 中,如果您试图运行一个未编译的程序,您可能会看到图 1-10 中的对话框。
图 1-10
“你愿意建造它吗?”窗户。每次回答都是一种痛苦
如果是这样,点击“不再显示此对话框”,然后点击“是”这意味着如果需要的话,它会在运行前尝试重新编译。
如果有错误,您可能会看到图 1-11 中的方框。
图 1-11
运行上一次成功的构建?从不
单击“不再显示此对话框”并单击“否”(否则,当您进行更改时,它将返回到以前的版本以找到一个可用的版本,而不是您的最新版本。扑朔迷离!)
如果您想再次看到这些对话框,比如说,如果您在想说“否”时单击了“是”,您可以通过菜单来修复它:工具➤选项➤项目和解决方案➤构建和运行。将“运行中…”空白重置为您想要的值(图 1-12 )。
图 1-12
如何重置图 1-10 和 1-11 中设置的偏好
你创建的文件
现在翻翻你的文件夹。(通过 Windows 资源管理器或在 Windows 中打开其文件夹来访问它,随您喜欢。)你应该会看到类似图 1-13 的东西。(布局可能会有所不同,为了简单起见,这里没有显示一些文件。)
图 1-13
项目文件夹中的文件
Extra: File Extensions
如果你看到的一些文件被命名为(比方说)main
而不是main
-点什么的,我 非常强烈地推荐 你改变这个,这样你就可以看到点后面的“文件扩展名”。这有助于了解您正在处理什么类型的文件!
为此,在 Windows 10 中,在文件夹的视图选项卡(图 1-14 )中,单击文件扩展名和隐藏项目的框。你完蛋了。
图 1-14
更改文件夹和搜索选项以显示文件扩展名
这里有另一种方式,将在早期的 Windows 版本也工作:选项➤改变文件夹和搜索选项,或组织菜单➤文件夹和搜索选项。你应该看到一个写着文件夹选项的框。选择文件夹选项框的视图选项卡(图 1-15 )。一旦有,取消选中隐藏已知文件类型的扩展名;并选择显示隐藏的文件、文件夹或驱动器。
图 1-15
Microsoft Windows 中的文件夹选项窗口
文件夹中的重要文件包括
SSDL_Project.sln
“解决方案”文件:知道其他文件在哪里的主文件。SSDL_Project.vcxproj
,“项目”文件:它知道程序存储在main.cpp
和其他几个东西里。没有它就无法编译。你的程序。
Debug
(或者,有时,Release
或x64
)文件夹:它包含你的可执行文件。
Tip
您可以删除任何Debug
、Release
、.vs
和x64
文件夹。Visual Studio 将根据需要重新创建它们。如果空间很重要,这一点很重要——比如说,如果你计划通过电子邮件发送文件夹。
如果您看不到.vs
文件夹,请参阅前面的“附加:文件扩展名”
重新打开您的项目
如果您的计算机设置正确,您可以双击hello.sln
启动 Visual Studio 并重新打开您正在处理的内容。(双击其他内容可能无法打开您需要的所有文件。)
Tip
重新打开.sln
文件,而不是.vcxproj
或.cpp
文件。
防错法
在“反欺诈”部分,我们将考虑可能出错的事情以及如何修复或防止它们。
以下是您在使用 Microsoft Visual Studio 时会发现的一些常见问题:
- 你打开一个溶液(。sln)文件,但是它说没有一个项目会加载。也许你移动了源代码文件夹中的东西;他们必须呆在原地。
也可能你根本不在文件夹里,而是在 zip 文件里!zip 文件的列表看起来像一个文件夹,但它不是。请确保您解压缩源代码(参见本章开头的“初始设置”),并使用新文件夹,而不是 zip 文件。
- 无法打开包含文件或. lib 文件。错误消息可能会说类似
fatal error C1083: Cannot open include file: 'SSDL.h': No such file or directory
或者
1>LINK : fatal error LNK1104: cannot open file 'sdl2_ttf.lib'
。
此时最有可能的解释是您的项目文件夹不在源代码存储库中的正确位置。确保它与basicSSDLProject
文件夹在同一个位置。
**它乐于接受编辑,但不提供编译选项;或者如果有,编辑没有效果。**很可能你没有打开
.sln
文件,而是打开了main.cpp
或其他文件。关闭您正在处理的文件(保存到某个地方,以便您可以使用这些编辑!)并通过双击.sln
文件重新打开。**你输入了一些它应该能识别的东西,但是编辑器没有按照你预期的方式给它上色,或者在它下面画了一条弯弯曲曲的红线。**通常它会识别
return
和void
并给它们上色,以表明它知道它们是关键词。你可能打错了。或者编辑可能会感到困惑。重新编译以确保正确。它说。无法打开 exe 文件进行写入。你可能还在运行这个程序;它不能覆盖程序,因为它正在使用中。终止程序并重试。
它给出了一些其他的错误信息,并且不会完成构建。通常,再试一次就足以让它成功。
运行完程序需要很长时间。你可以耐心等待,也可以通过 Windows 任务管理器将其杀死。如果这种情况持续发生,很可能是你的程序有问题。(还是在等你打东西?)
它抱怨 Windows SDK :
C:\...\Microsoft.Cpp.WindowsSDK.targets(46,5): error MSB8036: The Windows SDK version <some number or other> was not found. <More details.>
或者在试图编译之前就失败了。
解决方案:右键单击项目(不是解决方案或
main.cpp
),选择重定目标项目,并同意它所说的内容。你试图打开一个源代码解决方案,它警告你“你应该只打开来自可靠来源的项目”好消息是:我值得信任,所以你可以点击确定。取消勾选“为该解决方案中的每个项目询问我”的复选框,这样你就不会那么烦恼了。
Extra:
zip
文件
您可能想通过电子邮件将您的项目发送给某人;或者您可能希望将其紧凑地存放起来。
通常的方法是右击文件夹,选择添加到 Zip(或添加到<folder name>.zip
)或发送到… ➤压缩文件夹。然后你可以把它附在电子邮件里,如果这是你的计划的话。
不管你怎么做,确保你首先删除了所有的Debug
、Release
或.vs
文件夹。它节省空间,如果你不这样做,一些邮件程序不会发送附件。
如何不痛苦(无论你的平台是什么)
无论您使用什么编译器,都可能会遇到这些问题:
你会得到无数的错误。这并不意味着你做了无数的错事。有时一个错误会让编译器非常困惑,它会认为后面出现的都是错的。为此,**先修正第一个错误。**它可能会消除上百条后续错误信息。
**出错的那一行看起来没问题。**也许问题出在前面的线路上。编译器直到下一行才能判断出错误,所以它报告错误的时间比您预期的要晚。丢失
;
的情况经常发生。你得到警告 **,但是程序仍然准备运行。你应该吗?有错误,有警告。**如果只是给出警告,编译器仍然可以生成程序,但是错误会阻止编译。您可以忽略警告,但它们通常是一些确实需要修复的问题的很好的提示。
你写的每一个程序!似乎一开始就充满了错误。你怀疑自己是不是傻。如果是这样,那么我们其他人也是。我也许能得到一个你好,世界!程序第一次工作。再久一点,就算了。
这里有一个大问题:
- 你犯了一个错误,这个一直运行良好的程序现在根本无法运行了。每当你做出重大改变时(重大意味着“你害怕自己可能无法撤销”)…
Windows:复制包含您的项目的文件夹
(.sln
文件、.vcxproj
。cpp
,全部),然后粘贴它(跳过任何它不让你复制的文件——反正会是你不在乎的东西),这样就创建了一个备份。Unix:复制您的
.cpp
文件,比如说cp main.cpp main.cpp.copy1
。你也可以用cp -R
复制整个目录。
对于大型项目来说,一系列备份副本是绝对必要的。我敦促你现在就养成习惯*。如果不是…你已经在你的项目上工作了 6 个月。你做了一些使它崩溃或拒绝编译或给出错误输出的事情;更糟糕的是,你昨天就做了,而且从那以后你已经做了几次更新。回到昨天的代码并获得几乎可以工作的版本,而不是重新创建 6 个月的工作,这不是很好吗?备份副本是每个程序员,不管懒不懒,都需要的。*
*Golden Rule of Not Pulling Your Hair Out
编辑程序时制作备份-----------------------------。
Unix 和 Windows 在如何结束一行上意见不一。将一个文件从一个系统移到另一个系统并阅读,你可能会看到所有的东西都明显地挤在一行上,或者在每一行的末尾显示一个^M
。
如果它是一个 Unix 文件,而你在 Windows 系统中,试试 Notepad++ 或微软的写字板。如果是在 Unix 中显示的 Windows 文件,您可以忽略有趣的符号,或者(如果已安装)使用以下命令:
dos2unix < windowsfile.txt > unixfile
另一方面,使用unix2dos
。
Exercises
使用你的编译器,输入 Hello,world!程序,并让它工作。
再写一个程序把一首歌的歌词打印在屏幕上。
拿着你好,世界!编程并故意引入错误:去掉分号或花括号;在中间打断一句引语;尝试几种不同的东西。你会得到什么样的错误信息?
清理你的文件夹(即删除那些多余的大文件)并压缩它。
形状和绘制它们的函数
当然,我们想做的不仅仅是向世界问好。为了开始学习图形,让我们再看一下运行 SSDL 程序时创建的空白窗口(图 1-16 )。
图 1-17
在屏幕中央画一个点
图 1-16
基本 SSDL 窗的尺寸
我们放入该窗口的形状的位置在(x,y)坐标中。左上角是(0,0),右下角是(639,479)——所以 y 坐标在页面上向下而不是向上。横跨 640 个位置(0–639 包括在内), 480 个位置向下。每个(x,y)位置称为一个“像素”(图片元素)。
这一部分展示了我们可以做的一些事情。第一个例子 1-3 ,在位置(320,240)画一个点。(它不会像图 1-17 那么大,但是我想让它显现出来,所以我增强了它。)
Finding and Compiling Source Code
所有编号的示例都可以在本书的源代码中找到,在示例章节的文件夹中(在本例中为ch1
),带有一些以示例编号开头的描述性名称。例子 1-3 的名字,没有想象力的叫3-drawDot
。
用你在本章第一节所做的test-setup
一样的方式编译它。在源代码库的ch1
文件夹中
在 Unix 中,进入示例的目录(3-drawDot
)并输入cp Makefile.unix Makefile
和make
。
对于 MinGW,进入示例的目录(3-drawDot
)并输入copy Makefile.mingw Makefile
和make
。
对于 Visual Studio,进入ch1
文件夹,打开ch1
解决方案;右键单击3-drawDot
并选择调试➤启动新实例。
// Draw a dot at the center of the screen
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
// draws a dot at the center position (320, 240)
SSDL_RenderDrawPoint (320, 240);
SSDL_WaitKey ();
return 0;
}
Example 1-3Program to draw a dot at the center of the screen. It’s in source code under ch1, as 3-drawDot
. Output is in Figure 1-17
绘制基本形状的功能列于表 1-1 中。int
表示整数,即整数。形式为void <function-name> (<bunch of stuff>);
的函数声明是对如何调用函数的精确描述——它们的名字以及它们在()'
s. SSDL_RenderDrawPoint
之间期望什么样的值,它的两个参数x
和y
采用两个整数。SSDL_RenderDrawLine
取四:x1
、y1
、x2
、y2
。等等。
表 1-1
常见的 SSDL 绘图功能。有关更多 SSDL 函数,请参见附录 H
| `void``SSDL_RenderDrawPoint` | 在(`x`,`y`)处画一个点。 | | `void``SSDL_RenderDrawLine``int x2, int y2);` | 从(`x1`、`y1`)到(`x2`、`y2`)画一条线。 | | `void``SSDL_RenderDrawCircle``int radius) ;` | 以此半径画一个圆,圆心在(`x`,`y`)。 | | `void``SSDL_RenderFillCircle``int radius) ;` | 以此半径画一个实心圆,圆心在(`x`,`y`)。 | | `void``SSDL_RenderDrawRect``int w, int h);` | 画一个以(`x1`,`y1`)为左上角,宽度`w`,高度`h`的方框。 | | `void``SSDL_RenderFillRect``int w, int h);` | 画一个以(`x1`,`y1`)为左上角的填充框,宽度`w`,高度`h`。 |作为他们使用的一个例子,这行代码在左上角附近画了一个圆(见图 1-18 ,左):SSDL_RenderDrawCircle
(100, 100, 100);
。
图 1-18
左边是一个带SSDL_RenderDrawCircle (100, 100, 100);
的程序。右边,一个带有SSDL_RenderDrawCircle (0, 0, 100)
的程序;
而这个给你一个居中在左上角,所以你只能看到它的四分之一(图 1-18 ,右):SSDL_RenderDrawCircle (0, 0, 100);
。不显示可视区域之外的内容称为“剪辑”
现在让我们用这些函数来做一个有趣的设计。我们需要提前计划。很快会有一个关于提前计划的部分,但是现在,你可以像电影制作人或漫画制作人一样,为你想做的任何设计制作一个故事板。
我们可能需要绘图纸(图 1-19 ,源代码文件夹中有可打印的页面)。
图 1-19
查看区域的图形,用于设计您想要显示的内容
我决定做一个虫子脸:大眼睛,大脑袋,触角。于是我就画了我想要的(图 1-20 )。
图 1-21
一只虫子的头
图 1-20
bug 头程序的绘制
我现在可以看到位置了。左眼的中心在(320,250)左右,其半径大致为 45。大圆的圆心在(430,250)附近,半径约为 150。等等。
下面是我的程序(例 1-4 )。我最初写的时候犯了几个错误——把直径和半径搞混了,把图形线读错了。你也会的。如果没有,好吧,这是真正的简历素材。
// Program to draw a cartoonish bug's head on the screen
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
SSDL_RenderDrawCircle (430, 250, 200); // draw the bug's head
SSDL_RenderDrawCircle (320, 250, 45); // the left eye
SSDL_RenderDrawCircle (470, 270, 45); // the right eye
SSDL_RenderDrawLine (360, 140, 280, 40);// left antenna
SSDL_RenderDrawLine (280, 40, 210, 90);
SSDL_RenderDrawLine (520, 140, 560, 40);// right antenna
SSDL_RenderDrawLine (560, 40, 620, 80);
SSDL_RenderDrawLine (290, 350, 372, 410);// the smile
SSDL_RenderDrawLine (372, 410, 490, 400);
SSDL_WaitKey (); // Wait for user to hit a key
return 0;
}
Example 1-4A bug’s head. Found in source code’s ch1 folder as 4-bugsHead. The resulting output is shown in Figure 1-21
请注意,我是如何在注释中严格记录我所做的一切的目的的。假设我没有把那些评论放进去:
// Program to draw a cartoonish bug's head on the screen
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
SSDL_RenderDrawCircle (430, 250, 200);
SSDL_RenderDrawCircle (320, 250, 45);
SSDL_RenderDrawCircle (470, 270, 45);
SSDL_RenderDrawLine (360, 140, 280, 40);
SSDL_RenderDrawLine (280, 40, 210, 90);
SSDL_RenderDrawLine (520, 140, 560, 40);
SSDL_RenderDrawLine (560, 40, 620, 80);
SSDL_RenderDrawLine (290, 350, 372, 410);
SSDL_RenderDrawLine (372, 410, 490, 400);
SSDL_WaitKey ();
return 0;
}
真是一场噩梦!你几个月后回来重用或升级这个程序,看到代码,然后想,我到底在做什么?哪条线做什么?
然后你试着运行它,然后……你的系统管理员升级了编译器或者库,程序不再工作了。(软件腐烂;至少,有些东西会让你的程序随着时间的推移停止工作。)你有一个不工作的程序,即使要确定这些部分是用来做什么的,也需要进行侦查工作。
最好加上注释,这样你就可以理解、维护并根据需要更新你的程序。这里(例 1-5 )我决定给眼睛加瞳孔。根据评论,很容易判断他们去了哪里。
// Program to draw a cartoonish bug's head on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
SSDL_RenderDrawCircle (430, 250, 200); // draw the bug's head
SSDL_RenderDrawCircle (320, 250, 45); // the left eye
SSDL_RenderFillCircle (300, 250, 5); // ... and its pupil
SSDL_RenderDrawCircle (470, 270, 45); // the right eye
SSDL_RenderFillCircle (450, 270, 5); // ... and its pupil
...
}
Example 1-5A bug’s head, with pupils in the eyes. Found in source code’s ch1 folder as 5-bugsHead
. Output is in Figure 1-22
防错法
图 1-27
示例 1-8 的输出
图 1-26
示例 1-7 的输出
图 1-25
示例 1-6 的输出
图 1-24
Visual Studio 提示输入函数参数的类型
图 1-23
Microsoft Visual Studio“智能感知”自动完成函数名
图 1-22
一只虫子的头,加上了瞳孔
你调用了一个 SSDL 函数,但它不起作用。此时,最有可能的猜测是它在可视区域外画东西,所以你看不到。确定问题所在的最好方法是检查你给出的论点,确保它们是合理的。
**(针对 Visual Studio)你记不清具体怎么调用一个函数,也不想查。**那不是 bug,但却是现实,而且表现出了令人钦佩的懒惰,那就顺其自然吧。你有时可以在打字时得到提示(图 1-23 )。当你打开括号时,它可能会给你一个函数的描述或者它所期望的(图 1-24);如果您在描述上看到一个箭头或三角形,请单击它以查看多个选项。
…有时什么也没发生。或者它在完全正常的事情上画红色的曲线。尝试重新键入代码行或编译代码——其中一种方法通常可以做到这一点。
Exercises
设计一些你自己的东西,并写一个程序在屏幕上显示出来。
画一个立方体,看起来不太直,就像这里显示的。
const
s 和颜色
自然,我们也想给我们的形状涂上颜色。
电脑上的颜色分为三部分:红色、绿色和蓝色。在我们的库中,它们的范围从 0(最低)到 255(最高)。黑色是 0,0,0;白色是 255,255,255;红色是 255,0,0(红色最大,其他为零)。其他组合出其他颜色。你可以使用像 www.colorpicker.com
这样的网站来找到你想要的颜色的红色、绿色和蓝色成分。
您可以使用 SSDL 内置的几种颜色(BLACK, WHITE, RED, GREEN,
或BLUE
)或创建自己的颜色,如下所示:
const SSDL_Color
MAHOGANY = SSDL_CreateColor (192, 64, 0);
这里,SSDL_Color MAHOGANY
说我们正在创建一种颜色,并将其命名为MAHOGANY
。SSDL_CreateColor
(192, 64, 0)
给了它我们想要的数字。
颜色不会改变,所以我们将使用 C++ 的const
关键字来强调这一点,并让编译器防止它们被错误地改变。常量全部用大写字母书写,以使程序的读者清楚它们不会改变。(你习惯了,就明白无误了。)
要开始使用颜色,请执行以下操作:
SSDL_SetRenderDrawColor (RED); // draw things in RED, from now
// till the next call to this function
要清除屏幕,请执行以下操作:
SSDL_RenderClear (BLACK); // erase the screen and make it BLACK
这是一个程序,它使用内置的和新的颜色在屏幕上绘制方框。输出如图 1-25 所示。
// Displays boxes of colors
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Four squares in different colors");
// We'll use 2 new colors, plus GREEN and WHITE...
const SSDL_Color MAHOGANY = SSDL_CreateColor (192, 64, 0);
const SSDL_Color DARK_GREY = SSDL_CreateColor (100, 100, 100);
// Make a dark grey background
SSDL_RenderClear (DARK_GREY);
// We'll have two squares across
SSDL_SetRenderDrawColor (GREEN); // First square
SSDL_RenderFillRect ( 0, 0, 100, 100);
SSDL_SetRenderDrawColor (MAHOGANY); // Second
SSDL_RenderFillRect (100, 0, 100, 100);
// Program's end.
// Must restore color to white, or we'll get mahogany text!
SSDL_SetRenderDrawColor (WHITE);
sout << "Hit any key to end.\n";
SSDL_WaitKey();
return 0;
}
Example 1-6Use of colors to paint some rectangles. Found in source code’s ch1 folder as 6-colorSquares
表 1-2 包含与颜色和清除屏幕相关的功能。这里的一些声明与附录中的不完全匹配:它们被简化了,但是足够接近了。
表 1-2
与颜色相关的 SSDL 函数
| `SSDL_Color SSDL_CreateColor``(int r, int g, int b);` 9 | 创建并返回一种颜色。(`r` )ed、(`g` )reen、(`b` )lue 的最大值为 255。 | | `void` `SSDL_SetRenderDrawColor``(SSDL_Color c);` | 设置后续绘图,包括文本,使用颜色`c`。 | | `void` `SSDL_SetRenderEraseColor``(SSDL_Color c);` | 设置擦除,包括清除屏幕,以使用颜色`c`。 | | `SSDL_Color``SSDL_GetRenderDrawColor` | 获取当前绘图颜色。例如,`const SSDL_Color FOREGROUND =``SSDL_GetRenderDrawColor();` | | `SSDL_Color` `SSDL_GetRenderEraseColor ();` | 获取当前擦除颜色。 | | `void``SSDL_RenderClear` | 将屏幕清除为当前擦除颜色。 | | `void` `SSDL_RenderClear (SSDL_Color c);` | 清除屏幕颜色`c`。 |有些函数(以void
开头的那些)不会为你计算一个值;它们只是做一些事情(比如画一个形状,清空屏幕,或者设置一种颜色)。其他人,像SSDL_CreateColor
,有计算答案的工作。这个创建了一个颜色,所以它的“返回类型”不是void
,而是SSDL_Color
。
我们将在第七章进一步讨论函数和返回类型。
Exercises
将颜色添加到您编写的在屏幕上绘制图形的程序或本书源代码中的另一个程序中。
为你最喜欢的节日制造一个场景:万圣节的橙色恐怖脸或者绿色圣诞树。或者在色彩节胡里节尽情狂欢。
通过交替调用
SSDL_RenderClear
和SSDL_WaitKey
使屏幕闪烁各种颜色。写出几种颜色的名称,每种都用那种颜色写(“红”用红色写,等等。).
文本
sout
、转义序列和字体
你可以用 SSDL 库的sout
打印多种东西——不仅仅是文本,还有数字:
sout << "The number pi is " << 3.14159 << ".\n";
sout << "...and the number e is "
<< 2.71828
<< ".\n";
你在程序中如何设置行间距不会改变打印的内容;当到达\
n
字符,即“行尾”字符时,该行结束。用一种方式而不是另一种方式来分隔代码行的唯一原因是为了清晰。(对我来说,前面的版本看起来不错。)
但是引号内的间距很重要。注意我在“是”字后面加的空格;如果不放置它,您的第一行输出将如下所示:
The number pi is3.14159.
还有其他的转义序列,又名“转义码”——以\
开头的特殊字符:
\t
,制表符,带你到下一个制表位。制表位排列在 0、8 个空格、16 个空格等位置。(由于我们大部分字体都是变宽的,所以不能指望八个 Is 或者八个 Ms 和八个空格一样宽;它将是近似值。)\
"
,《人物》。如果我们只是把"
放在文本中,就像"Quoth the raven, "Nevermore""
,C++ 会被多余的"
弄糊涂。所以我们这样写:"Quoth the raven, \"Nevermore.\""
\\
,\字符(因为单个\字符会让 C++ 试图找出您要开始的转义序列)。
有关所有可用的转义序列,请参见附录 e。
你也可以决定文本出现在屏幕上的什么位置。下面是如何将 光标设置到 x 位置 100,y 位置 50:
SSDL_SetCursor (100, 50);
并且可以更改字体 和字体大小。字体文件必须是 TTF (TrueType 字体)格式;C++ 希望它们与您的项目在同一个文件夹中:
const SSDL_Font FONT = SSDL_OpenFont ("myFont.ttf", 18);
// my font; 18 point
SSDL_SetFont (FONT);
如果您想要一个系统自带的字体,在标准字体文件夹中,您可以使用这个调用:
const SSDL_Font FONT = SSDL_OpenSystemFont ("verdana.ttf", 18);
// Verdana font; 18 point
SSDL_SetFont (FONT);
你可以在微软 Word 中或者(在撰写本文时)在 en 上查看微软视窗/微软核心网页字体中的可用字体。维基百科。org/wiki/Core _ fonts _ for _ the _ Web
。文件名不总是显而易见的;例如,在 Microsoft Word 中显示为“Bookman Old Style”的实际上是四个文件—bookos.ttf
、bookosb.ttf
、bookosbi.ttf
和bookosi.ttf
,分别对应普通、粗体、粗斜体和斜体。
在 Unix 中,您可以使用这个命令获得已安装字体的列表:fc-list
。它们可能在/usr/share/fonts
或其子文件夹中。
SDL2_ttf 库很乐意把你给它的字体做成斜体,粗体,或者其他什么,但是它无法和人类艺术家竞争。在可能的情况下,使用字体中的增强版本,比如用Times_New_Roman_Bold.ttf
或timesbd.ttf
表示 Times New Roman Bold。 10
即使没有粗体或斜体,SDL 有时也很难让字体看起来流畅。如果你的字体看起来太不均匀,尝试另一种字体或更大的尺寸。
示例 1-7 展示了这些新功能。
// Prints an excerpt from Sir Walter Scott's _The Lady of the Lake_
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
// Window setup
SSDL_SetWindowTitle ("Hit any key to end");
// Always tell user what's expected...
// We'll be using Times New Roman font, bold...
// so load it, and tell SSDL to use it
const SSDL_Font FONT = SSDL_OpenSystemFont ("timesbd", 24);
SSDL_SetFont (FONT);
SSDL_SetCursor (0, 50); // Start 50 pixels down
// And now, the poem (or part of it)
sout << "from The Lady of the Lake\n";
sout << "\tby Sir Walter Scott\n\n";
// Tab over for author's name, then
// double space at the end of the line
sout << "\"Tis merry, 'tis merry, in Fairy-land,\n";
sout << "\tWhen fairy birds are singing,\n";
sout << "When the court cloth ride by their monarch's side,\n";
sout << "\tWith bit and bridle ringing...\"\n";
// End when user hits a key
SSDL_WaitKey ();
return 0;
}
Example 1-7Using escape sequences, cursor, and fonts to print a poem. Found in source code’s ch1 folder as 7-quotation. Output is in Figure 1-26
表 1-3
字体和文本位置的 SSDL 函数
| `void``SSDL_SetCursor` | 将光标定位在`x`、`y`处,以便下次使用`sout`或`ssin`(稍后描述)。 | | `SSDL_Font` `SSDL_OpenFont``(const char* filename, int point);` 11 | 从`filename`为 TrueType 字体和`point`创建字体。 | | `SSDL_Font` `SSDL_OpenSystemFont``(const char* filename, int point);` | 相同,但从系统字体文件夹加载。 | | `void``SSDL_SetFont` | 使用`f`作为文本字体。 |ssdl _ rendertext、ssdl _ rendertext 中心
我们可以通过以下两个函数调用将光标和字体的设置以及打印合并到一个语句中(以及居中文本),详见表 1-4 。如果您不指定字体,它将使用您已经在使用的任何字体:
const SSDL_Font FONT_FOR_YEAR = SSDL_OpenSystemFont ("verdana.ttf", 14);
SSDL_RenderText ("When did King Sejong publish the Korean alphabet?", 0, 0);
// didn't specify font; use whatever we were using before...
SSDL_RenderText (1446, 500, 0, FONT_FOR_YEAR); //...use new font here
// Year was 1446\. Print at location 500, 0.
如果你说SSDL_RenderTextCentered
,你给出的位置将是文本的中心,而不是它的左侧。
表 1-4 总结了这两种功能。
如果行尾字符在您正在打印的文本中,它会将您带到下一行——如果是SSDL_RenderTextCentered
则仍然居中,如果不是,则仍然缩进到您指定的位置——但是不支持制表符。
为了说明这一点,示例 1-8 采用了先前的示例 1-6 来使用这些新函数显示一些标签。
表 1-4
一些用于打印的 SSDL 函数
| `void` `SSDL_RenderText``(T thing, int x, int y,``SSDL_Font font = currentFont);` | 在`x`、`y`位置打印`thing`(可以是任何可打印类型),如果指定使用`font`,否则使用当前字体。 | | `void` `SSDL_RenderTextCentered``(T thing, int x, int y,``SSDL_Font font = currentFont);` | 打印`thing`,如上,以`x`、`y`为中心。 |// Displays boxes of colors, labeled
// -- from _C++ for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char** argv)
{
SSDL_SetWindowTitle("Two colored squares, with labels");
// New colors
const SSDL_Color MAHOGANY = SSDL_CreateColor(192, 64, 0);
const SSDL_Color DARK_GREY = SSDL_CreateColor(100, 100, 100);
// Make a dark grey background
SSDL_RenderClear(DARK_GREY);
// First square:
SSDL_SetRenderDrawColor(GREEN);
SSDL_RenderFillRect (0, 0, 100, 100);
SSDL_SetRenderDrawColor(WHITE);
SSDL_RenderTextCentered("GREEN", 50, 50); // dead center of
// green square
// Second square:
SSDL_SetRenderDrawColor(MAHOGANY);
SSDL_RenderFillRect (100, 0, 100, 100);
SSDL_SetRenderDrawColor(WHITE);
SSDL_RenderTextCentered("MAHOGANY", 150, 50); // dead center of
// mahogany square
// Report number of colors, thus demonstrating non-centered text
SSDL_RenderText ("Number of colors: ", 0, 100);
SSDL_RenderText (2, 150, 100); // two colors
sout << "Hit any key to end.\n";
SSDL_WaitKey();
return 0;
}
Example 1-8An adaptation of Example 1-6 to include labels. Found in source code’s ch1 folder as 8-labelSquares
图 1-27 显示了输出。清注意
SSDL_RenderText
和SSDL_RenderTextCentered
不影响sout
的光标。所以sout
还是从页面顶部开始。SSDL_RenderTextCentered
只能从左到右居中;它不关注 y 值。要使标签真正在盒子中居中,我们必须计算 Y 位置或者只是猜测。SSDL 的默认字体是 Arial 14 磅;14 的一半是 7,所以我们可以从 Y 方向的盒子的真实中心减去它,50,并把 50–7 传递给y
参数到SSDL_RenderTextCentered
,如果我们关心的话。
Exercises
将一些适当的文本放入您之前编写的或在源代码中找到的程序中。例如,你可以让虫子的头说点什么。
使用
SSDL_WaitKey
和SSDL_RenderClear
逐页打印一首长诗或一段文字。使用合适的字体和大小。编造一些统计数据——通常不都是这么做的吗?–并使用
\t
字符来排列一个表格,就像这样:Character Coolness ========= =============== Greta Garbo 83% Humphrey Bogart 87% Marilyn Monroe 98% me, if I were 99% in the movies
画一个停止标志:一个中间写着 stop 的八角形。
画一个屈服标志:中间是屈服的倒三角形。
二、图像和声音
这些线条画够多了。让我们来点漂亮的。
我们将会玩一个豪华的图形编辑包;我喜欢瘸子。参见附录 A (Unix)或 www.gimp.org
(Windows)进行安装。
图像和窗口特征
让我们从使用示例 2-1 中的代码显示图像开始。以与上一章相同的方式运行这个示例和后续示例:转到源代码,然后转到相关的子文件夹(在本例中是ch2
)。对于 g++,进入例子的文件夹,把Makefile.unix
或者Makefile.MinGW
复制到Makefile
,还有make
。对于 Visual Studio,打开ch2.sln
,右键单击适当的项目,选择调试➤启动新实例。
// Program to show an image on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char **argv)
{
// Show image
const SSDL_Image BEACH = SSDL_LoadImage ("beach.jpg");
SSDL_RenderImage
(BEACH, 0, 0);
SSDL_WaitKey();
return 0;
}
Example 2-1Displaying an image. Found in source code’s ch2 folder as 1-beach. Other numbered examples are found similarly by chapter and example number
这个程序加载一个名为beach.jpg
的图像,并在屏幕上的位置 0,0 显示它。
就这样。
C++ 将在与a.out
(g++)或.vcxproj
文件(Visual Studio)相同的文件夹中查找图片。如果我们有一个以上的图像,文件夹可能会变得凌乱。让我们把这些图像放在一个名为media
的子文件夹中,然后这样加载一个图像:const SSDL_Image BEACH = SSDL_LoadImage ("media/beach.jpg");
…其中media/
的意思是“在名为media
的文件夹中” 1
您目前可以加载 GIF(“jiff”)、JPG(“J-peg”)、BMP(“bitmap”)或 PNG(“ping”)格式或 LBM、PCX、PNM、SVG、TGA(“Targa”)、TIFF、WEBP、XCF、XPM 或 XV 格式的图像。
如果你有另一种格式,尝试在 GIMP 或其他图形编辑器中加载它,并保存/导出为 JPG 或 PNG。我推荐 PNG,因为它支持透明。
当你看到图 2-1 、*中的结果时,你可能想知道我们能缩放图像吗?*是的,SSDL_RenderImage(BEACH, 0, 0, 640, 480);
会把它做成 640 × 480 的图像。但是拉伸它可能会使图像模糊,所以让我们调整窗口大小以适应图像。
图 2-1
显示图像
首先,我们会发现它有多大。如果在 GIMP 中加载,顶栏会告诉你:
Unix 用户也可以说exiv2 beach.jpg
。如果没有安装exiv2
,和你的系统管理员好好谈谈。
Windows 用户可以右键单击文件夹中的文件(在ch2/beach/media
中)并选择属性,然后选择细节选项卡。你会看到如图 2-2 所示的宽度和高度。
图 2-2
Microsoft Windows 中图像的属性
然而,我们得到的信息,我们会告诉程序,使窗口的大小相同,给它的宽度和高度参数的顺序:
SSDL_SetWindowSize (400, 300); // make a 400x300 window
因为我们想让事情变得更酷,所以我们也给窗口本身添加一个标签:
SSDL_SetWindowTitle ("My trip to the beach ");
这将My trip to the beach
放在显示窗口的顶栏上。例 2-2 在一个完整的程序中这样做;结果如图 2-3 所示。
图 2-3
调整了标题窗口的大小,以显示没有多余空间的图像
// Program to show an image on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char **argv)
{
// Set window parameters
SSDL_SetWindowSize (400, 300); // make a 400x300 window
SSDL_SetWindowTitle ("My trip to the beach");
// Show image
const SSDL_Image BEACH = SSDL_LoadImage ("media/beach.jpg");
SSDL_RenderImage (BEACH, 0, 0);
SSDL_WaitKey();
return 0;
}
Example 2-2Displaying an image, resized and titled
在表 2-1 中是我们与图像和窗口属性相关的新函数的声明。
表 2-1
一些 SSDL 图像和窗口功能
| `SSDL_Image` `SSDL_LoadImage``(const char* filename);` | 加载名为`filename`的图像并提供一个`SSDL_Image`。 | | `void SSDL_RenderImage``(SSDL_Image img, int x, int y);` | 在位置`x, y`显示图像`img`,使用`img`的宽度和高度。 | | `void SSDL_RenderImage``(SSDL_Image img, int x, int y,``int width, int height);` | 在位置`x, y`显示图像`img`,指定宽度和高度。 | | `void` `SSDL_SetWindowSize``(int width, int height);` | 调整窗口大小。(这在一些平台上抹去了标题;先调整大小。) | | `void` `SSDL_SetWindowTitle``(const char* title);` | 给窗户一个`title.` | | `int``SSDL_GetWindowHeight` | 返回窗口高度。 | | `int``SSDL_GetWindowWidth` | 返回窗口宽度。 |最后两个函数返回整数,就像SSDL_CreateColor
返回一个SSDL_Color
一样,所以我们可以在任何有意义的地方使用它们。将示例 2-3 中高亮显示的行添加到我们的程序中,我们会得到图 2-4 中的结果。
图 2-4
使用SSDL_GetWindowWidth
、SSDL_GetWindowHeight
居中文本
int main(int argc, char **argv)
{
// Set window parameters
SSDL_SetWindowSize (400, 300); // make a 400x300 window
SSDL_SetWindowTitle ("My trip to the beach");
// Show image
const SSDL_Image BEACH = SSDL_LoadImage("media/beach.jpg");
SSDL_RenderImage(BEACH, 0, 0);
// Make a label in the middle, centered
SSDL_RenderTextCentered("BALI? BORA BORA? BEAUTIFUL, WHEREVER!",
SSDL_GetWindowWidth () / 2,
SSDL_GetWindowHeight() / 2);
SSDL_WaitKey();
return 0;
}
Example 2-3Using SSDL_GetWindowWidth () and SSDL_GetWindowHeight () to center a message on the screen
防错法
本章中的常见问题,无论是崩溃还是不可见的东西,都是以下两种情况之一:
图像未加载。
一种字体未加载。
以下是可能的元凶:
图 2-5
Microsoft Windows 中一种无法识别的文件类型
文件夹位置:文件应该和你的
a.out
或者.vcxproj
文件在同一个文件夹中,或者在指定的子文件夹中,比如media/
。拼写错误:如果你像我一样,你会把名字拼错。调试前面的程序时,我把
beach.jpg
拼错成了myImage.jpg
。去想想。(微软 Windows)你分不清是什么样的文件。看不到文件扩展名,所以看不出是不是。jpg,。png,或者 SDL 不能使用的非常不同的东西(见图 2-5 )。
解决方案:不隐藏已知文件类型的扩展名(参见第一章“创建你自己的项目”下的附加文件扩展名)...在微软视窗系统中”。
文件已损坏或具有您的图像加载器无法处理的功能。一个技巧是在图形编辑器中加载文件。如果加载了该文件,请将其导出为不同的格式或不同的导出选项,然后尝试新文件。
它正在加载,但正被粘贴到屏幕之外。试着把它放在位置 0,0,看看它是否变得可见。
不是这样的。你能做什么?
如果一个新的特性(比如说,图像)有问题,我可以做一个程序,或者从源代码复制一个程序,只做一个特性,并确保它能正常工作。
当它出现时,我会添加一些东西,使它更像我想要的最终版本。一旦成功了,我就添加一个又一个的修改,每次都备份上一个工作的程序,所以如果我把新版本搞砸了,我可以回到我刚才的版本。 2
对我来说,这种备份方式对于新功能的运行至关重要。
即使在您信任的示例程序中,您正在努力解决的那个特性也不起作用。你有合适的项目文件或 Makefile 吗?你是否从你的源文件中复制了这个文件夹,没有改变或者重新排列,并且全部复制正确吗?
这不太可能,但是编译器和库错误会发生。比如在调用
SSDL_SetWindowSize
之后,在 Unix 的一些发行版中,SDL_GetWindowSize
(需要SSDL_GetWindowWidth
和SSDL_GetWindowHeight
)可能会返回旧的窗口尺寸。这个问题很容易绕过:自己跟踪维度。我总是能够解决编译器或库的问题,即使我后来没有发现这一直是我的错误。
多个图像在一起
用 SDL 库粘贴多个图像很容易——你只需把它们从后到前按顺序放在屏幕上,就可以了。如果它们是部分透明的,那就更好了。
你可以通过互联网图片搜索找到类似图 2-6 中的透明图片,要求文件类型为 PNG。将示例 2-4 中的新代码粘贴到BEACH
背景之后,火烈鸟应该会出现,背景通过透明部分显示出来。结果如图 2-7 所示。
图 2-7
粘贴到背景上的部分透明的图像
图 2-6
透明的巴布亚新几内亚
// Load images
const SSDL_Image BEACH = SSDL_LoadImage("media/beach.jpg");
const SSDL_Image FLAMINGO = SSDL_LoadImage("media/flamingo.png");
// Paste in the background image, and the flamingo
constexpr int FLAMINGO_X = 0, FLAMINGO_Y = 175;
// Flamingo's on left, down screen
SSDL_RenderImage(BEACH, 0, 0);
SSDL_RenderImage(FLAMINGO, FLAMINGO_X, FLAMINGO_Y);
Example 2-4Adding code to display a PNG image
用 GIMP 增加透明度
我的火烈鸟图片没有背景。你可能一心想要照片中的某样东西,比如图 2-8 中可爱的小狗——但你只是想要小狗,而不是背景。这是我知道的最简单的方法,使背景透明,并保留小狗。(我建议你使用自己选择的图片。Pixabay.com 是一个很好的来源。)
图 2-8
一幅 JPG 的图像,见于ch2/5-pupdog/media
警告:除非你是一个真正的艺术家,否则最终的图像可能看起来边缘参差不齐。
加载您的图像在一个豪华的图形编辑器。我将在我的例子中假设 GIMP。
接下来,告诉 GIMP 您希望允许透明。在图层菜单下,选择透明度➤添加阿尔法通道。什么是阿尔法通道?Alpha 是像素的透明度。增加频道意味着透明是可能的。图 2-9 显示了这可能是什么样子。
图 2-9
在 GIMP 中增加透明度
现在我们将删除背景,留下一个透明的区域。你将需要 GIMP 所谓的“模糊选择工具”(见图 2-10 ),它选择相似颜色的区域(在这种情况下,是地板上的颜色)。这个工具看起来像仙女教母的魔杖。别问我是怎么知道的。
图 2-10
模糊选择工具
在部分背景上单击魔杖,然后单击删除。(如果你搞砸了,用 Ctrl-Z–按住 Control,按 Z–撤销。)你应该会看到一个棋盘图案,这意味着你看到的是到后面的图像。您也可以使用矩形选择、其他选择或橡皮擦进行清理,如果您愿意,还可以使用裁剪选择、缩放图像或其他方法。图 2-11 显示了它可能的样子。
图 2-12
粘贴到背景上的两个透明图像
图 2-11
背景透明的图像。我还剪了它
当你欣赏完自己的作品后,保存——不,导出3——成 PNG 格式,并在你的程序中使用结果,就像我在示例 2-5 中做的那样。
// Program that pastes two images onto a background
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char **argv)
{
// Set window parameters
SSDL_SetWindowSize(400, 300); // make a 400x300 window
SSDL_SetWindowTitle("Pup dog and flamingo at the beach");
// Load images
const SSDL_Image BEACH = SSDL_LoadImage("media/beach.jpg");
const SSDL_Image FLAMINGO = SSDL_LoadImage("media/flamingo.png");
const SSDL_Image PUPDOG = SSDL_LoadImage("media/pupdog.png");
// Locations and dimensions for .png images
constexpr int FLAMINGO_X = 0, FLAMINGO_Y = 175;
// Flamingo's on left, down screen
constexpr int PUPDOG_X = 320, PUPDOG_Y = 225;
// Pupdog's on right down screen
constexpr int PUPDOG_WIDTH = 50, PUPDOG_HEIGHT = 75;
// Pup dog is bigger than I want, so I
// make her 50x75\. It's better to
// resize when making the image, but
// this works too
// Paste in the background image, plus flamingo and pupdog
SSDL_RenderImage (BEACH, 0, 0);
SSDL_RenderImage (FLAMINGO, FLAMINGO_X, FLAMINGO_Y);
SSDL_RenderImage (PUPDOG, PUPDOG_X, PUPDOG_Y,
PUPDOG_WIDTH, PUPDOG_HEIGHT);
SSDL_WaitKey();
return 0;
}
Example 2-5Multiple images, with transparency. Output is in Figure 2-12
Exercises
制作一个幻灯片,展示小狗和火烈鸟(或你自己的狗或庭院侏儒)去过的所有精彩地点。如果你想让幻灯片自动播放,而不是等待用户按键,你可以使用
SSDL_Delay
。当程序点击SSDL_Delay
时,它会停止给定的时间,然后继续:ssdl
声音
声音在 SSDL 也很容易。我知道你在想什么:我会判断那个。但是你会同意的——除非你的声音决定不加载,程序崩溃。
有两种声音:一种是在背景中持续播放,让用户烦得要死的声音,称为“音乐”,另一种是随着特定事件(如碰撞)发生的声音,称为“声音”一句话:背景音是音乐;音效就是声音。
我们一次只能播放一首音乐,但是多种声音都可以。对这两种类型你能做的主要事情是加载它,播放它,暂停或恢复它,以及暂停它。我们通常使用的格式是 WAV,但是音乐也可以是 MP3 格式。(如果你有另一种格式的声音文件,而 SSDL 无法处理,那就找一个在线转换器。)
最常见的功能在表 2-2 中;更完整的列表在附录 h 中。当您看到给定了默认值的参数时,如SSDL_PlaySound
(SSDL_Sound
s, int repeats=0)
中的repeats
,这意味着如果您省略该参数,它将使用默认值:
表 2-2
常见的 SSDL 声音和音乐功能
| `SSDL_Music` `SSDL_LoadMUS``(const char* filename);` | 从`filename`载入音乐。 | | `void` `SSDL_PlayMusic``(SSDL_Music m,``int numTimesToPlay=-1);` | 播放指定次数的音乐;–1 表示永远重复。 | | `void``SSDL_PauseMusic` | 暂停音乐。 | | `void SSDL_ResumeMusic` `();` | 恢复音乐。 | | `int``SSDL_VolumeMusic` | 设置音量,应该是从 0 到`MIX_MAX_VOLUME`(128),返回新的音量。如果 volume 为–1,则仅返回卷。 | | `void``SSDL_HaltMusic` | 停止音乐。 | | `SSDL_Sound``SSDL_LoadWAV` | 从文件中加载声音。不管名字如何,它可以是`WAV`格式或其他支持的格式。有关详细信息,请参见 SDL2_mixer 的在线文档。 | | `void` `SSDL_PlaySound``(SSDL_Sound sound,``int repeats=0);` | 播放此声音,并重复指定的次数。如果 repeat 为–1,则永远重复。 | | `void``SSDL_PauseSound` | 暂停声音。 | | `void``SSDL_ResumeSound` | 恢复声音。 | | `int` `SSDL_VolumeSound``(SSDL_Sound snd,``int volume=MIX_MAX_VOLUME);` | 设置音量,从 0 到`MIX_MAX_VOLUME`,128;还卷。如果 volume 参数为–1,则仅返回体积。 | | `void``SSDL_HaltSound` | 停止声音。 |SSDL_PlaySound (mySound, 2); // repeat sound twice after you play it
SSDL_PlaySound (mySound); // repeat sound 0 times after playing it --
// that's the default
经常可以在网上找到声音;在网上搜索“免费 WAV”或诸如此类的东西。将您需要的内容复制到您的media
文件夹中。
示例 2-6 显示了一个简单的程序,当你按下一个键时,它会播放音乐并敲锣。
// Program to play sounds
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char** argv)
{
// Initial window setup
SSDL_SetWindowTitle("Simple sound example");
// Load our media
SSDL_Music
music = SSDL_LoadMUS ("Media/457729__razor5__boss-battle-2-0.wav");
SSDL_Sound bell = SSDL_LoadWAV ("Media/321530__robbo799__church-bell.wav");
SSDL_VolumeMusic (MIX_MAX_VOLUME/2); // play music at half volume,
// because...that was LOUD.
SSDL_PlayMusic (music, SSDL_FOREVER); // ...looping continuously
// SSDL_FOREVER means -1
sout << "Hit a key to hear the bell.\n";
SSDL_WaitKey();
SSDL_PlaySound(bell);
sout << "Hit another key to end.\n";
SSDL_WaitKey();
return 0;
}
Example 2-6A simple music and sound program
防错法
几乎任何一个声音出错都会让程序崩溃。主要怀疑是文件名错误或在错误的文件夹中,或使用不支持的文件类型。
如果音质有问题,请参见附录 a。
Exercises
制作您自己的音乐视频,包括歌词、图像和声音,并播放它。您需要记录幻灯片之间的延迟时间;参见上一节中的练习 1。
播放一首歌曲,在每四拍中加入锣声或其他恼人的声音。
三、数字
数字是计算机世界运转的动力,所以让我们来看看如何让计算机为我们处理这些数字。
变量
变量可能看起来像我们在代数中使用的字母——y = MX+b,诸如此类——但在 C++ 中,它们只是存储值的地方。示例 3-1 展示了我们创建变量时的样子。
(最后提醒:与所有编号的示例一样,您可以在源代码的相应章节中找到示例 3-1——参见第一章的“形状和绘制它们的函数”一节,了解如何找到并运行它。)
int main (int argc, char** argv)
{
int seasonsOfAmericanIdol = 18;
// after a while you lose track
float hoursIveWatchedAmericanIdol = 432.5F;
// missed half an episode, dang it
double howMuchIShouldCareAboutAmericanIdol = 1.0E-21;
// 1x10 to the -21 power
double howMuchIDoCareAboutAmericanIdol = 0.000000000000001;
// So why'd I watch it if I don't care
sout << "Through " << seasonsOfAmericanIdol << " seasons of American Idol...";
// ...and some more output...
// end program
SSDL_WaitKey();
return 0;
}
Example 3-1Variable declarations for my American Idol obsession
这给了我们一个整数变量;一个浮点变量,可以取小数位;和两个 double 变量,可以取更多的小数位。(还有多少取决于你用的机器。)
432.5F
上的尾随F
意味着它是一个float
,而不是一个double
值。(如果不指明,那就是一个double
。)如果你搞混了这些,可能会得到编译器的警告。为了避免警告,我使用了double
,忘记了F
。
1.0E-21
就是 C++ 怎么写 1.0 × 10 21 。
你可以把 main 函数想象成包含这些名称的位置,每个位置可以存储一个适当类型的值(见图 3-1 )。
图 3-1
在main
中存储数值的变量
我们一做好变量就给它们赋值,在同一行上。我们没必要,但这是很好的练习。当你发现你的银行账户中的美元数是–6800 万时,这是令人失望的,因为你没有告诉计算机用什么值来初始化它,它只是碰巧以一个非常不合适的数字开始。
Golden Rule of Variables
初始化它们。
**也可以像前面那样做描述性的名字。**搜索代码试图找出"z"
或"x"
的意思是令人沮丧的。但是你很清楚seasonsOfAmericanIdol
是什么意思。
变量名以字母开头(可能以_
开头),但之后可以有数字。大写事项:temp
和Temp
是不同的变量。
Extra
变量名和常量名应该是描述性的不应该与 C++ 中的任何内置关键字相同(const
、int
、void
等)。).
按照惯例,C++ 常量全部用大写字母书写,是为了向程序员表明这是一个常量,而不是变量值。例如,使用 _: MAX_LENGTH
来分隔挤在一起的单词。
变量名的约定很灵活。我使用“camel case”变量:你将单词组合在一起构成一个变量,将单词的所有首字母大写,除了第一个——firstEntry
、minXValue
。我为像SSDL_Image
这样的创造类型保留初始资本。首字母“_”代表编译器自己的标识符。还有其他公约;无论你使用什么约定,最好是尽可能清楚。
你认为它的名字是怎么来的?
常量
我们已经设定了一些常量:
const SSDL_Color MAHOGANY = SSDL_CreateColor (192, 64, 0);
const SSDL_Font FONT = SSDL_OpenSystemFont ("timesbd", 24);
现在请考虑这两个更简单的常量声明:
constexpr double PI = 3.14159265359;
constexpr int DAYS_PER_FORTNIGHT = 7+7; // A fortnight is two weeks, so...
有什么区别?const
简单地说就是“不变”constexpr
表示“不改变并且在编译时被设置”后者让程序快一点。我们不会注意到前面的声明,但是随着程序变得更大更复杂,它可能会更重要。(我不能用SSDL_CreateColor
或SSDL_OpenSystemFont
来做这件事,因为只有在运行时启动 SDL,它们才能工作。)
当初始值只是数字的时候我用constexpr
(比如3.14159265359
或者7+7
),当是函数调用的时候我用const
(比如SSDL_OpenSystemFont ("times", 18)
)。最终我们会完善这一点(见第二十六章),但目前来说还是不错的。
何时使用常量,而不是文字值
什么时候应该使用文字值,比如100
,什么时候应该使用常量符号,比如CENTURY
?答案几乎总是:使用常量而不是简单的文字值。原因有二:
一个是要明确的是,如前所示。你正在回顾一个程序,你看到一个对7
的引用。七什么?一周中的几天?《死罪》的数量?你写第一个程序的时候是多少岁?你必须做一些调查工作来找出它,尤其是如果你的程序中有不止一个7
。侦探工作不适合懒人。最好用一个清晰的名字记录下来。
另一个原因是容易改变值。例如,按照惯例有七宗罪,但是用程序员的术语来说,使用像7
这样简单的数字文字是一个非常致命的错误。所以也许那个constexpr int NUMBER_OF_DEADLY_SINS = 7;
需要更新为8
。如果你用了这个常量,你有一行需要修改。如果你让7
贯穿你的程序,你将不得不考虑哪些7
需要改变,哪些不需要改动。又是侦探工作。
底线是清晰。我们不会回到第一章的 bug face 程序,用constexpr
代替所有的数字,因为这会让程序更难理解;每个值都是唯一的,给它命名并不能让它更清晰。(反正我们有评论说明什么意思。)但虫脸程序是个例外。通常,值应该被命名。
Golden Rule of Constants
任何时候,如果数字文字值的用途不明显,就将其定义为常量符号,全部大写,并在引用它时使用该名称。
Extra: Adding
constexpr
向 Bug-Head 程序
是的,我勉强承认,出于前面给出的原因,第一章的 bug-head 程序中的简单数字可以保留。但是如果我们不止一次引用值呢?用它们来计算脸部的位置?在这种情况下,我们需要常量。因此
// draw the bug's head
SSDL_RenderDrawCircle (430, 260, 200);
// left eye, and right
SSDL_RenderDrawCircle (430-80, 260, 50);
SSDL_RenderDrawCircle (430+80, 260, 50);
会变成
constexpr int HEAD_X = 430, HEAD_Y = 260, HEAD_RADIUS = 200;
constexpr int EYE_RADIUS = 50;
constexpr int EYE_OFFSET = 80; // How far lt/rt an eye is from center
// draw the bug's head
SSDL_RenderDrawCircle (HEAD_X, HEAD_Y, HEAD_RADIUS);
// left eye, and right
SSDL_RenderDrawCircle (HEAD_X-EYE_OFFSET, HEAD_Y, EYE_RADIUS);
SSDL_RenderDrawCircle (HEAD_X+EYE_OFFSET, HEAD_Y, EYE_RADIUS);
当然,现在它更长了——但是它已经从“这些数字是如何相互关联的?为什么 430 和 260 一直出现?”一个内在的解释。不错。(完整的程序在源代码中的ch3
为bugsHead-with-constexpr
;照常用make
运行(g++)或者通过 ch3.sln (Visual Studio)。输出如图 3-2 所示。)
图 3-2
一个虫子的头,用常量和计算画出来的
数学运算符
表 3-1 包含了你可以在 C++ 中使用的算术运算符。你可能会想到它们的用法:2.6+0.4
或alpha/beta
或-2*(5+3)
。
表 3-1
算术运算符
|运算符
|
意义
+ |
添加 |
- |
减法,否定 |
* |
增加 |
/ |
分开 |
% |
系数 |
整数除法
在你学习分数之前,当你只用整数时,结果总是一个整数:5 除以 2 等于 2,余数是 1。C++ 的整数除法也是一样:5/2
给你另一个整数,2,
不是2.5
——那是浮点值。
这可能会令人困惑。1/2
看起来确实应该是0.5
,但是由于1
和2
是整数,1/2
也必须是整数:0
。
为了与我们划分整数的方式保持一致,C++ 还提供了%
,即模数运算符,意思是“除取余数”。5%2
给出了1
,5 除以 2 后的余数。我们将在第八章“随机数”一节中看到更多的%
赋值(=)运算符
我们已经在使用=了:
const SSDL_Color MAHOGANY = SSDL_CreateColor (192, 64, 0);
int seasonsOfAmericanIdol = 18;
常量不能超过第一行,否则它们就不是常量,但是变量可以随时变化:
x = 5; y = 10;
x = 10; // I changed my mind: put a 10 in X, replacing the 5
seasonsOfAmericanIdol = seasonsOfAmericanIdol + 1; // Another year! Yay!
后者意味着取那个seasonsOfAmericanIdol
存储单元中的任何数字,加 1,然后把结果值放回那个相同的位置。
也可以这么写:seasonsOfAmericanIdol +
= 1;
。
它们的意思是一样的:在seasonsOfAmericanIdol
上加 1。1
它适用于其他算术运算符: -=
、*=
、/=
和%=
都以相同的方式定义。
跳水板的例子
现在,让我们用一个将数学用于体育的程序来实践这一点。有人要离开跳水板了。我们将一秒一秒地拍摄角色跳入水中的图像(例如 3-2 )。
// Program to draw the path of a diver
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main(int argc, char** argv)
{
SSDL_SetWindowTitle("Sploosh! Hit a key to end");
// Stuff about the board
constexpr int BOARD_WIDTH = 60,2
BOARD_THICKNESS = 8,
BOARD_INIT_Y = 20;
SSDL_RenderDrawRect(0, BOARD_INIT_Y,
BOARD_WIDTH, BOARD_THICKNESS);
// ...the water
constexpr int SKY_HEIGHT = 440;
SSDL_SetRenderDrawColor(BLUE);
SSDL_RenderFillRect(0, SKY_HEIGHT,
SSDL_GetWindowWidth(),
SSDL_GetWindowHeight() - SKY_HEIGHT);
// height is window height - sky height
// ...the diver
constexpr int
WIDTH = 10, // Dimensions of "diver"
HEIGHT = 20,
DISTANCE_TO_TRAVEL = 20, // How far to go right each time
FACTOR_TO_INCREASE = 2; // Increase Y this much each time
constexpr int INIT_X = 50,
INIT_Y = 10;
int x = INIT_X; // Move diver to end of board
int y = INIT_Y; // and just on top of it
const SSDL_Color DIVER_COLOR = SSDL_CreateColor(200, 150, 90);
SSDL_SetRenderDrawColor(DIVER_COLOR);
// Now draw several images, going down as if falling, and right
// Remember x+=DISTANCE_TO_TRAVEL means x=x+DISTANCE_TO_TRAVEL
// ...and so on
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; // go right the same amount each time,
y *= FACTOR_TO_INCREASE; // down by an ever-increasing amount
SSDL_Delay(100); // 100 ms -- 0.1 seconds
// Same thing repeated several times
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; y *= FACTOR_TO_INCREASE;
SSDL_Delay(100); // 100 ms -- 0.1 seconds
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; y *= FACTOR_TO_INCREASE;
SSDL_Delay(100); // 100 ms -- 0.1 seconds
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; y *= FACTOR_TO_INCREASE;
SSDL_Delay(100); // 100 ms -- 0.1 seconds
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; y *= FACTOR_TO_INCREASE;
SSDL_Delay(100); // 100 ms -- 0.1 seconds
SSDL_RenderFillRect(x, y, WIDTH, HEIGHT);
x += DISTANCE_TO_TRAVEL; y *= FACTOR_TO_INCREASE;
SSDL_Delay(100); // 100 ms -- 0.1 seconds
// end program
SSDL_WaitKey();
return 0;
}
Example 3-2A program to show a diver’s path, using constexprs and math operators
注意事项:
我一如既往地初始化所有变量。
在任何计算或变量初始化中没有空的数字文本;一路都是定值。
我重复同样的一对线六次。真的吗?那是懒吗?我们在第五章会有更好的方法。
图 3-3 就是结果。
图 3-3
显示潜水员入水路径的程序
这很有效,并且在某种程度上唤起了我从高台跳水时的恐惧。
数学运算符的无忧列表
这里有一些 C++ 会很自然地处理的事情,你不需要为它们记住任何东西:
优先级 : 考虑一个数学表达式,
2*5+3
。在 C++ 中,就像在代数课上一样,我们在加法之前先做乘法;这是指(2*5)+3
=13
,而不是2*(5+3)
=16
。同样的,在8/2-1
中,我们先除后减。总的来说,用对你有意义的方式去做,它就会是正确的。如果不是,用括号强制它按照你的方式进行:8/(2-1)
。**关联性:**在
27/3/3
中,哪个除法先出现?是像27/(3/3)
,还是像(27/3)/3
那样做?算术运算从左到右执行。赋值是从右到左进行的:x=5+2
要求你在对x
做任何事情之前先评估5+2
。
优先级和结合性的精确细节在附录 b 中。
- 强制 : 如果你想把一种类型的变量塞进另一种类型,C++ 会做到:
double Nothing = 0; // Nothing becomes 0.0, not 0
int Something = 2.7; // ints can't have``decimal places
// C++ throws away the .7;
// Something becomes 2\. No rounding, alas
如果在计算中混合整数和浮点数,结果将是信息最多的版本,即浮点。比如10/2.0
,给你5.0
。
Exercises
使用厘米每英寸(2.54)和英寸每英尺(12)的常量,将某人的身高从英尺和英寸转换为厘米,并报告结果。
现在反过来做:厘米到英尺和英寸。
Accumulate this sum for as far as you’re willing to take it, 1/2 + 1/4 + 1/8 + 1/16 +…, using +=. Do you think if you did it forever you would reach a particular number? Or would it just keep getting bigger? The ancient philosopher Zeno of Elea would have an opinion on that (
https://en.wikipedia.org/wiki/Zeno%27s_paradoxes
, at time of writing). But he’d be wrong.你不能从这里到达那里。—芝诺。算是吧。
编写一个程序,让一个盒子以 0.1 秒的跳跃在屏幕上移动——就像潜水员移动一样,但每次跳跃都要清空屏幕,这样它看起来就像真的在移动。也许可以缩短延迟时间,以获得更好的运动错觉。
内置函数和转换
现在我想做一个几何图形,五角星。但是忘了第一章的图表吧。我想让电脑帮我算出来。让它自己做(虚拟的)绘图纸。
如果我把星星想象成一个圆,我可能知道圆心,所以我需要计算的是边缘的点。每个点都比前一个点绕圆远五分之一,所以如果一个圆是 360 度,它们之间的角度是 360/5 度。如果你用弧度而不是角度,比如 C++,那就是两点之间的 2π/5 弧度。
SDL 使用 x,y 坐标,所以我们需要一种方法从这个角度得到它。我们可以使用图 3-4 中的图片来完成。由于角度θ的正弦是 y 距离除以半径(如果数学不是你的菜,相信我),y 距离就是半径* sin (θ)。同样,x 距离是半径* cos (θ)。
图 3-4
与 x 和 y 相关的正弦和余弦
像大多数 C++ 数学函数一样,sin
和cos
函数在一个名为cmath
、、 3 、的包含文件中进行声明。因此
#include <cmath> // System include files (those that come with the compiler)
// have <>'s not ""'s.
#include "SSDL.h"
这个程序的意思是从中心到边缘画一条线,绕圆转五分之一圈,再做一遍,一直走,总共五行。
// Program to make a 5-point star in center of screen
// -- from _C++20 for Lazy Programmers_
#include <cmath>
#include "SSDL.h"
int main(int argc, char** argv)
{
constexpr double PI = 3.14159;
// Starting out with some generally useful numbers...
// center of screen
const int CENTER_X = SSDL_GetWindowWidth () / 2,
CENTER_Y = SSDL_GetWindowHeight() / 2;
constexpr int RADIUS = 200,
NUMBER_OF_POINTS = 5;
// angle information...
double angle = 0; // angle starts at 0
constexpr double ANGLE_INCREMENT = (2 / NUMBER_OF_POINTS) * PI;
// increases by whole circle/5 each time
// ...now we make the successive lines
int x, y; // endpt of line (other endpt is center)
x = CENTER_X + int(RADIUS * cos(angle)); // calc endpoint
y = CENTER_Y + int(RADIUS * sin(angle));
SSDL_RenderDrawLine(CENTER_X, CENTER_Y, x, y); // draw line
angle += ANGLE_INCREMENT; // go on to next
x = CENTER_X + int(RADIUS * cos(angle)); // calc endpoint
y = CENTER_Y + int(RADIUS * sin(angle));
SSDL_RenderDrawLine(CENTER_X, CENTER_Y, x, y); // draw line
angle += ANGLE_INCREMENT; // go on to next
x = CENTER_X + int(RADIUS * cos(angle)); // calc endpoint
y = CENTER_Y + int(RADIUS * sin(angle));
SSDL_RenderDrawLine(CENTER_X, CENTER_Y, x, y); // draw line
angle += ANGLE_INCREMENT; // go on to next
x = CENTER_X + int(RADIUS * cos(angle)); // calc endpoint
y = CENTER_Y + int(RADIUS * sin(angle));
SSDL_RenderDrawLine(CENTER_X, CENTER_Y, x, y); // draw line
angle += ANGLE_INCREMENT; // go on to next
x = CENTER_X + int(RADIUS * cos(angle)); // calc endpoint
y = CENTER_Y + int(RADIUS * sin(angle));
SSDL_RenderDrawLine(CENTER_X, CENTER_Y, x, y); // draw line
angle += ANGLE_INCREMENT; // go on to next
// end program
SSDL_WaitKey();
return 0;
}
Example 3-3A star using sin and cos functions
图 3-5 显示了结果。什么事?
图 3-5
一颗五角星——至少,它应该是
一旦我们在第九章中讨论了调试器,这将是一件轻而易举的事情,但是现在,我们只需要打开夏洛克·福尔摩斯的频道。那条线不变,说明angle
不变,说明ANGLE_INCREMENT
一定是 0。为什么会是 0?
看那个计算:ANGLE_INCREMENT = (2/NUMBER_OF_POINTS)*PI
。首先要做的是用 2 除以NUMBER_OF_POINTS
,即 5。因为两者都是整数,我们做整数除法:5 除以 2(余数是 2,不管值不值得),所以2/5
给我们零。零点乘以PI
为零。所以ANGLE_INCREMENT
是零。
我们需要浮点除法。
一种方法是强制 2 和 5 为float
或double
。你可以这样说。
double (whatEverYouWantToBeDouble)
。
这叫铸造。
double (2/NUMBER_OF_POINTS)
不起作用,因为它将 2 除以 5,得到 0,然后将 0 转换为 0.0。它还在做整数除法。
以下任何一种都可以。只要/
的参数之一是double
或float
,就会得到一个带小数位的结果:
double (2)/NUMBER_OF_POINTS
2/double (NUMBER_OF_POINTS)
2.0 / NUMBER_OF_POINTS
因此,将main
的开头改为你在示例 3-4 中看到的形式可以修复这个问题。
int main(int argc, char** argv)
{
...
// angle information...
double angle = 0; // angle starts at 0
constexpr double ANGLE_INCREMENT
= (2 / double (NUMBER_OF_POINTS)) * PI;
// increases by whole circle/5 each time
...
Example 3-4A new beginning to main, to make Example 3-3 work
图 3-6 显示了结果。
图 3-6
五角星
好像不是垂直的。练习 1 是关于把它转直。
其他常用的数学函数包括asin
和acos
(反正弦和反余弦)、pow
(将一个数提升到幂)、abs
(绝对值)和sqrt
(平方根)。更多信息见附录 F。
防错法
你调用了一个返回值的函数,但是没有效果。下面是我们之前看到的一个函数示例:
// Center "Blastoff!" on the screen
SSDL_GetScreenWidth();
SSDL_RenderTextCentered (320, 240, "Blastoff!");
当然,你叫了
SSDL_GetWindowWidth()
…但是你从来没有对结果做任何事情!C++ 很乐意让你浪费时间去调用函数,不去用它们给你的东西。(这是一种“让程序员搬起石头砸自己的脚,然后大笑”的语言。)如果要使用该值,请在需要该值的任何地方引用它:SSDL_RenderTextCentered(SSDL_GetScreenWidth ()/2,
SSDL_GetScreenHeight()/2,
"Blastoff!");
或者将其放入变量或常量中以备后用:
const int SCREEN_WIDTH = SSDL_GetScreenWidth ();
const int SCREEN_HEIGHT = SSDL_GetScreenHeight();
SSDL_RenderTextCentered (SCREEN_WIDTH/2, SCREEN_HEIGHT/2, "Blastoff!");
**你将两个整数相除,得到一个介于 0 和 1 之间的浮点数,得到的却是 0。**见本节示例 3-3 。
/
符号的其中一个操作数应该被强制转换为float
或double
。你会得到一个关于类型之间转换的警告。你可以忽略它,但是要让它消失,把讨厌的东西丢到你想要的地方。那么编译器就会知道这是故意的。
Exercises
调整示例 3-4 中的星形,使星形的顶点垂直向上。
制作一个钟面:一个在适当位置标有数字 1-12 的圆圈。
(用力)下面是如何以秒为单位获得系统时间:
#include <``ctime
...
int timeInSeconds = int
4(time
(nullptr));
使用
%
和/
运算符以小时、分钟和秒为单位查找当前时间。由于您所在的时区,时间可能会有所不同;可以适当调整。完成 2 和 3 后,制作一个显示当前时间的钟面。
四、鼠标,和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_GetMouseY
don’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/
)的创意人员。制作一个隐藏物体游戏:用户必须点击你提供的物体(有两个或更多)才能获胜。您可以要求用户按顺序执行。点击不是的某个物体,或者按顺序正确的物体,以失败结束游戏。
你可以把每个物体看作屏幕上的一个正方形区域。
写一个程序,在你点击的地方画一个泡泡,总是同样大小的泡泡…但是它不会让你在屏幕上放一个部分。如果您单击的位置太靠近边缘,它会将气泡从边缘移开,使其不会越过边缘。创建气泡时,您还可以添加声音效果。
五、循环、输入和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
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
前面可以加上关键词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 进制给出结果。菜单是一种由来已久的(老式的)获取用户输入的方式。制作一个菜单,为用户绘制一个圆或一条线,或者其他形状,然后绘制选定的形状。
六、算法和开发过程
让我们从 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 画的形状来画澳大利亚国旗、新西兰国旗、埃塞俄比亚国旗、斯堪的纳维亚半岛国旗或其他一些国旗。专注于一个连贯的子任务或重复的子任务。我们将在后续章节中看到更多的标志问题。
七、函数
在这一章中,我们得到了不会迷失在一页页的代码中直到你的眼睛变得模糊的最好方法:函数。
返回值的函数
想一想糖果厂的工作方式。它有制造我们想要的东西的机器。每台机器都有它需要的东西通过管道输入,它生产的东西通过管道输出。想要块糖吗?启动机器并给它输入,它将提供结果(见图 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 。你需要什么函数?
八、函数·续
关于函数的更多信息:随机数函数、布尔函数、变化的参数和“作用域”——也就是说,变量在哪里可以被看到和使用取决于它在哪里被声明。
随机数
当然,任何考虑用算术方法产生随机数的人都是有罪的。
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,如果单击在两个框之外,则继续等待。然后编写一个程序演示这些函数的用法。
将前面练习中的函数添加到第五章“
char
s 和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
在声明标识符的{}
之外看不到标识符。
关于算法的最后一点说明
通过我们的练习,我们继续为我们在第六章中建议的任何事情编写算法。我不会一直把提醒放进去,但我现在会做一个总括声明:最好养成习惯。跳过这一步感觉很懒,但总体来说工作量更大,所以懒的地方在于:解决算法编写这一步怎么做的问题。那么编码就可以相对容易。
**九、使用调试器
调试器可以让你一行一行或者一个函数一个函数地单步调试程序,查看变量值,这样你就可以判断出哪里出了问题。好主意,对吧?
我想是的。为了涵盖有用的调试器命令,让我们使用调试器来修复示例 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
还没有练习,只是确保在后续章节中使用您选择的调试器!
十、数组和enum
本章包括:数值序列(数组)、枚举类型、天气数据和棋盘游戏。
数组
“如果你不喜欢这些地方的天气,”老前辈眼睛一闪,说道,“等几分钟。”
让我们看看他是不是对的。我们将以一分钟为间隔测量十次温度,并观察其变化。我会这样开始:
double temp1; sout << "Enter a temperature: "; sin >> temp1;
double temp2; sout << "Enter a temperature: "; sin >> temp2;
double temp3; sout << "Enter a temperature: "; sin >> temp3;
那很快就过时了。也许有更好的方法来存储十个数字。这是:
constexpr int MAX_NUMBERS = 10;
double temperatures[MAX_NUMBERS]; // an array of 10 doubles
数组的 BNF 语法是 <基类型> <数组名称> [
<数组大小>]
;其中 <基类型> 是你想要的数组, <数组大小> 是你想要的数量。数组大小应该是一个常量整数。
现在我们有一个temperatures
数组,从第 0 个的开始,到第 9 个的结束。(C++ 从 0 开始计数。)
要使用数组的一个元素,只需说出:
temperatures[3] = 33.6;
sout << temperatures[3];
temperatures
是一个由double
组成的数组,所以你可以在任何可以使用double
的地方使用temperatures[3]
——因为这就是它。
注意这里,[]
中的数字不是数组的大小——那只是在声明中!而是你想要哪种元素。这个“index”应该是类似于int
的某种可数类型,我们通常用int
s:
// Get the numbers
for (int i = 0; i < MAX_NUMBERS; ++i)
{
sout << "Enter a temperature: ");
ssin >> temperatures[i];
}
For 循环是处理数组的一种自然方式,因为它们可以很容易地遍历每个元素。这个循环从 0 开始,只要i
小于 10 ( MAX_NUMBERS
,就一直循环下去,所以我们会看到temperatures[0]
、temperatures[1]
等等,一直到temperatures[9]
——都是 10。
你习惯了这样计数:一个 N 元素的数组从第 0 个开始,到第 N-1 个结束。也许你很快就会从 0 开始你的待办事项列表。
示例 10-1 是一个用于读入和传回温度的完整程序。
// Program to read in and print back out 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
<< " temperatures to make your report.\n\n";
double temperatures [MAX_NUMBERS];
// Get the numbers
for (int i = 0; i < MAX_NUMBERS; ++i)
{
sout << "Enter the next temperature: ";
ssin >> temperatures[i];
}
// Print the numbers
sout << "You entered ";
for (int i = 0; i < MAX_NUMBERS; ++i)
sout << temperatures[i] << ' ';
sout << "\nHit any key to end.\n";
SSDL_WaitKey ();
return 0;
}
Example 10-1Reading/writing a list of numbers using an array
我们通常初始化变量。下面是如何使用数组实现这一点:
// where MAX_NUMS = 4...
double Numbers[MAX_NUMS] = {0.1, 2.2, 0.5, 0.75};1
不幸的是,括号中的列表只在初始化时有效。我们以后不能将Numbers
设置为一组括号中的值。我们必须一次处理一个元素,可能会使用 for 循环,就像示例 10-1 中那样。
我不需要将MAX_NUMS
放在[]
中,因为 C++ 可以计算值并推断大小:
double temperatures[] = {32.6, 32.6, 32.7, 32.7, 32.7,
32.7, 32.7, 32.7, 32.7, 32.7};
// I think the old guy was messing with us
如果给的值太少,它会用 0 填充其余的值: 2
double temperatures[MAX_NUMS] = {};// If it's really 0's,
// I hope we're using Fahrenheit
数组的肮脏小秘密:使用内存地址
数组不像其他变量那样存储在内存中。
数组变量实际上是一个地址:包含元素的内存块的地址。当你声明一个数组时,这使得计算机做这些事情:分配数组变量(图 10-1 左图);分配一块内存来存储元素(右图 10-1);并将第 0 个元素的地址,即该块的开始地址,放入新的数组变量中。 3
图 10-1
数组在内存中的存储方式
这就是 C++ 从数组索引 0 开始的原因。为了计算第i
个元素的地址,它将i*
sizeof
(int)
,即i
乘以一个int
的大小,添加到数组的位置。如果你从 1 开始,那就得加上(i-1)*sizeof(int)
)。C++,尤其是它的祖先 C,喜欢尽可能高效地做事,即使在过程中牺牲了一些清晰性。
那么在图 10-1 中,数组大小存储在哪里呢?如何查询以后是否需要?
我不能。C++ 在分配内存时使用数组大小,但是之后,如果你想要那个数字——比方说,为了确保你不会在数组中走得太远——你必须自己跟踪它(目前)。如果您声明了一个包含五个元素的数组,并试图访问第五个元素(因为 C++ 使用基于 0 的计数,所以第五个元素不存在),它会让您。这意味着你正在读取一大块内存,而这些内存是用来做其他事情的。
如果像在A[5] = 0
中那样写入那块内存,情况会更糟。如果这样做,可能会覆盖构成其他变量的数据。
防错法
你的循环在数组中超出了一个元素。我敢打赌,比较运算符是罪魁祸首——通常都是这样!变化
for (int i = 0; i
< =N; ++i)
到
for (int i = 0; i
<变量的值改变了,但你没有告诉它。你可能对一个数组使用了太大的索引,从而覆盖了一个不同的变量。
**你的程序崩溃(停止运行)。**在 Unix 中,你得到
Segmentation fault: core dumped
。在其他地方,您可能会看到一个窗口,显示“<您的程序>已经停止工作”或者(在 Visual Studio 中)“抛出异常”此时一个可能的原因是…使用了一个对数组来说太大的索引。
Exercises
在下文中,如果出现问题,一定要使用调试器。
编写一个程序,从用户那里获得一周的所有七个每日高温和每日低温,并告诉用户哪一天有最低低温和最高高温。
做同样的程序,但不要问用户;使用
{}
初始化数组。给定一个
char
的数组(使用{}
初始化),报告字符是否按字母顺序排列。读入一个整数列表,并反向打印出来。
作为函数参数的数组
当我想把一个变量传递给一个函数时,我所做的就是复制声明(没有分号)并用它来定义参数。也就是说,为了声明x
,我会写int x;
,所以如果我想把x
发送到函数f
,我会写void f (int x);
。
数组也是如此:
void f (int myArray[ARRAY_SIZE]);
示例 10-2 显示了在数组中查找最小温度的函数。
double lowestTemp(double temperatures[MAX_NUMBERS])
// returns lowest entry in temperatures
{
double result = temperatures[0];
for (int i = 0; i < MAX_NUMBERS; ++i)
if (temperatures[i] < result)
result = temperatures[i];
return result;
}
Example 10-2lowestTemp, taking an array and returning its smallest element
这就是所谓的
sout << "The lowest temp was " << lowestTemp (temperatures);
根据我们目前所知,让一个函数返回一个数组是不可能的。现在我们只是把它们作为参数传入。
改变或不改变的数组参数
我不必对数组使用&
,即使我想改变数组的内容。原因如下。
记住一个数组变量并不是所有不同的元素;是 0 号的地址(见图 10-1 )。如果你不使用&
,你不能改变它——这没问题。我们不想改变地址;我们只想改变的内容——它们不是传入的东西!随心所欲地修改它们——当函数返回时,你的修改就会出现。
你可能会说,“这难道不违反安全吗?如果被调用的函数改变了它们,而它们本不应该被改变,那该怎么办?”说得好。解决方法如下:
double lowestTemp (const double temperatures[ARRAY_SIZE])
// returns lowest entry in temperatures
{
...
将数组声明为const
可以确保lowestTemp
不能改变它的元素——这是一个需要养成的好习惯。
数组参数和可重用性
因为 C++ 不关心你的数组有多大,当你把它传递给一个函数时,它完全忽略了在[]
之间给定的大小。C++ 不在乎。这意味着相同的函数可以用于相同基类型的任何数组,而不管大小如何。
这里有一个版本的lowestTemp
,它不会把你限制在某个特定的尺寸:
double lowestTemp (const double temperatures[], int arraySize)
// returns lowest entry in temperatures
{
bool result = temperatures[0];
for (int i = 0; i < arraySize; ++i)
...
}
现在让我们更加灵活。正如第七章所建议的,让你的函数通用化、通用化,从而可重用是很好的。我们称之为lowestTemp
的函数没有理由只对温度起作用。更一般地编写它,如示例 10-3 ,你可以在另一个程序中使用它。代码重用,耶。
double minimum(const double elements[], int arraySize)
// returns lowest entry in elements
{
double result = elements[0];
for (int i = 0; i < arraySize; ++i)
if (elements[i] < result)
result = elements[i];
return result;
}
Example 10-3minimum, a function that can be used for any double array, any size
防错法
函数调用时,编译器报错“从 int 到 int*的无效转换”或“int 到 int[]”。它需要一个数组,但只得到一个值。我们不是给了它一个数组吗?在这个例子中
minimum (temperatures[NUMTEMPS], NUMTEMPS);
…我没有。我给了它
NUMTEMPS
th 温度,不管那是什么。这个问题是混淆了
[]
的两种用法。当声明时,在[]
之间的是数组大小。在其他时候,它是我们想要访问的元素。由于数组的名称是
temperatures
,而不是temperatures[NUMTEMPS]
,我将把它传入:minimum (temperatures, NUMTEMPS);
Golden Rule of [ ] for Arrays
在数组引用中的[]
之间是你想要的元素(除了在声明的时候——然后是数组的大小)。
Exercises
在这些练习中,如果出现问题,记得使用您最喜欢的调试器。
编写一个
maximum
函数来对应示例 10-3 中的minimum
,并使用它们来查找给定温度数组中的范围,从而回答本章开始时提出的问题:天气变化有多快。如果范围超过半度,打印“你是对的;这里的天气变化真快!”对于一个月的温度,报告最高的最高,最低的最低,和最大差距的一天。
(更难)写一个程序,将一个给定的月份的最高和最低温度绘制成图形(你会想用的初始化数组;每次输入数值会很麻烦),显示 X 轴和 Y 轴上的日期和温度,并用点来标记数据点。你肯定想先写算法。
枚举类型
为了准备在棋盘游戏或一年中的几个月使用扑克牌或彩色棋子,我们来看看快速制作几个常量符号的方法:enum
。
enum class Suit {CLUBS, DIAMONDS, HEARTS, SPADES}; // Playing card
相当于
constexpr int CLUBS = 0, // we start at 0
DIAMONDS = 1, // and go up by one for each new symbol
HEARTS = 2,
SPADES = 3;
但是编写起来更快,并且还创建了一个新的类型Suit
,因此您可以声明该类型的变量:
Suit firstCardSuit = Suit::HEARTS, secondCardSuit = Suit::SPADES;
// Yes, we have to put Suit:: first,
// but we'll fix that in a few pages
firstCardSuit
是什么*,真的吗?真的是Suit
!但是,是的,它非常像一个int
。为什么不干脆做成int
?澄清:当你宣布一个int
时,不清楚你是否真的想把它当成一套牌。如果你把它声明为Suit
,那就很明显了。*
在 BNF 中,enum
声明是enum class
{
<值列表> };
。
根据本书中使用的命名约定,我们创建的新类型是大写的(像Suit
,但不像int
;标准中内置的类型是小写的)。
我们还可以指定符号对应的整数:
enum class Rank {ACE=1, JACK=11, QUEEN, KING}; // Playing card rank
我们没有为QUEEN
指定一个值,所以它一直从JACK
开始计数:QUEEN
是 12;KING
是 13。
enum
值意味着标签而不是数字,所以尽管你可以分配给enum
变量(=
)并比较它们(==
、<
、<=
等)。),你不能轻易和他们做数学;不能用++
、--
、+
、-
、*
、/
或%
。不能用sout
打印,也不能用ssin
阅读。你不能给它们赋值int
(myRank = 8
不会编译)。那么它们有什么好处呢?
有时候你不需要做那些事情。
如果你一定要做数学,有一个变通办法,但不好玩。(我们会在第十九章找到更好的方法。)需要给myRank
一个值 8?使用铸件:
myRank = Rank (8);
需要继续下一个吗?再次铸造:
myRank = Rank (int(myRank)+1); // ack, that's a lot of casting!
想打印一个enum
怎么办?编译器不可能知道我们想要如何打印一个Suit
,所以我们会告诉它:
void print (Suit suit) // prints a Suit
{
switch (suit)
{
case Suit::CLUBS : sout << 'C'; break;
case Suit::DIAMONDS: sout << 'D'; break;
case Suit::HEARTS : sout << 'H'; break;
case Suit::SPADES : sout << 'S'; break;
default : sout << '?'; break;
}
}
好了,就这样。我已经受够了老是打字。我会告诉 C++ 在Suit
中查找这个符号,而不用我指定(例如 10-4 )。
void print (Suit suit) // prints a Suit
{
using enum
Suit;
switch (suit)
{
case CLUBS : sout << 'C'; break;
case DIAMONDS: sout << 'D'; break;
case HEARTS : sout << 'H'; break;
case SPADES : sout << 'S'; break;
default : sout << '?'; break;
}
}
Example 10-4A function for printing playing card suit
using enum
是漂亮的出血边缘。 4 如果你的编译器不支持它,只需省去单词class
,稍后它会让你省去Suit::
:
enum Suit {CLUBS, DIAMONDS, HEARTS, SPADES}; // Playing card suit
...
switch (suit)
{
case CLUBS: ...; //No need for "Suit::"
比旧版本更安全,但两者都有效。
Extra
如果您的编译器还不支持using enum
,源代码会在编译可能导致问题的代码行之前测试是否支持:
// Suit and Rank enumeration types
#ifdef __cpp_using_enum
// if __cpp_using_enum is defined, that is,
// if the compiler supports "using enum
,"
// then use enum class
enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES };
enum class Rank { ACE = 1, JACK = 11, QUEEN, KING };
#else // otherwise use plain enum
enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES };
enum Rank { ACE = 1, JACK = 11, QUEEN, KING };
#endif
...
#ifdef __cpp_using_enum //do the "using enum," if supported
using enum Suit;
#endif
这让我们可以使用可用的新特性,如果不可用,我们可以找到其他方法来编写代码。其他最近添加的功能也有类似的“宏名”,比如(比如)__cpp_concepts
。在写作的时候,你可以在 https://en.cppreference.com/w/cpp/feature_test
找到一个完整的列表。
但是在编写你自己的程序时,你可以忽略这一点,使用你的编译器已经准备好的enum
版本。
防错法
C++ 一点也不在乎你的某个enum
类型的变量是否超出了你列出的值:
r = Rank (-5000);
解决办法是,嗯,不要那样做。
Exercises
声明棋子的枚举类型:国王、王后、主教、骑士、车和卒。
为太阳系中的行星声明一个枚举类型。地球是第三颗行星,所以调整你的编号,使
EARTH
是 3,所有其他行星也正确编号。写一个函数
printRank
,给定一个Rank
,适当地打印它——作为A, 2, 3, 4, 5, 6, 7 8, 9, 10, J, Q, or K
。这个练习和下一个练习在第十九章的纸牌游戏例子中会很有用。编写一个函数
readRank
,它使用与练习 3 中相同的格式返回它读入的Rank
。是的,这是一个问题,有些输入是数字,有些是字母——那么你需要什么类型的变量来处理这两者呢?写一个函数来播放这些风格的音乐:幽灵、狂欢节、外星人或者任何你喜欢的风格(关于音乐的复习见第二章)。传入一个
enum
参数来告诉它是哪种样式。编写另一个程序,根据用户点击的框选择使用哪种样式……另一个程序绘制框供用户点击。
把它们放在一起就成了音乐播放器。
多维数组
不是所有的数组都是简单的列表。您可以拥有二维或多维数组。
这是一个井字游戏棋盘的数组:一个 3 × 3 的网格。每个正方形可以包含一个 X、一个 O 或什么都不包含:
constexpr int MAX_COLS = 3, MAX_ROWS = 3;
enum class Square { EMPTY, X, O };
Square board[MAX_ROWS][MAX_COLS];
要将第 1 行第 2 列中的方块设置为X
,我们说board[1][2] = Square::X;
并检查第row
个、col
个个方块,我们说
if (board[row][col] == Square::X) ...
图 10-2 展示了 C++ 如何在内存中排列数组。首先,我们有第 0 行,从 0 到最后一列。然后是第 1 排第 1 排,然后是第 2 排第 2 排。
图 10-2
二维数组在内存中的排列方式。(这次我省略了实际地址,以强调我们不必知道它们)
每一行都有MAX_COLS
个方块,所以为了得到board[1][2]
,C++ 计算出它是 1 * MAX_COLS
+ 2 =向下 5 个方块。从图 10-2 中的初始元素开始向下数五,我们就到了board[1][2]
,这就是我们想要的。
展示公告板
画板的两个基本步骤是什么?
draw the board itself (the grid)
draw the X's and O's on the board
画网格只是做一些线,这里就不花时间了。我们可以像往常一样,一点一点地分解绘制 x 和 Os:
for every row
draw the row
我们如何画这条线?让我们提炼一下:
for every row
for every column
draw the square
我们怎么画正方形呢?上次优化:
for every row
for every column
if board[row][column] contains X draw an X
else if it has an O draw an O
示例 10-5 是结果程序。输出如图 10-3 所示。
// Program to do a few things with a Tic-Tac-Toe board
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
// Dimensions of board and text notes
constexpr int MAX_ROWS = 3, MAX_COLS = 3,
ROW_WIDTH = 100, COL_WIDTH = 100,
BOARD_HEIGHT = 300, BOARD_WIDTH = 300;
// enough room for 3x3 grid, given these widths
constexpr int TEXT_LINE_HEIGHT = 20;
// A Square is a place in the TicTacToe board
enum class Square { EMPTY, X, O };
// Displaying the board
void display (const Square board[MAX_ROWS][MAX_COLS]);
int main(int argc, char** argv)
{
using enum Square;
// Shrink the display to fit our board
// allowing room for 2 lines of text at the bottom;
// set title
SSDL_SetWindowSize (BOARD_WIDTH,
BOARD_HEIGHT + TEXT_LINE_HEIGHT * 2);
SSDL_SetWindowTitle ("Hit any key to end.");
// Colors
SSDL_RenderClear (SSDL_CreateColor(30, 30, 30)); //charcoal
SSDL_SetRenderDrawColor(SSDL_CreateColor(245, 245, 220)); //beige
// The board, initialized to give X 3 in a row
Square board[MAX_ROWS][MAX_COLS] =
{ {EMPTY,5
EMPTY, X},
{EMPTY, X, EMPTY},
{ X, O, O} };
display (board); // display it
// Be sure the user knows what he's seeing is the right result
SSDL_RenderText("You should see 3 X's diagonally, ",
0, MAX_ROWS * ROW_WIDTH);
SSDL_RenderText("and two O's in the bottom row.",
0, MAX_ROWS * ROW_WIDTH + TEXT_LINE_HEIGHT);
SSDL_WaitKey();
return 0;
}
void display (const Square board[MAX_ROWS][MAX_COLS])
{
// Make 'em static: loaded once, and local to the only function
// that needs 'em. What's not to like?
static const SSDL_Image X_IMAGE = SSDL_LoadImage("media/X.png");
static const SSDL_Image O_IMAGE = SSDL_LoadImage("media/O.png");
// draw the X's and O's
for (int row = 0; row < MAX_ROWS; ++row)
for (int col = 0; col < MAX_COLS; ++col)
switch (board[row][col])
{
case Square::X: SSDL_RenderImage(X_IMAGE,
col*COL_WIDTH, row*ROW_WIDTH);
break;
case Square::O: SSDL_RenderImage(O_IMAGE,
col*COL_WIDTH, row*ROW_WIDTH);
}
// draw the lines for the board: first vertical, then horizontal
// doing this last stops X and O bitmaps from covering the lines
constexpr int LINE_THICKNESS = 5;
SSDL_RenderFillRect(COL_WIDTH - LINE_THICKNESS / 2, 0,
LINE_THICKNESS, BOARD_HEIGHT);
SSDL_RenderFillRect(COL_WIDTH * 2 - LINE_THICKNESS / 2, 0,
LINE_THICKNESS, BOARD_HEIGHT);
SSDL_RenderFillRect(0, ROW_WIDTH - LINE_THICKNESS / 2,
BOARD_WIDTH, LINE_THICKNESS);
SSDL_RenderFillRect(0, ROW_WIDTH*2- LINE_THICKNESS / 2,
BOARD_WIDTH, LINE_THICKNESS);
}
Example 10-5Initializing and displaying a Tic-Tac-Toe (noughts and crosses) board
为了编写display
的参数列表,我们在()
之间复制ticTacToeBoard
的定义
但是与一维数组不同的是,你不能随意省略[]
之间的数字。正如我们在图 10-2 中看到的,C++ 使用MAX_COLS
来确定元素的内存位置。你可以省略掉第一个维度,但这并不能让它变得更清楚,所以我没有。
图 10-3
井字游戏棋盘
Tip
示例 10-5 不仅仅显示适当的输出;它打印出输出应该是什么。这是不是矫枉过正?
我不这么认为。在屏幕上看到结果要比搜索代码容易得多。稍后我们会看到更懒惰的测试方法。
二维以上的数组
在前面的例子中,我们的数组是二维的。我们能有三维数组吗?4-D?你可能想要多少维度就有多少维度。
若要初始化三维数组,请使用另一组嵌套的{}
。
但我发现的三维阵列的唯一用途是 1971 年基于文本的星际迷航游戏(在“象限”之间与克林贡船只战斗)和三维井字游戏。我从来没有发现四维数组的用处。如果你找到了,别告诉我。有些事情我不想知道。
防错法
你的二维数组中的东西进入了错误的位置。当你指的是
col
时使用row
或者当你指的是row
时使用col
可能会导致这种情况。最好的预防是在你所谓的行和列中保持一致:不要有时用
row
、col
、有时用x
、y
,有时用i
、j
。永远用row
、col
。也可以用row1
、col1
或rowStart
、colStart
,但总是名称中带row
或col
的东西。
Exercises
在这些练习和后续练习中,如果出现问题,请记住使用调试器。
做一个棋盘:八行八列,明暗相间的方格。
将棋子放在棋盘上,进行初始游戏配置:如图所示,交替放置方块。
对于棋盘,创建一个函数,对给定颜色的棋盘进行计数并返回计数。
…现在是一个决定哪一面有更多棋子的函数。如果两者都没有更多,它可以返回
EMPTY
。编写一个函数,该函数采用一个棋盘、一个棋子的位置和一个方向
LEFT
或RIGHT
(使用enum
),并返回该棋子是否可以在该方向上移动。一个棋子可以对角向前移动一格到一个空的方格,或者对角向前移动两格,跳过对手的棋子到一个空的方格。(更难)在游戏内存中,你有(比如说)八副牌,每副牌显示一个相同的图像。他们面朝下在一个 4
×
4 的格子里发牌;玩家选两张,把它们翻过来,如果它们是一样的,那两张卡就被拿走。在相对较少的回合中找到所有匹配的对子,你就赢了。制作一个程序来玩这个游戏。(肯定先写算法。)让用户点击一副牌;通过用“卡片正面”图像替换“卡片背面”图像来显示卡片;等待用户看到卡片正面(使用
SSDL_Delay
);然后,如果有一个匹配,不替换图像,增加玩家的分数,否则用“卡片背面”图像再次替换它们。根据是否匹配播放不同的声音。重复,直到所有的卡都匹配或玩家已经超过了一些最大的回合数。您将需要代码来识别鼠标在一个框区的点击。
(用力)写一个完整的井字游戏。对于计算机移动,你可以选择一个随机的位置进行下一次移动。或者你可以做一些更难的事情,让计算机计算出什么是好的一步。
(硬)玩连四。在这个游戏中,你有一个最初的空格子,玩家轮流将代币放入顶行。代币会自动尽可能远地下落:它不能越过最下面一行,也不能进入被占领的方块。赢家是在任何方向连续得到四个的人。
十一、使用结构和精灵的动画
是时候拍些电影了(不久之后,还有街机游戏)。我们需要更多的功能。
struct
年代
struct
是一种捆绑信息的方式:
struct <name>
{
<variable declaration>*
};
例如,这里有一种我们已经需要了一段时间的类型:几何点。它有两部分,x 和 y:
struct Point2D
{
int x_, y_;
};
(后面的_
是一个约定,意思是“其他事物的成员”我们将在第十六章中看到为什么这是值得考虑的。)
这个版本更好。我们将在struct
中构建默认值。0 是一个很好的默认值:
struct Point2D
{
int x_= 0, y_= 0;
};
现在我们可以使用新的类型来声明点:
Point2D p0; // The x_and y_ members are both 0
// since we made that the default
你可以像初始化数组一样初始化一个struct
——用一个有支撑的列表: 1
Point2D p1 = {0, 5}; // x is 0, y is 5
…但与数组不同的是,您可以在初始化后使用{}
创建一个新值:
p1 = {1, 5}; // now x is 1
functionThatExpectsAPoint ({2,6}); // make a Point2D on the fly
要获取Point2D
的各个部分,请使用。:
p1.x_ += AMOUNT_TO_MOVE_X;
p1.y_ += AMOUNT_TO_MOVE_Y;
SSDL_RenderDrawCircle (p1.x_, p1.y_, RADIUS);
应该可以了。
为什么有struct
s?
清晰:把一个点想成一个点,比想成一个 x 和一个 y 更容易。
更短的参数列表:检测鼠标点击是否在框内的函数不再需要像
bool containsClick (int x, int y, int xLeft, int xRight, int yTop, int yBottom);
中那样有六个参数
而是三个,就像
- 数组:假设你希望你的宇宙中有多个对象(看起来很有可能!).每个都有一个 x,y 坐标。你怎么能把这些排列起来呢?将 x 和 y 捆绑成一个
Point2D
,并有一个这样的数组:
bool containsClick (Point2D p,
Point2D upperLeft, Point2D lowerRight);
Point2D myObjects[MAX_OBJECTS];
要初始化它们,您可以使用初始化列表:
Point2D myObjects[MAX_OBJECTS] = {{1, 5}, {2, 3}};
示例 11-1 显示了这种新型的用途。输出如图 11-1 所示。
图 11-1
阶梯计划
// Program to draw a staircase
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
struct Point2D // A struct to hold a 2-D point
{
int x_, y_;
};
int main (int argc, char** argv)
{
SSDL_SetWindowSize (400, 200);
SSDL_SetWindowTitle ("Stairway example: Hit a key to end");
constexpr int MAX_POINTS = 25;
constexpr int STAIR_STEP_LENGTH = 15;
Point2D myPoints [MAX_POINTS];
int x = 0; // Start at lower left corner
int y = SSDL_GetWindowHeight()-1; // of screen
for (int i = 0; i < MAX_POINTS; ++i) // Fill an array with points
{
myPoints[i] = { x, y };
// On iteration 0, go up (change Y)
// On iteration 1, go right
// then up, then right, then up...
if (i%2 == 0) // If i is even...
y -= STAIR_STEP_LENGTH;
else
x += STAIR_STEP_LENGTH;
}
for (int i = 0; i < MAX_POINTS-1; ++i) // Display the staircase
// The last iteration draws a line from point
// i to point i+1... which is why we stop a
// little short. We don't want to refer to
// the (nonexistent) point # MAX_POINTS.
SSDL_RenderDrawLine ( myPoints[i ].x_, myPoints[i ].y_,
myPoints[i+1].x_, myPoints[i+1].y_);
SSDL_WaitKey();
return 0;
}
Example 11-1Staircase program, illustrating struct Point2D
Exercises
编写并测试前面给出的
containsClick
函数(点击并输入信息的函数)。Make an array of
Point2D
s, and set the value of each with this function:Point2D pickRandomPoint (int range) { Point2D where; where.x_ = rand()%range + rand()%range + rand()%range; where.y_ = rand()%range + rand()%range + rand()%range; return where; }
展示它们,注意:它们是均匀分布的吗?这显示了当你对随机数求和时会发生什么。
用struct
和while
拍电影
想想电影是怎么拍出来的。你看到一个接一个的静止画面,但是它们来得如此之快,看起来就像一个连续的运动图像。
我们会做同样的事情。一部真正的电影有特定的速度——每秒帧数——所以运动的速率总是相同的。我们将告诉 C++ 也保持一个恒定的帧速率。
这里有一个粗略的版本:
SSDL_SetFramesPerSecond (70); // Can change this,
// or leave at the default of 60
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (); // erase previous frame
// display things (draw shapes and images, print text, etc.)
// update variables if needed
// get input, if relevant...
}
等待足够的时间,以进入电影的下一帧。它还会刷新屏幕。它将每秒 60 帧,除非我们用SSDL_SetFramesPerSecond
改变它。如果用户试图通过关闭窗口或按下退出键来退出,则SSDL_IsNextFrame
返回false,
,循环结束。
但是必须要检查那些退出消息。之前我们用的是SSDL_WaitKey
、SSDL_WaitMouse
、SSDL_Delay
。因为我们现在没有使用它们,所以必须用其他东西来检查退出消息。
这个东西就是SSDL_DefaultEventHandler
,它处理事件——来自操作系统的消息告诉程序,“发生了一些你可能关心的事情”,比如退出请求:
void SSDL_DefaultEventHandler ()
{
SDL_Event event;
while (SSDL_PollEvent (event))
switch (event.type)
{
case SDL_QUIT: // clicked the X on the window? Let's quit
SSDL_DeclareQuit(); break;
case SDL_KEYDOWN: // User hit Escape? Let's quit
if (SSDL_IsKeyPressed (SDLK_ESCAPE)) SSDL_DeclareQuit();
}
}
SDL_Event
是一个struct
,它存储 SDL 识别的任何类型事件的信息。SSDL_PollEvent
获取下一个可用的事件,如果有的话,如果没有则失败;但是如果找到一个,它会将信息存储在event
中,然后由switch
语句决定如何处理它。
让我们在程序中使用它来让一个球在屏幕上来回移动。呜-呼!输出如图 11-2 所示。
图 11-2
在屏幕上来回移动的球
// Program to make a circle move back and forth across the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
constexpr int RADIUS = 20; // Ball radius & speed
constexpr int SPEED = 5; // ...move 5 pixels for every frame
enum class Direction { LEFT=-1, RIGHT=1 };
// Why -1 for left? Because left means going in the minus direction.
// See where we update the x_ in the main loop for how this can work
struct Point2D
{
int x_=0, y_=0;
};
struct Ball // A ball is an X, Y location,
{ // and a direction, left or right
Point2D location_;
Direction direction_;
};
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Back-and-forth ball example. "
"Hit Esc to exit.");
// initialize ball position; size; rate and direction of movement
Ball ball;
ball.location_ = { SSDL_GetWindowWidth () / 2,
SSDL_GetWindowHeight() / 2 };
ball.direction_ = Direction::RIGHT;
constexpr int FRAMES_PER_SECOND = 70;
SSDL_SetFramesPerSecond (FRAMES_PER_SECOND);
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
// *** DISPLAY THINGS ***
SSDL_RenderClear (); // first, erase previous frame
// then draw the ball
SSDL_RenderDrawCircle (ball.location_.x_, ball.location_.y_, RADIUS);
// *** UPDATE THINGS ***
// update ball's x position based on speed
// and current direction
ball.location_.x_ += int(ball.direction_)*SPEED;
// if ball moves off screen, reverse its direction
if (ball.location_.x_ >= SSDL_GetWindowWidth())
ball.direction_ = Direction::LEFT;
else if (ball.location_.x_ < 0)
ball.direction_ = Direction::RIGHT;
}
return 0;
}
Example 11-2A ball moving back and forth across the screen
如果我们希望不止一个物体运动呢?我们可以有一个数组Ball
并使用 for 循环来初始化它们,显示它们,等等。
越来越长,越来越不清楚,所以我将把几个任务放在它们各自的功能中。示例 11-3 的输出如图 11-3 所示。
图 11-3
多个移动球的示例
// Program to make circles move back and forth across the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
constexpr int RADIUS = 20; // Ball radius & speed
constexpr int SPEED = 5; // ...move 5 pixels for every frame
enum class Direction { LEFT=-1, RIGHT=1 };
struct Point2D
{
int x_=0, y_=0;
};
struct Ball // A ball is an X, Y location,
{ // and a direction, left or right
Point2D location_;
Direction direction_;
};
// Ball functions
void initializeBalls ( Ball balls[], int howMany);
void drawBalls (const Ball balls[], int howMany);
void moveBalls ( Ball balls[], int howMany);
void bounceBalls ( Ball balls[], int howMany);
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Back-and-forth balls example. "
"Hit Esc to exit.");
// initialize balls' position, size, and rate and direction
constexpr int MAX_BALLS = 3;
Ball balls [MAX_BALLS];
initializeBalls (balls, MAX_BALLS);
constexpr int FRAMES_PER_SECOND = 70;
SSDL_SetFramesPerSecond(FRAMES_PER_SECOND);
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
// *** DISPLAY THINGS ***
SSDL_RenderClear (); // first, erase previous frame
drawBalls (balls, MAX_BALLS);
// *** UPDATE THINGS ***
moveBalls (balls, MAX_BALLS);
bounceBalls(balls, MAX_BALLS); // if ball moves offscreen,
// reverse its direction
}
return 0;
}
// Ball functions
void initializeBalls (Ball balls[], int howMany)
{
for (int i = 0; i < howMany; ++i)
{
balls[i].location_ = { i * SSDL_GetWindowWidth () / 3,
i * SSDL_GetWindowHeight() / 3
+ SSDL_GetWindowHeight() / 6 };
balls[i].direction_ = Direction::RIGHT;
}
}
void drawBalls (const Ball balls[], int howMany)
{
for (int i = 0; i < howMany; ++i)
SSDL_RenderDrawCircle (balls[i].location_.x_,
balls[i].location_.y_, RADIUS);
}
// update balls' x position based on speed and current direction
void moveBalls (Ball balls[], int howMany)
{
for (int i = 0; i < howMany; ++i)
balls[i].location_.x_ += int (balls[i].direction_)*SPEED;
}
void bounceBalls(Ball balls[], int howMany)
{
// if any ball moves off screen, reverse its direction
for (int i = 0; i < howMany; ++i)
if (balls[i].location_.x_ >= SSDL_GetWindowWidth())
balls[i].direction_ = Direction::LEFT;
else if (balls[i].location_.x_ < 0)
balls[i].direction_ = Direction::RIGHT;
}
Example 11-3An example with multiple moving balls
Exercises
Make the balls capable of moving in other directions. A ball is no longer just an x, y location and a direction; it’s an x, y location and an x, y velocity. Each time you go through the main loop, you’ll update the location based on the velocity:
for (int i = 0; i < MAX_BALLS; ++i) { balls[i].location_.x_ += balls[i].velocity_.x_; balls[i].location_.y_ += balls[i].velocity_.y_; }
当速度碰到左墙或右墙时,速度的 x 分量总是反向的——如果速度碰到地板或天花板,速度的 y 分量也是如此。用
Point2D
代替velocity_
也可以,或者你可以为它创建一个新的struct
。每当球击中墙壁时添加音效。
Now let’s add gravity. Velocity doesn’t just change each time you hit a wall; it changes in each iteration of the loop, like so:
for (int i = 0; i < MAX_BALLS; ++i) balls[i].velocity_.y_+= GRAVITY; // adjust velocity for gravity
现在球应该更真实地移动。
现在加上摩擦力。每当球碰到墙时,它的速度并不完全相反;而是反过来,只是比原来小了一点。这将使球在每次碰撞后变慢。
鬼怪;雪碧
圈子够多了。让我们移动图像。
我们已经有了图像,但它们只是放在那里。精灵是移动的图像:它们可以移动、旋转、翻转和做其他事情。以下是基本情况。
创建精灵就像创建图像一样:
SSDL_Sprite mySprite = SSDL_LoadImage ("filename.png");
你现在可以用SSDL_SetSpriteLocation
设置它的位置,用SSDL_SetSpriteSize
设置它的大小。
在示例 11-4 中,我使用 sprite 将一条鱼放在屏幕中间——也许我要制作一个视频水族馆——并使用其他 sprite 函数打印一些关于它的信息。与 sprite 相关的代码会突出显示。输出如图 11-4 所示。
// Program to place a fish sprite on the screen
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
using namespace std;
int main (int argc, char** argv)
{
// Set up window characteristics
constexpr int WINDOW_WIDTH = 600, WINDOW_HEIGHT = 300;
SSDL_SetWindowSize (WINDOW_WIDTH, WINDOW_HEIGHT);
SSDL_SetWindowTitle ("Sprite example 1\. Hit Esc to exit.");
// initialize colors
const SSDL_Color AQUAMARINE(100, 255, 150); // the water
// initialize the sprite's image and location
SSDL_Sprite fishSprite = SSDL_LoadImage("media/discus-fish.png");
SSDL_SetSpriteLocation (fishSprite,
SSDL_GetWindowWidth ()/2,
SSDL_GetWindowHeight()/2);
// *** Main loop ***
while (SSDL_IsNextFrame ())
{
// Look for quit messages
SSDL_DefaultEventHandler ();
// Clear the screen for a new frame in our "movie"
SSDL_RenderClear (AQUAMARINE);
// Draw crosshairs in the center
SSDL_SetRenderDrawColor (BLACK);
SSDL_RenderDrawLine (0, SSDL_GetWindowHeight()/2,
SSDL_GetWindowWidth (),
SSDL_GetWindowHeight()/2);
SSDL_RenderDrawLine (SSDL_GetWindowWidth ()/2, 0,
SSDL_GetWindowWidth ()/2,
SSDL_GetWindowHeight());
// and print the statistics on the fish
SSDL_SetCursor (0, 0); // reset cursor each time or
// the messages will run off
// the screen!
sout << "Sprite info\n";
sout << "X:\t"
<< SSDL_GetSpriteX
(fishSprite) << endl;
sout << "Y:\t"
<< SSDL_GetSpriteY
(fishSprite) << endl;
sout << "Width:\t"
<< SSDL_GetSpriteWidth
(fishSprite) << endl;
sout << "Height:\t"
<< SSDL_GetSpriteHeight
(fishSprite) << endl;
// Show that fish
SSDL_RenderSprite
(fishSprite);
}
return 0;
}
Example 11-4Program to draw a fish, using a sprite
图 11-4
一个精灵和它目前的一些规格
我认为这条鱼太大了。根据程序,它的宽度是 225,高度是 197。我们可以把SSDL_SetSpriteSize
放在主循环之前,来调整它的大小: 2
constexpr int FISH_WIDTH = 170, FISH_HEIGHT = 150;
SSDL_SetSpriteSize (fishSprite, FISH_WIDTH, FISH_HEIGHT);
图 11-5
一个精灵,调整大小
如图 11-5 所示:现在我要它居中。我给它的 x,y 位置是中心的…但那是图像的左上角。
下面是偏移精灵的调用,因此它以我们给定的点为中心作为它的位置:
SSDL_SetSpriteOffset (fishSprite, FISH_WIDTH/2, FISH_HEIGHT/2);
如果它看起来仍然偏离中心,我可以用数字来得到不同的偏移量。
我不会重复整个程序,但示例 11-5 显示了改变精灵大小和居中的代码行。结果如图 11-6 所示。
图 11-6
一个精灵,调整大小并居中
int main (int argc, char** argv)
{
...
// Init size and offset. Image is offset so fish looks centered.
constexpr int FISH_WIDTH = 170, FISH_HEIGHT = 150;
SSDL_SetSpriteSize (fishSprite, FISH_WIDTH, FISH_HEIGHT);
// This offset looks right on the screen, so I'll use it:
SSDL_SetSpriteOffset (fishSprite,
FISH_WIDTH/2, int(FISH_HEIGHT*0.55));
...
return 0;
}
Example 11-5Code to resize and center a sprite
你可以用精灵做其他事情:旋转,水平或垂直翻转,或者只使用原始图像的一部分。您还可以对它们做任何您可以对图像做的事情——例如,SSDL_RenderImage (mySprite)
。这将忽略精灵的其他特征(位置,大小等)。)并且只使用图像方面。
现在你有了这个,你就可以(几乎)制作自己的街机游戏了。
防错法
- 雪碧没有出现。以下是可能的原因:
图像未加载。你在错误的文件夹中寻找,在文件名中打了一个错别字,或者正在使用一个坏的或不兼容的图像。
它出现了,但不在屏幕上。从
SSDL_GetSpriteX
和SSDL_GetSpriteY
得到什么数字?确保他们在射程内。
Exercises
表 11-1
常见的 sprite 命令。完整列表见附录 h。
| `SSDL_Sprite` `mySprite = SSDL_LoadImage ("image.png");` | 这就是如何创建一个。 | | `void``SSDL_SetSpriteLocation` | 设置精灵在屏幕上的位置。 | | `void``SSDL_SetSpriteSize` | …以及它的大小。 | | `void``SSDL_SetSpriteOffset` | …它的偏移量。 | | `void``SSDL_SetSpriteRotation` | …它的旋转角度。 | | `void``SSDL_RenderSprite` | 在当前位置绘制精灵。 | | `int``SSDL_GetSpriteX` | 返回精灵在屏幕上的`x`位置。 | | `int``SSDL_GetSpriteY` | …和它的`y`。 |制作一个视频水族馆:一个背景和来回移动的鱼(面向它们去的任何方向,所以你需要
SSDL_SpriteFlipHorizontal
)。做上一节的练习 2 或 3(弹跳球),但是不要画圆圈,而是用一个篮球的图像。让篮球旋转(见表 11-1 了解你需要的功能)。
十二、制作一个街机游戏:输入,碰撞,并把它们放在一起
在这一章中,我们将制作我们自己的 2-D 街机游戏,把我们到目前为止所获得的浪费时间的体验放在一起,让其他人偷懒,这样我们就可以发光——或者类似的事情。我们需要的新事物是更好的鼠标和键盘交互以及对象的碰撞。
确定输入状态
老鼠
我们已经可以等待鼠标点击,并获得其坐标…但街机游戏不等人。
假设我们想让我们的武器在鼠标按键按下时持续开火。我们需要一种方法来检测按钮被按下,而不需要停下来等待。这就行了。
| `int SSDL_GetMouseClick ();` | 如果没有按钮被按下,返回 0;`SDL_BUTTON_LMASK`(左键被按下)、`SDL_BUTTON_MMASK`(中间)或`SDL_BUTTON_RMASK`(右边)。 |...如在
if (SSDL_GetMouseClick () != 0) // mouse button is down
{
x = SSDL_GetMouseX(); y = SSDL_GetMouseY();
// do whatever you wanted to do if mouse button is down
}
在给出一个例子之前,让我们看看如何检查键盘的状态。
键盘
ssin
等待你按回车键。这对于街机游戏来说是行不通的;我们想知道一个键是否一被按下就被按下。函数SSDL_IsKeyPressed
告知给定的键是否被按下——任何键,包括与字母无关的键,如 Shift 和 Ctrl:
尽管这个函数接受的许多键值都与您预期的相符(0 键用'0'
,A 键用'a'
——但 A 键用'A'
!),并不总是很明显,所以最好用他们的正式名字。在撰写本文时,完整的列表在wiki.libsdl.org/SDL_Keycode
;一些列在表 12-1 中。示例 12-1 展示了如何使用它们。
表 12-1
SDL 的选定键码
| `SDLK_1``SDLK_2``...``SDLK_a``SDLK_b``...` | `SDLK_F1``SDLK_F2``...` | `SDLK_ESCAPE``SDLK_BACKSPACE``SDLK_RETURN``SDLK_SPACE` | `SDLK_LEFT``SDLK_RIGHT``SDLK_UP``SDLK_DOWN` | `SDLK_LSHIFT``SDLK_RSHIFT``SDLK_LCTRL``SDLK_RCTRL` |// Program to identify some keys, and mouse buttons, being pressed
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
int main (int argc, char** argv)
{
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (); // Clear the screen
SSDL_SetCursor (0, 0); // And start printing at the top
sout << "What key are you pressing? ";
sout << "Control, Shift, Caps lock, space, F1?\n";
if (SSDL_IsKeyPressed (SDLK_LCTRL)) sout << "Left ctrl ";
if (SSDL_IsKeyPressed (SDLK_RCTRL)) sout << "Right ctrl ";
if (SSDL_IsKeyPressed (SDLK_LSHIFT)) sout << "Left shift ";
if (SSDL_IsKeyPressed (SDLK_RSHIFT)) sout << "Right shift ";
if (SSDL_IsKeyPressed (SDLK_CAPSLOCK))sout << "Caps lock ";
if (SSDL_IsKeyPressed (SDLK_SPACE)) sout << "Space bar ";
if (SSDL_IsKeyPressed (SDLK_F1)) sout << "F1 ";
if (SSDL_IsKeyPressed (SDLK_ESCAPE)) break;
sout << "\n";
if (SSDL_GetMouseClick () == SDL_BUTTON_LMASK)
sout << "Left mouse button down\n";
if (SSDL_GetMouseClick () == SDL_BUTTON_RMASK)
sout << "Right mouse button down\n";
sout << "(Hit Esc to exit.)";
}
return 0;
}
Example 12-1A program to detect control keys, Shift, Caps Lock, space bar, and F1
防错法
你按下了一个功能键,但没有任何反应。在某些键盘上,你还必须按住 Fn 键。
**您同时按下多个键,但只有一些键会显示出来;或你按下一个键,它不会注册鼠标按钮。**键盘“重影”正在丢失按键,因为键盘一次只能处理这么多。它也可能会失去鼠标点击。在写的时候,如果你在意,可以在
https://keyboardtester.co/
测试你的键盘鼠标。
将 Ctrl、Shift 和 Alt 与其他键一起使用可能是安全的——这是意料之中的。
事件
有时我们并不关心鼠标按钮当前是向上还是向下,只关心它是否被点击过。也许你每次点击都能从你的 BFG 中得到一个镜头。或者您的程序使用鼠标来打开或关闭声音,如下面的代码所做的那样(或者至少尝试这样做):
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
if (SSDL_GetMouseClick ()) toggleSound (); // not gonna work
...
}
问题是,电脑很快。假设你的鼠标点击需要十分之一秒。以每秒 60 帧的速度,在你松开按钮之前,声音会打开和关闭六次,如果它以你想要的方式结束,那才是好运。
我们需要的是将点击本身检测为鼠标点击事件,就像第十一章中处理的退出事件。我们将有一个替代者SSDL_DefaultEventHandler
来检测该事件并将其报告给main
,这样main
就可以切换音乐(例如 12-2;截图如图 12-1 。
void myEventHandler (bool& mouseClicked) // replaces SSDL_DefaultEventHandler
{
SDL_Event event;
mouseClicked = false; // We'll soon know if mouse was clicked
while (SSDL_PollEvent (event))
switch (event.type)
{
case SDL_QUIT: SSDL_DeclareQuit();
break;
case SDL_KEYDOWN: if (SSDL_IsKeyPressed (SDLK_ESCAPE))
SSDL_DeclareQuit();
break;
case SDL_MOUSEBUTTONDOWN: mouseClicked = true; // It was!
break;
}
}
// and the following in main:
while (SSDL_IsNextFrame ())
{
bool mouseWasClicked;
myEventHandler (mouseWasClicked);
if (mouseWasClicked) toggleSound ();
....
}
Example 12-2Making your own event handler. For the complete program, see source code, ch12 folder; the project is 2-aliensBuzzOurWorld
图 12-1
一个用鼠标点击来切换声音的程序
这种思维方式——事件驱动——是 Windows、iOS 和 Android 等重要操作系统编程的核心。
冷却时间和寿命
假设我们想要一个效果在某件事情发生后持续一会儿。也许有一种视觉效果在鼠标点击创造出来后只存在一秒钟(它的“寿命”)。或者你的 BFG 需要等待一段时间才能再次开火,无论你点击得多疯狂——这是一个“冷却”期。
我们将有一个整数framesLeftTillItsOver
,当鼠标被点击时,它将被设置为你想要延迟的帧数。
这个推理不行:
framesLeftTillItsOver = 0 //effect is currently inactive
while SSDL_IsNextFrame ()
handle events
SSDL_RenderClear ()
draw things
if framesLeftTillItsOver == 0 && mouseWasClicked
framesLeftTillItsOver = HOWEVER MANY FRAMES WE WANT IT TO LAST
draw the visual effect
--framesLeftTillItsOver // 1 frame closer to disappearance
倒计时是可以的,但是当你绘制效果时,SSDL_RenderClear
会在下一次迭代中删除它!
我们必须将改变视觉效果的状态(从关到开)的与绘制它的的区分开来;它们是独立的行动。这将起作用:
framesLeftTillItsOver = 0 //effect is currently inactive
while SSDL_IsNextFrame ()
handle events
SSDL_RenderClear ()
draw things including, if it's on, the visual effect
(consider it to be on if framesLeftTillItsOver > 0)
if effect is on (that is, framesLeftTillItsOver > 0)
-- framesLeftTillItsOver; // 1 frame closer to disappearance
else if mouseWasClicked
framesLeftTillItsOver = HOWEVER MANY FRAMES WE WANT IT TO LAST
Tip
根据经验,在主动画循环中,有三个独立的部分:处理事件、绘图和更新变量。顺序并不重要,因为它们最终都会完成;重要的是,不要在更新部分绘制,不要在绘制部分检查事件,等等。
在示例 12-3 中,当你点击鼠标时,程序会在你点击的任何地方放置一个飞溅的图像。一秒钟后,它会删除图像,让您再次点击。
// Program that makes a splat wherever you click
// -- from _C++20 for Lazy Programmers_
#include "SSDL.h"
void myEventHandler (bool& mouseClicked);
int main (int argc, char** argv)
{
SSDL_SetWindowTitle ("Click the mouse to see and hear a splat; "
"hit Esc to end.");
const SSDL_Sound SPLAT_SOUND =
SSDL_LoadWAV ("media/445117__breviceps__cartoon-splat.wav");
// Set up sprite with image and a size, and offset its reference
// point so it'll be centered on our mouse clicks
SSDL_Sprite splatSprite = SSDL_LoadImage ("media/splat.png");
constexpr int SPLAT_WIDTH=50, SPLAT_HEIGHT=50;
SSDL_SetSpriteSize (splatSprite, SPLAT_WIDTH, SPLAT_HEIGHT);
SSDL_SetSpriteOffset(splatSprite, SPLAT_WIDTH/2, SPLAT_HEIGHT/2);
while (SSDL_IsNextFrame ())
{
static int framesLeftTillSplatDisappears = 0;
static constexpr int SPLAT_LIFETIME = 60; // It lasts one // second
// Handle events
bool isMouseClick;
myEventHandler (isMouseClick);
// Display things
SSDL_RenderClear();
if (framesLeftTillSplatDisappears > 0)
SSDL_RenderSprite(splatSprite);
// Update things: process clicks and framesLeft
if (framesLeftTillSplatDisappears > 0) // if splat is active
--framesLeftTillSplatDisappears; // keep counting down
else if (isMouseClick) // if not, and we have a click...
{
// Reset that waiting time
framesLeftTillSplatDisappears = SPLAT_LIFETIME;
// Play splat sound
SSDL_PlaySound (SPLAT_SOUND);
SSDL_SetSpriteLocation // move splat sprite to
(splatSprite, // location of mouse click
SSDL_GetMouseX(),
SSDL_GetMouseY());
}
}
return 0;
}
void myEventHandler (bool& mouseClicked)
{
// exactly the same as in Example 12-2
}
Example 12-3Using a visual effect with specified duration: splatter on the screen
Exercises
修改示例 12-3 中的程序,以允许多个飞溅同时存在;你可以每秒发射一次,但每次飞溅持续 5 秒。
一个粒子喷泉是一组不断产生的粒子,向各个方向运动。这就是电脑游戏和暴雨中火焰产生的原因。你也可以做一个泡泡喷泉或烟火。
让每个粒子从相同的位置开始,有一个初始随机速度。随着主循环的每次迭代,用
SSDL_RenderDrawPoint
绘制粒子。同样更新它的位置,如果你想让它看起来更像火焰,使用重力,但是方向相反;火焰粒子倾向于向上飞而不是向下飞。最后,当一个粒子已经存在了一定数量的帧,重置它到它的起点,让它再次去。
碰撞
在制作我们自己的游戏之前,我们还需要一样东西:碰撞。
用 SSDL 精灵检测碰撞很容易:
| `int` `SSDL_SpriteHasIntersection``(const SSDL_Sprite& a, const SSDL_Sprite& b);` | 返回子画面`a`和`b`是否重叠。 |就像:
if (SSDL_SpriteHasIntersection (robotSprite, playerSprite))
playerDead = true;
精灵的碰撞很容易,但并不总是准确的。因为你的精灵可能有很大一部分是透明的,你可能会发现 SDL 认为两个精灵在碰撞,即使可见的部分没有接触。没有一个理智的人会认为图 12-2 (a)中的糖果和万圣节篮子是冲突的——但 SSDL 会。
图 12-2
精灵之间的碰撞,由SSDL_SpriteHasIntersection
(a)和函数inCollision
(b)的基于圆的方法评估
这里有一个简单的解决方法:找出两点(可能是精灵的中心)之间的距离,如果该距离小于我们给物体的半径之和aSize
和bSize
——也就是说,如果边界圆相交(图 12-2 (b)),就认为发生了碰撞:
bool inCollision (Point2D A, Point2D B, int aSize, int bSize)
{
float aToBDistance = distance (A.x_, A.y_, B.x_, B.y_);
// see Chapter 7's first set of exercises --
// or Example 12-7 below --
// for the distance function
return (aToBDistance < aSize + bSize);
}
使用最适合你的精灵的方法。
大游戏
本章的其余部分是关于创建街机游戏:首先是我的,然后是你的。
我这里有一个在篮子里抓万圣节糖果的游戏(例子 12-4 到 12-8 )。如果你抓到足够多,你就赢了;错过太多,你就会死。它有声音、背景、愚蠢的图形和键盘交互(使用箭头键),为了显示鼠标的使用,我允许用户切换平视显示器(HUD ),显示失误和失误的统计数据。为了说明如何指定效果的生存期,一个浮动的“Yum!”当你抓到糖果时,信息会出现一会儿。输出如图 12-3 所示。
// Program to catch falling Hallowe'en candy
// -- from _C++20 for Lazy Programmers_
#include <cmath> // for sqrt
#include "SSDL.h"
// dimensions of screen and screen locations
constexpr int SCREEN_WIDTH=675, SCREEN_HEIGHT=522; // dimensions of bkgd
constexpr int CANDY_START_HEIGHT = 15; // where candy falls from
constexpr int MARGIN = 25; // As close to the left/right
// edges of the screen as moving // objects are allowed to get
constexpr int BOTTOM_LINE = 480; // Where last line of text is printed
// on instruction & splash screens
// dimensions of important objects
constexpr int CANDY_WIDTH = 60, CANDY_HEIGHT = 20;
constexpr int BASKET_WIDTH = 70, BASKET_HEIGHT = 90;
// how many candies you can catch or miss before winning/losing
constexpr int MAX_CAUGHT = 10, MAX_MISSED = 10;
// If you change this, change
// printInstructions too
// because it specifies this
// fonts for splash screens and catch/miss statistics
constexpr int SMALL_FONT_SIZE = 12,
MEDIUM_FONT_SIZE = 24,
LARGE_FONT_SIZE = 36;
const SSDL_Font SMALL_FONT
= SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
SMALL_FONT_SIZE);
const SSDL_Font MEDIUM_FONT
= SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
MEDIUM_FONT_SIZE);
const SSDL_Font LARGE_FONT
= SSDL_OpenFont ("media/Sinister-Fonts_Werewolf-Moon/Werewolf Moon.ttf",
LARGE_FONT_SIZE);
// how far our victory/defeat messages are from left side of screen
constexpr int FINAL_SCREEN_MESSAGE_OFFSET_X = 40;
// background
const SSDL_Image BKGD_IMAGE
= SSDL_LoadImage("media/haunted-house.jpg");
// sounds and music
const SSDL_Music BKGD_MUSIC
= SSDL_LoadMUS("media/159509__mistersherlock__halloween-graveyd-short.mp3");
const SSDL_Sound THUNK_SOUND
= SSDL_LoadWAV("media/457741__osiruswaltz__wall-bump-1.wav");
const SSDL_Sound DROP_SOUND
= SSDL_LoadWAV("media/388284__matypresidente__water-drop-short.wav");
// structs
struct Point2D { int x_ = 0, y_ = 0; };
using Vector2D = Point2D;1
struct Object
{
Point2D loc_;
int rotation_ = 0;
Vector2D velocity_;
int rotationSpeed_ = 0;
SSDL_Sprite sprite_;
};
// major functions called by the main program
bool playGame ();
// startup/ending screens to communicate with user
void printInstructions ();
void displayVictoryScreen();
void displayDefeatScreen ();
int main (int argc, char** argv)
{
// set up window and font
SSDL_SetWindowTitle ("Catch the falling candy");
SSDL_SetWindowSize (SCREEN_WIDTH, SCREEN_HEIGHT);
// prepare music
SSDL_VolumeMusic (int (MIX_MAX_VOLUME * 0.1));
SSDL_PlayMusic (BKGD_MUSIC);
// initial splash screen
printInstructions ();
// The game itself
bool isVictory = playGame ();
// final screen: victory or defeat
SSDL_RenderClear (BLACK);
SSDL_HaltMusic ();
if (isVictory) displayVictoryScreen ();
else displayDefeatScreen ();
SSDL_RenderTextCentered("Click mouse to end",
SCREEN_WIDTH/2, BOTTOM_LINE, SMALL_FONT);
SSDL_WaitMouse(); // because if we wait for a key, we're likely
// to have left or right arrow depressed
// when we reach this line... and we'll never
// get to read the final message
return 0;
}
//// Startup/ending screens to communicate with user ////
void printInstructions ()
{
constexpr int LINE_HEIGHT = 40;
SSDL_SetRenderDrawColor (WHITE);
SSDL_RenderTextCentered ("Catch 10 treats in ",
SCREEN_WIDTH/2, 0, MEDIUM_FONT);
SSDL_RenderTextCentered ("your basket to win",
SCREEN_WIDTH/2, LINE_HEIGHT , MEDIUM_FONT);
SSDL_RenderTextCentered ("Miss 10 treats and",
SCREEN_WIDTH/2, LINE_HEIGHT*3 , MEDIUM_FONT);
SSDL_RenderTextCentered ("the next treat is YOU",
SCREEN_WIDTH/2, LINE_HEIGHT*4 , MEDIUM_FONT);
SSDL_RenderTextCentered ("Use arrow keys to move",
SCREEN_WIDTH/2, LINE_HEIGHT*6 , MEDIUM_FONT);
SSDL_RenderTextCentered ("left and right",
SCREEN_WIDTH/2, LINE_HEIGHT*7 , MEDIUM_FONT);
SSDL_RenderTextCentered ("Click mouse to",
SCREEN_WIDTH/2, LINE_HEIGHT*9 , MEDIUM_FONT);
SSDL_RenderTextCentered ("toggle stats display",
SCREEN_WIDTH/2, LINE_HEIGHT*10, MEDIUM_FONT);
SSDL_RenderTextCentered ("Hit any key to continue",
SCREEN_WIDTH/2, BOTTOM_LINE, SMALL_FONT);
SSDL_WaitKey ();
}
void displayVictoryScreen ()
{
// sound and picture
static const SSDL_Sound VICTORY_SOUND
= SSDL_LoadWAV ("media/342153__robcro6010__circus-theme-short.wav");
SSDL_PlaySound (VICTORY_SOUND);
static const SSDL_Image GOOD_PUMPKIN
= SSDL_LoadImage ("media/goodPumpkin.png");
SSDL_RenderImage(GOOD_PUMPKIN, SCREEN_WIDTH / 4, 0);
// victory message
SSDL_SetRenderDrawColor (WHITE);
SSDL_RenderText ("Hooah!",
FINAL_SCREEN_MESSAGE_OFFSET_X, SCREEN_HEIGHT/4,
LARGE_FONT);
constexpr int LINE_DISTANCE_Y = 96; // an arbitrarily chosen number...
SSDL_RenderText ("You won!",
FINAL_SCREEN_MESSAGE_OFFSET_X,
SCREEN_HEIGHT/4+LINE_DISTANCE_Y,
LARGE_FONT);
}
void displayDefeatScreen ()
{
// sound and picture
static const SSDL_Sound DEFEAT_SOUND
= SSDL_LoadWAV ("media/326813__mrose6__echoed-screams-short.wav");
SSDL_PlaySound (DEFEAT_SOUND);
static const SSDL_Image SAD_PUMPKIN
= SSDL_LoadImage ("media/sadPumpkin.png");
SSDL_RenderImage (SAD_PUMPKIN, SCREEN_WIDTH / 4, 0);
// defeat message
SSDL_SetRenderDrawColor (WHITE);
SSDL_RenderText ("Oh, no!", FINAL_SCREEN_MESSAGE_OFFSET_X,
SCREEN_HEIGHT/4, LARGE_FONT);
}
Example 12-4Falling candy program, part one of five. The complete program in source code’s ch12 folder is 4-thru-8-bigGame
到目前为止,我们已经有了这个项目的大致轮廓。我在Object struct
中放了很多信息:位置、速度、精灵信息和旋转。有些并不总是需要的——例如,只有糖果会旋转——但是只有一种类型的Object
会让事情变得更简单。
///////////////////// Initializing /////////////////////////
void resetCandyPosition(Object& candy);
void initializeObjects (Object& basket, Object& candy, Object& yumMessage)
{
// load those images
SSDL_SetSpriteImage (candy.sprite_,
SSDL_LoadImage ("media/candy.png"));
SSDL_SetSpriteImage (basket.sprite_,
SSDL_LoadImage ("media/jack-o-lantern.png"));
SSDL_SetSpriteImage (yumMessage.sprite_,
SSDL_LoadImage ("media/yum.png"));
// two images are the wrong size; we resize them.
SSDL_SetSpriteSize (candy.sprite_, CANDY_WIDTH, CANDY_HEIGHT);
SSDL_SetSpriteSize (basket.sprite_, BASKET_WIDTH, BASKET_HEIGHT);
// move 'em so they're centered on the coords we set for them
SSDL_SetSpriteOffset (candy.sprite_,
CANDY_WIDTH/2, CANDY_HEIGHT/2);
SSDL_SetSpriteOffset (basket.sprite_,
BASKET_WIDTH/2, BASKET_HEIGHT/2);
// put the objects in their starting positions
basket.loc_.x_ = SCREEN_WIDTH / 2;
basket.loc_.y_ = SCREEN_HEIGHT - BASKET_HEIGHT/2;
SSDL_SetSpriteLocation (basket.sprite_,
basket.loc_.x_, basket.loc_.y_);
resetCandyPosition (candy);
// (We don't care about yumMessage position till we make one)
// And set velocities
// basket's can't be specified till we check inputs
constexpr int CANDY_SPEED = 11; //11 pixels per frame, straight down
candy.velocity_.y_ = CANDY_SPEED; //11 per frame straight down
//Increase speeds for faster game
yumMessage.velocity_ = { 1, -1 }; //Up and to the right
// And rotational speeds
candy.rotationSpeed_ = 1; //Candy spins slightly
}
/////////////////////////// Drawing /////////////////////////////
//Display all 3 objects (2 if yumMessage is currently not visible)
void renderObjects (Object basket, Object candy, Object yumMessage,
bool showYumMessage)
{
SSDL_RenderSprite (basket.sprite_);
SSDL_RenderSprite ( candy.sprite_);
if (showYumMessage) SSDL_RenderSprite (yumMessage.sprite_);
}
void renderStats(int Caught, int Missed)
{
// Stats boxes, for reporting how many candies caught and missed
SSDL_SetRenderDrawColor(BLACK);
constexpr int BOX_WIDTH = 90, BOX_HEIGHT = 25;
SSDL_RenderFillRect(0, 0, // Left box
BOX_WIDTH, BOX_HEIGHT);
SSDL_RenderFillRect(SCREEN_WIDTH - BOX_WIDTH, 0, // Right box
SCREEN_WIDTH - 1, BOX_HEIGHT);
// Statistics themselves
SSDL_SetRenderDrawColor(WHITE);
SSDL_SetFont (SMALL_FONT);
SSDL_SetCursor(0, 0); // Left box
sout << "Caught: " << Caught;
SSDL_SetCursor(SCREEN_WIDTH - BOX_WIDTH, 0); // Right box
sout << "Missed: " << Missed;
}
Example 12-5Falling candy program, part two of five
先前在示例 12-5 中声明并在示例 12-6 中定义的resetCandyPosition
,以随机的 X 位置开始屏幕顶部的糖果。它在initializeObjects
中被调用,在handleCatchingCandy
和handleMissingCandy
中再次被调用。
在两个黑盒上打印你抓到或错过的棋子数量,以便于阅读。
//////////////// Moving objects in the world ///////////////////
void resetCandyPosition (Object& candy) // When it's time to drop
// another candy...
{
// Put it at a random X location
candy.loc_.x_ = MARGIN + rand() % (SCREEN_WIDTH - MARGIN);
candy.loc_.y_ = CANDY_START_HEIGHT; // at the top of the screen
SSDL_SetSpriteLocation (candy.sprite_, candy.loc_.x_, candy.loc_.y_);
}
void moveObject (Object& object)
{
object.loc_.x_ += object.velocity_.x_; // Every frame, move object
object.loc_.y_ += object.velocity_.y_; // as specified
SSDL_SetSpriteLocation (object.sprite_, object.loc_.x_, object.loc_.y_);
// ...and spin as specified
object.rotation_ += object.rotationSpeed_;
object.rotation_ %= 360; // angle shouldn't go over 360
// (unlike sin and cos, SDL/SSDL
// functions use angles in degrees)
SSDL_SetSpriteRotation (object.sprite_, object.rotation_);
}
void moveBasket (Object& basket, int basketSpeed)
{
// Let user move basket with left and right arrows
if (SSDL_IsKeyPressed (SDLK_LEFT )) basket.loc_.x_ -= basketSpeed;
if (SSDL_IsKeyPressed (SDLK_RIGHT)) basket.loc_.x_ += basketSpeed;
// ...but don't let the basket touch the sides of the screen
if (basket.loc_.x_ < MARGIN)
basket.loc_.x_ = MARGIN;
if (basket.loc_.x_ > SCREEN_WIDTH - MARGIN)
basket.loc_.x_ = SCREEN_WIDTH - MARGIN;
// Tell the sprite about our changes on X
SSDL_SetSpriteLocation (basket.sprite_,
basket.loc_.x_, basket.loc_.y_);
}
Example 12-6Falling candy program, part three of five
moveObject
叫上了糖果和百胜!消息。球员控制篮筐,所以需要它自己的moveBasket
功能。moveBasket
用SSDL_IsKeyPressed
检查左右箭头的状态,并相应地移动篮子,用MARGIN
确保篮子不会离开屏幕。
////////What happens when a candy is caught or missed ////////
// Some math functions we need a lot...
int sqr (int num) { return num * num; }
double distance (Point2D a, Point2D b)
{
return sqrt(sqr(b.x_ - a.x_) + sqr(b.y_ - a.y_));
}
// Circular collision detection, better for round-ish objects
bool inCollision (Point2D a, Point2D b, int aSize, int bSize)
{
return (distance(a, b) < aSize/2 + bSize/2);
}
// Detect and handle collisions between basket and candy,
// and update numberCaught
bool handleCatchingCandy (Object basket, Object& candy, Object& yumMessage,
int& numberCaught)
{
if (inCollision (basket.loc_, candy.loc_, CANDY_WIDTH, BASKET_WIDTH))
{
SSDL_PlaySound (THUNK_SOUND);
++numberCaught;
resetCandyPosition (candy);
yumMessage.loc_.x_ = basket.loc_.x_;
yumMessage.loc_.y_ = basket.loc_.y_;
return true;
}
else return false;
}
// Detect and handle when candy goes off bottom of screen,
// and update numberMissed
void handleMissingCandy (Object& candy, int& numberMissed)
{
// you missed it: it went off screen
if (candy.loc_.y_ >= SCREEN_HEIGHT)
{
SSDL_PlaySound (DROP_SOUND);
++numberMissed;
resetCandyPosition (candy);
}
}
Example 12-7Falling candy program, part four of five
如果篮子和糖果发生碰撞,handleCatchingCandy
会将糖果重置到屏幕的顶部,定位好吃的!消息,并返回 true,这样main
将知道开始倒计时framesLeftTillYumDisappears
,保持 Yum!看得见一秒钟。
如果糖果落在屏幕底部——如果错过了—handleMissingCandy
也会将糖果重置到屏幕顶部。
无论哪种方式,统计数据都会得到适当的更新。
///////////////////// Events /////////////////////
void myEventHandler(bool& mouseClicked)
{
SSDL_Event event;
while (SSDL_PollEvent(event))
switch (event.type)
{
case SDL_QUIT: SSDL_DeclareQuit(); break;
case SDL_KEYDOWN: if (SSDL_IsKeyPressed(SDLK_ESCAPE))
SSDL_DeclareQuit();
break;
case SDL_MOUSEBUTTONDOWN: mouseClicked = true;
}
}
///// ** The game itself ** ////
bool playGame ()
{
bool isVictory = false; // Did we win? Not yet
bool isDefeat = false; // Did we lose? Not yet
bool letsDisplayStats = true; // Do we show stats on screen?
// Yes, for now
int numberCaught = 0, // So far no candies
numberMissed = 0; // caught or missed
// Initialize sprites
Object basket, candy, yumMessage;
initializeObjects (basket, candy, yumMessage);
// Main game loop
while (SSDL_IsNextFrame () && ! isVictory && ! isDefeat)
{
constexpr int FRAMES_FOR_YUM_MESSAGE = 60;
static int framesLeftTillYumDisappears = 0;
// Handle input events
bool mouseClick = false; myEventHandler (mouseClick);
if (mouseClick) letsDisplayStats = ! letsDisplayStats;
// Display the scene
SSDL_RenderImage(BKGD_IMAGE, 0, 0);
renderObjects (basket, candy, yumMessage,
framesLeftTillYumDisappears>0);
if (letsDisplayStats) renderStats (numberCaught, numberMissed);
// Updates:
// Move objects in the scene
constexpr int BASKET_SPEED = 7; //7 pixels per frame, left or right
moveBasket(basket, BASKET_SPEED);
moveObject(candy); moveObject(yumMessage);
// Did you catch a candy?
if (handleCatchingCandy(basket, candy, yumMessage, numberCaught))
framesLeftTillYumDisappears = FRAMES_FOR_YUM_MESSAGE;
if (numberCaught >= MAX_CAUGHT)
isVictory = true;
else // ...or did it go off screen?
{
handleMissingCandy (candy, numberMissed);
if (numberMissed >= MAX_MISSED)
isDefeat = true; // You just lost!
}
// Update yum message
if (framesLeftTillYumDisappears > 0) // if yumMessage is active
--framesLeftTillYumDisappears; // keep counting down
}
return isVictory;
}
Example 12-8Falling candy program, part five of five
当我们获得胜利或失败时,主循环停止。它分为活动部分、显示部分和更新部分。在事件部分,该语句
if (mouseClick) letsDisplayStats = ! letsDisplayStats;
切换是否显示统计信息。
处理好吃的!消息被适当地分发:它就像其他对象一样显示,但是它的生命周期在 update 部分持续递减。
图 12-3
万圣节糖果游戏
对于更容易或更难的游戏,在主游戏循环之前调用SSDL_SetFramesPerSecond
,或者调整BASKET_SPEED
或CANDY_SPEED
。
防错法
You’ve got a feature that’s supposed to display for a while but never shows up, something like the splat from earlier in this chapter. Maybe your code looks like this:
while (SSDL_IsNextFrame ()) { ... if (mouseClick) { framesLeftTillSplatDisappears = SPLAT_LIFETIME; while (framesLeftTillSplatDisappears > 0) { // display the splat --framesLeftTillSplatDisappears; } } ... }
它显示,好的——一次又一次,直到
framesLeftTillSplatDisappears
达到 0——这一切都发生在六十分之一秒内!它显示得太快了,你根本没机会看到它。问题是适应这种新的事件驱动的思维方式。我们不希望程序完成所有的 splat 显示,然后继续下一个六十分之一秒;我们希望它设置显示,并在其他事情发生时让帧通过(包括用户看到 splat)。
一个好的经验法则是避免在主动画循环中循环一个应该花费时间的动作。只需设置它,并让主循环用连续的帧更新它。
另一个好的规则是来自设计 splat 程序部分的提示;保持主循环的各个部分独立:处理事件,然后显示,然后更新。
So a way to fix this program is
while (SSDL_IsNextFrame ()) { // Events section ... if (mouseClick) framesLeftTillSplatDisappears = SPLAT_LIFETIME; ... // Display section // display the splat ... // Update section if (framesLeftTillSplatDisappears > 0) --framesLeftTillSplatDisappears; ... }
**你不能让一个新功能工作。**尝试一个能做的程序(一个来自文本或互联网或你之前做的东西的样本程序,甚至是一个带有空
main
的程序),然后逐渐改变,直到它成为新程序。您刚刚添加了一个新功能,但现在什么都没用了。以下是一些建议:
如第一章所述,保留一份备份记录,当你修改时,你整个文件夹的副本,这样如果出错了,你可以回到以前的版本。比拔头发好玩多了。
从一个行为不端的函数中删除所有代码。然后放一半回去。如果不好的行为又回来了,只放四分之一回去;如果没有,请重新添加更多代码。继续下去,直到你找到问题所在。
**试验毁灭。**如果我完全想不出哪里出了问题,我会复制一个文件夹(可能命名为“
ttd
”),做一个备份,然后删除代码,尤其是我认为与错误无关的代码。错误还在吗?如果没有,我已经找到了问题所在!但如果是这样,重复,仍然备份,直到程序如此之短,除了错误什么都没有了。不管怎样,我都在追踪引起麻烦的代码。有时我会找到两到三行代码,然后确定问题出在编译器上。它发生了。(那我换个方式写程序。)如果结果是一些愚蠢的事情,我很高兴完成了,而不是恨我自己犯了一个愚蠢的错误。
识别版本之间的差异。也许其中一个有你想要的特性,但有你不想要的缺陷。一份精确的差异报告可以帮助你缩小你感兴趣的范围。在 Unix 中,
diff
file1 file2
列出了不同的行。在 Windows 系统中, WinDiff 是微软的一个很棒的程序(你可能已经有了),它也能做到这一点。两者都适用于单个文件或整个文件夹。**把问题讲给一个真正能听的人:一只鸭子。**也许如果你向专家解释你的问题,它会变得清晰。当然,但是如果身边没有专家呢?认真地和一只橡皮鸭交谈。详细说明问题。当你这样做时,你可能会找到你的解决办法。橡皮鸭调试是个东西,目前甚至有自己的网站(
rubberduckdebugging.com
)。
如需更多提示,请查阅第九章末尾的反欺诈部分。
Exercises
在这些练习和后续练习中,请提前计划,并在出现问题时使用调试器。
让这些球弹跳起来,就像第十一章一样…让你的鼠标控制屏幕上的一个小玩家。避开弹跳球。
制作一个太空游戏:一个不明飞行物从头顶飞过,在你还击的同时投下导弹。你需要一排导弹。
制作一个游乐场鸭子射击游戏的版本。为了让它更有趣,你可以用慢速导弹(从你的准星直接射向鸭子,但是要花一秒钟到达那里)。
做一把可以旋转的枪,放在屏幕中间,射杀从随机方向过来的坏人。
制作自己的游戏,可以是现有街机游戏的副本,也可以是自己的创意。
十三、标准 I/O 和文件操作
我们玩得太开心了。是时候认真对待了。
或者,当你不使用图形和游戏库的时候,也许是时候学习如何编程了。毕竟,你通常不是。即使你是,你也可能需要访问文件(比如加载游戏关卡),在 C++ 中,我们处理文件就像我们处理基于文本的用户交互一样——就像我们到目前为止用sin
和sout
所做的那样。
标准 I/O 程序
示例 13-1 是一个使用标准 I/O 的程序,可能看起来很熟悉。
// Hello, world! program
// -- from _C++20 for Lazy Programmers_
// It prints "Hello, world!" on the screen.
// Quite an accomplishment, huh?
#include <iostream>
using namespace std;
int main ( )
{
cout << "Hello, world!" << endl;
return 0;
}
Example 13-1“Hello, world!” using C++ standard I/O
以下是 SSDL 的《你好,世界》的变化。,从底层开始,反向工作:
是时候澄清一下了:
ssin
和sout
是编译器自带的内置cin
(读作“C-in”)和cout
(C-out)的廉价仿制品。cin
和cout
与 SDL 窗口不兼容,所以我们需要一个替代品。cin
和cout
就像ssin
和sout
,但是 a)你不能设置光标——你只能向下移动屏幕——b)你不能设置字体或颜色。我们需要
main
拥有与 SDL 兼容的参数(int argc, char** argv
);现在可以省略了。using
namespace std;
:cout
是“标准”名称空间的一部分,你必须告诉编译器使用它,否则它会抱怨它不知道cout
是什么。我们加载的是
<iostream>
,而不是"SSDL.h
,"
,它就像<cmath>
和<cstdlib>
一样是编译器自带的。它定义了cin
、cout
、endl
(输出'\n'
)、 1 等东西。
编译标准 I/O 程序
您可以像以前一样构建和运行 Visual Studio 程序:打开解决方案文件,右键单击要运行的项目,然后选择 Debug ➤ Start New Instance。但是要做自己的项目,不是抄basicSSDLProject
而是抄basicStandardProject
。
在 Unix 或 **MinGW 中,**变得更加简单。进入项目文件夹,输入make
。(再也不用抄Makefile.unix
或者Makefile.mingw
;一个Makefile
现在服务于两个平台——这是不使用库的优势。)要运行,输入./a.out
(Unix)或a.out
(MinGW)。
要创建自己的项目,在资源库的newWork
文件夹中复制一份basicStandardProject
,并以同样的方式使用它:make
和a.out
。
从头开始构建项目(可选)
…在 Microsoft Visual Studio 中
要制作自己的项目而不引用basicStandardProject
,在启动 Visual Studio 时,告诉它创建一个新项目(图 13-1 ,然后选择控制台 App(图 13-2 )。在下一个窗口中(图 13-3 ),Visual Studio 更愿意将你的项目放在一个很难找到的名为“repos”的文件夹中;我用台式机。我也强烈建议您选中“将解决方案和项目放在同一个目录中”——否则,您会得到多个调试文件夹,并且清理文件会更加困难。
图 13-3
在 Visual Studio 中配置新项目
图 13-2
在 Visual Studio 中创建新项目
图 13-1
正在启动 Visual Studio
默认的控制台项目是微软的 Hello,world!(图 13-4 )。你可以删除它,输入你自己的代码。
图 13-4
新项目
如果你想使用 C++20 的最新特性(谁不想呢?),可能需要告诉 Visual Studio。需求很可能会改变,所以请查看本书源代码的newWork
文件夹,了解当前的指令。
现在你已经准备好输入你的程序了,但是在运行它之前,我建议你先阅读一下反欺诈部分。
防错法
以下是您在使用 Microsoft Visual Studio 迁移到标准 I/O 时可能会发现的一些问题:
在你有机会看到任何东西之前,程序就关闭了。解决方案:在工具➤选项➤调试下,取消勾选调试停止时自动关闭控制台。这将适用于所有控制台项目,直到你改变它。
之后,如果您正在运行它,并试图运行另一个副本,该后续副本可能会在您看到之前自动关闭。一次只运行一个副本。
编译器找不到 _WinMain@16 **,预编译头文件,或者其他让你去“嗯?”**您可能选择了错误的项目类型(图 13-2 )。简单的解决方法是重新开始并选择控制台应用程序。
…使用 g++
要创建自己的程序,为它创建一个文件夹,在该文件夹中创建main.cpp
,并使用以下命令进行编译:
g++ -std=gnu++2a -g main.cpp -o myProgram
就这样。-std=gnu++2a
表示“尽可能使用 g++ 支持的 C20 标准,加上一些额外的‘GNU’(g)特性”;-g
表示“支持用gdb
或ddd
调试”;-o myProgram
的意思是“命名可执行文件myProgram
如果您不选择-o
选项,可执行文件将是a.out
(Unix)或a.exe
(MinGW)。
要运行,请输入./myProgram
(Unix)或myProgram
(MinGW)。
要调试,使用ddd myProgram &
2 (Unix)或gdb
myProgram
(Unix 和 MinGW)。在 MinGW,我们习惯用break``SDL_main
;自从 SDL 走了之后,break main
取而代之。
Extra
为什么我们要把?/在 Unix 中的程序名前?
当您键入一个命令时,Unix 会查看一个名为 PATH 的目录列表:它认为可执行程序应该在其中的目录。如果当前目录(在 Unix 中称为.
,单个句点)不在路径中,它不会在那里查找,所以如果您键入当前目录中的程序名,Unix 不会找到它。
我不喜欢那样,所以让我们把.
(“点”)放在路径中。
假设这是它检查的第一个目录。然后,如果一个坏人可以让一个恶意程序进入你的目录,并将其命名为一个普通命令,如ls
,他可以让你做可怕的事情:你输入ls
,它就会删除操作系统或其他东西。
好的,那么我们将使它成为最后一个被检查的目录。现在,如果您键入ls
,Unix 将在/bin
(或任何地方)中查找,找到正确的ls
,并运行它。
但是如果坏人猜出人们犯了什么错别字,并命名他的邪恶程序sl
而你的手指错过了…他就抓住你了。
我不知道最后一种情况发生的可能性有多大,但如果你担心的话,这是一个让你置身事外的理由。
防错法
- **(gdb/ddd)调试器显示没有找到调试符号。**在
ddd
中,它也给出一个空白窗口。使用-g
选项重新编译,或者使用basicStandardProject
并键入make
。
Exercises
Write a program which prints all 99 verses of “99 bottles of beer on the wall.” In case you missed this cultural treasure, it goes like this:
99 bottles of beer on the wall 99 bottles of beer; Take one down, pass it around, 98 bottles of beer on the wall!
最后一节是以 0 个瓶子结尾的。
文件 I/O(可选)
除了家庭作业之外,有用的程序通常需要访问文件。因此,让我们看看如何做到这一点:首先是简单的方法(使用cin
和cout
),然后是更普遍适用的方法。
cin
和cout
作为文件
从某种意义上说,我们已经在使用文件了,至少有两样东西 C++ 认为是文件:cin
和cout
。
cin
是一个输入文件。它只是一个输入文件,当你输入的时候从键盘上获取信息。cout
是输出文件:即你电脑屏幕的输出文件。定义的延伸?也许吧,但是很快我们将使用cin
和cout
作为实际的文件。
为此,我们必须知道如何使用命令提示符。Unix 和 MinGW 用户可以跳过下一节;你已经知道了。
在 Windows 中设置命令提示符
打开 Windows 命令提示符(单击开始菜单并键入cmd
)并转到您正在使用的项目的文件夹。这里有一个简单的方法:在该文件夹的窗口中,单击地址栏左侧的文件夹图标,该部分显示类似于... > ch13 > 1-hello
的内容。当你这样做时,它将被一个高亮显示的路径所取代,如图 13-5 所示。
图 13-5
在 Windows 中获取用于命令提示符的路径
如果你在命令窗口提示中看到的驱动器与你刚才复制的地址左边的不同(在我的例子中是C:
),从地址输入驱动器,如图 13-6 。
图 13-6
在cmd
中切换到项目的驱动器和目录。这里,我们从D:
驱动器转到C:
,然后从cd
转到包含1-hello
项目的目录
现在,在命令窗口中,键入cd
并粘贴到您复制的路径中,然后按回车键。
Visual Studio 将您的可执行文件<your project>.exe
放在一个子文件夹中,可能是Debug
、Release
、x64
,或者是它们的某种组合。找到它,并将其复制到与你的.vcxproj
相同的文件夹中,它将能够找到你放在那里的任何文件。
在命令提示符下重定向 I/O
要让您的程序从in.txt
而不是键盘获得输入,请键入
myProgram < in.txt #3The < goes from file to program; makes sense
要让它也将其输出发送到out.txt
,而不是屏幕,请键入
myProgram < in.txt > out.txt
尝试将 Hello 程序的输出发送到一个文件,看看会发生什么。
Online Extra
“通过 I/O 重定向为你自己和你的用户节省一些时间”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=zQ3TY6oSAcQ
找到它。
while (cin)
我听说在英语文本中出现最频繁的字母是,以超级频繁的 E 开头,ETAOINSHRDLU。让我们看看这是不是真的,给程序一些巨大的文本,也许是关闭gutenberg.org
,并计算频率:
make an array of frequencies for letters, all initially zero
while there are characters left
read in a character // we won't prompt the user;
// it's all coming from a file
if it's a letter add 1 to frequency for that letter
print all those frequencies
我知道如何创建数组,给int
加 1,并读入字符。但是我怎么知道还有字符呢?
while (cin) ...
会做到的。如果你把cin
放在某个你期望有bool
的地方,它会被评估为类似于“如果cin
没有出错”cin
通常会出错的地方是到达输入文件的末尾。
示例 13-2 是结果程序。
// Program to get the frequencies of letters
// -- from _C++20 for Lazy Programmers_
#include <iostream>
using namespace std;
int main ()
{
// make an array of frequencies for letters, all initially zero
constexpr int LETTERS_IN_ALPHABET = 26;
int frequenciesOfLetters[LETTERS_IN_ALPHABET] = {}; // all zeroes
// read in the letters
while (cin) // while there are letters left
{
char ch; cin >> ch; // read one in
ch = toupper(ch); // capitalize it
if (cin) // Still no problems with cin, right?
if (isalpha(ch)) // and this is an alphabetic letter?
++frequenciesOfLetters[ch - 'A'];
// A's go in slot 0, B's in slot 1...
}
// print all those frequencies
cout << "Frequencies are:\n";
cout << "Letter\tFrequency\n";
for (char ch = 'A'; ch <= 'Z'; ++ch) // for each letter A to Z...
cout << ch << '\t' << frequenciesOfLetters[ch - 'A'] << '\n';
return 0;
}
Example 13-2Counting frequencies of letters in a text file
用a.out < in.txt > out.txt
(g++)或2-frequencies < in.txt > out.txt
(Visual Studio)试试这个,你会得到一个类似于out.txt
的文件
Frequencies are:
Letter Frequency
A 40
B 5
C 9
D 20
E 63
...
如果字母来自一个文件,while (cin)
会在到达文件末尾时停止。但是实际的键盘输入没有文件结尾。您可以通过按 Ctrl-Z 和 Enter (Windows)或 Ctrl-D (Unix)来模拟它。必须是该行的第一个字符,否则可能不起作用。
读入字符,包括空格
新任务:读入一个文件,每个字符,并大写一切。这是我们的输入文件。
这看起来应该可以工作,但却不能:
while (cin) // for each char in file
{
char ch; cin >> ch; // read in char
ch = toupper (ch); // capitalize
if (cin) cout << ch; // cin still OK? Then print
}
使用这个输入-Twinkle, twinkle, little bat! How I wonder what you're at!
–我们得到这个输出:TWINKLE,TWINKLE,LITTLEBAT!HOWIWONDERWHATYOU'REAT!
跳过空白。对于用户交互和 ETAOIN SHRDLU 程序来说很好,但是这里我们需要空白。
解决方法:ch = cin.get();
。cin.get()
返回下一个字符,即使是空格、制表符(\t
,或者行尾(\n
)。
示例 13-3 读入一个文件并生成一个全大写版本。要执行,请键入a.out < in.txt > out.txt
(g++)或3-capitalizeFile < in.txt > out.txt
(Visual Studio)。
// Program to produce an ALL CAPS version of a file
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cctype> // for toupper
using namespace std;
int main ()
{
while (cin) // for each char in file
{
char ch = cin.get(); // read in char
ch = toupper (ch); // capitalize
if (cin) cout << ch; // cin still OK? Then print
}
return 0;
}
Example 13-3Capitalizing a file, character by character
防错法
You told it to stop at the end of file , but it goes too far :
/////////// get an average -- buggy version ////////////// double total = 0.0; // initialize total and howMany int howMany = 0; while (cin) // while there are numbers in file { int num; cin >> num; // read one in total += num; // keep running total ++howMany; }
Your
input file is1 2
而你的平均分是…1.6667。啊?
用调试器跟踪它。它读入 1,将其相加,并递增
howMany
。它读入 2,将其相加,并再次递增howMany
。用while (cin)
测试文件结尾;它继续前进。但是我们不是在文件的末尾吗?也许不是。可能还有另一个\n 或者空间什么的。
所以程序继续进行。它读入下一个数字,但是没有,所以它把 num 保持为 2,再加一次(!),并再次递增。一个错误诞生了。
It couldn’t know there wasn’t going to be another number till it tried to read it. So the solution is to test the input file after every attempt to read, to ensure it didn’t run out of input while reading:
int num; cin >> num; // read one in if (cin) // still no problems with cin, right? { total += num; // keep running total ++howMany; }
average
见源代码,ch13
文件夹/解决方案,是本程序的完整正确版本。
Exercises
在所有这些练习中,使用标准 I/O。
从文件中读入一系列数字,并以相反的顺序打印出来。你不知道有多少,但你知道不超过,比如说,100。(这样你可以声明一个足够大的数组。)
计算文件中的字符数。
…不包括空格或标点符号。
使用文件名
一直重定向 I/O 工作量太大。也许我有多个输入文件——它们不可能都是cin
。或者,也许我只是想让程序记住文件名,而不是期望我在命令提示符下键入它。
假设我有一个游戏,游戏中愤怒的机器人四处游荡,试图与我的玩家发生冲突。播放器从屏幕的左边开始,我的工作是让它到右边,没有任何碰撞。
如果我把机器人放在特定的位置,设计一个比一个更难的关卡,可能会让游戏更有趣。我们将从三个机器人开始第一关,所以有三个位置。
如果我从cin
得到这个(太烦人了,但是我们马上会改变它),代码可能看起来像例子 13-4 。要运行这个例子,输入六个整数,它会把它们返回给你。令人兴奋,是的,我知道。
// A (partial) game with killer robots
// meant to demonstrate use of file I/O
// This loads 3 points and prints a report
// -- from _C++20 for Lazy Programmers_
#include <iostream>
using namespace std;
struct Point2D { int x_=0, y_=0; };
int main ()
{
// an array of robot positions
constexpr int MAX_ROBOTS = 3;
Point2D robots[MAX_ROBOTS];
int whichRobot = 0;
// while there's input and array's not full...
while (cin && whichRobot < MAX_ROBOTS)
{
int x, y;
cin >> x >> y; // read in an x, y pair
if (cin) // if we got valid input (not at end of file)
{
robots[whichRobot] = {x, y}; // store what we read
++whichRobot; // and remember there's 1 more robot
}
}
for (int i = 0; i < MAX_ROBOTS; ++i)
cout << robots[i].x_ << ' '
<< robots[i].y_ << endl;
return 0;
}
Example 13-4Code to read in several points from cin
现在,让程序在不重定向 I/O 的情况下获取文件。下面是使用命名输入文件必须做的事情:
#include <
fstream
>,
其中有我需要的定义。ifstream
inFile;
声明我的输入文件。ofstream
用于输出文件。inFile.open``("level1.txt");
–打开一个文件会将它与一个文件名相关联,并确保没有问题。验证打开的文件没有错误。如果是输入文件,错误可能是该文件不存在或者不在您认为的文件夹中。如果是输出文件,可能是磁盘有问题或者是只读文件。以下是如何验证:
在您想要使用新文件的任何地方,将
cin
更改为inFile
。如果是输出文件,将cout
改为outFile
。完成后,关闭文件
: inFile.close
();
。这告诉操作系统忘记inFile
和input.txt
之间的关联,从而让其他可能需要它的程序使用它。不可否认,当你的程序结束时,它引用的所有文件都将被关闭——但是,当你用完了你的玩具,我指的是文件,养成收起来的习惯是明智的。你妈妈会为你骄傲的。
if (! inFile) // handle error
示例 13-5 是程序的更新版本。
// A (partial) game with killer robots
// meant to demonstrate use of file I/O
// This loads 3 points and prints a report
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <fstream> // 1\. include <fstream>
using namespace std;
struct Point2D { int x_=0, y_=0; };
int main ()
{
// an array of robot positions
constexpr int MAX_ROBOTS = 3;
Point2D robots[MAX_ROBOTS];
// 2\. Declare file variables.
// 3\. Open the files.
// Here's two ways to do both; either's fine
ifstream inFile; inFile.open("RobotGameLevel1.txt");
ofstream outFile ("RobotSavedGame1.txt");
// 4\. Verify the files opened without error
if (! inFile)
{
cout << "Can't open RobotGameLevel1.txt!\n"; return 1;
// 1 is a conventional return value for error
}
if (! outFile)
{
cout << "Can't create file RobotSavedGame1.txt!"; return 1;
}
int whichRobot = 0;
// 5\. Change cin to inFile, cout to outFile
// while there's input and array's not full...
while (inFile && whichRobot < MAX_ROBOTS)
{
int x, y;
inFile >> x >> y; // read in an x, y pair
if (inFile) // if we got valid input (not at end of file)
{
robots[whichRobot] = {x, y}; // store what we read
++whichRobot; // and remember there's 1 more robot
}
}
for (int i = 0; i < MAX_ROBOTS; ++i)
outFile << robots[i].x_ << ' '
<< robots[i].y_ << endl;
// 6\. When done, close the files
inFile.close(); outFile.close();
// can still use cout for other things
cout << "Just saved RobotSavedGame1.txt.\n";
return 0;
}
Example 13-5Program that reads an input file and prints to an output file
这奏效了;它将RobotSavedGame1.txt
保存在与.vcxproj
文件相同的文件夹中。
程序启动时会删除RobotSavedGame1.txt
中的所有内容,并用新内容替换它们。
程序中可以有多个输入和输出文件。您也可以将文件传递给函数:
void readFile (ifstream& in, double numbers[], int& howManyWeGot);
void writeFile(ofstream& out, double numbers[], int howMany);
Exercises
写一个程序来判断两个文件是否相同。
写入和测试函数,以读取和打印到一个
Point2D
s 的文件。掷出两个骰子 100 次,并将结果存储在一个文件中…
…然后加载该文件并打印一个柱状图:一个柱状图显示你得到 2 的次数,另一个柱状图显示你得到 3 的次数,以此类推。在 SSDL 这样做(使用
basicSSDLProject
;继续使用文件变量;只是不要指望cin
和cout
管用);或者在屏幕上打印 x,显示每个值出现的次数——类似于:1 : 2 : X 3 : XXXXXXX 4 : XXXXXXXX ...
制作你自己的密码:一个字母方案,如 A 代表 R,B 代表 D,等等。然后使用您的加密方案对消息进行编码。还要写一个解密程序,验证一切正常。
(用力)地球变暖了吗?
在本章的示例代码中有一个文件
temperature.txt
4 ,它包含给定年份的年份和估计的全球平均温度。(给出的温度是相对于 1910-2000 年估计平均温度的摄氏度数)。那么我们能从中学到什么呢?
The degrees increase per year, which is
x 是年份,y 是温度。σx 读作“x 的总和”,意思是“所有 x 的总和”m 是与数据最匹配的直线 y = mx + b 的斜率。
How closely the yearly temperatures actually match this line. This is
如果 R 为–1 或 1,则相关性很强。如果 R 接近 0,就很弱。负 R 意味着温度随时间下降(但我们已经从 m 知道了)。
写一个程序,读取文件,并为用户提供每年增加的学位和 r。你需要什么功能?在给出你的答案之前,充分测试他们以确保你信任他们。
当然,相关性并不能证明因果关系。比如喝咖啡的人滑雪多(比方说)。这是否意味着咖啡会导致滑雪?也许是滑雪小屋提供免费咖啡。或者,喜欢找乐子的人更可能去滑雪和喝咖啡。对于因果关系,我们需要更多一点的(人类)思考。
十四、字符数组和动态内存
字符数组——也称为“字符串”或文本——对许多任务都很重要。本章展示了如何处理它们,以及如何在事先不知道大小的情况下创建这些或其他数组。在这个过程中,我们将尽可能以最有效的方式学习标准库的字符数组函数:通过构建它们。
字符数组
我们从一开始就使用了char
数组。我们从第一章和第十三章引用的"Hello, world!"
是一个字符数组,内容如图 14-1 所示。
图 14-1
“你好,世界!”字符数组文字
“空字符”是一个标记,告诉 C++ 这是我们的字符串结束的地方。cout
不是在到达分配空间的末端时停止打印——它不知道也不关心分配了多少空间——而是在到达'\0'
时停止打印。
让我们看看除了打印之外,我们还能用char
数组做什么。
这里有两种方法来初始化一个字符数组:
char A[] = {'d','o','g','\0'}; // they both
mean the same thing
char A[] = "dog"; // but this one's easier to read,
// don't you think?
你也可以从cin
或输入文件中把一个单词读入字符数组。我们需要确保我们声明的数组有足够的空间容纳输入的内容。我们通过分配比我们可能需要的更多的字符来做到这一点:
constexpr int MAX_STRING_SIZE = 250;
char name[MAX_STRING_SIZE];
cout << "What's your name? "; cin >> name;
那段代码读起来一个字。如果你想让读取整行(也许你想让用户输入名字和姓氏),你需要cin.
getline
(name, MAX_STRING_SIZE);
。
我们可以将数组传递给函数。在示例 14-1 中,我们有一个函数打印一个问题并得到一个有效的是或否的答案。我们不打算改变数组,所以我们把它作为const
传递。
bool getYorNAnswer (const char question[])
{
char answer;
do
{
cout << question; // print a question
cin >> answer; // ...and get an answer
answer = toupper (answer); // capitalized, so we can compare to Y, N
}
while (answer != 'Y' && answer != 'N');
// keep asking till we get Y or N
return answer == 'Y'; // "true" means "user said Y"
}
Example 14-1A function that takes a char array as a parameter. Since it’s short, it and Example 14-2 are together in source code folder ch14, in project/folder 1-and-2-charArrays
如果我们这样称呼getYorNAnswer
getYorNAnswer ("Ready to rumble (Y/N)? ") // same question, and reasoning,
// as in Chapter 5's
// section on while and do-while
我们的交互可能看起来像这样
Ready to rumble (Y/N)? x
Ready to rumble (Y/N)? 7
Ready to rumble (Y/N)? y
…此时getYorNAnswer
将返回true
。
现在让我们看看一个字符串有多长——不是分配的内存,而是正在使用的部分,直到空字符:
where = 0
while the whereth char isn't the null character (not at end of string)
add 1 to where
例 14-2 是完整版。
unsigned int myStrlen (const char str[]) // "strlen" is the conventional
// name for this function
{
int where = 0;
while (str[where] != '\0') // count the chars
++where;
return where; // length is final "where"
}
Example 14-2The myStrlen function
. Find it and Example 14-2 in source code folder ch14, in project/folder 1-and-2-charArrays
该功能和其他功能已经在包含文件cstring
中提供。表 14-1 列出了最常用的。
表 14-1
一些cstring
功能,为清晰起见进行了简化
Note
如果 Microsoft Visual Studio 看到strcpy
、strcat
等等,它可能会给出警告:
warning C4996: 'strcpy'
:此函数或变量可能不安全。考虑改用strcpy_s
。use _CRT_SECURE_NO_WARNINGS
禁用折旧。有关详细信息,请参见联机帮助。
strcpy_s
和strcat_s
是strcpy
和strcat
的版本,它们试图阻止你写超出数组的界限。听起来很明智,但这通常不会流行起来。我不使用它们,因为我希望代码可以在编译器之间移植。或者我只是喜欢生活在边缘。22
要取消警告,请将这一行放在引用strcpy
的任何文件的顶部,依此类推:
#pragma warning (disable:4996) // disable warning about // strcpy, etc.
防错法
在调试器 中,你看到你的
char
**数组看似合理,但到了最后却充满了随机字符。**没事;无论如何都不会打印或使用超过'\0'
的内容。可以忽略。你看到你的
char
数组中合理的字符被打印出来,然后后面跟着多余的垃圾。字符串缺少最后一个'\0'
。解决方案:在末尾插入'\0'
。It acts like it’s gotten some of your input before you had a chance to type it. (If using file I/O, it skips part of the file.) Here’s an example:
do { cout << "Enter a line and I'll tell you how long it is.\n"; cout << "Enter: "; cin.getline (line, MAX_LINE_LENGTH); cout << "That's " << strlen (line) << " characters.\n"; letsRepeat = getYOrNAnswer ("Continue (Y/N)? "); } while (letsRepeat);
第一次,你很棒。之后每次你说要再来一次,它就说你的线长是 0,问你要不要继续。
想象一下
cin
提供了一个从键盘发送到程序的字符序列,如图 14-2 。
图 14-2
cin
缓冲区 3 ,以用户键入的任何内容作为其内容
在(a)中,你已经输入了你的第一行,TO BE
。cin.getline
通过'\n'
获得所有这些。
在(b)中,您已经输入了对"Continue (Y/N)?"
问题的回答。
(c)中,getYOrNAnswer
做了其 input: cin>>answer. answer
变成了“y
”,getYOrNAnswer
做了。
我们准备再次开始循环并获得更多的输入……但是看看在cin
缓冲区中留下了什么:一个供cin.getline
读取的字符串。这是一个空字符串,就像你什么也没输入就按了 Enter 键,但它仍然是一个字符串。所以cin.getline
不会等你打字;它继续读取那个空字符串…然后我们回到getYOrNAnswer
被询问是否要继续。
我们需要在cin.getline
被愚弄之前甩掉剩下的'\n'
getYOrNAnswer
。这里有两种方法:
cin.getline
再来,就是为了摆脱'\n'
。cin.ignore (MAX_LINE_LENGTH, '\n');
。这忽略了从MAX_LINE_LENGTH
到第一个\n
的所有char
,以先出现的为准。
我觉得还是放在getYOrNAnswer
里吧;是提出问题的函数给我们带来了麻烦,所以它应该自己清理:
cin >>
读入一个char
数组,但是走得太远,覆盖了其他变量。让你的char
数组变大,希望没人输入超长的东西。C++20 通过只接受最大可用大小来防止这个问题,但是你的编译器可能还不支持这个特性。cin
**不会读入作为参数传入的数组。**见下一节最后一个反欺诈条目。
bool getYOrNAnswer (const char question[])
{
...
cin.ignore (MAX_LINE_LENGTH, '\n'); // dump rest of this line
return answer == 'Y';
}
Online Extra
“当你在字符串末尾发现垃圾的那一刻”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=57jkYKwN9hg
。
Exercises
编写并测试
myStrcpy
,你自己版本的strcpy
函数,如表 14-1 所述。(本章后面有答案。)为了测试它是否真的把'\0'
放在了末尾,在复制之前用 x 填充目标数组(比如说)。编写并测试你的
strcat
版本。…和
strcmp
。询问用户的姓名,并重复一遍。如果第一个字母是小写的,那就大写。
(使用文件 I/O)确定给定文件中的行数。
(使用文件 I/O)…以及它们的平均长度。
(使用文件 I/O)编写一个程序,找出并打印两个给定文件之间的共同点。假设每个单词在一个文件中最多出现一次。
数组的动态分配
有时直到程序运行时你才知道数组应该有多大。但这是行不通的:
int size;
// calculate size somehow
int A[size];// compiler should complain: size must be a constant value
以下是应该做的事情。
首先,声明数组,但不为其元素分配内存:
int* A;
A
不是一组int
的集合,而是一些int
的“指针”的地址。这就像我们用[]
声明它时一样,但是还没有为那些int
存储。
接下来,给它需要的内存:
A = new int [size];
这就要求 C++ 的一部分,“堆管理器”给我们这么多的int
中的一部分。有一整堆的内存可用于此,堆管理器可以在你需要的任何时候给你其中的一部分。这被称为“动态分配”,因为它是在程序运行时发生的。到目前为止,我们在编译时进行分配的方式是“静态分配”(以这些方式分配的内存称为“动态”或“静态”)
我们像以前一样使用数组。完成后,我们告诉堆管理器它可以取回它,因此:
delete [] myArray;
对堆管理器来说,这是一个提醒,你所返回的是一个数组。如果你忘记了[]
,唉,编译器不会告诉你——你得记住你自己。
总之,要“动态分配”任何<type>
的数组:
<type>* myArray = new <type> [size];
。像平常一样使用数组。
当你完成后。
很容易忘记delete []
。有关系吗?当然可以。如果你一直分配内存并且从来不归还——如果你一直做****【内存泄漏】——内存最终耗尽,程序崩溃。以后我们会有一种方法让删除变得更容易记忆。
**示例 14-3 展示了如何使用动态分配。
// Program to generate a random passcode of digits
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cstdlib> // for srand, rand
#include <ctime> // for time
using namespace std;
int main ()
{
srand ((unsigned int) time(nullptr));// start random # generator
int codeLength; // get code length
cout<< "I'll make your secret passcode. How long should it be? ";
cin >> codeLength;
int* passcode = new int[codeLength]; // allocate array
for (int i = 0; i <codeLength; ++i) // generate passcode
passcode[i] = rand () % 10; // each entry is a digit
cout << "Here it is:\n"; // print passcode
for (int i = 0; i < codeLength; ++i)
cout << passcode[i];
cout << '\n';
cout << "But I guess it's not secret any more!\n";
delete [] passcode; // deallocate array
return 0;
}
Example 14-3A program that dynamically allocates, uses, and deletes an array of ints
以下是一个会话示例:
I'll make your secret passcode. How long should it be? 5
Here it is:
14524
But I guess it's not secret any more!
Extra
1992 年,人工智能研究员 Edmund Durfee 在全国人工智能会议(AAAI-92)上做了一次受邀演讲,“你的计算机需要知道的,你在幼儿园就学会了”——引用了罗伯特·富尔亨的畅销书我真正需要知道的一切我在幼儿园就学会了。以下是 Durfee 所说的你的电脑从你的早期儿童教育中需要的:
分享一切。
公平竞争。
不要打人。
把东西放回原处。
收拾你自己的烂摊子。
不要拿不属于你的东西。
当你伤害别人时,说对不起。
同花。
当你走向世界时,注意交通,手拉手,团结在一起。
最终,一切都会消亡。
其中许多在操作系统中是有用的。也许你有一个占用内存和 CPU 时间的程序,所以如果你想和一个不同的程序交互,你不能。(分享一切。)也可能只有占满整个屏幕才能运行。(公平竞争。)
就记忆而言,我们需要“把东西放回你发现它们的地方。”就像蜡笔和玩具一样,如果我们总是把它们放回去,就更容易找到我们需要的东西。
防错法
动态内存最常见的问题是程序崩溃。可能是什么原因造成的?
- **忘记初始化:**如果你还没有初始化
myArray
,它的地址指向某个随机的位置。它几乎肯定会崩溃。这比得到错误的输出却不知道要好。
防止这种情况的两种方法如下:
int* A = new myArray[size]; // initialize as soon as you // declare
或者
int* A = nullptr; // we'll initialize to something // sensible later
按照惯例,nullptr
的意思是“不指向任何地方,所以不要考虑查看任何元素。”在老的程序中,不是nullptr
而是NULL
。
忘记 删除 : 长时间这样做,程序会耗尽内存并崩溃。
忘记使用
delete []
中的[]
:这会导致“未定义的行为”,这意味着它可能会崩溃,表现完美,或者引发第三次世界大战。我不会冒这个险。
其他问题包括:
错误声明 一行多指针:很奇怪,但是
int* myArray1, myArray2;
没有创建两个指针。它创建了一个
int
指针myArray1
和另一个(单个)int myArray2
。为什么这么困惑?这是 C 标准遗留下来的东西。解决方案:int* myArray1;
int* myArray2;
在不必要的时候使用动态内存:这不是错误,但会导致错误。动态记忆有更多可能出错的地方;你必须记住用
new
分配,用delete
解除分配。如果你没有任何收获(比方说,如果你在编译时知道你的数组有多大),给自己省点事;按老办法分配。cin 不会读入你动态分配的数组或者你的数组作为函数参数传入:
void read (char str [])
{
cin >> str; // compile-time // error
}
int main ()
{
char s1 [FIXED_SIZE]; cin >> s1; // No problem
char* s2 = new char[someSize]; cin >> s2; // compile-time // error
...
}
正如前面的反阻塞部分所提到的:当cin
知道它要读入的数组的大小时,就像它对s1
所做的那样,它只读入它有空间的数组。 4 但是如果给它一个动态数组什么的传入作为参数,它不知道;它可能读入太多,并超过数组界限。C++20 标准通过说“不,你不能这样做”来解决这个问题,因此导致了一个新的问题:编译时错误。
公平地说,这是一个巨大的安全漏洞。
我的解决方法是总是读入一个本地声明的固定大小的数组,并从该数组复制到动态数组或传入的数组中:
static char buffer [REALLY_BIG_NUMBER];
if there is room in str to store what we get, then
strcpy (str, buffer);
else
complain
Exercises
询问用户的姓名。您需要一个足够长的缓冲区来存储任何合理的名称。然后把它存储在一个足够长的数组里。
(使用 SSDL)询问用户要画多少颗星星;生成随机星阵列;画出来。根据需要使用函数。
(使用文件 I/O)编写一个程序,计算文件中的行数(参见上一节中的练习 5),动态分配一个数组来存储这些行,并将它们全部读入。提示:您可以打开文件,计算行数,关闭它,然后再打开它。
(更难)动态分配一个游戏板,像棋盘但大小可变。我不能只分配一个二维数组,所以我们只能用一维数组来凑合。决定它的大小以及如何访问第行,第列的位置。
(使用 SSDL;硬)自己写位图:动态分配的数组,每个数组包含图像中一个像素的颜色。和前面的练习一样,我们需要使用一个一维数组。
提供渲染功能;给定位图、屏幕上的起始位置以及位图的宽度和高度,在该位置显示位图。要绘制像素,请使用
SSDL_SetRenderDrawColor
和SSDL_RenderDrawPoint
。位图作为一个
struct
,包含数组加上宽度和高度,这是一个好主意吗?
使用*符号
我们已经使用了*
来声明动态分配的数组:
double* myArray = new double[sizeOfArray];
我们也可以用它来指代单个元素。*A
的意思是A[0]
,因为*A
的意思是“什么A
指向什么”,而A
指向第 0 个元素。
*(A+1)
的意思是A[1]
。编译器足够聪明,知道A+1
意味着下一个元素的地址。(像这样给指针加上一些东西叫做“指针算术”。)
A[1]
比*(A+1)
更容易阅读——那么为什么要用这种新的符号呢?一个原因是让你为我们稍后将和*
一起做的事情做好准备。
另一个是这个有趣的遍历数组的新方法。考虑本章“字符数组”一节的练习 1 中的myStrcpy
函数。下面的代码片段是在ch14/strcpyVersions
中收集和编译的。
void myStrcpy (char destination [], const char source[])
{
int i = 0;
while (source[i] != '\0')
{
destination[i] = source[i];
++i;
}
destination[i] = '\0'; // put that null character at the end
}
这里有一个不使用[]
的版本:
void myStrcpy (char* destination, const char* source)
{
int i = 0;
while (*(source + i) != '\0')
{
*(destination + i) = *(source+i);
++i;
}
*(destination + i) = '\0'; // put null character at the end
}
没有明显的改善,但它会起作用。接下来,我们取消使用i
,直接更新source
和destination
:
void myStrcpy (char* destination, const char* source)
{
while (*source != '\0')
{
*destination = *source;
++source; ++destination;
}
*destination = '\0'; // put null character at the end
}
有用吗?是的。现在我们每次遍历循环时都要给source
加 1——所以每次,它都指向它的下一个元素(对于destination
也是如此)。当source
到达空字符时,循环停止。
记住测试条件时,0 表示false
,其他都表示true
(第四章)。所以while (*source != '\0')
可以写成
while (*source) // if *source is nonzero -- "true" -- we continue
因此,我们可以将函数写成
void myStrcpy (char* destination, const char* source)
{
while (*source)
{
*destination = *source;
++source; ++destination;
}
*destination = '\0'; // put null character at the end
}
这是我真正应该停止的地方。习惯了就可读了;它很短,给了我们用*符号的练习,我们会在第二十章及以后经常用到。但是现在退出太有趣了。
回忆第五章中的后增量运算符(如X++
)。Y=X++;
真正的意思是Y=X; X=X+1;
。获取值,然后递增。
在这里,我们可以将它用于目的地和源——因为我们使用它们的值进行赋值,然后递增。
void myStrcpy (char* destination, const char* source)
{
while (*source)
*destination++ = *source++;
*destination = '\0'; // put null character at the end
}
还记得,X=Y
的值是分配的值——在*destination++ = *source++
的情况下就是简单的*source
。只要这个非零,我们就想继续下去:
void myStrcpy (char* destination, const char* source)
{
while (*destination++ = *source++); *destination = '\0';
}
这近乎邪恶和粗鲁。我不想这样写我的代码,但是它的确展示了我们通过使用*
获得的灵活性。
Note
被称为“解引用”操作符——因为它接受一个对象的引用(地址)并给出对象本身。
&
是它的反义词:“reference”运算符。它接受一个对象并给你地址:
int x;
int* addressOfX = &x;
我在 C++ 里不经常用;它在 C 中更有用,C 中缺少我们的引用参数,引用参数也使用符号&
。这让生活更加混乱。啊,好吧。如果*
可以表示取整(*addressOfX
) 和相乘(x*y
,我想&
也可以表示不止一个意思。
防错法
编译器抱怨你正在用一个字符串常量初始化一个 char* ,就像在
char* str = "some string";
中一样。改为说
char str[] = "some string";
。
Exercises
在所有这些练习中,使用*
符号,而不是[]
符号。
写
strcmp
。…对我们的其他基本字符数组函数做同样的事情。
(Harder) Write and test a function
contains
which tells if one character string contains another. For example,contains ("'Twas brilling, and the slithy toves" " did gyre and gimble in the wabe", "slithy")
会返回 true。
(Harder) Write a function
myStrtok
which, like thestrtok
incstring
, gets the next word (“token”) in a character array. It might be called thus:char myString[] = "Mary Mary\nQuite contrary"; const char* nextWord = myStrtok (myString, " \t\n\r"); // I use space, tab, return, and // the less-used carriage return \r // as "delimiters": separators // between words while (nextWord) { cout << "Token:\t" << nextWord << '\n'; nextWord = myStrtok (nullptr, " \t\n\r"); }
预期产出:
Token: Mary
Token: Mary
Token: Quite
Token: contrary
当你第一次为一个给定的字符串调用它时,它应该返回一个指向第一个单词的指针。(如果没有,则返回
nullptr
。)然后,每次你传入nullptr
,它会给出你之前使用的字符串中的下一个单词——用完时返回nullptr
。您将需要上一个练习中的
contains
函数。您还需要一个static
局部变量。就像编译器的
strtok
一样,您可以随心所欲地修改输入字符串。通常的方法是将'\0'
放在您希望当前返回的令牌结束的地方,覆盖一个空白字符。
十五、类
到目前为止,我们所介绍的基本上都是 C 语言,只是做了一些调整,特别是cin
和cout
。现在是时候添加在 C++ 中放置+的东西了:类。他们不会给我们新的能力,比如cin
/ cout
、SSDL 函数,或者像循环这样的控制结构。他们将做的是帮助我们保持事情有条理,这样我们就不会混淆;尽量减少错误;让表达事情变得更简单——这样我们就可以信任我们写的代码,并将其用于更大更好的项目。
例如,这里有一个存储日历日期的类类型:
class Date
{
int day_;
int month_;
int year_;
};
...
Date appointment; // Variables of a class type are called "objects"
// Using the term makes you sound smart at job interviews
我们可以用一个struct
来完成。就像使用struct
s 一样,我们可以声明这种类型的变量,将它们作为参数传递,使用.
(点号)来获取部分,等等——看起来非常相似。但是我们即将找到重用代码和避免错误的新方法。
第一种方式:我不想让day_
、month_
和year_
在程序的任何部分都可用;他们可能会搞砸。我将允许某些函数访问它们,称为成员函数。
这个安全措施来自一个可能更容易记住的隐喻。考虑物理世界中的物体。一个物体——比如说,一个橡胶球——有一些特征:也许它是红色的,有弹性,有一定的质量和成分。你不能只是在你想要的时候把那些特征设置成你想要的。可以把真球上的色域设置成蓝色吗?叫它轻如鸿毛?相反,对象本身提供了与它交互的方式。不能设置色域,但是可以上色。你不能直接改变它的质量,但是你可以做一些事情来改变它的质量,比如切掉它或者烧掉它。你不能把它的位置设置成 90 公里直上,但你可以扔出去,看它能飞多远。
我们将以同样的方式创建我们的类:用特征(成员变量)和方法(成员函数)与这些特征进行交互。
那么,如何处理Date
才是合适的呢?首先,您可以打印它:
class Date
{
public:
void print (ostream&);1
private:
int day_;
int month_;
int year_;
};
...
Date appointment;
...
appointment.print (cout); // or we could pass in a file variable -- see // Chapter 13
public 部分是为外界(比如main
)可以访问的东西准备的:也就是说,main
可以告诉一个Date
打印自己。私有部分是只有Date
可以直接访问的部分。(如不注明,均为私人。) 2
一个类的 BNF 大致是
class <name>
{
public:
<function declarations, variables, and types;
usually declarations, almost never variables>
private:
<function declarations, variables, types; usually variables>
};
看前面的print
调用。为什么我们不告诉appointment
日、月和年?它已经知道-它包含了它们!它不知道我们是想打印到cout
还是一个文件,所以我们必须告诉它。
我还没说怎么打印呢。以下是如何:
void Date::print (std::ostream& out)
{
out << day_ << '-' << month_ << '-' << year_;
}
“Date
::
”告诉编译器,“这不仅仅是一个名为print
的函数——它是属于Date
的函数。”
当你调用它的时候——appointment.print (cout);
——它会打印谁的day_
?appointment
夏侯惇
如果你正在使用一个友好的编辑器,比如微软的 Visual Studio,当你输入appointment
和一个句号(见图 15-1 ,编辑器会列出可用的成员函数——到目前为止,只有print
,但是我们很快会有更多的。你可以点击一个,它会为你粘贴。加上开头部分,它会提醒你它期望什么样的论点。
图 15-1
Microsoft Visual Studio 提示函数声明信息
Date::
也一样。它会列出可用的成员。
如果没有,不用担心;有时编辑会感到困惑。
构造器
我们已经知道初始化变量是明智的。在类中,我们有一个特殊类型的函数叫做构造器(常见的缩写是“ctor”)来做这件事(见例子 15-1 中突出显示的部分)。
// A program to print an appointment time, and demo the Date class
// ...doesn't do that much (yet)
// -- from _C++20 for Lazy Programmers_
#include <iostream>
using namespace std;
class Date
{
public:
Date (int theDay, int theMonth, int theYear); // constructor declaration
void print (std::ostream& out);
private:
int day_;
int month_;
int year_;
};
Date::Date (int theDay, int theMonth, int theYear) : // ...constructor // body
day_ (theDay), month_ (theMonth), year_ (theYear)
// theDay is the parameter passed into the Date constructor
// function. day_ is the member that it will initialize.
{
}
void Date::print (std::ostream& out)
{
out << day_ << '-' << month_ << '-' << year_;
}
int main ()
{
Date appointment (31, 1, 2595);3
cout << "I'll see in you in the future, on ";
appointment.print (cout);
cout << " . . . pencil me in!\n";
return 0;
}
Example 15-1The Date class, with a constructor, and a program to use it
构造器的名字总是和它的类一样。当你声明一个类Date
的变量时,它调用这个函数来初始化成员变量。(没有返回类型;本质上,构造器“返回”对象本身。)
函数定义的第二行
day_ (theDay), month_ (theMonth), year_ (theYear)
告诉它将day_
初始化为等于theDay
等等。当我们到达{}
的时候,已经没什么可做的了,所以这次{}
是空的。
相反,您可以在函数体中初始化,使用=
:
Date::Date (int theDay, int theMonth, int theYear)
{
day_ = theDay; month_ = theMonth; year_ = theYear;
}
然而,带有()
的成员初始化语法更常见,更不容易出错(参见反调试部分),并且在某些情况下是必要的,所以现在养成这种习惯是比较懒惰的。
为了形象化成员函数如何与数据成员交互,考虑示例 15-1 中发生的情况。main
的第一个动作是为appointment
分配空间,调用其构造器(图 15-2 ),传入参数。我把构造器画在了main
之外,因为它是一个单独的函数……但它是appointment
的一部分,所以我用虚线把它和数据成员统一起来。
图 15-2
调用Date
构造器
构造器将theDay
复制成day_
、theMonth
复制成month_
、theYear
复制成year_
(图 15-3 )。
图 15-3
Date
构造器初始化appointment
的数据成员
完成后,构造器离开(图 15-4 )。
图 15-4
Date
构造器完成
这显示了(例如)成员day_
和构造器参数theDay
的不同角色:day_
是持久的并且记住你的约会的日期部分;theDay
是Date::Date
用来将信息从main
传递到day_
的参数,当构造器Date::Date
完成时,该参数消失。
此后,main
继续,打印"I'll be getting up at "
,然后进入appointment
的print
功能,该功能了解day_
、month_
、year_
(图 15-5 )。
图 15-5
调用appointment
的print
函数
Golden Rule of Member Function Parameters
不要传入对象的数据成员。该函数已经知道它们。
防错法
**构造器被调用,但数据成员从未被初始化。**如果我们使用这个构造器
Date::Date (int theDay, int theMonth, int theYear) { theDay = day_; theMonth = month_; theYear = year_; }
也许我们会得到奇怪的输出
I'll see in you in the future, on -858993460--858993460--858993460...pencil me in!
我已经将day_
和theDay
交换了位置,所以我正在从复制我想要初始化的数据成员(显然其中有-858993460——有未初始化的变量,你永远不知道)到有我想要的值的参数(图 15-6 )。
图 15-6
一个完全颠倒的构造器
如果你在{
前使用()
的初始化方法,就不会发生这种情况,例如 15-1 。如果你试图初始化错误的东西,它会报告一个错误。
Exercises
写一个
Time
类,用于记住早上什么时候起床,什么时候午睡等等。包括相关的数据成员、打印函数以及适当的构造器。编写并测试一个函数
Time currentTime()
。它会调用time
(就像我们初始化随机数生成器一样),获取 1970 年 1 月 1 日以来的秒数。我们只关心从午夜开始的秒数。将其转换为秒、分和小时,并返回当前的Time
。当前是不是的成员Time
。像前一个问题一样,用调用
time
的函数Date currentDate ()
来扩充Date
程序,并获得当前的Date
。该功能是而不是的Date
成员。我之前假设时间从 1970 年 1 月 1 日开始,在你的机器上正确吗?(更难)添加一个函数
Date::totalDays ()
,该函数返回自公元前 1 年 12 月 31 日以来的天数。你需要处理闰年。项目2-date-bestSoFar
中的示例代码中有一个解决方案。(更难)添加一个函数
Date::normalize ()
,如果Date
有一个或多个字段超出范围,该函数将进行修正:例如,Date tooFar (32, 12, 1999);
将使tooFar
成为日期 1-1-2000。它应该由构造器调用。一个解决方案就在本书的样本代码项目2-date-bestSoFar
中。
有没有更简单的方法写normalize
和totalDays
?
const
对象,const
成员函数…
考虑以下代码:
const Date PEARL_HARBOR_DAY (7, 12, 1941);
cout << "A date which will shall live in infamy is ";
PEARL_HARBOR_DAY.print (cout);
cout << ".\n";
认为PEARL_HARBOR_DAY
是一个常量是合理的,因为它永远不会改变(除非你有一台工作的时间机器)。然而,如果我们把它设为const
,代码将不再编译。为什么不呢?
C++ 区分了能改变对象的成员函数和不能改变对象的成员函数。这是防止错误的一种方法。如果print
是那种可以改变Date
的东西,我们就不应该允许它在常量Date
上被调用。
由于print
对于const
对象来说是安全的,我们将这样告诉 C++:
class Date
{
...
void print (std::ostream&) const;
...
};
void Date::print (std::ostream& out) const
{
out << day_ << '-' << month_ << '-' << year_;
}
在()
后面的单词const
告诉编译器这个函数可以用于常量对象。它还告诉编译器,在编译print
时,如果对数据成员做了任何更改,就会产生一个错误。
如果你遇到很多错误,有时很容易将单词const
从你的程序中完全删除。抵制这种诱惑。这个特性确实可以保护我们免受真正的错误。
防错法
你会得到关于从转换到
const
的错误。这通常意味着您忘记了成员函数声明末尾的const
(以及其函数体的第一行)。它说你的成员函数体和声明不匹配,但是它们看起来确实一样。检查他们都是或者都不是。
…和const
参数
假设我们想将一个Date
传递给一个函数fancyDisplay
,它以一种可爱的方式打印时间:
void fancyDisplay (Date myDate, ostream& out)
{
cout << "*************\n";
cout << "* "; myDate.print (out); cout << " *\n";
cout << "*************\n";
}
我没有用&
来表示myDate
,所以myDate
本身不是传入的,而是一个副本。
在某种程度上,这很好,因为我们不想改变myDate
。但是复制的成本比仅仅int
要高——三倍多,因为它有三个int
。当我们创建更大的类时,我们可能会发现它会减慢我们的程序。
以下是部分解决方案:
void fancyDisplay (Date& myDate, ostream& out);
这并不完美,因为现在我们允许fancyDisplay
改变myDate
!这更好:
void fancyDisplay
(const Date& myDate, ostream& out);
现在fancyDisplay
不会花时间去复制myDate
也改不了。
*Golden Rule of Objects as Function Parameters
如果你想改变一个作为参数传入的对象,把它作为TheClass& object
传递。
如果没有,作为const TheClass& object
传递。
多个构造器
没有必要将我们自己局限于一个构造器。我们可能想用其他方式来创造Date
s:
Date d (21, 12, 2000); // using our old constructor...
Date e (d); // e is now exactly the same as d
Date f; // now one with no arguments
Date dateArray [MAX_DATES]; // still no arguments
Date g (22000); // 22,000 days -- nearly a lifetime
让我们一个一个来看。
复制构造器
Date
的复制构造器将另一个Date
作为唯一参数。我们称之为这个是因为它复制了(咄):
Date::Date (const Date& other) : // "copy" constructor
day_(other.day_), month_(other.month_), year_(other.year_)
{
}
这份宣言使用了它
Date e (d);
这个也是
Date e {d};
还有这个:
Date e = d; // Looks like =, but it's really calling the copy ctor
这种形式被称为“语法糖”:让代码更具可读性的不必要的东西。
复制构造器还有一些特殊之处。如果 C++ 需要复制一个Date
,它会隐式地调用**,也就是说,不用你告诉它。这里有两个例子:**
void doSomethingWithDate (Date willBeCopied);
// I'd rarely do this, but if I did...
Date currentDate (); // No &, so it returns a copy
不写复制构造器怎么办?C++ 会对如何复制做出最好的猜测,而这种猜测有时是危险的错误。一个好的规则:总是指定复制构造器。
默认构造器
Date::Date () : // "default" ctor
day_ (1), month_ (1), year_ (1) // default is Jan 1, 1 AD
{
}
...
Date f; // or: Date f {};
Date dateArray[MAX_DATES];
如果你不知道如何初始化你的Date
,你什么也不告诉它,它使用它的默认构造器:不带参数的那个。它还需要这个来初始化日期数组的元素。 4 所以我们总是写默认的构造器。
转换构造器
此构造器调用
Date g (22000); // or Date g {22000}; -- or Date g = 22000;
需要将这些太多的日子转换成更传统的日、月、年安排。我将使用前面练习 5 中的函数normalize
;如果我们给它 22,000 天,它会将其转换为 26 天、3 个月和 61 年:
Date::Date (int totalDays) : // conversion ctor from int
day_ (totalDays), month_ (1), year_ (1)
{
normalize ();
}
由这个函数和任何需要它的构造器调用的normalize
函数应该放在私有部分。函数会在需要的时候调用它,所以其他人不需要。 5
一个只有一个参数的构造器(不是一个Date)
)被称为转换构造器,因为它从其他类型(比如int
)转换成我们正在编写的类。
就像复制构造器一样,如果 C++ 需要一个Date
,但是你给了它一个int
,它会隐式地调用它。假设你调用了void fancyDisplay (const Date& myDate, ostream& out);
,但是传入了一个int
: fancyDisplay (22000, cout);
。C++ 会把 22000 转换成一个Date
,把fancyDisplay
转换成那个Date
。很好!
摘要
每个构造器都有责任确保数据成员处于某种可接受的状态。不幸的是,C++ 的基本类型让你不用初始化就可以声明它们。但是我们可以构造我们的类,这样 C++ 可以初始化所有的数据成员。
由于前面提到的问题,我推荐以下准则:
Golden Rule of Constructors
总是指定默认和复制构造器。
Extra
使用{}
来初始化变量和老式的()
的 and =:
Date g (22000.5); // No problem: casts from int to double
Date g {22000.5}; // WRONG: "narrowing" conversion loses data // -- compile-time error
这可能会防止我们无意中做一些愚蠢的事情。
此外,如果你不使用()
的,你不能混淆使用()
的调用构造器(如在Date d (21, 12, 2000);
中)和使用()
的函数声明——参见下面的反调试部分。
让事情变得更复杂的是,还有initializer_list
s,它支持将{}
用于另一种类型的构造器(见第二十三章)。
我倾向于像我们已经做的那样使用{}
——对于struct
和数组初始化——部分是为了清晰。但是其他人,包括 C++ 创始人比雅尼·斯特劳斯特鲁普本人,建议所有初始化都使用{}
,原因如下。
防错法
You declared an object, using the default constructor, but the compiler doesn’t recognize it :
Date z (); z.print (); // Error message says z is a Date() (?) // or at least isn't of a class type
使用
()
会让编译器认为你在为返回Date
的函数z
写声明。我是说,它怎么能说不是这个意思呢?The solution is to ditch the parens or replace them with
{}'
s:Date z1; Date z2 {};
**“非法复制构造器”或“无效构造器”**带此复制构造器声明:
Date (Date other);
。假设编译器让你调用这个函数。因为没有
&
,它要做的第一件事就是复制other
。怎么做?通过调用复制构造器,这意味着它必须复制一个other, w
,这意味着它调用复制构造器,等等,直到你用完内存。
这就是偶然递归,也就是一个函数在你不希望的时候调用自己。还好编译器(Visual Studio 或 g++)捕捉到了这个问题。解决方法:const &
。
代码重用的默认参数
我们可以通过告诉 C++ 来节省更多的工作,如果我们不给我们的一些函数参数,它应该适当地填充它们。
比如我已经厌倦了在myDate.print (cout);
中指定cout
。一般不都是cout
吗?但是我不想将cout
硬编码到函数中,因为我以后可能想要打印到一个文件中。
所以我改声明:void print (ostream& out = cout) const;
。
现在我可以只说myDate.print();
,编译器会想,他没说,所以他一定要 cout
。
我有一个带三个int
的构造器,一个带一个,一个不带任何东西。如果我使用默认值,我可以将它们合并成一个函数:
class Date
{
public:
Date (int theDay=1, int theMonth=1, int theYear=1);
// Defaults go in the declaration
...
};
...
Date::Date (int theDay, int theMonth, int theYear) :
day_ (theDay), month_ (theMonth), year_ (theYear)
{
normalize ();
}
现在我可以用 0 到 3 个参数来调用它:
Date Jan1_1AD;
Date convertedFromDays (22000);
const Date CINCO_DE_MAYO (5, 5); // Got a new one free that takes // day & month
Date doomsday (21, 12, 2012); // Doomsday? Well, that didn't // happen
默认参数也适用于非成员函数:
void fancyDisplay (const Date& myDate, ostream& out = cout);
//if not specified, print with cout
如果有些参数有默认值,而有些没有,那么那些有默认值的参数会放在最后。编译器不想混淆你打算省略哪些参数。
Date
程序(目前为止)
这里是我们所拥有的(例子 15-2 )。
我添加了一个枚举类型Month
和对isLeapYear
以及另外两个函数的声明。他们与日期有关,但他们不是Date
的成员。
我可以让Month
成为会员。但是为了引用(比如说)JUNE
,我不得不写Date::Month::JUNE
……那已经长得离谱了。
isLeapYear
成为Date
的成员没有意义:不是你对一个Date
做的事,而是你对一年int
做的事。它不需要访问Date
的数据成员day_
、month_
和year_
。它属于与 Date
,但我不会让它成为成员。
Tip
如果一个函数不能被合理地认为是对一个对象做了一些事情(包括“暴露它的秘密”,像一个访问函数),它可能就不应该是一个成员。
main
的目的是测试类和相关的函数。当设计一个新的类时,这是一个基本的实践(在第十七章和第十九章中再次展示,在这两章中,我们在使用新的类来创造一些有趣的东西之前测试它们)。
// A program to test the Date class
// -- from _C++20 for Lazy Programmers_
#include <iostream>
using namespace std;
enum class Month {JANUARY=1, FEBRUARY, MARCH, APRIL, MAY, JUNE,
JULY, AUGUST, SEPTEMBER, OCTOBER, DECEMBER};
bool isLeapYear (int year);
int daysPerYear (int year);
int daysPerMonth (int month, int year);
// We have to specify year in case month
// is FEBRUARY and it's a leap year
class Date
{
public:
Date (int theDay=1, int theMonth=1, int theYear=1);
// Because of default parameters,
// this serves as a ctor taking 3 ints;
// a new one taking days and months;
// the conversion from int constructor;
// and the default constructor
Date (const Date&); // copy constructor
void print (std::ostream& out = std::cout) const;
int totalDays () const; // total days since Dec 31, 1 B.C.
private:
int day_;
int month_;
int year_;
void normalize ();
};
Date::Date (int theDay, int theMonth, int theYear) :
day_ (theDay), month_ (theMonth), year_ (theYear)
{
normalize ();
}
Date::Date (const Date& other) :
day_ (other.day_), month_ (other.month_), year_ (other.year_)
{
}
void Date::print (std::ostream& out) const
{
out << day_ << '-' << month_ << '-' << year_;
}
// Date::totalDays and Date::normalize from earlier exercises
// as well as isLeapYear, daysPerYear, and daysPerMonth
// are omitted here, but they're in the book's sample code
void fancyDisplay (const Date& myDate, ostream& out = std::cout)
{
cout << "*************\n";
cout << "* "; myDate.print (); cout << " *\n";
cout << "*************\n";
}
int main ()
{
constexpr int MAX_DATES = 10;
Date d (21, 12, 2000); // using our old constructor...
Date e = d; // e is now exactly the same as d
Date f; // now one with no arguments
Date dateArray [MAX_DATES]; // still no arguments
Date g (22000); // 22,000 days, nearly a lifetime
cout << "This should print 26-3-61 with lots of *'s:\n";
fancyDisplay (22000); // tests conversion-from-int constructor
return 0;
}
Example 15-2A program using the Date class
Exercises
- 更新
Time
类以使用你在本章剩余部分学到的知识。
十六、类·续
更多的事情使你的类工作,并且工作得很好,特别是把你的程序的一部分放在多个文件中。
inline
提高效率的功能
考虑一下第八章和第十五章的函数调用图。它们展示了计算机的功能。它创建函数的新副本,即“激活记录”,包含函数实例需要的所有内容,尤其是局部变量。它将参数复制到内存中该函数可以访问的部分。它存储它需要知道的关于它所在函数的信息(CPU 中寄存器的状态——如果你不知道那是什么,不用担心)。最后,它将控制权转移给新功能。
完成后,它反转这个过程:丢弃函数及其变量的副本,并恢复旧函数的状态。
在运行时,计算机上的工作量很大。那我们该怎么办?停止使用函数?
解决方案是内联函数。一个内联函数被写成一个函数,就程序员而言,它的行为就像一个函数,并且看起来像是作为一个函数编译的,但是编译器做了一些偷偷摸摸的事情:它用一段代码代替函数调用来做同样的事情。这里有一种方法可以让一个函数内联——只要在它前面加上inline
:
inline
void Date::print (std::ostream& out) const
{
out << day_ << '-' << month_ << '-' << year_;
}
当你写这个的时候
d.print (cout);
编译器会像你说的那样处理它
cout << d.day_ << '-' << d.month_ << '-' << d.year_;
// but there's no problem with these members being private
从而节省了函数调用的开销。
如果一个函数足够大,那么调用的时间开销与花在函数本身上的时间相比并不显著,所以帮助不大。并且inline
引入了新的开销:内联扩展的大函数的多个副本将占用大量内存。以下是如何知道是否应该内联一个函数。
Golden Rule
inline
功能
**一个函数应该是内联的,如果它
适合单行。
不包含循环(for、while 或 do-while)。
扩展仅仅是对编译器的一个建议,而不是命令。如果编译器认为函数不应该扩展,它会否决你。我没意见;在这种情况下,编译器最清楚。
这里有一个快速、简单的方法来使成员函数内联——将整个事情放在类定义中:
class Date
{
...
void print (ostream& out) const
// inline, because it's inside the class definition
{
out << day_ << '-' << month_ << '-' << year_;
}
...
};
访问功能
有时我们希望世界上的其他人能够看到我们的数据成员,但不能改变它们。这就像一个时钟:你必须通过适当的控件(成员函数)来设置它,但是你可以随时看到时间。
事情是这样的:
class Date
{
public:
...
// Access functions -- all const, as they don't change data, just // access it
int day () const { return day_; }
int month () const { return month_; }
int year () const { return year_; }
....
};
调用它们的方法与使用print
相同——使用.
:
cout << myBirthday.year () << " is the year of the lion. Fear me.\n";
使用访问函数通常是个好主意,甚至在成员函数中。假设我决定转储day_
、month_
和year_
,并且只有一个数据成员totalDays
,成员函数可以根据需要从中计算日、月和年。引用day_
的函数将不再编译!但是如果它指的是仍然存在的days()
,那就好了。
这就是为什么我们在数据成员名称:day_
和其他之后使用下划线。这就是为什么我用了有趣的名字theDay
、theMonth
等等,作为我写的第一个构造器的参数。如果我这么做了
class Date
{
public:
Date(int day, int month, int year) :
day(day),month(month),year(year)
{
}
int day() const { return day; }
....
private:
int day, months, years;
};
我有太多的东西要命名,我永远也不会把它们分类! 1 也不会编译。名称必须不同。
单独的编译和包含文件
现在我们的程序足够长了,我们应该把它们分成多个文件。这里有一些通用指南,这样你就知道在哪里可以找到东西了:
我们通常给每个类一个自己的文件。
…让每一组明确相关的函数共享一个文件。例如,如果你在写三角函数正弦、余弦、正切等等,你可以把它们放在一起。
我们给
main
它自己的文件,可能与main
调用的函数共享,这些函数对其他程序没有用。比方说,如果你正在为扑克写一个程序,与叫牌相关的函数可能会放在main
的文件中(因为只有扑克才进行扑克式的叫牌),但是与洗牌和发牌相关的函数会放在其他地方(因为许多游戏都涉及到牌)。
然而,就让这一切发挥作用而言,还有一个问题。这个新文件需要知道某些事情(比如类定义!)—main
也会如此。这些信息必须共享。
幸运的是,我们已经知道如何做到这一点:包含文件。
在单独编译中会发生什么
假设我创建了这些文件:myclass.h
(如“header”中的“h”),包含类定义;myclass.cpp
,包含成员功能;和main.cpp
,包含主程序。(我给.h
和.cpp
文件起了和类一样的名字,小写。习俗各不相同;保持一致。)我是这样收录的:
#include "myclass.h" // <-- Use "" not <>; and let the file end in .h
下面是编译器构建程序的几个阶段。
首先,它编译c++“源”文件(你的.cpp
文件;见图 16-1 。当它遇到一个#include
指令时,它停止读取源文件,读取你包含的.h
文件,然后返回源文件。
图 16-1
构建程序的编译阶段
编译器用机器语言为每个源文件生成一个“目标”文件。
如果没有错误,编译器准备好链接(图 16-2 )。目标文件知道如何做它们要做的事情,但是它们不知道在哪里可以找到函数引用,无论是从彼此之间还是从系统库中。链接阶段通过解析引用将这些文件“链接”在一起,并生成一个可执行文件。如果您使用的是 Visual Studio,可执行文件将以.exe
结尾;g++ 是灵活的。
图 16-2
构建程序的链接阶段
看到这个过程使我们能够准确地理解什么应该和不应该进入包含文件。
写你的。h 文件
以下是包含文件中可以包含的内容(到目前为止):
类型,包括类定义和枚举类型
函数声明
任何事情
以下是不应该的:
功能
常量或变量声明(除了
inline
–继续阅读)
原因如下。如果你把一个函数(或变量声明)放在一个包含文件中,它将被包含到不同的.cpp
文件中。当你编译这些文件时,你会得到同一个函数的多个副本。当你调用这个函数的时候,编译器不知道使用哪个副本,也不知道它们是相同的。你会得到一个错误,说它有个重复的定义。
如果你想让你的函数出现在包含文件中…就把它设为inline
。
如果您想在包含文件中包含变量、const
s 或constexpr
s…也让它们成为inline
:
inline constexpr int DAY_PER_WEEK = 7;
inline const SSDL_Color BABY_BLUE = SSDL_CreateColor (137, 207, 240);2
不仅仅是为了提高函数调用的效率——它还防止了重复定义的问题。也许不是最清晰的关键字,但是allowInIncludeFilesWithoutDuplicateDefinitionError
太难输入了。
只包含一次. h 文件
假设time.h (
来自第十五章的习题;它定义了新的类Stopwatch
需要类Time
。我们需要#include "time.h"
,这样我们就可以声明start_
和stop_
:
// stopwatch.h: defines class Stopwatch
// -- from _C++20 for Lazy Programmers_
#include "time.h" // trouble ahead...
class Stopwatch
{
public:
Stopwatch () {}
private:
Time start_, stop_;
};
然后我们到了main.cpp
。
// Program that uses Stopwatches and Times
// -- from _C++20 for Lazy Programmers_
#include "time.h"
#include "stopwatch.h"
int main (int argc, char** argv)
{
Time duration;
Stopwatch myStopwatch
;
// ...
return 0;
}
Example 16-1A program that includes time.h and stopwatch.h. Since this and Example 16-2 work together, they’re in the same project in source code’s ch16 folder; it’s named 1-2-stopwatch
编译时main.cpp
首先,编译器包含了time.h
,它定义了类Time
。
然后包括stopwatch.h
。做的第一件事*是#include "time.h"
,它定义了类Time
。*又来了。编译器抱怨:类Time
的重复定义!
解决方案是告诉编译器只在还没有被读取的时候才读取一个.h
文件。有一个常用的技巧:在.h
文件中定义一些东西;然后在整个文件周围放些东西,说“如果你从来没听说过,就只看这个。”
// time.h: defines class Time
// -- from _C++20 for Lazy Programmers_
#ifndef TIME_H // If TIME_H is not defined...
#define TIME_H
class Time
{
// ...
};
#endif //TIME_H
Example 16-2time.h, written so it will only be processed once. Part of the 1-2-stopwatch project in source code’s ch16
第一次通过时,它从未听说过TIME_H
,所以它读取了.h
文件。这定义了类别Time
和TIME_H
。
下一次,它听说过TIME_H
,所以它跳到#endif
。阶级Time
没有被重新定义。任务完成。
为了防止这个问题,我对所有的包含文件都这样做。对常量(MYFILE_H
)使用相同的形式意味着我总是记得我如何拼写它,并防止名称冲突。
避免包含文件中的using namespace std;
using
namespace std
;
不应该在你的包含文件里。如果有人包含了您的文件,但不想使用std
名称空间怎么办?为了避免强加给他们,跳过using
宣言。为了让 C++ 仍然能够识别cin
和cout
以及std
名称空间中的其他东西,在它们前面加上std::
,就像在std::cin >> x;
中一样。
备份多文件项目
在 Unix 中,要备份目录myproject
,请输入以下命令:cp -R myproject myprojectBackup1
。
在 Windows 中,复制并粘贴整个文件夹,忽略不会复制的内容。
防错法
循环包括制造一个奇怪的错误。让我们修改time.h
,这样它就需要Stopwatch
:
#include "stopwatch.h"
class Time
{
void doSomethingWithStopwatch (const Stopwatch&);
};
假设某个.cpp
文件包含了time.h
。这定义了TIME_H
,然后包括stopwatch.h
(图 16-3 ,左)。
图 16-3
time.h
包括stopwatch.h
,其中包括time.h
——会有麻烦的
所以它暂时停止读取time.h
,读取stopwatch.h
(图 16-3 ,中间)。这定义了STOPWATCH_H
,然后又包含了time.h
(图 16-3 ,右图)。
因为已经定义了TIME_H
,所以#ifndef
让我们跳过内容。我们回到stopwatch.h
,当它到达第Time start_, stop_;
行时,它对我们大喊它从来没有读过Time
的定义,这是真的。所以程序不会编译。
一个包含文件可以包含另一个——但是它们不能包含彼此的*。*
*一些修复:
重新考虑这个功能是否应该在
Time
中。Time
真的要靠Stopwatch
吗?难道不应该反过来吗?(那是这个代码的最佳答案。)如果那样不行…只要代码不需要细节,你可以在不知道是什么的情况下参考
Time
中的Stopwatch
。只告诉time.h
说Stopwatch
是一个班:class Stopwatch; class Time { void doSomethingWithStopwatch (const Stopwatch&); };
问题解决了。
下一个问题:如果你有很多文件,却不记得把其中一个函数放在哪里,该怎么办?
Visual Studio:右击函数名。“转到申报”将带您到申报;“转到定义”将带你到函数本身,如果它可用的话。
Unix:尽管有一些软件包可以帮助解决这个问题(emacs 的 ggtags 就是其中之一),但不能保证它们就在您的系统上。这个命令是一个快速的 3 的方法来找到函数和对它的所有引用:
grep
functionIWant *
。MinGW:我用 Windows Grep——在网上找——来搜索函数名。
Microsoft Visual Studio 中的多文件项目
**要添加新文件,**进入项目菜单,选择添加新项目。您可以通过这种方式添加任何您需要的.h
或.cpp
文件;它会把它们放在正确的地方。
然后像往常一样构建并运行您的项目。
Extra
现在您已经有了多个源文件,您可能想要一种更简单的方法来清理 Visual Studio 创建的额外文件。 4
在项目所在的文件夹中,使用记事本或其他编辑器创建一个文件clean.txt
。把它放在一个文件夹里,里面只有你的工作(也许还有我的),没有任何不可替代的东西。这里面应该有些什么:
REM Erase folders you don't want -- here's my picks
for /r . %%d in (Debug,.vs) do @if exist "%%d" rd /s/q "%%d"
REM Erase other files -- here's my picks.
REM /s means "in subfolders too"
del /s *.obj REM Not needed, but now you know how to
REM erase all files with a particular extension
保存您的文本文件,并将其名称从clean.txt
改为clean.bat
。(看不到.txt
?取消选中隐藏已知文件类型的扩展名–参见第一章。)在图 16-4 的警告对话框中点击是。
图 16-4
Microsoft Windows 关于更改文件扩展名的警告
…并获取新的clean.bat
文件。
每当您想要抹掉额外的文件时,您可以双击这个“批处理”文件,即一个命令文件。被警告:del
永久删除东西。 5
g++ 中的多文件项目
要在 g++ 项目中使用多个文件,只需将它们添加到从basicStandardProject
(或basicSSDLProject
)复制的项目文件夹中。make
将照常建造一切。
不管有没有自己的 Makefiles,都要自己做,请继续阅读。
命令行:多打字,少思考
您可以使用以下命令构建程序:
g++ -g -std=gnu++2a -o myprogram myprogram.cpp myclass.cpp
您可以将编译和链接阶段分开:
g++ -g -std=gnu++2a -c myprogram.cpp #-c means "compile only -- don't link
g++ -g -std=gnu++2a -c myclass.cpp
g++ -g -std=gnu++2a6 -o myprogram myprogram.o myclass.o #now link
Makefiles:多思考,少打字(可选)
Makefiles 跟踪项目中已更改的文件。当你make
的时候,它只会重建它需要的部分。这减少了编译时间。输入make
比输入g++ -g -o myprogram file1.cpp file2.cpp...
更好
Makefiles 并不容易,但对于大型项目或使用大量库的项目来说,它们是必不可少的。本节将向您展示如何制作它们,从最简单的版本(例如 16-3 )到最复杂但也是最常用的版本(例如 16-5 )。
简单的版本
#This is a basic Makefile, producing one program from 2 source files
myprogram: myclass.o main.o #link object files to get myprogram
g++ -std=gnu++2a -g -o myprogram myclass.o main.o
main.o: main.cpp myclass.h #create main.o
g++ -std=gnu++2a -g -c main.cpp
myclass.o: myclass.cpp myclass.h #create myclass.o
g++ -std=gnu++2a -g -c myclass.cpp
clean:
rm -f myprogram # for Unix; with MinGW, rm -f myprogram.exe
rm -f *.o
Example 16-3A simple Makefile. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex-16.3\. To use it, copy it to Makefile and type make
第一行是注释,因为它以#开头。
为了简单起见,我将把事情打乱。这条线
main.o: main.cpp myclass.h
说要编译 main 的.o
(对象)文件,你需要main.cpp
和myclass.h
。如果其中任何一个发生变化,make
就会重建main.o
。(make
根据文件的修改时间检测更改。)
下一行g++ -std=gnu++2a -g -c main.cpp
是编译它的命令。如果失败,make
会停止,以便您可以纠正错误。
对myclass.o
的理解是一样的。
让我们回到顶部:
myprogram: myclass.o main.o
g++ -std=gnu++2a -g -o myprogram myclass.o main.o
这确定了myprogram
依赖于myclass.o
和main.o
,并告诉如何创建它。
因为这是 Makefile 中的第一件事,所以当你键入make
时,这就是计算机试图构建的东西。
clean
很好:如果你说make clean
,它会清除可执行文件和所有的.o
文件。-f
选项是这样的,如果没有错误,它不会报告错误,因为这不是问题。注意,Windows 将.exe
附加到它的可执行文件中,所以 MinGW 版本的clean
需要删除myprogram.exe
。
更好的版本
Makefile 的工作量太大了:我们必须指定每个.cpp
文件以及它所依赖的.h
文件。我们现在将创建一个 Makefile 文件,它应该适用于您将在本文剩余部分和其他地方遇到的大多数项目。
# Makefile for a program with multiple .cpp files
PROG = myprogram # What program am I building?
# MinGW: make this myprogram.exe
SRCS = $(wildcard *.cpp) # What .cpp files do I have?
OBJS = ${SRCS:.cpp=.o} # What .o files do I build?
$(PROG): $(OBJS) # Build the program
g++ -std=gnu++2a -g -o $@ $^
%.o: %.cpp # Make the .o files
g++ -std=gnu++2a -g -o $@ -c $<
clean: # Clean up files
rm -f $(PROG)
rm -f *.o
Example 16-4A Makefile for any project of .cpp source files – first attempt. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex-16-4.unix and Makefile.Ex-16.4.mingw. To test, copy the one for your platform to Makefile and make
首先,我们定义一些变量。
之后,我们看到我们的程序依赖于目标文件(和以前一样)。注意变量用$()
括起来。然后第一个 g++ 命令告诉我们如何从目标文件创建程序。
$@
表示“上面的:
左边的东西”,$^
表示“:
右边的一切”——即所有的对象文件。
产生.o
文件的部分为每个.cpp
文件制作一个。$<
的意思是“下一个文件右边有什么*,在这种情况下,下一个.cpp
文件。*
(如果你想知道如何使用这些看起来很奇怪的结构的一切,互联网是你的。如果你只是想找点有用的……互联网还是你的。我就是这么做的:查阅教程,看看什么能解决我的问题。)
要查看这些变量是如何被翻译成实际命令的,输入make
——它会在执行命令时打印出命令。
完整的版本
Makefile 仍然有一个大错误(除了看起来像是用埃及象形文字写的)。它不引用任何.h
文件。如果你改变了一个.h
文件,make
不会知道在此基础上重新编译——它应该知道。
所以我们在 Makefile 的末尾添加了一个神奇的咒语:
%.dep: %.cpp # Make the .dep files
g++ -MM -MT "$*.o $@" $< > $@
ifneq ($(MAKECMDGOALS),clean) # If not cleaning up...
-include $(DEPS) # bring in the .dep files
endif
第一部分说对于我们拥有的每个.cpp
文件,我们需要一个.dep
文件,它将包含依赖关系的信息。g++ -MM
行生成它。main.dep
文件可能看起来像这样
main.o main.dep: main.cpp myclass.h
意思是“每当main.cpp
或myclass.h
改变时,重新制作main.o
和main.dep
”
-MT "$*.o $@"
选项指定了:
左边的内容——它应该包含相关的.o
文件main.o
,加上main.dep
,这里指定为$@
。我们放置main.dep
的原因是,如果main.cpp
或myclass.h
中有任何变化(比方说,我们添加另一个#include
), main.dep
也会更新。
$<
是相关的.cpp
文件。
>
表示将输出存储在一个文件中,特别是$@
,这是我们正在创建的.dep
文件。
include $(DEPS)
表示将这些规则包含到 Makefile 中。最初的-
表示如果有错误就不要报告,比如文件不存在,这将在clean
之后第一次运行make
时发生。而ifneq...
说,如果你无论如何都要更新.dep
文件,那就不要担心它们。
是的,这很复杂,但这是突破口。
这是我们的结果。它应该不加改变地为新项目工作;如果您想要不同的可执行文件名称,请更改myprogram
。
# Makefile for a program with multiple .cpp files
PROG = myprogram #What program am I building?
# MinGW: make this myprogram.exe
SRCS = $(wildcard *.cpp) #What .cpp files do I have?
OBJS = ${SRCS:.cpp=.o} #What .o files do I build?
DEPS = $(OBJS:.o=.dep) #What .dep files do I make?
##########################################################
all: $(PROG)
$(PROG): $(OBJS) # Build the program
g++ -std=gnu++2a -o $@ -g $^
clean: # Clean up files
rm -f $(PROG)
rm -f *.o
rm -f *.dep
%.o: %.cpp # Make the .o files
g++ -std=gnu++2a -g -o $@ -c $<
%.dep: %.cpp # Make the .dep files
g++ -MM -MT "$*.o $@" $< > $@
ifneq ($(MAKECMDGOALS),clean) # If not cleaning up...
-include $(DEPS) # bring in the .dep files
endif
Example 16-5A complete Makefile. It’s in source code, ch16/3-4-5-makefiles, as Makefile.Ex16-5.unix and Makefile.Ex16-5.mingw. Copy the version for your platform into any folder where you have a project, naming it Makefile, and run by typing make
防错法
**Makefile:16: ***** 缺少分隔符 。停下来。
不开玩笑:这是因为在指定的行上,你用空格而不是制表符缩进。解决方案:哼哼,翻白眼,或者其他什么,使用制表符。
最终Date
程序
示例 16-6 至 16-8 显示了Date
的完成程序,如前所述,该程序被分成文件。main
是驱动程序,也就是设计用来测试类的程序。
// A "driver" program to test the Date class
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "date.h"
using namespace std;
int main ()
{
Date t (5,11,1955); // Test the 3-int constructor
// ... and print
cout << "This should print 5-11-1995:\t";
t.print (cout);
cout << endl;
// Test access functions
if (t.day () != 5 || t.month () != 11 || t.year () != 1955)
{
cout << "Date t should have been 5-11-1955, but was ";
t.print ();
cout << endl;
}
Date u = t; // ...the copy constructor
if (u.day () != 5 || u.month () != 11 || u.year () != 1955)
{
cout << "Date u should have been 5-11-1955, but was ";
u.print ();
cout << endl;
}
const Date DEFAULT; // ...and the default constructor
// I do consts to test const functions
if (DEFAULT.day () != 1 || DEFAULT.month () != 1 ||
DEFAULT.year () != 1)
{
cout << "Date v should have been 1-1-1, but was ";
DEFAULT.print ();
cout << endl;
}
// ...and total days
constexpr int DAYS_FOR_JAN1_5AD = 1462; // I found this number myself
// with a calculator
Date Jan1_5AD (1, 1, 5);
if (Jan1_5AD.totalDays () != DAYS_FOR_JAN1_5AD)
cout << "Date Jan1_5AD should have had 1462 days, but had "
<< DAYS_FOR_JAN1_5AD << endl;
// Test normalization
const Date JAN1_2000 (32, 12, 1999);
if (JAN1_2000.day () != 1 || JAN1_2000.month () != 1 ||
JAN1_2000.year () != 2000)
{
cout << "Date JAN1_2000 should have been 1-1-2000, but was ";
JAN1_2000.print ();
cout << endl;
}
cout << "If no errors were reported, "
<< " it looks like class Date works!\n";
return 0;
}
Example 16-8A driver program for class Date. It’s in source code, ch16, as part of the 6-7-8-date project/folder
// class Date -- functions
// -- from _C++20 for Lazy Programmers_
#include "date.h"
bool isLeapYear (int year)
{
//...
}
int daysPerYear (int year)
{
//...
}
int daysPerMonth (int month, int year)
{
//...
}
void Date::normalize ()
{
//...
}
int Date::totalDays () const
{
//...
}
Example 16-7date.cpp, abbreviated for brevity. A complete version is in source code, ch16 folder, as part of the 6-7-8-date project/folder
// class Date
// -- from _C++20 for Lazy Programmers_
#ifndef DATE_H
#define DATE_H
#include <iostream>
enum class Month { JANUARY=1, FEBRUARY, MARCH, APRIL, MAY, JUNE,
JULY, AUGUST, SEPTEMBER, OCTOBER, DECEMBER};
bool isLeapYear (int year);
int daysPerYear (int year);
int daysPerMonth (int month, int year);// Have to specify year,
// in case month is FEBRUARY
// and we're in a leap year
class Date
{
public:
Date(int theDay=1, int theMonth=1, int theYear=1) :
day_(theDay), month_(theMonth), year_(theYear)
{
normalize();
}
// Because of its default parameters, this 3-param
// ctor also serves as a conversion ctor
// (when you give it one int)
// and the default ctor (when you give it nothing)
// Default is chosen so that the default day
// is Jan 1, 1 A.D.
Date(const Date& otherDate) : // copy ctor
day_ (otherDate.day_ ),
month_(otherDate.month_),
year_ (otherDate.year_ )
{
}
// Access functions
int days () const { return day_; }
int months () const { return month_; }
int years () const { return year_; }
int totalDays () const; // convert to total days since Dec 31, 1 BC
void print (std::ostream& out = std::cout) const
{
out << day_ << '-' << month_ << '-' << year_;
}
private:
int day_;
int month_;
int year_;
void normalize ();
};
#endif //DATE_H
Example 16-6date.h. It’s in source code, ch16, as part of the 6-7-8-date project/folder
输出是
This should print 5-11-1995: 5-11-1955
If no errors were reported, looks like class Date works!
为了对用户友好,如果一切正常,驱动程序给出尽可能少的输出——测试时需要费力处理的输出也越少。
Exercises
在这些练习中,使用单独的编译;为您的类提供适当的构造器,在有用的地方使用默认参数;以及相关的写访问函数和内联函数。
如果结果表明.cpp
文件中没有任何内容(可能发生!),不需要写一个:
更新
Time
类以使用本章中所涉及的内容。添加一个常量
Time MIDNIGHT
,如果你的编译器支持的话,使用inline
让它对main.cpp,
可用。添加
Time
函数sum
和difference
,返回这个Time
与另一个Time
的和/差。它的返回值也是一个Time
。你想在你的墓碑上刻什么?创建一个
Tombstone
类,包含出生日期、死亡日期、姓名和墓志铭。除了一个成员函数print
之外,给它一个lifespan
函数,它返回这个人生命的持续时间作为一个Date
。将示例 16-1 和 16-2 中的
Stopwatch
具体化,以使用本章涵盖的内容。还要添加函数start
和stop
,它们启动和停止Stopwatch
(您可能已经完成了第十五章第一组练习中的练习 2:将Time
设置为当前系统时间),以及duration
,它们返回差值。然后使用Stopwatch
记录用户按 Enter 键的速度。创建一个类
Track
,它包含一段音乐的标题、艺术家和持续时间(aTime
)。现在创建一个类
Album
,它包含一个标题和一个Track
的数组。在你知道你将需要的其他函数中,包括一个函数duration()
,它是所有Track
持续时间的总和。
十七、运算符
您可能看到过以下错误:
char string1[] = "Hello", string2[] = "Hello";
if (string1 == string2) ...
这个条件不成立,因为数组的==
比较的是内存地址而不是内容,并且地址不同。
这也导致了一些问题:
string2 = string1;
它复制的不是string2
的内容,而是它的地址到string1
。string1
的内容丢失。这既浪费又容易出错:
string2[1] = 'a'; // string1 becomes "Hallo", though it
// wasn't even mentioned here!
因此,让我们创建自己的字符串类,迫使操作符做我们想做的事情,再也不用担心这个问题。
附录 B 列出了 C++ 让我们重载的运算符。简而言之:几乎任何,但你不能自己编。
基本字符串类
class String
{
public:
String (const char* other=""); // conversion from char* constructor;
// default constructor
String (const String &other);
private:
char* contents_;
};
我想让我的String
类处理任意长度的字符串,所以我将使用动态内存,就像在第十四章一样。
以下是设置默认值的两种方法:
因为
nullptr
:nullptr
按照惯例什么都不是,所以这是有意义的。但是如果我这样做,我将需要每个函数在访问contents_
之前检查nullptr
。工作太多。作为长度为 1 只包含
'\0'
的字符数组,即作为""
(空字符串)。我会用这个。
我现在写构造器: 1
String::String2 (const char* other = "")// conversion from char* constructor;
// default constructor
{
contents_ = new char[strlen(other) + 1];
// The +1 is room for final '\0'
strcpy(contents_, other);
}
String::String (const String &other)
{
contents_ = new char[strlen(other.contents_) + 1];
strcpy(contents_, other.contents_);
}
冗余代码太多了。也许我可以让一个建筑商把工作分包给另一个?当然可以。这个“委托构造器”让其他人做所有的工作。代码重用,更少的输入,耶:
String (const String &other) : String (other.c_str()) {}
现在我将创建一些新的函数。这些放在类定义中:
const char* c_str() const { return contents_; }
int size () const { return (int) strlen (c_str()); }
// Inefficient! Is there a better way?
析构函数
当使用动态分配的数组时,我们需要delete []
在完成后抛出内存。但是contents_
是String
的私有成员,所以main
做不到。也不应该;这是String
的工作。我们需要一个函数在完成String
后被调用。
输入析构函数(或常见的缩写“dtor”):
String::~String () { if (contents_) delete [] contents_; }
//Why "if (contents_)?" Paranoia. Deleting nullptr gives a crash.
这个名为~
加上类名的函数,每当String
消失时(例如,当String
在函数内部声明并且函数结束时),就会被自动调用。
与我们在第十四章中所做的相比,这是
工作少:写一次就完事了。
自动的,所以你不会忘记。
只要您记住黄金法则,内存管理就变得简单多了:
Golden Rule of Destructors
如果你在一个类中使用动态内存,总是写析构函数。
我再补充一条黄金法则。你可以违反它,但是它确实减少了错误。
Golden Rule of Dynamic Memory
如果你不用它也能得到你想要的,那就不要用它。如果必须的话,试着将它隐藏在一个类中,并用析构函数来清理。
析构函数可以在一个变量的生命周期结束时用于其他事情…但是我从来没有这样做过。
二元和一元运算符:==,!=,和!
这是我们的第一个运算符:
bool String::operator== (const String& other) const
{
return strcmp
(c_str(), other.c_str()) == 0;
}
使用==
操作符如下所示:
if (stringA == stringB)...
当计算机到达stringA == stringB
时,它进入功能String::operator==
。 3 有两个String
用了。左边的那个stringA
,是“这个”的:拥有这个operator==
功能的那个。右边的那个stringB
,是“另一个”的,作为参数传入的那个。
当在一个成员函数中,你引用一个指定所有者的成员,如在other.c_str()
中,那是属于other
的c_str()
。如果你不说它属于谁,它属于“这”一个——左边的那个。
这里有个好听的:!=
是==
的反义词吧?所以不用两个都写:写==
c++ 会隐式写!=
作为它的否定。 4
这些运算符是二元;每个都需要两个String
,一个一元运算符只有一个参数,比如(-myInt)+2
中的-(“一元减号”)或者if (! isReady)
中的!
。举个例子,我来写!对于String
类。! myString
将意味着myString
为空:
bool String::operator! () const { return ! size(); }
Golden Rule of Operators
如果一个操作符有一个参数,那么“this”对象——我们可以引用它的成员而不指定它们是谁的——是操作符调用中提到的唯一对象。
如果一个操作符有两个参数,“this”对象是调用中操作符左边的那个。作为参数传递的是右边的那个。
运算符没有三个参数。 5
所有其他比较运算符
还有其他比较String
s 的方法:<
(意为“按字母顺序排在前面”)、<=
、>
、>
、??。为了避免让使用我们的类的程序员猜测哪些是我们写的,让我们把它们都提供出来。
但是如果我懒得写全部四个,我就只写一个操作符,让 C++20 完成其余的。6T7】我要写的是一个特殊的操作符<=>
(称为“宇宙飞船”操作符,因为如果你仔细看,它看起来像一个 UFO)。它被定义为如果String1 < String2
,那么String1 <=> String2
应该返回一个负数;如果String1 > String2
,那么应该返回一个正数;如果它们相等,那么应该返回 0。
…就像strcmp
一样,一个<cstring>
函数,给定两个字符数组,返回确切的值。这是我们的三向比较(“飞船”)运算符:
int String::operator<=> (const String& other) const
// automagically generates <, <=, >, and >=
{
return strcmp (c_str(), other.c_str());
}
Exercises
制作一个
Fraction
类。您应该能够创建分数(指定分子和分母或默认为 0/1),打印它们,并使用所有可用的比较运算符进行比较。制作一个
Point2D
类。您应该能够创建点(指定坐标或默认为(0,0)),打印它们,并比较它们。如果两个Point2D
的 Xs 和 Ys 相同,则它们相等,但是如果它们的大小(与(0,0)的距离)大于另一个,则其中一个大于另一个。
赋值运算符和*this
我们如何将一个String
分配给另一个?
operator= (other)
delete the old memory
allocate the new memory, enough to hold other's contents
copy the contents over
还有一件事=总是做:它返回一些东西。我们通常称之为A=B;
,但这也是合法的:
A=B=C;
由于=是从右向左处理的,这就意味着A=(B=C);
真正的意思是:在做B=C
的时候,把C
的值赋给B
;返回您获得的值;并通过=
发送到A
。因此B=C
必须返回B
变成的值:
operator= (other)
delete the old memory
allocate the new memory, enough to hold other's contents
copy the contents over
return "this"
或者
String& String::operator= (const String& other)7
{
if (contents_) delete[] contents_; // delete old memory
contents_ = new char[strlen(other.c_str()) + 1]; // get new memory
//The +1 is room for final '\0'
strcpy(contents_, other.c_str()); // copy contents over
return *this
;
}
this
被定义为“this”对象的内存地址。既然this
是指向对象的指针,*this
就是对象本身。(我们很少在没有*
的情况下使用this
,尽管我们可以。)我们希望=
返回“这个”对象已经变成的样子;现在有了。
*this
是不是永远是从=
返回的东西。因为+=
、-=
等的操作符也返回新修改的对象,所以它们也返回*this
。
我想我会重写转换构造器和 operator=来提取它们共有的代码,并把它放在一个新函数copy
中。代码重用:
String::String (const char* other="") { copy(other); }
String& String::operator= (const String& other)
{
if (contents_) delete[] contents_; copy (other.c_str());
return *this;
}
void String::copy(const char* str)
{
contents_ = new char[strlen(str) + 1]; // get new memory
// The +1 is room for final '\0'
strcpy(contents_, str); // copy contents over
}
另一个最需要解释的是=
的返回类型。
假设是这样写的
String String::operator= (const String& other);
由于没有&
,它将调用复制构造器来复制它返回的内容。这需要时间,因为它必须一个字符一个字符地复制数组。如果我们返回的不是副本而是事物本身(*this
),我们可以节省时间:
String& String::operator= (const String& other);8
Golden Rule of Assignment Operators
每个赋值运算符(=
、+=
等)。)应该返回*this
。
…通过引用(如String&
)。
这是另一条规则。
Golden Rule of =
始终指定=。
原因和复制构造器一样:如果你不这么做,编译器会帮你做,而且可能会用很笨的方法来做。对于String
,会定义它复制内存地址。我们试图让远离。
防错法
一个常见的错误是把TheClassName::
放在错误的东西前面:
String::const char* c_str() const; //const is a member of String?!
根据编译器的不同,错误消息可能会令人困惑或一清二楚。无论哪种方法,解决方案都是将TheClassName::
放在函数名的左端。const char*
是返回类型;String::c_str
是函数名。
Exercises
将前面练习中的
=
添加到Fraction
类中。将前面练习中的
=
添加到Point2D
类中。如果你这么说:
myStr = myStr;
在String::operator=
会发生什么?修复=
以避免问题。我的答案在例 17-2 中。
算术运算符
现在我们将做一个“算术”操作符:+
。我觉得把+
定义为串联的意思是合理的。如果word
是"cat"
,addon
是"fish"
,那么word+addon
应该是"catfish"
。
我们会写+=
和+
。使用String
的程序员可能想要其中任何一个,如果他们不得不猜测我们提供了哪个,他们有理由感到恼火。
operator+= (other String)
remember the old contents
allocate new contents, big enough that we can add other.contents
copy the old contents into the new
append other contents
delete the old contents
return *this
秩序很重要。如果我们在使用之前删除旧内容,我们将会丢失其中的内容。
下面是有效的 C++ 代码:
String& String::operator+= (const String& other)
{
char* oldContents = contents_;
contents_ = new char [size() + other.size() + 1];
// 1 extra space at the end for the // null char
strcpy (contents_, oldContents); // copy old into new
strcat (contents_, other.c_str());// append other contents
delete [] oldContents;
return *this;
}
很好。现在我可以在类定义中内联operator+
。它也应该返回String&
吗?
String& operator+ (const String& other) const
// There's something wrong here...
{
String result = *this; result += other; return result;
}
让我们追踪调用它时会发生什么。
假设,在main
中,我们说copied = word+addon
。首先,我们称之为operator+
。使其result
(图 17-1 )。
图 17-1
运算符+(有缺陷的版本)在工作
然后它返回它的result
并离开(图 17-2 )。但是result
,作为+
的局部变量,在+
完成时被析构,所以main
得到的在它得到它的时候已经不存在了。使用它将是一个坏主意。
图 17-2
运算符+(有缺陷的版本)返回其值
解决办法就是还一份。它会一直持续到不再需要为止: 9
String String::operator+ (const String& other) const //That's better
{
String result = *this; result += other; return result;
}
Golden Rule of Returning
const &
局部变量不应该用&
返回。
函数返回后会持久的东西,包括*this
和数据成员,可能是。如果它们是类类型的,就应该是。
为什么 make + call +=,而不是反过来?+制作了两个副本:局部变量result
和我们返回时制作的副本。+=没有局部变量,返回String&
,所以效率相当高。如果我们让它调用+,它将不得不做额外的复制。
+
应该总是像这里写的那样,不管我们是添加String
s,数字,还是长鼻怪(不管它们是什么)——只要把String
改成你想要的任何新类型。
Exercises
增加
+
、-
、*
、/
、+=
、-=
、*=
、/=
至Fraction
。在
Point2D
中增加+
、+=
、-
、-=
。还要加上*
、*=
、/
、/=
。引用point1/point2
可能没有意义,但是引用point1/2
会有意义——你可以将两个坐标除以 2 得到一个新的Point2D
。因此,*
、*=
、/
和/=
的“其他”参数将是一个数字。
[]
和()
现在我们将支持使用[]
来访问单个字符。
char String::operator[] (int index) const { return contents_[i]; }
我们只完成了一半,因为虽然我们可以说char ch = myString[0];
,但是如果我们说myString[0] = 'a';
,编译器会抱怨“需要 L 值”
这意味着(非常粗略地——我保持简单)在=
(L 代表左)左边的东西不是那种可以在=
左边的东西;它是不可修改的。如果你想改变myString
,你不需要元素的副本*,而是元素本身:*
char& String::operator[] (int index) { return contents_[i]; }
如果两个函数除了返回类型之外完全相同,编译器不会感到困惑吗?但是它们和 ?? 的不一样;一个是const
。所以 C++ 会将const
应用于不能改变的事物,将非const
应用于可以改变的事物:
const String S ("Hello");
cout << S[0] << '\n'; //OK; uses the const version of []
String T ("Goodbye");
T[0] = 'Z'; //Also OK; uses the non-const version, which
// returns something that can be changed
Golden Rule of [ ] Operators
如果定义[]
,需要两个版本:
<type> operator[] (int index) const { ... }
<type>& operator[] (int index) { ... }
{}
之间的代码几乎肯定是相同的。
还可以添加()
运算符。我们可能想说mystring (2, 5)
来获得包含字符 2–5 的子串。这是它的宣言;实施例见 17-2 :
String String::operator () (int start, int finish) const;
您可以让()
操作符具有不同数量的参数。
我不用()
是因为对我来说像mystring (2, 5)
这样的东西并不清楚,但是如果你想要它,它就在那里。
Exercises
在
Fraction
中增加[]
(const
和非const
版本)。myFraction[0
是分子,myFraction[1]
是分母。…或者到
Point2D
。point1[0]
是 x 坐标,point1[1]
是 y 坐标。
++ 和-
说myString++
没有多大意义,所以我将从练习转移到Fraction
的例子。
myFraction++
应该在myFraction
上加 1。回想一下++
有两个版本:++myFraction
,意思是加 1,返回你得到的东西,myFraction++
,意思是加 1,返回你加之前拥有的东西。
这是预增量版本:
Fraction& Fraction::operator++ () // used for ++myFraction
{
*this += 1; // add 1 to this Fraction
// (Surely Fraction can convert from int?)
return *this;
}
怎样才能区分后增量版本?不是通过参数的名称或数量…所以 C++ 有一个 hack 10 就是为了这个:
Fraction Fraction::operator++ (int junk) //used for myFraction++
{
Fraction result = *this;
++(*this); //code reuse again
return result;
}
这里的int
论证真的是垃圾;这只是一个占位符,用来区分这个++
操作符和其他操作符。
Exercises
将++ 和-两个版本都添加到
Fraction
,并测试。将
++
和--
两个版本添加到Point2D
中,并进行测试。myPoint++
会给x_
分量加 1。
不是类成员的>>
和<<:
运算符
我还想用>>
和<<
打印String
s,用cin
和cout
或其他文件。
我们不能把这些操作符写成成员:操作符左边的东西永远是“这个”对象。但是在cout << myString
中,左操作数是cout
。如果我们把operator<<
写成String
的一员,cout
就得是String
了。
解决方法是让运算符成为非成员:
// this goes OUTSIDE the class definition
ostream& operator<< (ostream& out, const String& foo)11
{
...
return out;
}
我让它返回ostream&
,因为当我把<<
链接在一起的时候(比如在cout << X << Y
)。操作顺序为(cout << X) << Y
;也就是说,cout << X
做了它的工作,然后返回它的cout
的“值”,所以下一个<<
有cout
作为它的左操作数,可以用来打印。
这是我的第一次尝试:
inline // in string.h
std::ostream& operator<< (std::ostream& out, const String& foo)
{
out << foo.contents_; return out;
}
这不会编译。foo.contents_
是私人的。
我们可以通过访问函数返回contents_
,但这是一个更通用的解决方案:
class String
{
public:
...
void print (std::ostream& out) const { out << c_str(); }
...
};
inline
std::ostream& operator<< (std::ostream& out, const String& foo)
{
foo.print(out); return out;
}
我们只是以一种适用于我们编写的任何类的方式修复了隐私侵犯——这是一件好事。让我们以类似的方式处理cin >>
:
void String::read (std::istream& in);
inline
std::istream& operator>> (std::istream& in, String& foo)
// foo is not const!
{
foo.read (in); return in;
}
String::read
比String::print
更棘手。这是我的第一次尝试。
void String::read (istream& in) { in >> contents_; }
问题是我们不知道contents_
是否有足够的空间来存储输入的内容。
解决方案:
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
// biggest string we can read, incl '\0'
// What's this "static" thing? We'll get to that in the next section
...
};
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
如果你是 C++20 兼容的,in >>
会在溢出buffer
之前停止。如果没有…最好确保buffer
足够大。
你可以写其他操作函数作为非成员——传入我们调用的对象*this
作为第一个参数:
const String& operator= ( String& left, const String& right);
bool operator== (const String& left, const String& right);
我们通常不会,因为这些函数明显属于String
,需要访问私有数据成员。
Exercises
为
Fraction
类添加ostream <<
和istream >>
运算符。…还有
Point2D
类。
static
成员
C++ 爱重用——过度使用——关键字,所以static
有三个意思。
一个你知道的是一个局部 变量,当函数关闭时它不会消失,而是保留到下一次调用。这种情况我们已经见过很多次了,最近一次是在这里:
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
另一个我们不会想太多的是一个全局 常量、变量或函数,我们只希望在 .cpp
文件中可见,它是写在中的。
最后一个是应用于整个类的类成员,而不是它的一个特定实例:
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
//biggest string we can read, incl '\0'
...
};
这不是某个*String
的特征,而是所有*String
都有的特征**
**你也可以有一个static
成员函数,来报告对所有String
都成立的事情:
class String
{
public:
...
static12 int biggestReadableString ()
{
return BIGGEST_READABLE_STRING_PLUS_ONE - 1;
}
...
};
对构造器的显式调用
这很好:
String A;
A = "moo"; // conversion constructor creates a
// String containing "moo", passes to =
A += "moo"; // conversion constructor creates another, passes to +=
// now A == "moomoo"
这里有一些事情不会工作:
A = "moo" + "moo";
当 C++ 处理+
时,它不知道你想要属于String
的+
,因为两个操作数都不是String
!所以它会尝试使用字符数组的+
。那不会有好结果。
这是可行的:
A = String("moo") + "moo";
对String
的调用是“对构造器的显式调用”它创建了一个临时的String
变量,从未命名,然后 C++ 将operator+
应用于它。当它完成将结果复制到A,
时,它删除它。 13
我发现它对Point2D
特别有用:
myPoints[0] = Point2D (2, 5);
myPoints[1] = Point2D (3, 7);
...
Exercises
编写一个程序,声明五个
Fraction
并将它们相乘,不将它们命名为变量,通过使用对构造器的显式调用。编写一个程序,声明五个
Point2D
并打印它们,不将它们命名为变量,通过使用对构造器的显式调用。
最终字符串程序
...如示例 17-1 至 17-3 所示。
// class String, for char arrays
// -- from _C++20 for Lazy Programmers_
#include <cstring>
#include "string.h"
using namespace std;
String& String::operator= (const String& other)
{
if (this == &other) return *this; // never assign *this to itself
if (contents_) delete[] contents_; copy(other.c_str());
return *this;
}
void String::copy (const char* str)
{
contents_ = new char[strlen(str) + 1];
// The +1 is room for final '\0'
strcpy(contents_, str);
}
String& String::operator+= (const String& other)
{
char* temp = contents_;
contents_ = new char [size() + other.size() + 1];
// 1 extra space at the end for the null char
strcpy (contents_, temp);
strcat (contents_, other.c_str());
delete [] temp;
return *this;
}
String String::operator () (int start, int finish) const
{
// This constructs the substring
String result = *this;
strcpy (result.contents_, contents_+start);
// contents_+start is the char array that is
// "start" characters after contents_ begins
result.contents_[finish-start+1] = '\0';
// the number of chars in this sequence
// is the difference plus 1
return result;
}
void String::read (std::istream& in)
{
static char buffer [BIGGEST_READABLE_STRING_PLUS_ONE];
in >> buffer;
*this = buffer;
}
Example 17-2string.cpp
// String class
// -- from _C++20 for Lazy Programmers_
#ifndef STRING_H
#define STRING_H
#include <cstring> // uses cstring functions all over
#include <iostream>
class String
{
public:
static constexpr int BIGGEST_READABLE_STRING_PLUS_ONE = 257;
// biggest string we can read, incl '\0'
static int biggestReadableString()
{
return BIGGEST_READABLE_STRING_PLUS_ONE - 1;
}
String (const char* other="") { copy(other); }
String (const String &other) : String (other.c_str()) {}
// a "delegated" constructor
~String() { if (contents_) delete [] contents_; }
// access function
const char* c_str() const { return contents_; }
// functions related to size
int size () const { return (int) strlen (c_str()); }
//Inefficient! Is there a better way?
bool operator! () const { return ! size(); }
// comparisons
bool operator== (const String& other) const
{
return strcmp (c_str(), other.c_str()) == 0;
}
int operator<=> (const String& other) const
{
return strcmp (c_str(), other.c_str());
}
// assignment and concatenation
String& operator= (const String& other);
String& operator+= (const String& other);
String operator+ (const String& other) const
{
String result = *this; result += other; return result;
}
// [] and substring
char operator[] (int index) const { return contents_[index]; }
char& operator[] (int index) { return contents_[index]; }
String operator () (int start, int finish) const;
// I/O functions
void read (std::istream& in );
void print (std::ostream& out) const { out << c_str(); }
private:
char* contents_;
void copy(const char* str);
};
inline
std::istream& operator>> (std::istream& in, String& foo)
{
foo.read (in); return in;
}
inline
std::ostream& operator<< (std::ostream& out, const String& foo)
{
foo.print(out); return out;
}
#endif //STRING_H
Example 17-1string.h. The source code project containing this and Examples 17-2 and 17-3 is 1-2-3-string
驱动程序(例如 17-3 )使用函数void
assert
(bool condition)
,该函数验证condition
为真,如果不为真,则使程序崩溃。很好。如果有什么不对劲,我们会知道的。
// Driver program to test the String class
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert
> // for assert, a function which crashes
// if the condition you give is false
// used for debugging
#include "string.h"
using namespace std;
int main ()
{
// using consts to ensure const functions are right
const String EMPTY;
const String ABC ("abc");
// Testing default ctor, conversion ctor from char*, ==, !=, !
assert (EMPTY == ""); assert (! EMPTY); assert (! (EMPTY != ""));
assert (ABC != ""); assert (! (ABC == ""));
// Testing c_str, size ...
assert (strcmp (ABC.c_str(), "abc") == 0);
assert (ABC.size() == 3);
// Test >, >=, <, <=, !=,
// We're doing lots of implicit calls to conversion ctor
// from const char*, so that's tested too
assert (ABC < "abd"); assert (! (ABC >= "abd"));
assert (ABC <= "abd"); assert (! (ABC > "abd"));
assert (ABC > "abb"); assert (! (ABC <= "abb"));
assert (ABC >= "abb"); assert (! (ABC < "abb"));
assert (ABC <= ABC); assert (ABC >= ABC);
// Test []
String xyz = "xyz";
assert (xyz[1] == 'y'); xyz[1] = 'Y';
assert (xyz[1] == 'Y'); xyz[1] = 'y';
assert (ABC[1] == 'b'); //const version
// Test =, ()
xyz = "xyz and more";
assert(xyz(4, 6) == "and");
// Test copy ctor
assert (ABC == String(ABC));
// Test + (and thereby +=)
String ABCDEF = ABC+"def";
assert (ABCDEF == "abcdef");
// Testing << and >>
String input;
cout << "Enter a string:\t"; cin >> input;
cout << "You entered:\t" << input << '\n';
cout << "If no errors were reported, "
<< "class String seems to be working!\n";
return 0;
}
Example 17-3A driver for String
Exercises
使用
assert
和对构造器的显式调用,测试你的Fraction
类。…或者你的
Point2D
课。C++ 的内置库使用从
unsigned int
派生的一种类型size_t
作为数组大小。更新String
类,包括它的测试器,将size_t
用于howMany_
和其他合适的东西。
#include <string>
下面是我一直隐藏的:C++ 已经有了一个 string 类,你现在知道如何使用它了。你需要#include <string>
。型号是string
,不是String
。如果编译器完全兼容 C++20,您可以声明constexpr string
s。
十八、异常、移动构造器和=
、递归和 O 符号
一个必要的东西——处理错误条件的更好的方法——和一些非常好的东西:更有效的复制(“移动”函数),调用自己的函数(递归),以及计算函数的时间效率的方法。
例外
程序应该如何处理运行时错误?一些选项:
鸵鸟算法:简单地希望问题永远不会发生——你的整数永远不会超过
INT_MAX
,你对strcpy
的调用永远不会超过char
数组,等等。我们做了很多,而且很有效!我们可以在核反应堆的软件中试试吗?验证谁能进入你的银行账户?哎呀。崩溃:也不要在核电厂提出这个建议。
打印一条错误消息:适合您的笔记本电脑;对于微波炉或智能手表来说不太好。
返回一个错误代码:让你的函数返回
int
,如果返回值是表示“错误”的东西(可能是-1),一定是哪里出错了。不过,总是检查返回值是一项很大的工作。
不同的情况需要不同的解决办法。我们需要一种简单的方法来区分错误检测(不会改变)和错误解决(会改变)。
为了说明做到这一点的“异常”机制,让我们举一个例子。栈是计算机科学中常用的数据结构。(你已经在第九章中遇到了调用栈。)就像一堆自助餐厅的托盘。你所能做的,就是在不打扰食堂工作人员的情况下,把一个托盘放在上面(“推”托盘),从上面拿走一个(“砰”一声),看着最上面的一个,注意这个托盘是否是空的。That 和(因为这是 C++)构造并可能破坏它。
我会偷懒(当然!)并避免动态记忆。
以下是我们可能会遇到的问题:
我们可能会尝试将一个项目推到一个已经满了的栈上(“溢出”)。
我们可能试图从空的栈中查看顶部的项目,或者弹出一个项目(“下溢”)。
成员函数应该只注意到的错误,比如这里的top
:
class // A stack of strings
{
public:
class UnderflowException {}; // An "exception class"
...
const std::string& top () const
{
if (empty()) throw UnderflowException ();
else return contents_[howMany_-1]; // the top element
}
...
};
如果一切顺利,top
返回Stack
中的最后一项,即howMany_
-1。但是如果Stack
为空,top
使用对构造器的显式调用创建一个UnderflowException
类型的对象。然后它抛出 s(报告)。
(BNF 版本:throw
<某些变量或值> 。)如果没有人知道如何处理错误,程序就会崩溃,这是应该的。我们已经看到过未处理的异常,比如,当我们试图加载一个文件名拼错的图像时,SSDL 抛出了一个异常。这可能是我们所需要的,为了这个或者为了溢出。
示例 18-1 显示了Stack
类,突出显示了与异常相关的代码。
// Stack class, with a limited stack size
// -- from _C++20 for Lazy Programmers_
#ifndef STACK_H
#define STACK_H
#include <string>
class Stack
{
public:
class UnderflowException {}; // Exceptions
class OverflowException {};
static constexpr int MAX_ITEMS = 5;
Stack() { howMany_ = 0; }
Stack(const Stack& other) = delete
;1
const Stack& operator= (const Stack& other) = delete;
const std::string& top () const
{
if (empty()) throw UnderflowException ();
else return contents_[howMany_-1];
}
void push (const std::string& what)
{
if (full ()) throw OverflowException ();
else contents_[howMany_++] = what;
}
std::string pop ()
{
std::string result = top(); --howMany_; return result;
}
bool empty () const { return howMany_ == 0; }
bool full () const { return howMany_ >= MAX_ITEMS; }
// Why not just see if they're equal? howMany_ *can't* be
// bigger than MAX_ITEMS, can it?
// Not if I did everything perfectly, but...
// better to program defensively if you aren't perfect
private:
std::string contents_ [MAX_ITEMS];
int howMany_; // how many items are in the stack?
};
#endif //STACK_H
Example 18-1A Stack class. Find it and Example 18-2 in source code, ch18, as 1-2-stack
但是如果我们愿意,我们可以让调用栈中的函数知道如何捕捉并处理抛出的内容。
把异常当成一个烫手山芋。if
语句抛出。如果它所在的函数知道如何捕捉它,很好。如果没有,它就把这个烫手山芋,我指的是UnderflowException
,传递给任何一个被称为 it 的函数,然后是那个调用那个函数的函数,以此类推。每一次,函数停止它正在做的事情并立即返回,延迟足够丢弃它的局部变量,必要时析构。这种情况一直持续到我们返回一个可以捕捉错误的函数,或者我们带着错误退出程序。
假设我们决定main
应该处理这个错误。我将为main
配备一个试抓块(例如 18-2 )。try
部分包含了我想做的事情;catch
部分包含出错时的错误处理代码。
int main ()
{
Stack S;
try
{
...
cout << S.top (); // if top fails, skip the rest of the try block
// and go straight to the catch block
...
}
catch (const Stack::UnderflowException&)
{
cout << "Error in main: stack underflow.\n";
cout << "Saving everything and quitting...\n";
... code that handles any cleanup we need to do ...
cout << "Quitting now.\n";
return;
}
//maybe a catch for Stack::OverflowException too
return 0;
}
Example 18-2Code to catch an UnderflowException. Also in project/folder 1-2-stack
try
- catch
块的结构是
try { <do stuff> }
catch (<parameter>) {<error handling code> }
可能还有更多的渔获
**那么什么是是UnderflowException
?这就是你所看到的:一个类型为UnderflowException
的对象,没有数据成员和成员函数。这很蠢吗?一点也不。throw
ing 它告诉main
发生了下溢。它还想知道什么?如果出于某种原因,你确实希望你的异常类包含数据成员、函数等等,没问题;throw
和catch
的工作方式相同。我几乎从来没有。
如果你在一个异常的catch
块中,并想再次抛出它,请不带任何参数地说throw
。
如果你想禁止一个函数抛出一个异常,在最上面一行追加noexcept
:void mustNotThrowExceptions () noexcept
。我很少这样做,但是下一节将展示一种用法。
应该使用异常吗?
没错。它们非常适合在应该处理错误的地方进行错误处理,只需编写最少的额外代码。我经常使用它们。(我承认我很少抓到他们。也许那是因为我更喜欢为那些核电站写库而不是软件。)
Exercises
改编上一章的
String
类,如果传递给[]
操作符或子串操作符()
的索引超出范围,抛出异常。测试以确保其工作正常。添加并测试一个
Fraction
成员函数以转换为double
;如果分母为 0,则抛出异常。将前一章中的
istream operator>>
修改为Point2D
或Fraction
,这样如果istream
出错(就像在需要int
的时候输入了char
,它就会抛出一个异常。可以这样检测问题:if (! myIstream) ...
移动构造器并移动=
(可选)
做了过多的工作,这可能会减慢我们的速度。(好的,我从来没有注意到,但是 C++ 社区对效率很挑剔。)考虑以下代码:
newString = str1 + str2;
运算符+
中有一个临时副本,重复如下。就是我们返回result
的时候。这将调用复制构造器,其中包含对strcpy
的调用。字符串越大,strcpy
耗时越长。
String String::operator+ (const String& other) const
{
String result = *this; result += other; return result;
}
现代 C++ 有一种机制,通过这种机制,某些东西可以将它的内存让给需要它的其他东西,从而避免了复制的需要(例如 18-3 )。
String (String&& other) noexcept // The "move" constructor.
// I'll explain "noexcept" in a moment
{
contents_ = other.contents_; // 2 statements; no loops,
other.contents_ = nullptr; // no strcpy. Cheap!
}
Example 18-3A move constructor for String. In source code as part of project/folder 3-4-string
&&
的意思是“如果参数可以放弃它的值,就应用这个函数。”operator+
的result
肯定是这样!所以我们从result
(在 move 构造器中暂时称为other
)获取内容。我们给它nullptr
,这样当它遇到自己的析构函数时,它不会delete[]
它给我们的contents_
。
其他额外的工作是在我们离开+之后,当它提供的临时副本被operator=
复制时,它也做一个strcpy
。如果我们只是将副本的内容移动到newString
中,我们可以节省时间。示例 18-4 展示了新的=
运算符。
像这样移动,而不是复制,被称为移动语义——我们将再次看到这个术语。
String& String::operator= (String&& other)
noexcept //move =
{
if (contents_) delete[] contents_;
contents_ = other.contents_; //no loops! no strcpy!
other.contents_ = nullptr;
return *this;
}
Example 18-4A move assignment operator for String. Also part of 3-4-string in source code
为了测试这是否真的有效,我建议您这样做!–加载源代码(ch18
的3-4-string
项目),在新的“move”函数中放置断点,并查看它们是否被调用。这条线应该叫两个:newString = str1 + str2
;。
如果 move =
或 move 构造器抛出异常,可能会发生奇怪的事情,所以如果我不把noexcept
放在最后,编译器可能会警告我。我喜欢让它开心,所以我喜欢。
因为我们现在有了更多的选择,所以我想用下面的内容来取代旧的构造器和=的黄金法则:
Golden Rule of Constructors and =
要么有
没有构造器并且没有指定=(本质上是旧式的
struct
s ),或者指定了默认构造器、复制构造器和=或
默认构造器、复制构造器和=,加上移动构造器和移动=。
Exercises
- 改编第十六章的练习 5 及其
Track
和Album
来使用动态记忆。然后写一个 move 构造器,为Album
移动=
。使用调试器进行测试,以确保您在应该使用 move 函数的时候真的使用了它。
递归(可选;在下一节中引用)
有时候从事物本身来定义它是最简单的。
例如,考虑“阶乘”函数。5!(读作“5 阶乘”)就是 54321 = 120。0!和 1!都是 1;总的来说,n!是 n*(n-1)(n-2)...21.(n-1)!is (n-1)(n-2)...21;所以 n!= n(n-1)!。随着 n 的增加,结果越来越大。
这是一个计算 n 的算法!;示例 18-5 显示了完整的功能。
if n is 0, return 1
else return n * (n-1)!
这说明了确保递归(一个调用自身的函数)终止所需的两个一般原则:
**必须有一个结束条件。**否则,递归永远不会结束(直到程序因内存不足而崩溃)。
出于同样的原因,必须朝着那个结束条件前进。
int factorial (int n) // maybe give it unsigned --
// and return unsigned long long?
{
if (n == 0) return 1;
else return n * factorial (n-1);
}
Example 18-5A simple recursive function, in source code as 5-factorial
递归有效的原因是 C++ 为每个调用创建了一个新的副本(“激活记录”)。假设您的主程序调用了参数为 3 的factorial
。
n
不是 0,只好调用factorial (n-1)
。
我们一次又一次地调用factorial
,直到我们得到一个版本的factorial
,它的 n = 0,所以不再递归。它将向factorial(1)
调用返回 1。
然后,factorial(1)
返回1*factorial(0)
,也是 1。
factorial(2)
返回2*factorial(1)
,为 2。
最后,factorial(3)
返回3
* factorial(2)
,即 6。
如果你在微软 Visual Studio 中调试一个带有递归函数的程序,你可以在调用栈中看到该函数的副本(图 18-1 )。
图 18-1
Visual Studio 中的调用栈
在ddd
或gdb
中,where
向您显示调用栈。up
和down
带你在上面的函数副本之间切换——当你print
一个变量时,它会使用你正在看的副本的上下文。
因为一个递归函数有多个副本,所以对于编译器来说,比我们通常的循环一个动作的方法,也就是迭代,要多做更多的工作。但是有时候递归编写函数比迭代更容易。
The Golden Rule of Recursion
每个递归函数都必须有,所以它会终止,
没有进一步递归调用的基本情况
在每次递归调用中向基本情况前进
防错法
你的程序运行了一会儿,然后崩溃说“分段错误”或“栈溢出”
要么你忘记了结束条件,要么你没有朝它前进。您可以使用调试器来判断。
Exercises
斐波那契数列是这样的:
斐波那契(1) = 1
Fibonacci (2) = 1
如果 n > 2,则 Fibonacci(n)= Fibonacci(n–1)+Fibonacci(n–2)。
使用递归来编写斐波那契函数和一个程序来测试它。
编写并测试一个函数
void indent (const char* what, int howMuch)
,它在缩进howMuch
空格后打印字符串what
。如果howMuch
为 0,则只打印字符串。如果没有,它打印一个空格并用howMuch-1
空格来调用自己。编写并测试
pow
函数的递归版本。pow (a, b)
返回 a b 。提示:a b = a * a b-1 。编写并测试一个递归函数 log,给定一个正整数
number
和一个整数base
,返回 log basenumber
。log base (number)定义为在达到 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。
效率和 O 符号(可选)
假设我们想对一个名字列表进行排序。我知道!让我们生成所有可能的名字排序,当我们得到一个完全有序的名字时就停止!电脑很快,对吧?
do
generate a new permutation of the elements
while we haven't found an ordered sequence
这不是很多细节,但我预测一个问题。假设有四个元素。第一个元素有四种可能。这就给下一个留下了三种可能性,给下一个留下了两种可能性,给最后一个留下了一种可能性;有 432*1 种可能:4!。所以对于 N 个元素,我们有 N 个!要考虑的顺序。用 N = 100,那就是 10 158 。电脑很快,但它们没有 ?? 那么快。
有时候算法显然是个坏主意。有时直到你运行它,你才意识到它有多糟糕——除非你使用符号,这样你就能知道什么程序而不是要带给管理层,什么程序而不是要花时间去写。
考虑以下代码:
for (int i = 0; i < N; ++i)
sum += array[i];
初始化完成一次;比较、数组引用、赋值和递增都要进行 N 次。我们可以说有 1 + 4N 个东西被执行。
其他一些有循环的片段呢?可能有点不同,我们得到,哦,5 + 3N。哪个更快,或者它们是一样的?嗯。我们需要一个比较的方法。
O 符号极大地简化了我们描述这些时间需求的方式,从而帮助我们比较和评估它们。以下是 O 符号的简化规则:
当数据集很大时,如果一个加数明显小于另一个加数,则丢弃较小的加数。所以如果我们有 1 + 3N,我们丢弃 1,得到 3N。
放弃常量乘数。3N 变成 n。
结果写成 O(N)。for 循环是 O(N),或者“是 N 阶的”
这种简化是合理的。我们关心的是当数据集很大时会发生什么(小数据集总是很快)。当 N 较大时,3N + 1 约为 3N;3,000,001 和 3,000,000 的差别可以忽略不计。我们也不关心常量乘数。无论是 N 从 3000 翻倍到 6000,还是 3N 从 9000 翻倍到 18000,都还是翻倍,我们想知道增加 N 是如何降低性能的。这告诉我们。
这里还有几个例子。考虑这个算法:
read in N 1
read in M 1
read in P 1
add them 1
divide by 3 1
print the average 1
每一行是一个动作。把它们加起来,我们有六个。我们可以抛弃常量乘数;6 = 6 * 1,所以 O(6)=O(1)。这个算法是一阶的;不管我们给它什么值,它都需要相同的时间。
这里还有一个:
for each element in an array N x
if this element is negative (1
change it to positive + 1)
最后一行是一个动作。包含它的 if 语句多一个;O(2)=O(1)。因为是在 for 循环中,所以会做 N 次,其中 N 是数组的长度。所以这个算法是 O(N)。
这里还有一个:
do
for each successive pair of elements in an array
if they are in the wrong order
swap them
while our last iteration of the do-while loop had a swap in it
这个算法是一种对数组进行排序的方法。它是这样工作的。考虑一组芝麻街角色。
| 科米蛙 | 格罗弗 | 伯特(男子名ˌ等于 Burt) | 奥斯卡金像奖 | 小猪 | 埃尔默 |do-while 循环的第一次迭代根据需要对每个连续的对进行交换。克米特应该换成格罗弗:
伯特:
我们继续在数组中移动,直到到达末尾,交换任何顺序错误的数组对。
| 格罗弗 | 伯特(男子名ˌ等于 Burt) | 科米蛙 | 奥斯卡金像奖 | 埃尔默 | 小猪 |它仍然不正常,但我们取得了进展。这是我们再次遍历数组后得到的结果:
| 伯特(男子名ˌ等于 Burt) | 格罗弗 | 科米蛙 | 埃尔默 | 奥斯卡金像奖 | 小猪 |另一个:
| 伯特(男子名ˌ等于 Burt) | 格罗弗 | 埃尔默 | 科米蛙 | 奥斯卡金像奖 | 小猪 |另一个:
| 伯特(男子名ˌ等于 Burt) | 埃尔默 | 格罗弗 | 科米蛙 | 奥斯卡金像奖 | 小猪 |这种算法被称为“冒泡排序”,因为元素逐渐“冒泡”到正确的位置。(还有“bogo-sort”被人说是邪恶的,因为太慢了。我不知道他们会把我之前的排列方法叫做什么,但它不会很好。)
用 O 表示法需要多长时间?“如果它们的顺序不对,就交换它们”是 O(1)。我们遍历整个数组,进行 N-1 次比较;所以通过数组的次数是 O(N-1)=O(N)。我们通过了多少次?如果数组非常无序——比方说,如果 Bert 在最后一个槽中——我们将需要 N-1 次传递,因为每次传递最多将 Bert 向左移动一个槽。O(N(N-1))= O(N2-N)= O(N2)。
O(N 2 )称为“二次时间”;O(N)可预测地称为“线性时间”O(1)是“恒定时间”我们尽量避免 O(2 N ),“指数时间”
Online Extra
要测量实际的时间…“测量你的代码到毫秒”:在 YouTube 频道“以懒惰的方式编程”,或者在 www.youtube.com/watch?v=u_zyO7LgXog
找到它
运筹学
在 Apress 博客上用 C++ 计时的事情: www.apress.com/us/blog/all-blog-posts/timing-things-in-c-plus-plus/17405398
。
Exercises
对于示例 10-3 中返回数组中最小数字的函数,用 O 表示的时间是多少?
…对于一个函数来说,确定一个单词是否是回文?
…对于打印 N
×
N 网格中所有元素的函数?写一个函数
intersection
,在给定两个数组的情况下,找到所有相同的元素,并将它们放入一个新数组中。用 O 符号表示的时间要求是什么?编写冒泡排序,并验证它的工作原理。
写一个程序,使用厄拉多塞筛找出所有达到某个极限的素数:你遍历并消除除 2 之外的所有能被 2 整除的数,然后除 3 之外的所有能被 3 整除的数,依此类推。
用 O 符号表示的时间要求是什么?
上一节的练习 2、3 和 4 需要多长时间?
使用“在线附加”链接中的方法,对不同大小的数组进行时间泡排序。和 O 符号预测的相符吗?预计您测量的时间会有很多随机变化。
十九、继承
本章的目的是让我们能够在相似的类之间重用代码。
继承的基础
有一个不成文的规则,在 C++ 介绍文本中,你必须有一个使用雇员记录的例子,比如 Example 19-1 。有道理。我个人想不出比员工记录更令人兴奋的事了。
// Class Employee
// -- from _C++20 for Lazy Programmers_
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
#include <iostream>
#include <string>
#include "date.h"
class Employee
{
public:
Employee ();
Employee (const Employee&) = delete;
Employee (const std::string& theFirstName,
const std::string& theLastName,
const Date& theDateHired, int theSalary);
Employee& operator= (const Employee&) = delete;
void print(std::ostream&) const;
// access functions
const std::string& firstName () const { return firstName_; }
const std::string& lastName () const { return lastName_; }
const Date& dateHired () const { return dateHired_; }
int salary () const { return salary_; }
bool isOnPayroll () const { return isOnPayroll_; }
int badPerformanceReviews () const
{
return badPerformanceReviews_;
}
void quit () { isOnPayroll_ = false;}
void start () { isOnPayroll_ = true; }
void meetWithBoss () { ++badPerformanceReviews_; }
private:
std::string firstName_, lastName_;
Date dateHired_;
int salary_;
bool isOnPayroll_;
int badPerformanceReviews_;
};
inline
std::ostream& operator<< (std::ostream& out, const Employee& foo)
{
foo.print (out); return out;
}
#endif //EMPLOYEE_H
Example 19-1Class Employee. To find this or any subsequent numbered example in source code, go to the appropriate chapter and find the project/folder with the example number near the start – in this case, 1-2-employees
由于我正在使用Date
类,我需要将前一章中的date.h
和date.cpp
复制到我的新项目的文件夹中。用 g++ 我就做到了这一点;在 Visual Studio 中,我右键单击该项目,说添加➤现有项,并添加两者。
下面是一个Employee
的示例声明。它显式调用了Date
的构造器,以节省我们的一些输入:
Employee george ("George P.", "Burdell", Date (10, 3,1885), A_MERE_PITTANCE);
但不是所有的员工都一样。如果我在斯科特·亚当斯的呆伯特中读到的是准确的,经理就像任何其他员工一样,有名字和薪水,但有额外的特征:雇佣和解雇的权力,低智商,以及对折磨员工的痴迷。
为Manager
写一个全新的类是多余的,重复Employee
中的那些部分,比如firstName
、lastName
和salary
。因此,我们将使Manager
成为Employee
的子类(或派生类,或子类),如示例 19-2 所示。这就是传承。
// Class Manager
// -- from _C++20 for Lazy Programmers_
#ifndef MANAGER_H
#define MANAGER_H
#include "employee.h"
using Meeting = std::string;
class Manager: public Employee
{
public:
Manager ();
Manager (const Manager&) = delete;
Manager (const std::string& theFirstName,
const std::string& theLastName,
const Date& theDateHired,
int theSalary);
~Manager () { if (schedule_) delete [] schedule_; }
Manager& operator= (const Manager&) = delete;
void hire (Employee& foo) const { foo.start (); }
void fire (Employee& foo) const { foo.quit (); }
void laugh() const
{
std::cout << firstName() << " says: hee-hee!\n";
}
void torment (Employee&) const;
private:
Meeting* schedule_;
int howManyMeetingsOnSchedule_;
void copy (const Manager& other);
};
#endif //MANAGER_H
Example 19-2Class Manager
class Manager: public Employee
表示Manager
是Employee
的子类,一个Employee
有一个Manager
也有(图 19-1 )。(暂时不用担心public
这个词。)
图 19-1
继承是如何工作的。Employee
中的所有东西也在Manager
中——但如果是private
,就看不到了
所以这段代码是合法的:
Manager alfred ("Alfred E.", "Neumann", Date (10, 1,1952), OBSCENELY_LARGE_SALARY);
// firstName, salary are inherited from Employee
cout << alfred.firstName() << " makes " << alfred.salary ()<< " per month!\n";
没有重写这些函数,只是使用它们。你可以用一个Employee
做什么,你也可以用一个Manager
做什么——因为一个Manager
就是一个Employee
。
继承和成员变量的构造器和析构函数
当我们写Manager
的时候,我们肯定想要调用Employee
的构造器。没问题。我们将使用与初始化数据成员相同的语法:
Manager::Manager (const string& theFirstName,
const string& theLastName,
const Date& theDateHired,
int theSalary) :
Employee (theFirstName, theLastName, theDateHired, theSalary),
schedule_ (nullptr), howManyMeetingsOnSchedule_ (0)
{
}
构造器按以下顺序工作:
“:”后面的构造器被调用,先调用父类构造器(即使你把它放在后面)。
在
{}
之间的任何事情都会被完成(在这种情况下,什么都不做)。
你不说调用什么父构造器,它就调用默认的。
当一个Manager
超出范围时,它的析构函数首先被调用,然后是它的父类(然后是它的祖父类,依此类推)。你不需要考虑这个,这是自动的。
如果你不写析构函数,你会得到默认的析构函数,它告诉所有有自己析构函数的成员——比如Employee
的firstName_
和 lastName_
——清理它们的内存。同样,这是自动的——你不需要记住。
Exercises
对于这些练习,请确保
An eyeglasses prescription lists: sphere or power, additional correction for bifocals, and stuff for astigmatism (cylinder, axis) – see Figure 19-2. The two eyes are called “OD” and “OS,” not “left” and “right,” because what’s cooler than calling your eyeballs weird Latin abbreviations?
图 19-2
眼镜处方
编写并测试一个类来包含并整齐地打印这些信息。
为隐形眼镜编写并测试一个子类,以包含并打印更多的内容:背部曲率和直径,这些数字确保镜片舒适地适合眼球。
接受特殊教育的孩子可能会有一个“IEP”,一个个人教育计划,以阐明存在哪些特殊需求以及学校将如何解决这些需求。编写并测试一个学生记录的类(名字,其他相关的东西)和一个有 IEP 的学生记录的子类。你可以让 IEP 是一个单独的字符串。
数据成员是私有的(当然)。
目前,没有访问功能。这是为了确保子类只访问自己的数据,而不是父类的数据。例如,要打印,子类应该调用其父类的打印函数,然后打印自己的数据成员。
作为一个概念的继承
子类 ss 也是我们在计算机之外思考的一部分。生物学中,动物是生物的一个子类;哺乳动物是动物的一个子类;人类是哺乳动物的一个子类;而 ubergeek 是人类的子类(图 19-3 )。在每种情况下,子类都具有超类(或父类或基类)的所有特征,外加额外的特征。动物是可以移动的有机体;超级极客是一个编程非常好的人,连上帝都为之折服。
柏拉图和亚里士多德,两个最早争论面向对象编程的人。有几分地
你不是人类的子类,因为你不是一个类。你是人类的实例。(向我的外星读者道歉。)
这是提及面向对象思维中常用的区别的最佳点,在是-a 和有-a 之间。
超级极客是人类(以及哺乳动物和其他东西)。一个超级极客有一台电脑。所以当上帝创造超级极客时,他的代码一定是这样的:
class Ubergeek: public Human // an Ubergeek is-a human
{
...
private:
Computer myComputer_; // an Ubergeek has-a computer
};
事情一Ubergeek
已经走在了私人路段。什么是Ubergeek
在第一行。
*Extra
你当地的哲学教授可能会因为我这么说而枪毙我,但是面向对象编程实在是太…柏拉图式了。
柏拉图认为阶级(“理想”)是最终真实的,而特定的例子——我们词汇中的物体或变量——是最终现实的不完美的例子。所以Human
是真实的东西;你和我只是例子。
在激进的唯物主义中——离柏拉图越远越好——阶级是不真实的;只有实物才是。当然,由于激进的唯物主义不是一个物质对象,这可能是一个问题,但是懒惰的哲学,不像懒惰的编程,超出了本书的范围。
亚里士多德认为事物本身是真实的,类是它们固有的性质。你是真实的,人类才是真实的你。他分摊差额。
然而在现实中,C++ 是柏拉图式的:类是第一位的。(在创建该类型的变量之前,必须有类定义。)
纸牌游戏的课程
人们喜欢在电脑上玩纸牌,所以让我们制作课程来帮助我们建立各种各样的纸牌游戏。代码重用。
我将提供类Card
(例子 19-3 )并赋予它任何类都应该有的东西:默认和复制构造器、operator=
、访问函数和 I/O。我还将第十章的Rank
和Suit enum
拼凑起来。
// Card class
// -- from _C++20 for Lazy Programmers_
#ifndef CARD_H
#define CARD_H
#include <iostream>
// Rank and Suit: integral parts of Card
// I make these global so that I don't have to forget
// "Card::" over and over when I use them.
enum class Rank { ACE=1, JACK=11, QUEEN, KING }; // Card rank
enum class Suit { HEARTS, DIAMONDS, CLUBS, SPADES }; // Card suit
enum class Color { BLACK, RED }; // Card color
inline
Color toColor(Suit s)
{
using enum Suit;
using enum Color;
if (s == HEARTS || s == DIAMONDS) return RED; else return BLACK;
}
// I/O on Rank and Suit
std::ostream& operator<< (std::ostream& out, Rank r);
std::ostream& operator<< (std::ostream& out, Suit s);
std::istream& operator>> (std::istream& in, Rank& r);
std::istream& operator>> (std::istream& in, Suit& s);
// Told you we'd find a way to do arithmetic with enums
...
inline Rank operator+ (Rank r, int t) { return Rank(int(r) + t); }
inline Rank operator+= (Rank& r, int t) { return r = r + t; }
inline Rank operator++ (Rank& r) { r = Rank(int(r) + 1); return r; }
inline Rank operator++ (Rank& r, int junk)
{
Rank result = r; ++r; return result;
}
inline Suit operator++ (Suit& s) { s = Suit(int(s) + 1); return s; }
inline Suit operator++ (Suit& s, int junk)
{
Suit result = s; ++s; return result;
}
class BadRankException {}; // used if a Rank is out of range
class BadSuitException {}; // used if a Suit is out of range
// ...and class Card.
class Card
{
public:
Card (Rank r = Rank(0), Suit s = Suit(0)) : rank_ (r), suit_ (s)
{
}
Card (const Card& other) : Card(other.rank_, other.suit_){}
Card& operator= (const Card& other)
{
rank_ = other.rank(); suit_ = other.suit (); return *this;
}
bool operator== (const Card& other) const
{
return rank() == other.rank () && suit() == other.suit();
}
Suit suit () const { return suit_; }
Rank rank () const { return rank_; }
Color color() const { return toColor (suit()); }
void print (std::ostream &out) const { out << rank() << suit(); }
void read (std::istream &in );
private:
Suit suit_;
Rank rank_;
};
inline std::ostream& operator<< (std::ostream& out, const Card& foo)
{
foo.print (out); return out;
}
inline std::istream& operator>> (std::istream& in, Card& foo)
{
foo.read (in); return in;
}
#endif //CARD_H
Example 19-3The Card class (card.h). card.cpp is in the book’s source code
图 19-3
阶级等级制度。超级极客是人类,是哺乳动物,等等
继承层次结构
我们可能会创造一些游戏:
自由电池(图 19-4 ,左侧)。左上角是单元格,每个单元格可以存储一张卡;右上方是基础,各取 a,再取 2,以此类推,同花色;底部是一堆,随机分配。你可以从一堆牌中取出一张牌,或者如果牌是交替颜色的,你可以将一张牌添加到一堆牌中。例如,如果你有一张黑方 10,你可以把它移到方块 j 上。
图 19-4
两个流行的纸牌游戏:空当接龙(左)和克朗代克(右)
克朗代克(图 19-4 ,右)。和 Freecell 一样,它有基础(右上);它也有一个甲板,一个废物堆,在底部有自己类型的堆。
红心、黑桃和其他有一副牌和手牌的多人游戏。
常见的卡分组包括
甲板:你可以洗牌,并处理顶部。
废物(弃牌堆):你可以放一张牌在上面或者拿走一张。
单元格:像废物一样,你可以添加到单元格中或从顶部取走-但只能有一张卡片。
基础:清一色,从 ace 开始往上。
手:你可以添加它,并拿出任何你想要的卡。
自由电池堆:加一张牌,颜色交替向下;取下一张卡片。
克朗代克堆:更复杂,作为练习留下。
这些都有两个共同点:内容和大小。有更多的共同点吗?
我会说一个单元格是一个废物堆,因为你与它互动的方式是一样的:添加一张卡片,然后从顶部拿走一张。它只是在尺寸上有限制。
除此之外,我会说没有。你不能说基础是甲板的特殊情况,或者克朗代克桩是废物堆的特殊情况。(有些可能是见仁见智。)但它们都有一个共同点:都是一组组的牌。所以我们可以有一个CardGroup
类,并从它继承。(A Hand
是没有添加任何东西的CardGroup
,所以我们将Hand
作为CardGroup
: using Hand = CardGroup;
的别名)。
我基于CardGroup
提出了图 19-5 中的继承层次。CardGroup
如示例 19-4 所示。
图 19-5
卡片组的类别层次结构
// CardGroup class (for playing cards)
// -- from _C++20 for Lazy Programmers_
#ifndef CARDGROUP_H
#define CARDGROUP_H
#include "card.h"
class OutOfRange {}; // Exception classes
class IllegalMove {};
class CardGroup
{
public:
static constexpr int MAX_SIZE = 208; // if anybody wants a game
// w/ more than 4 decks,
// change this.
CardGroup () { howMany_ = 0; }
CardGroup (const CardGroup& other){ copy(other); }
CardGroup (const Card& other)
{
howMany_ = 0; addCard (other);
}
CardGroup& operator= (const CardGroup& other)
{
copy(other); return *this;
}
bool operator== (const CardGroup& other) const;
Card& operator[] (unsigned int index);
Card operator[] (unsigned int index) const;
Card remove (unsigned int index);
Card top () const { return (*this)[size()-1]; }
Card removeTop () { return remove (size()-1); }
unsigned int size () const { return howMany_; }
bool isEmpty() const { return size() == 0; }
bool isFull () const { return size() >= MAX_SIZE; }
// addCard does NOT check that it's legal to add a card.
// We need this for creating CardGroups during the deal.
void addCard (const Card&);
// makes sure the addition of the card is legal, then adds it
void addCardLegally (const Card& other);
void print (std::ostream&) const;
private:
unsigned int howMany_;
Card contents_ [MAX_SIZE];
void copy (const CardGroup&); // copy cards over; used by =, copy ctor
};
inline
std::ostream& operator<< (std::ostream& out, const CardGroup& foo)
{
foo.print(out); return out;
}
using Hand = CardGroup;
#endif //CARDGROUP_H
Example 19-4cardgroup.h
private
继承
考虑一下Waste
类。我们不应该允许通过[]
操作符随机访问Waste
;你只能看一堆Waste
的顶牌。
为了限制访问,我们更改了继承的类型:
class Waste: private CardGroup
{
...
这使得CardGroup
的公共成员进入Waste
的私有部分(图 19-6 )。
图 19-6
公共(a)和私有(b)继承。如果有任何继承的公共成员必须在子类中保持私有,则使用私有
operator[]
现在是私有的——很好——但是有一些CardGroup
的公共成员我们希望 Waste
对外开放:例如isEmpty
和print
。因为它们是私有的,我们用相同的名字创建新的公共函数,简单地调用父函数,如例 19-5 所示。
// Waste class
// -- from _C++20 for Lazy Programmers_
#ifndef WASTE_H
#define WASTE_H
#include "cardgroup.h"
class Waste: private CardGroup
{
public:
Waste () {}
Waste (const Waste& other) : CardGroup (other) {}
Waste (const CardGroup& other) : CardGroup (other) {}
Waste& operator= (const Waste& other) = delete;
bool operator== (const Waste& other) const
{
return CardGroup::operator== (other);
}
bool isEmpty () const { return CardGroup::isEmpty (); }
bool isFull () const { return CardGroup::isFull (); }
unsigned int size () const { return CardGroup::size (); }
Card top () const { return CardGroup::top(); }
Card removeTop () { return CardGroup::removeTop(); }
void addCardLegally (const Card& foo)
{
CardGroup::addCardLegally (foo);
}
void print (std::ostream& out) const{ CardGroup::print (out); }
};
inline
std::ostream& operator<< (std::ostream& out, const Waste& foo)
{
foo.print (out); return out;
}
#endif //WASTE_H
Example 19-5Class Waste, in waste.h, using private inheritance
隐藏继承的成员函数
Waste
有来自CardGroup
的成员函数isFull
,如果Waste
有MAX_SIZE
卡则为真。它的子类Cell
有不同的版本。如果你在一个Cell
上调用isFull
,它会用哪个?子版本“隐藏”了继承的版本;如果你在一个Cell
上调用isFull
,你会得到Cell
的版本。
但是如果有时候我们仍然需要继承的版本呢?在 19-6 的例子中,Cell
的版本addCardLegally
调用Waste
的版本,通过在调用前加上Waste::
来指定。
// Cell class
// -- from _C++20 for Lazy Programmers_
#ifndef CELL_H
#define CELL_H
#include "waste.h"
class Cell: public Waste
{
public:
Cell () {}
Cell(const Cell& other) : Waste (other) {}
Cell& operator= (const Cell& other) = delete;
// public inheritance, so all public members of Waste are here...
bool isFull () const {return ! isEmpty (); }
void addCardLegally (const Card& card)
{
if (isFull ()) throw IllegalMove (); // Cell must be empty
else Waste::addCardLegally (card);
}
};
#endif //CELL_H
Example 19-6cell.h
蒙大拿州的一场比赛
蒙大拿纸牌游戏使用Cell
和Deck
,所以它应该是对我们等级制度的一个很好的测试。
规则是:在一个 4 × 13 的格子中分发所有的牌,去掉 a,得到一个如图 19-7 所示的混乱局面。
图 19-7
蒙大拿州的一场比赛
您的目标是从 2 到 king 排成四排,每排一套。
唯一有效的方法是把一张牌放进一个空的格子里。你放的牌必须跟在它左边的牌后面,在同一套牌里;比如,你只能跟随 2♥和 3♥.如果它在最左边的一列,你就要放一个 2。国王后面的空格不可用。
当你陷入困境时,从左边的 2 开始,按花色递增,重发所有不在序列中的牌。你有四笔交易。
示例 19-7 至 19-9 显示了程序,为简洁起见省略了一些内容;本书的示例代码包含一个完整的版本。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include "gridLoc.h"
#include "cell.h"
#include "deck.h"
#ifndef MONTANA_H
#define MONTANA_H
class Montana
{
public:
static constexpr int ROWS = 4, COLS = 13;
static constexpr int NUM_EMPTY_CELLS = 4;// 4 empty cells in grid
static constexpr int MAX_TURNS = 4;// 4 turns allowed
class OutOfRange {}; // Exception class for card locations
Montana () {};
Montana (const Montana&) = delete;
Montana& operator= (const Montana&) = delete;
void play ();
private:
// displaying
void display () const;
// dealing and redealing
void deal (Deck& deck, Waste& waste);
void cleanup (Deck& deck, Waste& waste); // collect cards // for redeal
void resetGrid (); // make it empty
// playing a turn
void makeLegalMove (bool& letsQuitOrEndTurn);
void makeMove (const GridLoc& oldLoc,
const GridLoc& newLoc);
bool detectVictory () const;
void congratulationsOrCondolences(bool isVictory) const;
// working with empty cells
// store in emptyCells_ the location of each empty cell
void identifyEmptyCells ();
// which of the empty cells has this row and col? A B C or D?
char whichEmptyCell (int row, int col) const;
// Is this a valid cell index? It must be 0-3.
bool inRange (unsigned int emptyCellIndex) const
{
return (emptyCellIndex < NUM_EMPTY_CELLS);
}
// placing cards
Cell& cellAt (const GridLoc& loc)
{
if (inRange (loc)) return grid_[loc.row_][loc.col_];
else throw OutOfRange();
}
const Cell& cellAt (const GridLoc& loc) const
{
if (inRange (loc)) return grid_[loc.row_][loc.col_];
else throw OutOfRange();
}
// Is this location within the grid?
bool inRange (const GridLoc& loc) const
{
return (0 <= loc.row_ && loc.row_< ROWS && 0 <= loc.col_ && loc.col_< COLS);
}
// Can Card c follow other card?
bool canFollow (const Card& c, const Card& other) const
{
return c.suit() == other.suit() && c.rank() == other.rank() + 1;
}
// Can card c go at this location?
bool canGoHere (const Card& c, const GridLoc& loc) const;
// Is the cell at row, col ordered at its location? That is,
// could we put it here if it weren't already?
bool cellIsCorrect (int row, int col) const
{
return ! grid_[row][col].isEmpty () &&
canGoHere (grid_[row][col].top(), GridLoc (row, col));
}
// data members
Cell grid_ [ROWS][COLS]; // where the cards are
GridLoc emptyCells_ [NUM_EMPTY_CELLS];// where the empty cells are
};
#endif //MONTANA_H
Example 19-8montana.h
// A game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include <cstdio> // for srand, rand
#include <ctime> // for time
#include "io.h" // for bool getAnswerYorN (const char[]);
#include "montana.h"
int main ()
{
srand ((unsigned int) time (nullptr)); // start rand# generator
Montana montanaGame;
do
montanaGame.play ();
while (getYorNAnswer ("Play again (Y/N)? "));
return 0;
}
Example 19-7montana_main.cpp: a game of Montana
每次被调用时,Montana::play
创建一个新的Deck
和Waste
。你可以从中看出它们是如何被使用的。Montana::makeMove
显示了如何使用Cell
(cellAt
返回给定位置的Cell
)。
Montana::makeLegalMove
使用 try-catch 块以防输入出错。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "deck.h"
#include "io.h" // for bool getAnswerYorN (const char[]);
#include "montana.h"
using namespace std;
// Playing the game
...
void Montana::play ()
{
Deck deck;
Waste waste;
bool isVictory = false;
resetGrid (); // prepare for deal by ensuring grid is empty
for (int turn = 1; turn <= MAX_TURNS && ! isVictory; ++turn)
{
cout << "********************* New turn! "
"**********************\n";
// To easily test the detectVictory func: uncomment // setupForVictory,
// comment out deal, and see if isVictory becomes true
// setupForVictory(grid_, deck, waste);
deck.shuffle (); // Shuffle deck
deal (deck, waste); // fill grid with cards
// and remove aces
identifyEmptyCells (); // remember where the aces were
// in a list of 4 emptyCells_
bool letsQuitOrEndTurn = false;
isVictory = detectVictory(); // already won? Unlikely, but...
while (! isVictory && ! letsQuitOrEndTurn)
{
display();
makeLegalMove (letsQuitOrEndTurn); // play a turn
isVictory=detectVictory(); // did we win?
}
cleanup (deck, waste); // collect cards for redeal
// If user won, we go on and leave loop
// If we're out of turns, we go on and leave loop
// Otherwise give user a chance to quit
if (!isVictory && turn < MAX_TURNS)
if (getYorNAnswer("Quit game (Y/N)?"))
break;
}
congratulationsOrCondolences (isVictory);
}
void Montana::makeMove (const GridLoc& oldLoc,
const GridLoc& newLoc)
{
cellAt(newLoc).addCardLegally (cellAt(oldLoc).removeTop ());
}
void Montana::makeLegalMove (bool& letsQuitOrEndTurn)
{
bool isValidMove = false;
do
{
cout << "Move (e.g. A 1 5 to fill cell A with "
<< "the card at row 1, col 5; q to quit/end turn)? ";
// Which empty space will we fill -- or are we quitting?
char letter; cin >> letter;
if (toupper(letter) == 'Q') letsQuitOrEndTurn = true;
else
{
int emptyCellIndex = toupper(letter) - 'A';
try
{
// Which cell are we moving from?
GridLoc from; cin >> from;
// Which cell are we moving to?
GridLoc to = emptyCells_[emptyCellIndex];
// If the empty cell exists, and is really empty...
if (inRange (emptyCellIndex) && cellAt(to).isEmpty())
// if card to move exists, and move is legal...
if (!cellAt(from).isEmpty() &&
canGoHere (cellAt(from).top(), to))
{
isValidMove = true;
makeMove(from, to);
emptyCells_[emptyCellIndex] = from;
}
}
catch (const BadInput&) {}
catch (const OutOfRange&) {}
// reading GridLoc went bad -- just try again
}
} while (! isValidMove && ! letsQuitOrEndTurn);
}
Example 19-9Part of montana.cpp (the rest is in the book’s source code)
下面是一个示例游戏的一部分,玩家将黑桃 2 和 3 移到最下面一行,并在顶部为更多的低牌腾出空间。看起来玩家可能会赢:
********************* New turn! **********************
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H >A< 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD 2S 3C JD 6D 10C 3S 7S 10D 5D
3: >D< 3H 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 3 1
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD 2S 3C JD 6D 10C 3S 7S 10D 5D
3: >D< >A< 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? d 2 4
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C 3S 7S 10D 5D
3: 2S >A< 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 2 9
0 1 2 3 4 5 6 7 8 9 10 11 12
0: JC 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C >A< 7S 10D 5D
3: 2S 3S 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)? a 0 0
0 1 2 3 4 5 6 7 8 9 10 11 12
0: >A< 2H 3H 7C 10S 5C QS KC 5S KH 8D 7D 4D
1: >B< 6C JS 8C 8H >C< 6H KS 9C JH QD 2D 4C
2: 9S 6S 5H KD >D< 3C JD 6D 10C JC 7S 10D 5D
3: 2S 3S 8S QH 3D 7H 4H 4S 2C 9H 9D 10H QC
Move (e.g. A 1 5 to fill cell A with card at row 1, col 5; q to quit/end turn)?
Exercises
编写前面的类
Date
的子类,添加一个函数printInText
。它不会以数字格式(例如 12/12/2012)打印Date
,而是以您喜欢的使用 ASCII 的语言:12 de diciembre de 2012
(西班牙语)、December 12, 2012
(英语),或者您喜欢的任何语言。你会用什么样的传承?Write a subclass of
string
calledUnixFilename
that doesn’t allow spaces – it immediately replaces them with_
’s. And it won’t let you interfere by changing the string’s individual letters:UnixFilename myFileName ("my file name"); // becomes "my.file.name" myFileName[2] = ' '; // forbidden
你会用什么样的传承?
写一个
Reserve
类。这是一组卡片。唯一合法的举动是从顶部拿走一张牌。它应该继承什么?接下来的四个练习对第二十一章的 Freecell 游戏练习特别有用:
为
Deck
写洗牌算法,比源码里的快。(需要 O 符号)…你的洗牌在 O 符号中的时间要求是多少?你能把它降低到 O(N)吗?
写一个
Foundation
类。一个Foundation
,记住,以ACE
开头,穿西装上去,也可能是空的。如果调用方试图添加不合适的卡,则引发异常。写
FreecellPile
类。适当时抛出异常。写
KlondikePile
类。克朗代克牌堆类似于自由细胞牌堆,因为你只能通过交替颜色来添加一些东西,但它不同于其他牌堆,因为你可以添加这样的牌的序列,只要牌堆的顶部牌符合该标准。例如,如果克朗代克牌堆的顶部是一张黑王,你可以在上面放一个以红皇后开始的序列。您也可以从克朗代克牌堆中移除一系列牌,只要它是交替颜色的。所以显然你需要添加和删除序列。什么类是序列?你的 add-sequence 和 remove-sequence 函数应该属于哪个类?
此外,克朗代克堆底部有 0 张或更多面朝下的牌。您不能移动任何序列,包括面朝下的牌。如果您移除所有面朝上的牌,则可以露出(面朝下的)顶牌。
适当时抛出异常。
确保你理解规则的最好方法是玩游戏,但我绝不会鼓励任何人去寻找另一种在工作中浪费时间的方式。
(涉及,但不难)写一个围棋鱼的游戏(网上找规则)。
设计一个简单的计算器类,它可以有两个数字,并且可以完成四个基本函数+、-、*和/。
现在编写一个工程师计算器类,它完成所有这些工作,但也做一些其他有趣的事情(比如说,平方根和取幂)。你会用什么样的传承?*
二十、模板
我会不会写一个函数或类,一个带int
s,另一个带string
s,另一个带double
s?听起来不像是懒惰!这一章使我们能够使用模板编写一次*。*
*## 功能模板
回忆一下第八章中的这个交换int
的函数,为了方便起见,在这里重新命名。
void mySwap
(int& arg1, int& arg2)
{
int temp = arg2; arg2 = arg1; arg1 = temp;
}
对int
来说没问题,但是如果我想要double
s 呢?长鼻怪?还是混合?下面是修复方法,这样我就可以把int
换成int
,int
换成double
,长鼻怪换成 snarks,任何东西,只要 C++ 知道如何使用=
:
template <typename T, typename U>
void mySwap (T& arg1, U& arg2)
{
T temp = arg2; arg2 = arg1; arg1 = temp;
}
这是一个函数模板:它本身不是一个函数,而是关于一旦知道我们想要什么类型,如何让成为一个函数的指令。
最上面一行告诉编译器这将是一个模板,我们调用了要交换的类型,T
和U
。T
和U
都是一种空白,当我们决定用一个实际的类型来填充它。例 20-1 说明了它的用法。
// Utterly useless program that uses a function template
// -- from _C++20 for Lazy Programmers_
template <typename T, typename U>
void mySwap (T& arg1, U& arg2)
{
T temp = arg2; arg2 = arg1; arg1 = temp;
}
int main ()
{
int i = 10 , j = 20 ;
double m = 0.5, n = 1.5;
mySwap (i, j);
mySwap (m, n);
mySwap (i, n); // You'll get a warning abt loss of data
// from mixing ints and doubles, but it'll work
return 0;
}
Example 20-1Using the mySwap function template
编译器不会创建任何 mySwap
函数,直到它到达第mySwap (i, j);
行。然后它注意到i
和j
是int
的,所以它用int
替换模板中的T
和U
,并创建一个带两个int
的mySwap
函数
在下一行,它为double
生成一个mySwap
,之后,它又生成一个带int
和double
的。
我把功能模板放在 main 上面。如果编译器在使用它之前看不到它,它就不会知道如何创建函数——它还没有读过指令。所以函数模板放在程序的开头,或者放在一个.h
文件中代替声明。
总结如何将函数转换为函数模板:
放
template
<typename
这个,typename
那个...>
在前面。**把要替换的类型改成 T 或者 const T &(或者 U/const U &之类的)。**如果是在 C++ 会隐式调用某个东西的复制构造器的地方——没有
&
–const T&
的返回类型或参数会防止不必要的复制。将新的函数模板放在声明它的地方。
防错法
- **链接,编译器说找不到函数,但是你可以在程序后面或者另一个里面看到。cpp 文件。**见步骤 3。
其他可能的问题:
- 在不该的时候把
int
转换成T/const T&
。假设我有这个代码来搜索一个int
数组:
bool contains (int array[], int howMany, int item)
{
for (int i = 0; i < howMany; ++i)
if (array[i] == item)
return true;
return false;
}
and convert it to
template <typename T>
bool contains (T array[], const T& howMany, const T& item)
{
for (int i = 0; i < howMany; ++i)
if (array[i] == item)
return true;
return false;
}
如果是字符串数组,howMany
为const string&
就没有意义了!它应该仍然是一个int
。
- 对类型
T
使用不正确的运算符。也许你发送给mySwap
的东西没有=
定义;或者=
没有做你想做的事情(比如,T
是一个指针,你想复制内容而不是做指针赋值)。通常的解决办法是只对有意义的东西使用模板。下一节会有所帮助。
函数模板的概念(可选)
我对概念持观望态度。它们确实给了程序员一些帮助,尤其是让错误消息更加清晰。如果您的编译器还不支持它们,或者您还没有准备好投入时间,那么现在跳过它们当然是可以的。
现在我已经说过了…考虑一下这段代码:
class A {};
class B {};
int main ()
{
A a; B b;
mySwap (a, b);
return 0;
}
我们会得到一个错误消息,说我们没有办法将一个B
分配给一个A
。
从 C++20 开始,我们可以告诉编译器,我们对mySwap
的参数有一定的期望(例如 20-2 ,所以它可以在进入函数体之前就知道它是否可以工作。
#include <concepts>
...
template <typename T, typename U>
requires std::swappable_with<T&,U&>
void mySwap (T& t, U& u)
{
T temp = t; t = u; u = temp;
}
Example 20-2Converting mySwap to use concepts
在<concepts>
头文件中的概念——类型限制——告诉模板它可以接受什么类型。在这种情况下,mySwap
需要一对可以互相交换的类型。(&
是因为我们想要交换的是事物本身,而不是副本,如参数表所示。)当我们给它一个A
和一个B
时,它们不满足swappable_with
约束,所以编译器甚至不会尝试构建mySwap
。它只是给出一个类似“相关的约束没有得到满足”的错误消息,这是真的。
那好一点了。当我们在本章后面将模板应用于类时,它会变得更加有用。
表 20-1 列出了一些你可能会用到的来自<concepts>
的概念。其他包含文件中有更多,特别是<algorithm>
和<range>
(见第二十三章)。其余的<concepts>
的概念,见(撰写时) en.cppreference.com/w/cpp/concepts
。
表 20-1
使用模板最简单和最有用的概念。所有这些都在std::
名称空间中
防错法
你可以打印出一个概念是否适用于类型,这在你很难弄清楚为什么有概念的代码不起作用时很方便(例如 20-3 )。
cout << "Is int same as double? " << same_as<int, double>
<< "\nIs B derived from A? " << derived_from<B, A>
<< "\nIs char8_t integral? " << integral<char8_t>
<< "\nIs double floating-pt? " << floating_point<double>
<< '\n';
Example 20-3Printing concepts as they relate to various types
这将为不适用的概念打印 0,为适用的概念打印 1。
潜在的问题包括:
- 当你使用概念时,你会看到红色的曲线,但实际上似乎没什么不对。可能会好;Visual Studio 的编辑,在写作的时候,不能可靠地识别概念。编译一下就知道了,看看是不是真的错了。
Exercises
对于每个练习,如果您选择涵盖概念,请使用它们。
编写一个函数模板,它接受任何浮点数并返回其最高有效(最左边)位。例如,给它 678.9,它将返回 6。你可能想要
log10
功能。将示例 10-3-查找数组的最小值-转换为函数模板。
Vector 类
数组是个麻烦。你可以给一个数组一个-2000 的索引,它会很乐意给你一些愚蠢的东西。如果你声明一个数组包含 50 个元素,但是你决定要 51 个,那就太糟糕了。
我们可以通过创建一个名为Vector
的行为更好的类似数组的类来解决这个问题(例子 20-4 , 20-5 )。
// Vector class: a variable-length array
// -- from _C++ for Lazy Programmers_
#ifndef VECTOR_H
#define VECTOR_H
class Vector
{
public:
class OutOfRange {}; // exception, for [] operators
Vector () { contents_ = new int[0]; howMany_ = 0; }
Vector (const Vector& other) { copy (other); }
~Vector() { if (contents_) delete [] contents_; }
Vector& operator= (const Vector& other);
bool operator== (const Vector& other) const;
unsigned int size () const { return howMany_; }
int operator[] (unsigned int index) const;
int& operator[] (unsigned int index);
void push_back (int newElement); // add newElement at the back
private:
int* contents_;
unsigned int howMany_;
void copy (const Vector& other);
};
#endif //VECTOR_H
Example 20-4vector.h, for a vector of ints
很像String
,只是当然我用不了strcpy
之类的。特别注意push_back
。
// Vector class: a variable-length array of ints
// -- from _C++20 for Lazy Programmers_
#include "vector.h"
Vector& Vector::operator= (const Vector& other)
{
if (this == &other) return *this; // don't assign to self -- you'll // trash contents_
if (contents_) delete[] contents_; copy(other);
return *this;
}
bool Vector::operator== (const Vector& other) const
{
if (size() != other.size()) return false; // diff sizes => not equal
bool noDifferences = true;
// quit if you find a difference or run out of elements
for (unsigned int i = 0; i < size() && noDifferences; ++i)
if ((*this)[i] != other[i]) noDifferences = false;
return noDifferences;
}
int Vector::operator[] (unsigned int index) const
{
if (index >= size()) throw OutOfRange(); // don't allow out-of-range // access
else return contents_[index];
}
int& Vector::operator[] (unsigned int index)
{
if (index >= size()) throw OutOfRange(); // don't allow out-of-range // access
else return contents_[index];
}
// add newElement at the back
void Vector::push_back (int newElement)
{
int* newContents = new int[howMany_ + 1]; // make room for 1 more...
for (unsigned int i = 0; i < size(); ++i) // copy old elements into new // array...
newContents[i] = contents_[i];
newContents[howMany_] = newElement; // add the new element...
++howMany_; // remember we have 1 more...
delete[] contents_; // throw out old contents_
contents_ = newContents; // and keep new version
}
// Sort of like String::copy from Chapter 17, but without strcpy
void Vector::copy (const Vector& other)
{
// set howMany to other's size; allocate that much memory
contents_ = new int[howMany_ = other.size()];
// then copy the elements over
for (unsigned int i = 0; i < size(); ++i)
contents_[i] = other[i];
}
Example 20-5vector.cpp
示例 20-6 向您展示了如何使用它。
// Example with a Vector of int
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "vector.h"
using namespace std;
int main ()
{
Vector V;
for (int i = 1; i < 11; ++i) V.push_back (i);
cout << "Can you count to 10? The Count will be so proud!\n";
for (unsigned int i = 0; i < V.size(); ++i) cout << V[i] << ' ';
cout << '\n';
return 0;
}
Example 20-6Using Vector
所以它是安全的(如果我们给出了一个不好的索引,就会抛出一个异常),我们可以添加任意多的元素。
效率和 O 符号(可选)
在第二十二章,我们将有另一个元素容器,即“链表”这样我们就可以决定这个或那个任务需要哪个容器——为了练习 O 符号——让我们考虑一下Vector
的成员函数的效率(时间需求)。(如果您跳过了 O 符号,请跳过这一小节。)
你可能需要时间自己决定这些函数在 O 符号中是什么。
好吧,你回来了。由operator=
和复制构造器使用的Vector::copy
中有一个循环,它迭代size ()
次。push_back
也有这样的循环。其他人只是有一些if
的说法。表 20-2 显示了一些函数的效率,给定 N 为当前大小。
表 20-2
一些Vector
功能所需的时间
功能
|
效率(时间要求)
size |
O(1) |
operator[] |
O(1) |
operator= |
O(N) |
复制构造器 | O(N) |
push_back |
O(N) |
底线是:如果你想对整个向量做些什么,所需的时间是 O(N)——没什么好惊讶的。如果你只是用一个元素做一些事情,所需时间是 O(1)—除了 push_back
。这需要 O(N)时间,因为您必须将旧的contents_
复制到新的内存块中。
好吧。总比没有灵活性好。也许有办法让它变得更快(见练习 3)。
Exercises
在下面,如果你没有做“效率和 O 符号”小节,就跳过每个小节中的 O 符号问题。
写
pop_back
。它的时间要求是什么,用 O 表示法?如果不是 O(1),那就是你工作量太大了!(更难)重写
push_back
,这样就不用每次添加新元素时都重新分配,而是为十个新元素分配足够的空间——并且只需要每十次重复一次。它改变了时间成本吗,用 O 表示?你认为值得做吗?(使用 move 语义)编写 move 构造器并为
Vector
移动=
,如果你愿意,可以对照我在书中的源代码中的解决方案进行检查。他们需要多长时间来记 O 符号?写一个类
Queue
。它就像一个Stack
,除了你从你添加物品的地方把物品从端对面的端拿走。所以它们出来的顺序和进去的一样。按照惯例,在一端“入队”,在另一端“出列”。
在 O 表示法中,入队和出列的时间是多少?
(使用 move 语义)编写 move 构造器并为
Queue
移动=
。他们需要多长时间来记 O 符号?
制作Vector
模板
我是不是要写一个全新的类,取决于我是想要一个整数,字符串,还是 1960 年代的摇滚音乐家的向量?我是一个懒惰的程序员。我不可能做那件事。
进入类模板:本质上是一组制作类的指令,就像函数模板是制作函数的指令一样。
将Vector
转换为一个并存储不同类型所需的更改有一个简短列表:
改变任何一个矢量声明来表示它是一个什么
Vector
**的。**在例 20-7 中,Vector
变成了Vector<int>
。这是我们在该文件中所做的唯一的更改。将
vector.cpp
的内容放入vector.h
**;**擦掉vector.cpp
。
// Example with a Vector of int
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
#include "vector.h"
using namespace std;
int main ()
{
Vector<int> V; // Step #1: change declaration to say what it's a // Vector of
for (int i = 1; i < 11; ++i) V.push_back(i);
cout << "Can you count to 10? The Count will be so proud!\n";
for (unsigned int i = 0; i < V.size(); ++i) cout << V[i] << ' ';
cout << '\n';
return 0;
}
Example 20-7Example 20-6, updated to use a class template for Vector
这和“函数模板”部分是一样的:在你调用 push_back
之前,和int
s 一起工作的push_back
版本是不存在的。在那一行,编译器需要知道如何创建函数,这意味着它需要函数模板的主体。所以主体必须在.h
文件中。
将模板
<typename T>
放在前面类定义
类定义之外的每个函数体
与函数模板一样,在适当的地方用 T 或 const T &替换 int。
用矢量替换矢量
当它是矢量的一部分::
在返回类型中,如在
Vector<T>& Vector<T>::operator= (const Vector& other);
任何你不在班的时候,比如
Vector<T> merge (const Vector<T>& a, const Vector<T>& b); // not a member
如果你把Vector<T>
放在太多的地方,没有人会开枪打你。但是它对构造器名不起作用。
让我们看看这给了我们什么(示例 20-8 )。
// Vector class: a variable-length array
// -- from _C++ for Lazy Programmers_
#ifndef VECTOR_H
#define VECTOR_H
template <typename T> // Step #3 (a): add template <typename T>
class Vector
{
public:
class OutOfRange {}; // exception, for [] operators
Vector () { contents_ = new T[0]; howMany_ = 0; }// #4: int -> T
Vector (const Vector& other) { copy (other); }
~Vector() { if (contents_) delete [] contents_; }
Vector& operator= (const Vector& other);
bool operator== (const Vector& other) const;
unsigned int size () const { return howMany_; }
const T& operator[] (unsigned int index) const;
// #4: int -> const T&
T& operator[] (unsigned int index); // #4: int& -> T&
void push_back (const T& newElement); // #4: int -> const T&
private:
T* contents_; 1800 // #4: int -> T
unsigned int howMany_;
void copy (const Vector& other);
};
// #2: move contents of vector.cpp into vector.h
// (still contained in #ifndef)
template <typename T> // #3b: add template <typename T>
Vector<T>& Vector<T>::operator= (const Vector& other)
// #5a: Vector:: -> Vector<T>::
// #5b: Vector& -> Vector<T>&
{
if (this == &other) return *this; // don't assign to self -- you'll trash contents
if (contents_) delete[] contents_; copy (other);
return *this;
}
...
#endif //VECTOR_H
Example 20-8Changing Vector to a class template. Along with Example 20-7, it’s in source code as 07-08-vectorTemplate
现在您可以将那个Vector
用于您选择的基本类型。在示例 20-9 中,我们将它与string
s 一起使用。
// Example with a Vector of strings and more
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
#include <string>
#include "vector.h"
using namespace std;
int main ()
{
// Setting up the band...
Vector<string> FabFour;
string names[] = { "John","Paul","George","Ringo" };
constexpr int NUM_BEATLES = 4;
for (int i = 0; i < NUM_BEATLES; ++i)
FabFour.push_back(names[i]);
// Printing them out...
cout << "The Fab Four: ";
for (int i = 0; i < NUM_BEATLES; ++i)
cout << FabFour[i] << ' ';
cout << endl;
// Ensuring other base types compile...
Vector<int> V; for (int i = 0; i < 10; ++i) V.push_back(i);
Vector<Vector<double>> G1, G2; assert(G1 == G2);
return 0;
}
Example 20-9Using the new Vector template from Example 20-8 with strings and more. In source code as 09-vectorTemplate
防错法
编译器说你没有写你的类模板的一些成员函数,你知道你写了。是不是所有东西都移入了
.h
文件?编译器在你的变量声明中说,类模板不是类型。也许你停止了
<yourBaseType>
。
Exercises
在这一章的结尾有进一步的相关练习;它们可以带概念使用,也可以不带概念使用。请查看该部分来尝试它们,尤其是涉及队列的部分。
将
Vector
的push_back
、copy
和operator=
(如果你知道的话,也可以移动函数)转换成使用Vector
作为模板。我的解决方案在书的源代码中。将第十七章练习中的
Point2D
类改编成一个类模板。你现在可以拥有由double
s、int
s、float
s 或任何其他合理类型制造的Point2D
s。重写上一章的
CardGroup
作为Vector
<Card>
的子类。
类别模板的概念(可选)
让我们将这些代码添加到一个测试程序中,就像示例 20-9 中的那样。(本节没有编号的示例,但是片段收集在ch20
的项目/文件夹vectorTemplateWConcepts
中;请随意尝试,评论/取消评论,并尝试自己的方法。)
struct B { B() {}; }; // Two simple classes
struct A { A& operator= (const A&) = delete; };
Vector<A> As;
Vector<B> B1, B2;
看起来还好吗?编译器是这么认为的。但是随着我们对代码的深入研究
As.push_back (A ());
assert (B1 == B2);
它会发现问题。它不能添加一个A
,因为push_back
需要=
,而A
没有提供;它不能比较B1
和B2
,因为它需要==
而B
没有提供。
当我们试图声明没有提供它所需要的类时,它真的应该很快发现这个问题。我们可以用概念来实现。
要在类模板中使用概念,就像在函数模板中一样:在任何一个template <...>
后面的行中放一个requires
子句,如
template <typename T>
requires...
void Vector<T>::push_back ...
我们应该对Vector
的基类型提出什么样的要求?我们需要=
和==
。这就要求T
是assignable_from
本身,也是equality_comparable
。而且由于copy
创建数组,T
会需要一个默认的构造器,也就是必须是default_initializable
。
我们可以在requires
子句中使用&&
、||
和!
以任意组合的方式组合概念。在我们的例子中,我们只需要&&
:
template <typename T>
requires std::assignable_from<T&, T> && std::equality_comparable<T>
&& std::default_initializable<T>
void Vector<T>::push_back ...
我们每次写template <typename T>
都要打太多了!我们可以用提供的零件制作我们的自己的概念,从而节省工作:
template <typename T>
concept VectorElement = std::assignable_from<T&, T> && std::equality_comparable<T> &&
std::default_initializable<T>;
...
template <typename T>
requires VectorElement<T>
void Vector<T>::push_back (const T& newElement) ...
还有一种减少打字的方法。如果你的概念只有一个类型参数,你可以把它放在typename
的位置,作为一种速记。简短是好的。
template <VectorElement T>
void Vector<T>::push_back (const T& newElement) ...
当声明A
或B
的Vector
时,错误信息将类似于“约束不满足”
如果您想要的概念不在包含文件中,您可以创建自己的概念。假设您想确保类型是可打印的。把你想做的事情放在requires
之后的{}
中——在这个例子中是{ out << t; }
,其中out
是ostream
的类型——在一个参数列表之后,该列表定义了你所引用的任何内容:
template <typename T>
concept Printable = requires (std::ostream& out, const T& t)
{
out << t;
};
关于概念还有很多事情要做,但是这应该可以处理你发现的大多数情况。
值得吗?
C++ 社区似乎对此很兴奋。你可以自己决定。
一副
我使用了pair
——一个将任意两种类型捆绑到一个struct
中的类模板——很多。在示例 20-10 中进行了描述和测试。
template <typename S, typename T>
struct pair
{
pair ();
pair (const S& s, const T& t);
pair (const pair& other);
// operators =, ==, !=, and others
S first;
T second;
};
...
int main ()
{
pair3 p (1, "C++20 for Lazy Programmers");
cout << "The number " << p.first << " C++ text EVER is "
<< p.second << "!\n";
return 0;
};
Example 20-10Using pair, here simplified from C++’s version in #include <utility>
本章末尾的练习就是一个有用的例子。
非类型模板参数
您也可以让模板参数成为一个值(例如 20-11 )。
template <int SIZE>
class Stack // Stack of chars with at most SIZE elements
{
public:
//...
bool full () const { return howMany_ >= SIZE; }
private:
char contents_[SIZE];
int howMany_;
};
int main ()
{
Stack<30> stack;
//...
return 0;
}
Example 20-11A class template that allows you to specify an integer argument
如练习 5 所示,这也很有用。
Exercises
在练习中,如果你已经涵盖了概念,就使用它们——无论如何都是好的。
将前面练习中的类
Queue
转换为模板。给
Vector
增加一个print
成员函数。使用上一节中的
Que
ue
类模板,创建一个子类PriorityQueue
,其中每个条目都有一个附加的优先级。当您将一个新项目加入队列时,它会排在Queue
中优先级较低的所有项目之前。你会想要pair
。(对于概念)编写函数模板
sqr
对任何数值类型的值求平方。Write a function template that takes an array and gets from the user each element of that array. Here’s the cool part: it asks for exactly the right number of elements. Here’s how:
template <typename T, int SIZE> ... void inputArray(T (&myArray)[SIZE])
这样,函数不仅知道数组的类型,还知道它的大小。(在函数模板制作完成之前,&用来保存数组的大小信息)。 4
它只对静态数组有效,对动态数组或作为参数传入的数组无效。
(从 C++20 开始,
istream& operator>>
就是这样保证它读取的输入不会超过给定的char
数组所能存储的。)(硬)做一个类模板
BigInteger
,充当任意大小的整数。让模板参数成为您在BigInteger
中想要的字节数(unsigned char
s)或位数。支持所有合理的算术运算符和流 I/O。
#include <vector>
我又对你隐瞒了一些事情;我不能再那样做了。C++ 在#include <vector>
中已经有了一个std::vector
类模板。它没有我们的酷,因为它缺少练习 2 中给出的print
功能,但我们不能拥有一切。 5
std::
swap
和std::
pair
也是内置的,在#include <utility>
。
pair
s 和(从 C++20 开始)vector
s 可以声明为constexpr
,如果那是你的事情的话。我们将在第二十六章进一步探索constexpr
s。
二十一、虚函数和多重继承
虚函数和多重继承在我写的大多数类中都没有出现——但是当我需要它们的时候,我就需要它们。
虚函数,加上:移动具有可移动父类和类成员的函数
多态性本质上是用同一个词来表示不同的东西。我们一直都是这么做的。考虑操作符+
。我们用它来添加int
s 或double
s 或string
s。这些在概念上是相似的,但它们由机器来完成是非常不同的。
另一个例子可能是函数start
,它可以应用于汽车、飞机或割草机。在每种情况下,功能的主体将是不同的(转动钥匙,通过飞行检查,拉动曲柄绳)。但是名字是一样的。
考虑一下我们想要在计算机屏幕上绘制二维形状的类:圆形、矩形、正方形和大块文本。这些有很多共同点:位置、颜色以及被绘制和移动的能力。我们可以把那些共同的品质放在一个父类Shape
(图 21-1 ,例 21-1 )。
图 21-1
一个等级制度
//Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef SHAPE_H
#define SHAPE_H
#include "SSDL.h"
struct Point2D // Life would be easier if this were a full-fledged class
{ // with operators +, =, etc. . . . but that
int x_, y_; // was left as an exercise
};
class Shape
{
public:
Shape (int x = 0, int y = 0, const char* text = "");
Shape (const Shape& other);
~Shape() { if (description_) delete[] description_; }
Shape& operator= (const Shape& s);
// Color
void setColor(const SSDL_Color& c) { color_ = c; }
const SSDL_Color& color () const { return color_; }
// Access functions
const Point2D& location () const { return location_; }
const char* description() const { return description_; }
// Drawing
void drawAux() const;
void draw () const
{
SSDL_SetRenderDrawColor (color ()); drawAux();
}
// Moving
void moveBy (int deltaX, int deltaY)
{
moveTo (location_.x_ + deltaX, location_.y_ + deltaY);
}
void moveTo (int x, int y)
{
location_.x_ = x; // Point2D::operator= would help here!
location_.y_ = y;
}
private:
Point2D location_;
SSDL_Color color_;
char* description_; // Using char* not std::string helps // illustrate how this chapter
// affects dynamic memory
void copy(const char*); // Used for copying descriptions
};
#endif //SHAPE_H
Example 21-1shape.h
注意函数draw
:它使用SSDL_SetRenderDrawColor
来告诉 SSDL 开始使用Shape
的颜色,然后调用drawAux
,一个助手函数来完成实际的绘制。对于Circle
s 来说drawAux
将是不同的(这里它将使用SSDL_RenderDrawCircle
),Text
s(这里它将调用SSDL_RenderText
),等等。Circle
的drawAux
如示例 21-2 所示。
//Circle class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef CIRCLE_H
#define CIRCLE_H
#include "shape.h"
class Circle: public Shape
{
public:
Circle () : radius_ (0) {}
Circle (const Circle& c) : Shape(c), radius_ (c.radius()) {}
Circle (int x, int y, int theRadius, const char* txt="") :
Shape (x, y, txt), radius_ (theRadius)
{
}
Circle& operator= (const Circle& c)
{
Shape::operator= (c); radius_ = c.radius (); return *this;
}
int radius () const { return radius_; }
void drawAux() const
{
SSDL_RenderDrawCircle (location().x_, location().y_, radius());
}
private:
int radius_;
};
#endif //CIRCLE_H
Example 21-2circle.h
我们现在可以试着画一个Circle
:
Circle c (10,10,5); c.draw (); // draw a Circle of radius 5 at (10,10)
...但是行不通;编译器抱怨在Shape::draw
中调用的Shape::drawAux
没有被编写。没错。Circle
的drawAux
写了,但是Shape::draw
对Circle
的功能一无所知。
我们需要的是一种方法,让Shape::draw
调用drawAux
的右版本:Circle
s 用Circle::drawAux
,Text
s 用Text::drawAux
等等。
这是修复:虚函数。
class Shape
{
public:
...
virtual void drawAux ();
...
};
Example 21-3The Shape class, with a virtual function
在示例 21-3 中,我们告诉Shape
类,“无论何时调用drawAux
,使用子类的版本,如果有的话。”
Circle
需要被告知它的drawAux
正在覆盖一个虚函数,所以我们也这样做(例子 21-4 )。
class Circle: public Shape
{
...
void drawAux () const override;1
...
};
Example 21-4The Circle class, with the override specifier
在幕后
以前,一个给定类的对象只存储数据成员(图 21-2 )。它没有将成员函数和对象一起存储在内存中,因为这样会浪费空间。
图 21-2
一个Circle
对象,在虚函数之前
但是现在对象也包含了任何虚函数的地址,所以它记住了要调用哪个版本(图 21-3 )。
图 21-4
例 21-6 中奥林匹克标志程序的输出
图 21-3
一个Circle
对象,使用虚函数
我们增加了一些开销:在每个Circle
中为drawAux
函数增加了一个额外的指针。但是开销很小,没什么好担心的。
纯虚函数和抽象基类
下一个问题:不管怎样,我们怎么写Shape::draw()
?
除非我们指定它是Shape
的什么种类——除非它是Circle
或其他子类——否则这个问题没有答案。所以我们将采取简单的方法:我们不会为Shape
编写它,而是告诉编译器,“你不能有一个Shape
那只是一个Shape
,这个函数就是为什么。”
class Shape
{
public:
...
virtual void drawAux ()=0;
...
};
Example 21-5The Shape class, with a pure virtual function. This makes Shape an “abstract” class
通过添加=0
,我们将drawAux
变成了一个纯虚拟的函数,将Shape
变成了一个抽象类,这意味着你不能用它来声明变量:
Shape myShape; // Nope, can't do this, the compiler will stop you
Circle myCircle; // No problem: it's a shape, but it's also a Circle,
// and we can drawAux Circles
为什么虚函数经常意味着使用指针
我们可能想对Shape
s 做些什么:将它们放入一个向量,并对向量中的每个Shape
做些什么(比如draw()
):
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<Shape> olympicSymbol; // No, this isn’t going to work...
constexpr int RADIUS = 50;
// consisting of five circles
olympicSymbol.push_back (Circle ( 50, 50, RADIUS));
olympicSymbol.push_back (Circle (150, 50, RADIUS));
olympicSymbol.push_back (Circle (250, 50, RADIUS));
olympicSymbol.push_back (Circle (100, 100, RADIUS));
olympicSymbol.push_back (Circle (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back (Text (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); // clear the screen
// draw all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].draw ();
// move all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i].moveBy (1, 1);
}
return 0;
}
这很有意义:创建一系列Shape
的序列,然后绘制它们。但是行不通。一个原因是Shape
现在是一个抽象类;既然你不能创造出仅仅是Shape
的东西,你当然也不能创造出它们的vector
。
另一个原因是,例如,olympicSymbol[0]
有足够的空间来存储单个Shape
。这意味着它有空间容纳一个color_
、一个location_
、一个description_
和一个指向drawAux
的指针。你会把Circle
的radius_
存放在哪里?Text
对象的contents_
?没有地方了!
为了解决这个问题,我们需要动态内存。是的,我知道;懒惰的程序员避免使用动态内存,因为它容易出错,也更难编写。但有时你必须拥有它。在这种情况下,当您使用new
创建一个Circle
时,它将分配它需要的数量。
和我们之前使用动态内存的方式不一样。然后,我们想要一个数组,所以我们用[]
: char* str = new char [someSize]
,和delete [] str
来清理。这一次,当我们分配一个Shape
时,我们分配一个??。所以我们省略了[]
的:new
Circle
而不是new Circle [...]
;delete
不是delete []
。我们将在下一章中得到更多分配/释放单个元素的练习。
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<Shape*> olympicSymbol;
constexpr int RADIUS = 50;
// consisting of five circles
olympicSymbol.push_back (new Circle ( 50, 50, RADIUS));
olympicSymbol.push_back (new Circle (150, 50, RADIUS));
olympicSymbol.push_back (new Circle (250, 50, RADIUS));
olympicSymbol.push_back (new Circle (100, 100, RADIUS));
olympicSymbol.push_back (new Circle (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back (new Text (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); //clear the screen
// draw all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).draw ();
// move all those shapes
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
(*olympicSymbol[i]).moveBy (1, 1);
}
// done with our dynamic memory -- throw it back!
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
delete olympicSymbol [i];
return 0;
}
Example 21-6A program that successfully uses a vector of Shapes to display and move a complex symbol. Output is in Figure 21-4
在这段代码中,我们使用的vector
不是Shape
的,而是Shape*
的。然后,当我们使用new
来创建一个Circle
或Text
时,它可以为我们获得一个适合该子类大小的内存块。
由于olympicSymbol[i]
是一个指针,我们说的不是olympicSymbol[i].draw ()
而是(*olympicSymbol[i]).draw ()
。 2
最后,和使用动态内存时一样,当我们完成时,我们用delete
把内存扔回去。
为了确保所有的数据都被返回,我们需要下一部分。
虚拟析构函数
考虑前面例子中的Text
对象,它包含“奥林匹克运动会”
// Text class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef TEXT_H
#define TEXT_H
#include "shape.h"
class Text : public Shape
{
public:
Text(const char* txt = "") { copy(txt); }
Text(const Text& other) : Shape(other) { copy(other.contents_); }
Text(int x, int y, const char* txt = "") : Shape(x, y)
{
copy(txt);
}
~Text() { if (contents_) delete contents_; } // Not quite right...
Text& operator= (const Text& other);
const char* contents () const { return contents_; }
void drawAux () const override
{
SSDL_RenderText(contents_, location().x_, location().y_);
}
private:
char* contents_;
void copy(const char* txt); // used for copying contents
};
#endif //TEXT_H
Example 21-7text.h
它使用动态内存来分配它的字符数组。自然地,当我们完成后,我们需要把它扔回去。但是main
中应该这样做的语句——for (int i = 0; i < olympicSymbol.size(); ++i) delete olympicSymbol [i];
——却没有这样做。olympicSymbol [i]
是一个Shape
的指针,不是一个Text
;所以只会删除属于Shape
的东西。它不知道Text
的contents_
。
解决方法还是使用知道的函数版本:?? 的版本。我们通过让Shape
的析构函数virtual
和Text
的析构函数override
来做到这一点:
virtual Shape::~Shape () { if (description_) delete[] description_; }
Text::~Text () override { if (contents_) delete contents_; }
现在,当调用Shape
上的析构函数时,Shape
将知道调用哪个版本。如果是一个Text
,被调用的析构函数将是Text::~Text ()
——当它完成时,调用父类Shape
的析构函数,因为析构函数无论是否是虚拟的都会这样做。
当你建立一个继承层次时,很容易忘记你是否在这个类或那个类中使用了虚函数。当你写父类的时候,没有办法知道没有后代会使用动态内存。因此,如果有任何人动态分配某个子类的任何成员的可能性,你怎么知道呢?–将析构函数设为虚拟的更安全。
Golden Rule of Virtual Functions
(普通版本)如果你在一个类层次结构中使用虚函数,让析构函数是虚拟的。
(更强版本)既然你不知道写类的时候写子类的人会做什么……那就把所有析构函数都虚拟化吧。句号。
我在Text
中使用了char* contents_
,在Shape
中使用了char* description_
,以清楚地表明我们需要析构函数。但是如果它们是string
的,同样的事情也会发生:当你删除一个指向Shape
的指针,而这个指针实际上是一个指向Text
的指针时,delete
不会知道它实际上是一个Text
,所以Text
的析构函数——不管是我们写的还是编译器的默认——都不会被调用,也没有什么能告诉Text
的成员contents_
删除它的内存。所以你仍然需要给Shape
一个虚拟析构函数:
virtual Shape::~Shape () { }
使用可移动的父类和类成员移动函数(可选)
如果你为Shape
及其子类编写 move 构造器,你可能会发现Shape
的工作很好,但是当你试图从Text
使用它时:
Text::Text (Text&& other) noexcept: Shape (other) // Nope, not working right...
{
...
}
它调用Shape
的常规复制构造器,尽管Shape
也是可移动的(有移动函数)。
当您使用Text
的 move 构造器时,这是因为 C++ 认为被复制的东西是一个“r 值”,一个您可以安全地用完、破坏等等的东西,因为它不会被再次使用,所以我们窃取它的内存是完全安全的。
但是当它在构造器中的时候,我们需要保留它直到我们完成,所以它的 r 值被去掉了。所以当我们调用Shape (other),
时,它不知道使用 move 构造器。
C++ 的解决方法是强制它返回到调用的 r 值。std::move(other)
将other
变回 r 值:
#include <utility> // for std::move
...
Text::Text (Text&& other) noexcept : Shape (std::move
(other))
{
contents_ = other.contents_; other.contents_ = nullptr;
}
移动=
也是如此:在将other
交给Shape
的移动=
之前,我们需要将它变回 r 值。
Text& Text::operator= (Text&& other) noexcept
{
Shape::operator= (std::move(other));
//...
return *this;
}
对于使用移动语义的数据成员也应该这样做。例如,如果我们让Text::contents_
被声明为String
(它也使用移动语义)而不是char*
,Text
的移动函数在访问它时将需要使用std::move
(示例 21-8 ,突出显示)。
Text::Text (Text&& other) noexcept :
Shape (std::move(other)), contents_(std::move (other.contents_))
{
}
Text& Text::operator= (Text&& other) noexcept
{
Shape::operator= (std::move (other));
contents_ = std::move (other.contents_);
return *this;
}
Example 21-8How to call move functions of parents and movable data members: use std::move
防错法
编译器说你的子类的 覆盖 **函数与基类中的任何一个都不匹配。**您可能已经将
const
从函数头中去掉,或者拼写了不同的函数名,或者给了它一个稍微不同的参数列表。编译器说你的子类是抽象的,但它不包含纯虚函数。比如,
Circle myCircle;
可能会抱怨Circle
很抽象——但是你根本没有在里面放任何虚函数!
你可能忘了覆盖父类的纯虚函数。这也使得子类变得抽象。
或者您可能忘记了将相应的子类函数设置为override
,并且它的头文件与父类函数的头文件不完全匹配(请参见反屏蔽一节中的前一条)。解决方法:添加override
。
Exercises
图 21-6
从Model
和Object
继承ModelObject
图 21-5
通电练习,展示“虫洞”动画
- 在这个简单的射击游戏中,你可以点击鼠标击中一个目标。启动的类型——闪速启动、巨型启动或虫洞——在你点击其中一个时会给出不同的点数并显示不同的动画(见图 21-5 )。
在本章的示例代码部分,您会发现一个部分编写的程序,用于拍摄加电。它使用了Shape
层次结构。为了让它工作,你需要修改main.cpp
来使用指针,并在Powerup
类层次结构的正确位置添加virtual
和override
。我建议将Powerup
抽象化,原因和Shape
一样。
(使用移动语义)修改练习 1 中的类以使用移动语义。
在第十九章的
CardGroup
类层次中,给每个可能需要它们的子类添加函数isLegalToAdd
和isLegalToRemove
。(例如,只有当单元格为空时,Cell::isLegalToAdd
才返回 true,只有当单元格不为空时,Cell::isLegalToRemove
才返回 true。)让
CardGroup
的addCardLegally
调用isLegalToAdd
,使用虚函数,因此它调用适当的子类版本。其他职业都不应该有自己的addCardLegally
。测试以确保实际调用了正确的函数。你可能需要一些试抓块。
(更大的项目)扩展练习 2,写一个非图形化的自由细胞游戏。你应该有一个
CardGroup
(实际上是CardGroup*
)的数组或向量,包括FreecellPile
、Cell
和Foundation
s。让用户指定将卡片从哪个CardGroup
移出或移入哪个CardGroup
(比如说,“F1”代表基础 1,“P2”代表堆 2)。选择的CardGroup
知道,基于它是什么子类,使用isLegalToAdd
或isLegalToRemove
的哪个版本:CardGroup* from = askUserToPickCardGroup(); if ((*from).isLegalToRemove()) // can we take card from top? ...
多重继承(可选)
考虑三维图形程序的这两个类:
类别Object
有位置、速度和加速度。这是为了研究运动定律。
类是具有物理外观的东西。它由三角形组成,这些三角形组合在一起形成一个明显的固体。
你能拥有不是 ?? 的 ?? 吗?当然可以。你可能有一个其他格式的模型,不使用三角形——也许是一个球体。
你能拥有不是 ?? 的 ?? 吗?当然可以。你可以使用 CAD/CAM 来设计制造产品。
所以一个Model
不是一个Object
,一个Object
也不是一个Model
……但是两者兼而有之是有道理的。我会把那个东西叫做ModelObject
。
由于ModelObject
是一个Model
和一个Object
,它继承了两者的特征:它将拥有类似于Object
和矢量Triangle
s 的位置、速度和加速度,加上来自Model
的load
和display
函数。
我们可以使用公共或私有继承。public
有道理:
class ModelObject: public Model, public Object
{
public:
ModelObject () {}
ModelObject (const ModelObject& other)
: Model (other), Object (other)
{
}
...
};
要调用父构造器,就像处理普通继承一样使用:
;但是这一次,调用两个父构造器(或者使用它们的默认值)。
这不是经常需要的,但是当需要时,它很方便。
防错法
假设我们做一个角色扮演游戏。其中我们有类Player
,有一个name_
和一些hitPoints_
。它还有一个成员函数takeAttack (int howMuch)
,可以减少一定数量的生命值。
我们创建两个子类,Fighter
和Magician
。一个Fighter
有一个成员attack
,带走一个Player
,降低其生命值。A Magician
有一个成员bespell
,做魔法攻击。
但是有些游戏让你为你的角色制造混合职业。我们将使FighterMagician
成为Fighter
和Magician
的子类。现在我们有了一个既能attack
又能bespell
的类。酷。
但是有一个问题。Fighter
有成员hitPoints_
和name_
(继承自Player
)。Magician
还有hitPoints_
和name_
(继承自Player
)。所以FighterMagician
有两份hitPoints_
和两份name_
!
出于某种原因,这被称为“钻石问题”。也许图 21-7 会给我们一点线索?
图 21-7
多重遗传中的钻石问题
我们无法推理出解决这个问题的方法;C++ 将会帮助我们。它确实做到了:它让我们可以创建Fighter
和Magician
“虚拟”基类,本质上是说,“不要做那种额外复制公共祖父母成员的事情”:
class Fighter: virtual public Player ...
class Magician: virtual public Player ...
另一个问题:由于Fighter
和Magician
可能调用不同的Player
构造器,这将导致歧义,FighterMagician
必须明确声明它想要调用什么Player
构造器,就像这样:
FighterMagician (const char* name) : Fighter (...some args...),
Magician (...some args...),
Player (name)
{
}
如果我们不指定,编译器将使用缺省值。
Exercises
编写反欺诈部分的类—
Player
、Fighter
、Magician
和FighterMagician
。因为你不是在做一个真正的游戏,为了简单起见,
attack
和bespell
都可以从对手的生命值中随机抽取数字。FighterMagician
用attack
还是bespell
都没关系——从main
中选一个作为它的战斗方式。现在让一个
Fighter
(比方说)和一个FighterMagician
对抗,看谁赢了这场比赛。使用
Shape
类,创建一个既有Shape
又有location
、virtual drawAux
等的Composite
类。)和一个Shape*
的向量(所以可以用Circle
s、Text
s,随便什么)。请确保您的Composite
可以被创建、移动、显示并被正确地析构。如果您已经讨论了移动语义,那么编写移动构造器并移动=
。一个棘手的问题是,
Composite
有两种位置:从Shape
继承的位置和它所有子组件的位置。让你的move
函数更新所有位置。如果你做了虚函数部分的练习 1,你可以用一个
PowerupSet
类来扩展它,它既是一个Shape
也是一个Powerup*
的向量。确保你的PowerupSet
可以被创建、绘制和动画化,并且被正确地析构。main.cpp
中采用vector<Powerup>
的函数(或类似的函数)应该改为采用PowerupSe
t。如果您已经讨论了 move 语义,那么编写 move 构造器并移动=
。t 有两种位置:继承自 ?? 的位置和其子组件的位置。让你的
move
函数更新所有位置。
二十二、链表
要向Vector
添加一个元素,我们必须分配新的内存并复制现有的元素。那还不够快!这里有一个更新更快的存储方案。
什么是列表,为什么有列表
整个城市,一群超级英雄在等待。如果需要他们的权力,他们有一个通知对方的计划:每个人都有另一个人的电话号码,谁有另一个人的电话号码,直到名单上的最后一个人没有电话号码(见图 22-1 )。
图 22-1
我们的城市,三个超级英雄在一个链表中。百变少女第一,在 555-0169;有点能力的男生在 555-0145;神童在 555-0126
在计算机中,我们不用电话号码,而是用内存地址。这个数据结构包含每个人拥有的信息:
struct Superhero
{
std::string name_; // The Superhero's name
Superhero* next_; // The address of the next Superhero
};
记住Superhero*
的意思是指向Superhero
的指针——在计算机的内存中去哪里找一个Superhero
,就像在第二十一章中Shape*
的意思是去哪里找一个Shape
。神童排在最后,她的下一个电话号码是“无”,所以我们将她的next_
字段设置为nullptr
。
假设我们想在列表中添加另一个英雄。简单快捷:给他列表中当前第一个的号码。他会把它放进他的领域。然后记住他的联系方式作为新的第一个号码(见图 22-2 )。
图 22-2
同样的列表在我们添加了喷虫人之后
更正式地说,算法是
create a new Superhero struct
put the name of the new person into the Superhero
put the pointer to the start of the list into the new Superhero, as "next"
set the start of the list to the address of the new Superhero
这里没有循环,所以比Vector::push_back
快多了。
效率和 O 符号(可选)
在 O(1)处,List
添加元素的方式比Vector
的 O(N)有了很大的进步!
但是假设我们想要查看 indexth 元素(无论是什么索引)——也就是说,使用operator[]
。我们如何做到这一点?对于Vector
,它只是contents_[index]
——没有循环,没有重复,因此是 O(1)。在这里,我们必须按顺序进行:
current position = start;
for j = 0; j < index; ++j
current position = the address of the next Superhero
if we go off the end of the list, throw an exception
return the name in the current position
这个是否有循环——它的时间要求是 O( index
)。平均来说,这将是 O(N/2),或 O(N)。
*表 22-1 显示了你如何知道对于一个给定的任务,Vector
和List
哪个更好。如果你做了大量的查找(operator[]
),那么Vector
会更快。如果你添加了很多元素,List
会更快。
表 22-1
一些Vector
和List
功能所需的时间
功能
|
效率(向量)
|
效率(列表)
operator[] |
O(1) | O(N) |
operator= |
O(N) | O(N) |
复制构造器 | O(N) | O(N) |
push_back |
O(N) | 未写 |
push_front |
未写 | O(1) |
我经常使用Vector
,因为我发现我查看序列的次数比构建序列的次数多。如果序列很小,也没多大关系。如果它很大,我会更注意选择最快的。
启动链表模板
现在让我们开始写List
。我们将放弃超级英雄的类比,将List
作为模板(例如 22-1 )。
// class List: a linked list class
// from _C++20 for Lazy Programmers_
#ifndef LIST_H
#define LIST_H
#include <iostream>
template <typename T>
class List
{
public:
class Underflow {}; // Exception
List ();
List (const List <T>& other);
~List();
List& operator= (const List <T>& other);
bool operator== (const List <T>& other) const;
int size () const;
bool empty () const { return size() == 0; }
void push_front (const T& newElement); // add newElement at front
void pop_front (); // take one off the front
const T& front () const;
private:
struct Entry1
{
T data_;
Entry* next_;
};
Entry* start_; // Points to first element
};
#endif //LIST_H
Example 22-1The List class, first version
就像Vector
,除了
我们用
push_front
而不是push_back
。数据成员是不同的。
我漏掉了
operator[]
。它的效率如此之低,以至于在抱怨了几年之后,我不在乎它是否效率低下;就让我用吧!我已经向社区低头,不再理会它。无论如何,我们将在下一章中找到更合适的方法来访问元素。
现在让我们从默认的构造器开始写一些函数。
List<T>::List ()
让默认值List
为空是有意义的。我们如何指定一个列表是空的?按照惯例,当指针start_
为nullptr
时,这是正确的。你可以说它什么也没有指向——因为列表中什么也没有:
template <typename T>
class List
{
public:
List () { start_ = nullptr; }
...
};
void List<T>::push_front (const T& newElement);
当我们开始push_front
的时候,我们有了原来的List
,包含了start_
和一个newElement
。我们需要在前面添加newElement
,如图 22-3 所示。
图 22-3
在添加新元素到List
之前和之后
以下是实现这一点的方法:
create an Entry
put the newElement into its data field
put the old version of start into its next field
put the address of the new Entry into start
似乎很简单。这是代码:
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an Entry
(*newEntry).data_ = newElement;// put newElement in its data_ field
(*newEntry).next_ = start_; // put old version of start_
// in its next_ field
start_ = newEntry; // put address of new Entry
// into start_
}
让我们一行一行地追踪。
第一行Entry* newEntry = new Entry;
,使用动态内存创建新的Entry
。就像第二十一章中的新Shape
一样,我们一次只分配一个Entry
,而不是一个数组,所以我们不需要[]
在第二行,newEntry
是新Entry
的地址,所以*newEntry
就是那个Entry
本身。(*newEntry).name_
故为其name_
场。第三行类似。
第四行将newEntry
的地址存储在List
的start_
字段中,这样我们就可以记住在哪里找到它。我们的新Entry
现在指引我们去List
的其他地方。如果List
有元素,好;我们会见到他们的。如果List
为空,那么指向列表其余部分的指针就是nullptr
。也不错。我们会知道这就是结局。
void List<T>::pop_front ()
…去掉第一个元素的函数。这是我的第一次尝试。(为了简洁,我只展示了代码,但是当然我会先写算法。)
template <typename T>
void List<T>::pop_front ()
{
if (empty()) throw Underflow();
delete start_; // delete the item
start_ = (*start_).next_; // let start_ go on to the next
}
(和第二十一章一样,我们使用delete
而不是delete []
——因为我们使用了没有[]
的new
,分配的不是一个数组而是一个Entry
。)
假设我们把杀虫剂从名单上去掉了。图 22-4 显示了我们从什么开始。
图 22-4
准备好pop_front
我会追踪这些步骤。是空的吗?没问题。现在我们删除start_
所指向的,得到图 22-5 。
图 22-5
我们删除了pop_front
中的元素。不,这是不对的…
然后我们访问(*start_).next_
。但是start_
所指向的已经被删除,不复存在。名单上其他人的地址都没了。撞车了。
也许我们可以用不同的顺序来做这件事——只有在我们确定已经完成之后才删除它们:
template <typename T>
void List<T>::pop_front ()
{
if (empty ()) throw Underflow ();
Entry* temp = start_; // store location of thing to delete
start_ = (*start_).next_; // let start_ = next thing after start_
delete temp; // delete the item
}
现在让我们看看情况如何。
我们检查空的情况。没问题。
我们设定temp
等于start_
(图 22-6 )…
图 22-6
开始pop_front
(再次)
我们移动start_
指向列表的其余部分(图 22-7 )…
图 22-7
将start_
设置到它应该去的地方…
…我们删除不再需要的Entry
(图 22-8 )。
图 22-8
现在可以正常工作了
列清单时,我总是画这些方框和箭头;没有它们,我必然会失去指针,跟随不好的指针,等等。所以我得到了以下黄金法则:
Golden Rule of Pointers
当改变或删除指针时,画出你正在做的事情的图表。
List<T>::~List ()
最终,我们不得不把所有这些都扔回去。
我可以写一个 while 循环来删除它们,制作一个图表来确保我不会丢失任何指针,但是我是一个懒惰的程序员。我已经有东西可以安全地扔回去了吗?确定,pop_front
:
template <typename T>
List<T>::~List () { while (!empty()) pop_front(); }
完成了。
一点语法上的好处
因为使用 Shift 键,我的小指都快磨破了。幸运的是,C++ 提供了另一种写完全相同的东西的方法,更容易输入,也更容易阅读:
newEntry->next_; // means (*newEntry).next_;
这是我们新版本的push_front
:
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an Entry
newEntry->data_ = newElement;// put newElement in its data_ field
newEntry->next_ = start_; // put old version of start_ in
// its next_ field
start_ = newEntry; // put address of new Entry
// into start_
}
更友好的语法:指针作为条件
我们经常需要这样的代码:if (next_ != nullptr)...
或者while (next_ != nullptr) ...
。
考虑 if 语句(以及 while 循环和 do-while)的条件是如何工作的。评估()
之间的表达式。如果它的值为 0,那就意味着假;其他都是真的。
嗯,nullptr
有点像0——至少,它的意思是“没有”什么都没有,假的,0,随便。所以你可以把if (next_ != nullptr)...
写成
if (next_)...
“如果next_
不是空的,如果有下一件事……”是这个条件在说什么。你觉得方便就用吧。
链表模板
示例 22-2 包含前面函数的完整版本,以及一些其他的。有的留着当练习。
还有一件事值得注意。考虑一下operator=
。在复制另一个列表之前,我们必须用delete
扔掉旧的内存。我们不是已经在析构函数里这么做了吗?是的,所以我们做了一个函数eraseAllElements
,可以被operator=
和析构函数调用,供代码重用。
createEmptyList
是代码重用的另一个实用函数。
// class List: a linked list class
// from _C++20 for Lazy Programmers_
#ifndef LIST_H
#define LIST_H
#include <iostream>
template <typename T>
class List
{
public:
class Underflow {}; // exception
List () { createEmptyList(); }
List (const List <T>& other) : List () { copy(other); }
~List() { eraseAllElements(); }
List& operator= (const List <T>& other)
{
eraseAllElements (); createEmptyList(); copy(other);
return *this;
}
bool operator== (const List <T>& other) const; // left as exercise
int size () const; // left as exercise
bool empty () const { return size() == 0; }
void push_front (const T& newElement); // add newElement at front
void pop_front (); // take one off the front
const T& front () const; // left as exercise
void print (std::ostream&) const; // left as exercise
private:
struct Entry
{
T data_;
Entry* next_;
};
Entry* start_; // points to first element
void copy(const List <T>& other); // copies other's entries
// into this List
void eraseAllElements (); // empties the list
void createEmptyList ()
{
start_ = nullptr; // the list is...nothing
}
};
template <typename T>
inline
std::ostream& operator<< (std::ostream& out, const List <T>& foo)
{
foo.print(out); return out;
}
template <typename T>
void List<T>::eraseAllElements () { while (!empty()) pop_front(); }
template <typename T>
void List<T>::push_front (const T& newElement)
{
Entry* newEntry = new Entry; // create an entry
newEntry->data_ = newElement;// set its data_ field to newElement
newEntry->next_ = start_; // set its next_ field to start_
start_ = newEntry; // make start_ point to new entry
}
template <typename T>
void List<T>::pop_front ()
{
if (empty ()) throw Underflow ();
Entry* temp = start_; // store location of thing to delete
start_ = start_->next_; // let start_ = next thing after start_
delete temp; // delete the item
}
template <typename T>
void List<T>::copy (const List <T>& other)
{
Entry* lastEntry = nullptr; // last thing we added to this list,
// as we go thru other list
Entry* otherP = other.start_; // where are we in the other list?
// while not done with other list...
// copy its next item into this list
while (otherP)
{
// make a new entry with current element from other;
// put it at end of our list (so far)
Entry* newEntry = new Entry;
newEntry->data_ = otherP->data_;
newEntry->next_ = nullptr;
// if list is empty, make it start_ with this new entry
// if not, make its previous Entry recognize new entry
// as what comes next
if (empty ()) start_ = newEntry;
else lastEntry->next_ = newEntry;
lastEntry = newEntry; // keep pointer for lastEntry updated
otherP = otherP->next_;// go on to next item in other list
}
}
...
#endif //LIST_H
Example 22-2list.h, containing the List class, some functions omitted. I invite the reader to do the exercises that follow before examining the completed solution in ch23’s project 1-2-lists
防错法
当指针出错时,它们真的会出错。你可能会遇到程序崩溃。
以下是最糟糕和最常见的指针相关错误:
- 崩溃,来自跟一个
nullptr
:比如说myPointer
是nullptr
的时候说*myPointer
。最好的预防措施:在你指向指针指向的地方之前(把*
放在前面或者->
放在后面),一定要检查
if (myPointer !=``nullptr
当涉及指针时,偏执是一件好事。
崩溃,因为使用了尚未初始化的指针。我的解决方案是总是初始化每个指针。如果不知道初始化成什么,就用
nullptr
。然后if (myPointer != nullptr) ...
(见上一段)将防止错误。崩溃,来自于跟随一个指向已经被删除的东西的指针:正如我在前面的章节中所做的,跟踪代码对图做了什么是我所知道的最好的预防措施。一旦你有了几个你信任的函数,你就可以偷懒了,就像我对
eraseAllEntries
一样;让像pop_front
这样可信的函数来做可怕的工作。程序陷入循环:
Entry* p = start_; while (p) { ... }
这里的问题是我忘记让 while
循环继续到下一个条目:
p = p->next_;
如果我把它写成 for 循环的形式,我就不太可能忘记:
for (Entry* p = start_; p != nullptr; p = p->next)...
Exercises
对于这些练习,从ch22/listExercisesCode
中的项目开始。练习 1–6 在章节源代码(ch22/1-2-lists
)中有答案。
自始至终,你都可以使用操作符->
来保存你的手指输入。
写
List
的成员函数front ()
。…和
size ()
。你能让它在没有循环的情况下工作吗?…和
operator==
。…和
print
。通过给
Entry
一个将next_
字段设置为nullptr
的构造器来稍微清理一下代码。这应该有助于防止忘记初始化的错误。(需要移动构造器,移动
=
)给List
一个移动构造器,移动=
。(需要概念)用你的
List
使用概念。确保你放在List
里的任何东西都是可打印的,为了print
。(更难)添加一个数据成员
Entry::prev_
,这样就可以向后遍历列表,添加List::rStart_
,这样就知道从哪里开始了。还增加了List
成员功能push_back
、pop_back
和back
。(更难)重写蒙大拿,增加一个新选项:撤销。为了支持它,你需要保存一个
List
的移动,这样你就可以撤销你的上一个移动,上一个移动,等等,直到没有移动可以撤销。一招包含哪些信息?当新回合开始时,可以清空List
。(为什么我们不用Vector
?)
#include <list>
是的,链表类也是内置的。它比我的好:它有push_back
、pop_back
(见练习 8),还有很多其他你可以自己查找的函数。
所以这里有一个问题:如果你不能使用[]
,你如何用和来获取列表的元素呢?如果你不能,那就没用了!有一种方法:它被称为“迭代器”,将在下一章中介绍。
二十三、标准模板库
是不是每个程序员都应该自己做向量,列表等等?哦,当然不是。所以前段时间,标准模板库(STL)被放到了标准里。在 STL 中,你会发现像list
和vector
这样的容器;string
年代,正如我们已经使用它们一样;以及swap
、find
、copy
等常用功能。
你还会发现对效率的过分强调。我说“烦人”是因为 STL 通过禁用低效的东西来提升效率。如果你想把operator[]
和一个list
一起用,那就算了;太慢了(O(N)时间,如果你做 O 记号的话)。如果你想要[]
,STL 的开发者认为你可以使用vector
。他们是对的。但我还是会生气。
我们必须以某种方式获取列表的元素。怎么做?让我们现在就解决这个问题。
迭代程序
list
的operator[]
不存在。Entry
年代是私有的。我们能做什么?
STL 的list
类提供了迭代器——说明我们在list
中的位置并可以遍历它。以下是您可以对它们做的事情:
// doSomethingTo each element of myList, from beginning to end
for (list<T>::iterator i = myList.begin
(); i != myList.end (); ++i)
doSomethingTo (*i);
像我们的Entry
结构一样,iterator
类型是list<T>
的成员,但是是公开可用的。如图所示,它往往从begin()
开始,即列表的开始,一直持续到到达end()
(图 23-1 )。
图 23-1
一个列表,有它的begin()
和end()
访问函数,一个迭代器i
表示第二个元素。end()
是最后一个元素的下一步
end()
不是指最后一个元素,而是指最后一个元素之后的一个*。把list
想象成一列汽车,这是它的车尾。我们检查它,看看我们是否走得太远,使用!=
而不是<
。(一个迭代器是否小于另一个迭代器没有定义——但是它们是否相等是有定义的。)*
*++i
和i++
按预期工作;他们带你到下一个元素。
要得到的不是迭代器而是它所引用的东西,把*
放在前面,就像前面的循环一样。
仅此而已!
你的反应类似于“但是什么是迭代器呢?”?
形式上,就像描述的那样:它是一个引用列表中一个元素的东西,当你说++
的时候,它就转到下一个元素。
非正式地…它不完全是一个指针,但是它确实指向一些东西。你可以使用*
和->
,就像你使用指针一样。但是++
的意思是转到列表中的下一个元素,不是下一个内存地址,就像用指针一样。把它想象成一个手指,你可以把它放在一个条目上,并在你喜欢的时候移动到下一个条目——但它实际上是一个类,如示例 23-1 所示。
template <typename T>
class List
{
...
private:
...
Entry* start_; // points to first element
Entry* end_; // ...and the caboose
public:
class iterator // an iterator for List
{
public:
iterator (const iterator& other) : where_ (other.where_) {}
iterator (Entry* where = nullptr) : where_ (where) {}
const iterator& operator= (const iterator& other)
{
where_ = other.where_;
}
bool operator== (const iterator& other) const
{
return where_ == other.where_;
}
const iterator& operator++() // pre-increment, as in ++i
{
if (where_->next_ == nullptr) throw BadPointer();
else where_ = where_->next_;
return *this;
}
iterator operator++ (int) // post-increment, as in i++
{
iterator result = *this; ++(*this); return result;
}
T& operator* ()
{
if (where_->next_ == nullptr) throw BadPointer();
return where_->data_;
}
T* operator->() // This really is how you do it. It works!
{
if (where_->next_ == nullptr) throw BadPointer();
return &(where_->data_);
}
private:
Entry* where_;
};
iterator begin() { return iterator(start_); }
iterator end () { return iterator(end_ ); }
};
Example 23-1An iterator class for List
现在我们可以得到List
类之外的List
的数据*。*
…也和vector
一起
list
s 需要迭代器。但是 STL 为vector
和其他容器提供了它们,STL 行家推荐使用它们。为什么呢?
轻松重写代码。考虑带有
vector
:for (int i = 0; i < v.size(); ++i) doSomething(v[i]); and for (vector<T>::iterator i = v.begin(); i != v.end(); ++i) doSomething(*i);
的这两个版本的代码
我写这一点后来又想,不行,我现在看 vector
不是办法; list
更好。
如果我使用第一个版本,我对两行都做了重大的修改。如果我用第二个,我要做的就是把vector
改成list
。
vector
的一些成员函数已经需要迭代器。比如insert
。泛型编程。假设您想对不同类型的容器做些什么:
myIterator = find(digits.begin(), digits.end(), 7); // Where's that 7?
- 因为
list
的find
必须使用迭代器,所以vector
的find
也使用迭代器。这样你可以学习一种调用函数的方法,不管你选择什么样的容器,它都会工作。本章末尾的“<algorithm>
(可选)”一节介绍了 STL 提供的一些功能。
但是如果你像以前一样用int index
代表vector
s,天就不会塌下来。
const
和反向迭代器
在这段代码中,使用迭代器会导致类型冲突:
class EmptyList {}; // Exception
template <typename T>
double findAverageLength (const List<T>& myList)
{
if (myList.empty()) throw EmptyList();
double sum = 0;
for (List<string>::iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
return sum / myList.size();
}
编译器给出一个错误信息,归结为:myList
是const
,但是你对它使用了一个iterator
,这是一个类型冲突。
这就是 STL 如何防止你使用一个带有const
容器的迭代器并做一些改变内容的事情。解决方案是使用一个不允许修改的迭代器版本:
template <typename T>
double findAverageLength(const List<T>& myList)
{
if (myList.empty()) throw EmptyList();
double sum = 0;
for (List<string>::const_iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
return sum / myList.size();
}
这就够了。
如果你愿意,你可以倒着穿过集装箱(注意rbegin
和rend
——与begin
和end
相同,只是方向相反):
for (list<T>::
反向 _ 迭代器 i=myList.
rbegin ();
i !=myList.rend();
++i)
doSomethingTo (*i); // myList must be non-const
...or do backwards and const:
for (List<string>::const_reverse_iterator i = myList.begin();
i != myList.end();
++i)
sum += i->size();
要知道使用哪一种:
如果不打算换容器,使用
const_iterator
。如果有,使用
iterator
。如果你要倒退,把
reverse_
放在那里的某个地方。
防错法
You get an error message too long to read:
conversion from 'std::__cxx11::list<std::pair<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >::const_iterator' {aka 'std::_List_const_iterator<std::pair<std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >'} to non-scalar type 'std::__cxx11::list<std::pair<std::__cxx11::basic_string<char>, int> >::const_iterator' {aka 'std::_List_const_iterator<std::pair<std::__cxx11::basic_string<char>, int> >'} requested
Rip out things that don’t look important. This gets us
conversion from list<pair<string, string > >::const_iterator to list<pair<string, int> >::const_iterator requested.
我的错误现在更清楚了:我忘记了我想要什么。
你得到几页错误信息,报告系统库中的错误。在信息中寻找你自己的文件名,并专注于这些文件名。
Exercises
在 for 循环中使用迭代器,编写一个函数
reverse
,它返回一个你给它的List
的反向副本。现在编写它,这样你不是传递一个
List
而是传递两个迭代器,可能是它的begin()
和它的end()
。给
List
添加一个const_iterator
类。你需要
begin
和end
的新const
版本,来返回const_iterator
而不是iterator
。第二十二章最后一组练习中的练习 8 是关于装备一个
List
向后移动。使用它,将reverse_iterator
和const_reverse_iterator
合并到List
类中。在 for 循环中使用迭代器,编写函数在 STL
list
和vector
之间来回转换。
变得非常懒惰:基于范围的for
和auto
当然,这适用于遍历容器:
for (vector<string>::const_iterator i = myVector.begin();
i != myVector.end();
++i) // A lot of typing...
cout << *i << ' ';
但是这个更短的版本也有效:
for (const string& i : myVector)
cout << i << ' '; // <-- no *: i is an element, not an // iterator
这是一个“基于范围的”for 循环:它使用迭代器——期望有begin()
和end()
,因此适用于vector
、list
或其他——但是我们不必把这些都写出来(耶)。(本节代码在源代码项目/文件夹ch23/range-based-for-and-auto
中收集和编译。)
它也适用于数组:
int myArray[] = { 0, 1, 2, 3 };
for (int i : myArray) cout << i << ' ';
很好,但是我现在更懒了。让编译器计算出元素类型:
for (auto
i : myArray) cout << i << ' ';
// Overkill? I *did* know it was an int...
for (auto i : myVector) cout << i << ' ';
您可以将auto
用于编译器能够识别类型的任何变量声明——也就是变量被初始化的地方。我们确实需要给 ?? 一些帮助;它不会应用&
的,除非我们告诉它:
for (auto& i : myArray) i *= 2; // Without & it won't change the array
你也可以做我们熟悉的const &
:
for (const auto& i : myVector) cout << i << ' ';
当类型名太长,我觉得我的手指会掉下来的时候,我就用auto
(list<vector<int>>::const_reverse_iterator
–aigh!)并且在这些基于范围的循环中。我认为对于简单的基本类型声明来说是过度了。<
跨度
Pascal 语言(始于 1970 年)允许将数组作为函数参数传递,大小作为类型的一部分。问题:每个数组大小都需要不同的函数。
如你所知,C 和 C++ 不会保留这个大小,所以你需要额外的工作来传递它作为另一个参数。
直到 C++20。现在你可以传入一个数组,并让它转换成一个跨度为的数组。span 记住了数组的内容和大小,所以我们可以这样做:
void print (const span<Heffalump> &s)
{
for (int i = 0; i < s.size(); ++i) cout << s[i];
}
或者,既然我们现在有了 ranges 和auto
,这:
void print (const span<Heffalump> &s)
{
for (const auto &element : s) cout << element;
}
我们能做到这一点吗——不需要指定长度就可以传递一个序列——不需要跨度?当然,我们可以使用vector
s。但是将一个数组复制到vector
需要时间,尤其是如果有很多元素的话。一个跨度并不真正拥有它的记忆;不复制,只是用大小捆绑;并使两者都可用,正如我们刚才看到的。示例 23-2 显示了如何使用它。
// Program to read in & dissect a phone number
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <span> // for std::span
using namespace std;
// Converts a char to a single-digit number
int char2digit (char c) { return c - '0'; }
// Read/print arrays, I mean, spans
void read (const span< int>& s);
void print (const span<const int>& s);
int main(void)
{
int phoneNumber[10];
cout << "Enter your phone # (10 digits, no punctuation, ";
cout << "e.g 2025550145): "; read (phoneNumber);
cout << "You entered: "; print (phoneNumber);
cout << '\n';
cout << "Area code was: ";
print (span (phoneNumber).subspan
(0, 3)); cout << '\n';
// subspan is better, but this shows how to use
// a span with a pointer
int* remainder = phoneNumber + 3;
cout << "Rest of number is: "; print (span (remainder, 7));2
cout << '\n';
return 0;
}
void print (const span<const int>& s)
{
for (const auto& element : s) cout << element;
}
void read (const span< int>& s)
{
for (auto& element : s)
element = char2digit (cin.get());
// read in a digit, convert to int
// keeping it simple: no check for bad input
}
Example 23-2A program that uses spans to read, print, and dissect a phone number
print
和read
中跨度前的const
表示跨度的结构不会被该功能改变。(所以你可以传入一个 r 值,一个不能赋值的东西,像span (remainder, 7)
。)但是它的元素可以改变。
print
跨度中的const int
意味着其中的int
也是const
。如果你不想函数改变数组,使用这个。
Exercises
图 23-2
冠状病毒病例的样本数据,使用 7 天平均值进行平滑
重写你在第十章中的程序,其中数组被传递到函数中,使用 ranges,auto 和 spans。
假设你有数据,每天都有新的病例,从冠状病毒第一次成为大新闻开始(图 23-2 )。为了平滑数据并帮助我们了解它总体上是增加还是减少——如果我们将曲线拉平——计算每天之前三天、之后三天以及自身的平均值。(您可以跳过数据集的前 3 天和后 3 天。)然后打印那些平均值。使用范围、
auto
、跨度和subspan
。(使用文件 I/O)做前面的练习,从文件中获取数据。你不知道文件中有多少数据,所以使用动态数组。至少应该向一个函数(打印的函数)传递一个动态数组。
initializer_list
s(可选)
从数组到更复杂的容器,我很遗憾地放弃了一件事,那就是括号内的初始化列表,就像在int array [] = {0, 1, 2, 3};
中一样。逐个元素初始化比较麻烦。
但是我们也可以用自己的类来做这件事,它是为 STL 内置的。示例 23-3 用Vector
说明了这一点。
#include <initializer_list
> // for std::initializer_list
template <typename T>
class Vector
{
public:
Vector (const std::initializer_list<T>& other);
// ...
};
template <typename T>
Vector<T>::Vector (const std::initializer_list<T>& other) : Vector ()
{
for (auto i = other.begin(); i != other.end(); ++i)
push_back (*i);
}
Example 23-3Adapting Vector to use an initializer_list
内置了迭代器,所以这可以工作。不过,由于它内置了迭代器,所以这个更简单的版本也会如此:
template <typename T>
Vector<T>::Vector (const std::initializer_list<T>& other) : Vector ()
{
for (auto i : other) push_back (i);
}
然后我们可以像以前一样初始化数组
Vector<int> V = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
或者因为新的构造器将根据需要隐式调用:
Vector<int> U;
// ...
U = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Exercises
在类
Point2D
中编写并测试一个接受initializer_list
的构造器。…在
Fraction
班。如果你按顺序通过一个
initializer_list
,使用List::push_front
将它的元素添加到一个List
,顺序将被颠倒。(如有必要,将它写在纸上,以便确认。)所以如果你还没有做上一章的练习 8,它提供了List::push_back
,并给List
一个带initializer_list
的构造器。
<algorithm>
(可选)
您可能想对容器做的许多事情都在包含文件<algorithm>
中。
下面是如何在名为digits
的容器中找到一个元素。可以是list
、vector
,或者随便什么。(这些代码片段是在源代码项目/文件夹ch23/algorithm
中收集和编译的。)
using namespace std; // the ranges namespace is part of std;
// without "using namespace std," say std::ranges::
auto i = ranges::find (digits, 7);
3
if (i != digits.end()) cout << "Found a 7!\n";
find
返回引用第一个元素digits
等于 7 的迭代器。如果没有这样的元素,它返回digits.end()
。
下面是如何将一个容器中的内容复制到另一个容器中。它们可以是不同类型的容器:
ranges::copy
(digits, back_inserter (newContainer));
或者只是复制那些符合某种标准的。我们可以传入一个函数:
bool isEven (int i) { return i % 2 == 0; }
..
ranges::copy_if
(digits, back_inserter (anotherNewContainer), isEven);
这些功能中的大多数应该适用于任何类型的容器。ranges::sort (digits);
做你认为它做的事。(运算符<
对其元素必须进行定义。)但是如果你想对一个列表进行排序,你就必须使用它的成员函数:myList.sort ();
。去想想。
这些函数从你的容器中删除一个值或者删除所有符合某些条件的元素: 4
erase_if (digits, isEven); // returns quantity erased. Not part of ranges:: // (just std::)
erase (digits, 11); // ditto. Erases all instances of 11
STL 容器不会让 I/O 的<<
和>>
操作符过载。这是可以理解的,因为我们可能都想用不同的方式在容器中打印或读取,但这仍然是一种痛苦。STL 提供了另一种打印方式,名字很奇怪
ranges::copy (digits, ostream_iterator<int>(cout, " ")); // "copy" digits // to cout
int
是我们的列表,cout
是它的去向," "
是每个元素后要打印的内容。您将需要#include <iterator>
(尽管它可能由另一个系统提供)。
有用的功能比较多;网上搜索会找到你需要的东西。cplusplus.com 和 cppreference.com 是开始的好地方。
防错法
You add or remove elements in or from a loop, and get a crash. When you do anything to change the contents of your container, it may invalidate iterators already in the container. Consider this code (assume
digits
is avector
):for (auto i = digits.begin(); i != digits.end(); ++i) if (isEven(*i)) digits.erase (i); // erase element referenced by i // (different "erase" -- a member of // vector)
在你删除了
i
的内容后,i
指向了一个不存在的元素。循环增加了i
,并到达了另一个不存在的元素,结果是不可预知的。这种
erase
使哪些迭代器失效,如何修复取决于容器类型。您可以研究并为您选择的任何容器编写一个修复程序,或者使用内置的erase (myContainer, predicate)
函数。我知道我会推荐什么。
Exercises
在字符串“SoxEr776asdCsdfR1234qqE..T12Ci-98jOapqwe0DweE“有一个秘密代码。使用一个
<algorithm>
函数只提取大写字母并读取代码。(使用文件 I/O)首先,创建一个字符串文件。然后复印两份。一种方法是,改变一些字符串的顺序和大小写。另一方面,更换一些琴弦。
Now write a program that can read two files into vectors or lists and, using STL functions, tell if the files are different, ignoring order and capitalization. Use an STL function not covered in this section (so you’ll need an Internet search) that changes a string to all caps or all lowercase. To find what’s in one sequence but not the other, consider this algorithm:
for each element in sequence 1 (using iterators) use the "find" function to see if it's also in sequence 2
做前面的问题,但是不使用
find
和一个循环,而是使用set_difference
(之前也没有提到)。
二十四、构建更大的项目
有一天你可能想要建立一个更大的项目。本章介绍了一些有用的工具:名称空间、条件编译和库的构造。
命名空间
假设我为地理信息编写了一个库,用于地图、划分选区或其他用途。我创建了一些类:map
s,vector
s(用于图形的 XY 对),region
s,等等。
然后我注意到我不能编译了,因为map
和vector
在 C++ 中已经有意义了。好的。称它们为GeoLib_map
、GeoLib_vector
等等,就像 SDL 和 SSDL 函数一样。
我使用的是第三方库,它碰巧将region
定义为别的东西……这变得很乏味。有捷径吗?
当然可以。制作一个命名空间 GeoLib
,把你的代码放进去,如图 24-1 。
图 24-1
一个命名空间可以包含来自不同文件的代码
程序员现在可以输入GeoLib
::
map
或std::map
,编译器会知道它们的意思。
如果他们厌倦了反复输入GeoLib::
,他们可以使用using
:
using GeoLib::region;
// after this you can omit the GeoLib:: in GeoLib::region
using namespace GeoLib;
// now *all* GeoLib members can have GeoLib:: omitted
using namespace std;
// now all std:: members can have std:: omitted too
// If the compiler gripes, you can still use GeoLib::
// or std:: to clarify which you want
您还可以用普通的::
来指定没有在的任何名称空间中声明的内容(因此在“全局”名称空间中),比如:::myNonNamespaceFunction();
。
为了说明名称空间的构造,示例 24-3 和 24-4 示出了名称空间Cards
的创建;示例 24-5 使用。
这是一个关于using namespace <whatever>;
是否邪恶的争论问题,也就是说,不可原谅的可怕。我说你可以在你自己的.cpp
文件中随心所欲地使用它,但不要把它放在别人可能包括的.h
文件中,从而搞乱别人的文件。
条件编译
现在我正在使用我的GeoLib
代码,我发现我的计算是错误的,错误的,错误的。很难说哪些功能搞砸了。我想生成这些计算的报告,以便我可以检查它们:
map::area(region) thinks area of block group 6709 is 672.4
dist to center is 356.2
map::area(region) thinks area of block group 6904 is 312.5
dist to center is 379.7
...
我不希望这段代码一直被打印出来——只是在调试的时候。
所以我在一个.h
文件中创建了一个#define
,其他所有文件都包含在其中(例如 24-1 )。
// debugSetup.h
#ifndef DEBUGSETUP_H
#define DEBUGSETUP_H
#define DEBUG_GEOLIB // Yes, that's the whole thing
#endif //DEBUGSETUP_H
Example 24-1A .h file containing #define DEBUG_GEOLIB, for conditional compilation
我在任何需要打印调试信息的地方都使用它
#ifdef DEBUG_GEOLIB
cout << " map::area(region) thinks area of block group "
<< bg->id() << " is " << bg->area() << endl;
cout << "dist to center is "
<< distance (region.loc(), bg->loc()) << endl;
#endif
并根据我是否想看到它来注释或取消注释# define DEBUG_GEOLIB
。
库
库有两种风格,静态和共享。静态库的代码在链接时直接进入可执行程序;共享库在另一个文件中,在运行时加载。所以据说静态库运行起来更快(我从未注意到有什么不同),而且你不必担心你的共享库被移到哪里了,因为它总是在可执行文件中。但是共享可以节省空间,因为许多程序可以使用相同的代码,而且更容易更新。
我倾向于分享。这在 Unix 中很常见,似乎有助于编译器版本之间的可移植性。
在这里,我将对两种编译器都尝试这两种方法。在我的例子中,我将使用第十九章中的纸牌游戏代码,以及一般有用的类(Card
、Deck
等)。)走进库。蒙大拿的比赛将使用这个库。
您可能会创建一些其他的库。参见本章末尾的“练习”,或者自己选择。
g++
证明这一点的代码在源代码中,ch24/g++
。库在子目录cardLibg++
下,测试程序在子目录montana
下。
编译库
要创建一个静态库,像往常一样编译目标文件
g++ -g -c deck.cpp
...
然后链接到
ar rcs libcards.a
deck.o card.o cardgroup.o
#ar for "archive"; rcs is needed program optionsShared libraries
共享库需要在内存中“可重定位”的目标文件,所以像这样编译它们:g++ -g -fPIC
-c deck.cpp #PIC: "position independent code." All righty then ...
在 Unix 中,共享库以.so
结尾,所以这样链接:g++ -shared
-o libcards.so deck.o card.o cardgroup.o
。
Windows 使用扩展名.dll
,所以对于 MinGW,键入这个:g++ -shared -o libcards.dll deck.o card.o cardgroup.o
。
链接库
g++ 需要知道在哪里可以找到包含文件,在哪里可以找到库文件,以及使用什么库。
我们用这些命令行选项告诉它:
-I
<name of directory>
查找包含文件;-L
<name of directory>
查找库文件;-l
<library>
表示我们想要链接的库。库名的首字母是lib
,扩展名是.a
、.so
或.dll
,去掉后,如下所示:
g++ -o montana -g montana.o io.o montana_main.o
\1
-I../cardLibg++ -L../cardLibg++ -lcards
#uses libcards.<something>
您可以拥有这些选项的任意多个副本。
运行使用动态库的程序
如果你使用了一个静态库,你可以像往常一样运行程序。
如果它是动态的,系统需要知道在哪里可以找到它。解决方案:
贿赂系统管理员将
.dll
或.so
文件放在系统路径中。如果多个程序使用它并且你的程序足够重要,这是有意义的。将
.dll
或.so
复制到包含可执行文件的文件夹中——这对于单个项目来说很好,但如果您有很多文件夹并因此有很多副本,就不太好了。设置环境变量,以便系统可以找到它。源代码文件夹里有这个的脚本(
runx
、runw
、gdbx
等)。,正如 SSDL 的情况一样)。内容是这样的:
export LD_LIBRARY_PATH=../cardLibg++ #Unix
PATH="../cardLibg++:$PATH" #Windows
文件
这些长命令重复输入会变得很乏味,所以它们被打包成本章源代码中的文件:用于在 Unix 或 MinGW 中构建库的 Makefiles,就像 SSDL 一样;另一个用于构建使用该库的程序(它适用于两种平台);以及在后者的文件夹中,用于运行程序的脚本(见上文)。
要创建自己的库,请编辑构建库的 Makefile 文件,选择要创建的库的类型,然后编辑所有文件的路径、可执行文件名称以及您喜欢的任何内容。
工具
演示这一点的代码在源代码中。ch24/VisualStudio
。cardLib*
(有不同版本;创建库,uses*
是使用这些库的项目。
静态库:简单的选择
若要在 Visual Studio 中生成静态库,请单击“创建新项目”并选择“静态库”。
当它创建项目时,它希望您使用“预编译头” 2 你可以:
- 通过在每个源文件的开头放置这一行来支持这一点:
#include "``pch.h
它必须在任何其他包含之前,否则你的代码将无法编译。
或者一步到位:
- 消除预编译头文件。在“项目属性”下,对于“所有配置/所有平台”(第一行),将配置属性“➤ C/C++ ➤预编译头➤预编译头”设置为“不使用预编译头”。您可以忽略编译器在项目中提供给您的文件。
然后建库。我这样做是为了调试和版本,x86,和 x64,所以我不需要想我做了什么。
现在创建一个使用该库的项目。它需要知道在哪里可以找到包含文件。在所有配置/所有平台的项目属性下,适当地设置配置属性➤ C/C++ ➤通用➤附加包含目录(见图 24-2 )。
图 24-2
在 Visual Studio 中告诉项目在哪里可以找到库包含文件
将库的路径添加到配置属性➤链接器➤通用➤附加库目录(图 24-3 )。它的位置将因配置和平台而异;在名为Debug
、Release
和x64
的子文件夹中搜索。
图 24-3
告诉项目在 Visual Studio 中何处可以找到您的库
现在你必须告诉它什么是库。在项目属性下,所有配置/所有平台,在配置属性下添加库名➤链接器➤输入➤附加依赖项(图 24-4 )。会是<your library project>.lib
。
图 24-4
在 Visual Studio 中添加库依赖项
动态链接库(dll):不那么容易的选择
要创建自己的 DLL,请回到上一节,但对于项目类型,请选择动态链接库(DLL)。但是先不要建!
在指导新程序使用你的库的时候,当你告诉项目属性关于这个库的时候(图 24-4 ,它仍然是<your project>.lib
。我以为我们在创建一个 DLL?是的,但我们实际上是在创建两件事:包含运行时代码的.dll
,以及在编译时告诉程序“你稍后将从 DLL 中导入这些东西”的.lib
文件
这就是奇怪的地方。当编译器看到一个函数声明时,它需要知道是要编译和导出它(因为它正在编译库)还是从 DLL 导入它(因为它正在编译一个使用库的程序)。换句话说,就是在声明前面加上__declspec
(
dllexport
)
或__declspec(
dllimport
)
。__declspec
的意思是“我马上要告诉你一些关于这个功能的事情”和dllexport
/ dllimport
,嗯,那是显而易见的。
那么我们是不是应该为每个函数写两个版本,一个用于导入,一个用于导出?
这种常见的黑客手段意味着我们不必。
- 像示例 24-2 那样写一个
.h
文件。
// Header to make DLL functions import or export
// -- from _C++20 for Lazy Programmers_
#ifndef CARDSSETUP_H
#define CARDSSETUP_H
# ifdef IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
# define DECLSPEC __declspec(dllexport)
# else
# define DECLSPEC __declspec(dllimport)
# endif //IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
#endif //CARDSSETUP_H
Example 24-2A .h file to help with DLL projects
现在DECLSPEC
意味着“这将被导出”或“这将被导入”……这取决于我们是在编译库还是在使用它。刚刚好。
- 在库中的每个
.cpp
文件中,写入这个#define
:
#define IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
这就是它如何知道DECLSPEC
应该是导出版本。
这必须在与您的项目相关的任何.h
文件之前完成,这样他们就可以看到它,但是如果我们使用的是#include "
pch.h
"
之后,因为它总是在前面。
将
DECLSPEC
放在从.cpp
文件导出的所有内容之前。…以及在
.h
文件中相应的函数声明之前。它们必须匹配。根据需要,包括步骤 1 中的
.h
文件,以定义整个过程中的DECLSPEC
。我把它放在cards.h
。
库文件将看起来像示例 24-3 和 24-4 。示例 24-5 展示了如何在montana.h
中使用Cards
成员;在montana.cpp
中,我只是说了using
namespace
Cards;
,没有做其他改动。
// class Montana, for a game of Montana solitaire
// -- from _C++20 for Lazy Programmers_
#include "gridLoc.h"
#include "cell.h"
#include "deck.h"
#ifndef MONTANA_H
#define MONTANA_H
class Montana
{
public:
static constexpr int ROWS = 4, CLMS = 13;
static constexpr int NUM_EMPTY_CELLS = 4;// 4 empty cells in grid
...
private:
...
// dealing and redealing
void deal (Cards::Deck& deck, Cards::Waste& waste);
void cleanup (Cards::Deck& deck, Cards::Waste& waste);
...
// data members
Cards::Cell grid_ [ROWS][CLMS]; // where the cards are
GridLoc emptyCells_ [NUM_EMPTY_CELLS];// where the empty cells are
};
#endif //MONTANA_H
Example 24-5Parts of montana.h, showing use of namespace Cards
// Card class
// -- from _C++20 for Lazy Programmers_
#include "pch.h
"
#define IM_COMPILING_MY_CARD_LIBRARY_RIGHT_NOW
// see cardsSetup.h. Must come before card
// related includes, after "pch.h" if any
#include "card.h"
using namespace std;
namespace
Cards
{
DECLSPEC void Card::read (std::istream &in )
{
try { in >> rank_ >> suit_; }
catch (BadRankException&) // if reading rank_ throw an exception
{
in >> suit_; // consume the suit as well
throw; // and continue throwing the exception
}
}
DECLSPEC istream& operator>> (istream& in, Suit& s)
{
...
}
...
} //namespace Cards
Example 24-4Part of card.cpp, set up to make a DLL and forming a namespace
// Card class
// -- from _C++20 for Lazy Programmers__
#ifndef CARD_H
#define CARD_H
#include <iostream>
#include "cardsSetup.h" // defines DECLSPEC
namespace
Cards
{
// Rank and Suit: integral parts of Card
// I make these global so that I don't have to forget
// "Card::" over and over when I use them.
enum class Rank { ACE=1, JACK=11, QUEEN, KING }; // Card rank
enum class Suit { HEARTS, DIAMONDS, CLUBS, SPADES }; // Card suit
enum class Color { BLACK, RED }; // Card color
inline
Color toColor(Suit s) // DECLSPEC isn't needed for inlines
{
if (s == HEARTS || s == DIAMONDS) return RED; else return BLACK;
}
// I/O on Rank and Suit
DECLSPEC std::ostream& operator<< (std::ostream& out, Rank r);
DECLSPEC std::ostream& operator<< (std::ostream& out, Suit s);
DECLSPEC std::istream& operator>> (std::istream& in, Rank& r);
DECLSPEC std::istream& operator>> (std::istream& in, Suit& s);
...
class Card
{
public:
Card (Rank r = Rank(0), Suit s = Suit(0)) :
rank_ (r), suit_ (s)
{
}
Card (const Card& other) : Card(other.rank_, other.suit_){}
...
DECLSPEC void read (std::istream &in );
private:
Suit suit_;
Rank rank_;
};
...
} //namespace Cards
#endif //CARD_H
Example 24-3Parts of card.h, set up to make a DLL, and forming a namespace
如果一切顺利,在使用你的库的程序中还有一件事需要设置:它需要在运行时找到 DLL。
最简单的方法是将 DLL 复制到项目文件夹中。或者将其放在系统路径中(这可能需要管理员访问)。
如果这不是你想要的,转到项目属性(图 24-5 ,并设置配置属性➤调试➤环境。它需要对 PATH 变量进行更新,不要忘记旧路径……如果 DLL 的位置是..\cardLibDLL\Debug
,可以给它PATH=..\cardLibDLL\Debug\;%PATH%
。
图 24-5
在 Visual Studio 中设置路径
防错法
您对项目属性进行了更改,但这些更改没有影响。很容易忽略项目属性窗口的第一行(图 24-5 )。有时你纠正了一个配置,但你用的是另一个。我更喜欢尽可能编辑所有配置/所有平台。
编译器抱怨像
cout
**这样常见的东西不存在。**将#include "
pch.h
"
放在之前,其他包含或停止使用预编译头;请参见本节的开头。在运行时,程序找不到 DLL 。但是你设置了路径变量。
如果不是路径中的错别字,可能是你把
.user
文件抹掉了。这是包含环境信息的内容。重建它应该能解决问题。**运行时,程序不能启动;错误信息不清楚。**也许你的程序平台(Win32 或 x86 对 x64)与 DLL 不匹配。
Exercises
将
PriorityQueue
、List
、Time
、Fraction
或您在之前练习中创建的其他类放入它自己的库中,并将它链接到使用它的某个对象。创建一个名为
shapes
(在名称空间Shapes
中)的库,使用第二十一章中的Shape
层级,随意扩展;并将它链接到一个使用Shape
的程序中。在basicSSDLProject
的 vcxproj。
二十五、秘籍(推荐)
您现在已经掌握了 C++ 的基本知识以及更多。是时候添加铃铛和口哨了。 1
在做这一章和接下来的两章之前,回过头来做你可能跳过的“可选”部分是有意义的。我们尤其需要文件 I/O(来自第十三章)和移动语义(第 18 和 21 章)。
这些章节是这样组织的:
第二十五章:新功能——更好的输出格式,使用命令行参数(最后使用
int main (int argc, char** argv)
中的argc
和argv
),以及位操作第二十六章:对组织和安全有用的东西
第二十七章:更多的组织帮助,在不太常见的情况下有用
我们开始吧。
sstream
:像cin
/ cout
一样使用strings
假设您想要将游戏的详细信息打印到您的平视显示器上,居中显示在顶部:
Points: 32000 / Time left: 30.2 / Mood: Annoyed
您可以计算每个标签和每个值的宽度(使用可变宽度的字体,祝您好运),并从中计算打印每个项目的位置……如果您这样做了,现在就交出您的懒惰程序员徽章。
或者您可以进行大量的转换和字符串连接,并将其发送给SSDL_RenderTextCentered
,如下所示。再一次,交出徽章。
string finalString = string ("Points: ")
+ to_string (points) // to_string is in #include <string>
+ "/ Time left: "
+ to_string (time)
+ " / Mood: " + mood;
SSDL_RenderTextCentered(finalString, SSDL_GetWindowWidth()/2, 10);
如果我们发送其他类型的变量,比如Point2D
,我们将需要更多的字符串转换函数。那是许多工作。
输入stringstream
。这就像是把cin
、cout
和string
合二为一。您可以向它写入数据,然后提取产生的字符串,或者将一个字符串放入其中并从中读取。
要使用<<
构建字符串,请执行以下操作:
#include <``sstream
声明一个
stringstream
。使用
<<
打印到stringstream
。使用
str()
成员函数访问您构建的字符串。
如果你想再次使用它,你可以将它的内容重置为""
,就像在myStringStream.str("")
中一样。 2
示例 25-1 使用stringstream
向SSDL_RenderTextCentered
发送文本。输出如图 25-1 所示。
图 25-1
stringstream
程序的输出
表 25-1
常用stringstream
功能,简化
// Program that uses stringstream to center multiple things on one line
// -- from _C++20 for Lazy Programmers_
#include <sstream> // Step #1: #include <sstream>
#include "SSDL.h"
using namespace std;
int main(int argc, char** argv)
{
int points = 3200; // Some arbitrary data to test
double time = 30.2; // printing to HUD
char mood = "Annoyed";
stringstream out; // #2: Declare a stringstream
// #3: Print to stringstream
out << "Points: " << points << " / Time left: "
<< time << " / Mood: " << mood;
string result = out.str(); // #4: Access with str()
SSDL_RenderTextCentered(result.c_str(),
SSDL_GetWindowWidth()/2, 10);
SSDL_WaitKey(); // Wait for user to hit a key
return 0;
}
Example 25-1A rudimentary heads-up display (HUD) using stringstream
您也可以使用stringstream
作为输入源——使用str
设置字符串,然后使用>>
从中提取:
dataLine.str ("Flourine 0.52 0.63");
dataLine >> elementName >> firstNumber >> secondNumber;
使用stringstream
输入包括以下步骤:
#include <``sstream
声明一个
stringstream
。用
str()
成员函数初始化stringstream
。使用
>>
从stringstream
中读取。
如果您有可能耗尽输入,您可以调用clear()
,就像在dataLine.clear()
中一样,在重用之前清除错误条件。
Exercises
假设我们有一组文件:
file1.txt, file2.txt…file100.txt
。使用stringstream
为第 x 个文件构建文件名。编写并测试一个函数模板,该模板接受一个变量,将其打印成一个字符串,然后返回该字符串。
用包含单词和数字的文本初始化一个字符数组,然后使用
stringstream
将它的各个部分适当地读入变量。读入一个数字文件,忽略注释标记
#
之后的所有内容。方法如下:排成一行阅读;在你找到任何#
后丢弃任何东西;然后从剩下的数字中读入所有数字,并将它们推入一个向量中。
带有格式字符串的格式化输出
假设我想按列打印表格。我如何把事情安排好?自己数空格?不可能!简单的方法是将它们放入一个格式的字符串:
cout << format("{0:10} -- {1:10}.\n", "Column 1", "Column 2");
cout << format("{0:10} -- {1:10}.\n", "col1 data", "col2 data");
...
第一行说取第 0 个参数("Column 1"
)和第 1 个参数("Column 2"
),把它们放在字符串中,中间有--
,然后把结果发送给cout
。每个参数占位符中的:10
表示其参数至少要占用十个字符;format
会用空格填充每一个来实现这一点。如果我们打印的内容超过十个字符,它会根据需要占用尽可能多的空间。
由于第二行以同样的方式格式化它的参数,代码产生了均匀排列的列(见图 25-2 (a))。
图 25-2
format
命令的输出。(a)两个十字符栏;(b)两列以定点格式显示浮点数,精确到两位数;(c)三列,左对齐、中对齐和右对齐
我通常省略参数的编号;它将按顺序执行这些操作:
cout << format("{:10} -- {:10}.\n", "Column 1", "Column 2");
如果要打印的东西是浮点数,我可能会添加一个f
来指定我想要定点,而不是科学记数法(默认是“让format
决定”)。我也可以指定小数点右边显示多少位数。在这个片段中
cout << format("{:6.2f}{:8.2f}", 21.0, 22.5) << "\n";
我希望第一个数字占用六个空格,精度两位数;第二个占用八个空格,也是两位数的精度。输出如图 25-2 (b)所示。
默认情况下,字符串在它们的列中是左对齐的,如图 25-2 (a)所示,但是数字默认情况下是右对齐的,如(b)所示。我可以通过要求左(<
)、中(^
)或右(>
)对齐来覆盖默认值。所以cout << format("{:<7} {:⁷} {:>7}\n", "left", "center", "right");
会打印出你在图 25-2 (c)中看到的内容。
示例 25-2 使用这些工具打印几个熟悉星球的气候条件。
// Program to print temp, pressure for some familiar planets
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <format>
3
using namespace std;
int main()
{
// planetary temperature and pressure
constexpr double VENUS_TEMP = 464; // celsius
constexpr double EARTH_TEMP = 15;
constexpr double MARS_TEMP = -62;
constexpr double VENUS_PRESSURE = 92000; // millibars
constexpr double EARTH_PRESSURE = 1000;
constexpr double MARS_PRESSURE = 1;
// Print a 3-column table
// Left column is 7 chars wide;
// char* is left-justified by default
// Other columns are 11 chars wide, right-justified
// Headers
cout << format("{:7} {:>11} {:>11}\n",
"Planet", "Temperature", "Pressure");
cout << format("{:7} {:>11} {:>11}\n\n",
"", "(celsius)", "(millibars)");
// Data
cout << format("{:7} {:11.1f} {:11.0}\n",
"Venus", VENUS_TEMP, VENUS_PRESSURE);
cout << format("{:7} {:11.1f} {:11.0}\n",
"Earth", EARTH_TEMP, EARTH_PRESSURE);
cout << format("{:7} {:11.1f} {:11.0}\n",
"Mars", MARS_TEMP, MARS_PRESSURE);
cout << "\n...I think I'll just stay home.\n\n";
return 0;
}
Example 25-2Program to neatly print a table of astronomical data using format strings
以下是示例 25-2 的输出:
Planet Temperature Pressure
(celsius) (millibars)
Venus 464.0 90000
Earth 15.0 1000
Mars -62.0 1
I think I'll just stay home.
示例 25-3 展示了如何用format
做一些更酷的事情。想打印不同格式不同基数或浮点的int
s?见下文。想打印到一个string
,就像上一节一样?如图所示使用format_to
。
// Program to illustrate further capabilities of C++20's format strings
// -- from _C++ for Lazy Programmers_
#include <iostream>
#include <string>
#include <format>
using namespace std;
int main()
{
// Print the same integer using base 2, 8, 16, and 10
cout << "Here's 15 written in...\n";
cout << format("{0:>8}{1:>8}{2:>8}{3:>8}\n",
"binary", "octal", "hex
", "decimal");
//(if you don't specify, you get decimal)
cout << format("{0:>8b}{0:>8o}{0:>8x}{0:>8}\n\n", 15);
// We used argument #0 four times; no law against that...
// Print the same item with different types of padding
cout << "Here's 15 padded with x's, .'s, and *'s: ";
cout << format
("{0:x>4} {0:.>4} {0:*>4}\n\n", 15);
// Print a floating point number with different formats
cout << "And here's 0.01234 in scientific, fixed, general, ";
cout << "and default format,\n";
cout << "showing how they interpret a precision of 2.\n";
cout << format("{0:>10.2e} {0:>10.2f} {0:>10.2g} {0:>10.2}\n\n",
0.01234);
// You can also print to a string with format_to:
string str;
format_to
(back_inserter(str), "The language of choice is {}.\n",
"C++");
cout << str;
return 0;
}
Example 25-3More format string tricks
输出是
Here's 15 written in...
binary octal hex decimal
1111 17 F 15
Here's 15 padded with x's, .'s, and *'s: xx15 ..15 **15
And here's 0.01234 in scientific, fixed, general, and default format,
showing how they interpret a precision of 2.
1.23e-02 0.01 0.012 0.012
The language of choice is C++.
一个论点的规范必须按照特定的顺序来完成。除了开头和结尾,可以跳过任何或所有内容。顺序是
打开{
参数编号
😦如果有任何格式规范,则需要)
对准:
<
、^
或>
。宽度
精度:小数点后跟随所需精度的位数
浮点类型:
f
、g
或e
;整数的b
、o
、x
或d
(二进制、八进制、十六进制、十进制)关闭}
这是我常用的,但还有更多。参见cppreference.com
了解format
能做的所有事情的完整列表。
Online Extra: I/O Manipulators
关于这在 C++20 之前是如何完成的细节,如果你在处理遗留代码的话很有用, 4 在 github.com/apress/cpp20-for-lazy-programmers
。
防错法
格式字符串的错误通常在运行时被检测到,因为直到它尝试,C++ 才知道字符串-参数组合是否有效。你会得到一大堆无法理解的错误信息。只要回到调用format
的地方,使用调试器中的调用栈,看看哪个format
有问题。
Exercises
使用
format
打印某人可能填写的表格——可能是一份申请(填写姓名、地址等)。),可能是更有趣的东西。你决定。打印一些二进制、八进制、十六进制、十进制的加法题,像这样:
1111 17 f 15 + 1 + 1 + 1 + 1 ----- ----- ----- ----- 10000 20 10 16
打印一个表格,显示你每年摔倒的次数,从 2 岁开始,到你现在的年龄结束。我希望它有所下降。弄干净点。
打印一份冲浪用品店各种物品的价格表:冲浪板、冲浪板包、滑冰鞋或其他任何东西。美元金额中的
.
应该对齐。打印练习 4 中物品重量的表格。
使用科学记数法,打印出在任何给定的一年中,这些事件发生的概率:有一个重大的政治丑闻;生命在火星上自发形成;一颗彗星撞击了尤卡坦半岛,让我们重蹈恐龙的覆辙;比用 C++ 编程更有趣的事情发生了(当然是最低概率)。使用科学符号。编造数字——其他人都是这么做的。
命令行参数
有时有必要,尤其是在 Unix 世界中,给程序提供参数:cd myFolder
、gdb myProgram
等等。
假设你想让一个程序检查文本文件的差异。该命令可能类似于
./mydiff file1.txt file2.txt #in Windows, leave off the ./
你需要这样写main
的第一行:
int main (int argc
, char** argv)5
argc
(“参数计数”)是参数的个数,argv
(“参数向量”,但它是一个数组,而不是vector
)是一个字符数组的数组,每个数组包含一个参数,以程序名开始。
所以如果你的命令是./mydiff file1.txt file2.txt
,argc
将是 3,argv
将包含如图 25-3 所示的值。
图 25-3
可能的命令行参数
示例 25-4 显示了程序的代码。
首先,它确保我们有正确数量的参数。如果出了问题,通常会告诉用户预期会发生什么。argv[0]
始终是程序名。(我们不硬编码为“./mydiff
”,以防程序名改变。)
cerr
类似于cout
,但是没有被>
重定向,所以它对错误消息很有用。但是cout
也可以。
// Program to find the difference between two files
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <fstream>
#include <cstdlib> // for EXIT_FAILURE, EXIT_SUCCESS
#include <string>
using namespace std;
int main (int argc, char** argv)
{
// Did we get right # of arguments? If not, complain and quit
if (argc != 3) // 3 args: 1 program name, plus 2 files
{ // On failure, tell user what user should've entered:
cerr << "Usage: " << argv[0] << " <file 1> <file 2>\n";
return EXIT_FAILURE
;
}
// Load the 2 files
ifstream file1(argv[1]), file2(argv[2]); // open files
if (! file1) // On failure, say which file wouldn't load
{
cerr << "Error loading " << argv[1] << endl;
return EXIT_FAILURE;
}
if (!file2) // On failure, say which file wouldn't load
{
cerr << "Error loading " << argv[2] << endl;
return EXIT_FAILURE;
}
string line1, line2;
while (file1 && file2) // While BOTH files are not // finished
{
getline (file1, line1); // read line from file1
if (!file1) break;
getline (file2, line2); // ...from file2
if (!file2) // if file2's done but file 1 // wasn't
{
cout << "<: " << line1 << endl; // spit out last line read from// file 1
break;
}
if (line1 != line2) // if lines differ print them
{
cout << "<: " << line1 << endl; // < means "first file"
cout << ">: " << line2 << endl; // > means "second file"
// this is conventional
}
}
// If either file has more lines than the other, print remainder
while (file1)
{
getline(file1, line1);
if (file1) cout << "<: " << line1 << endl;
}
while (file2)
{
getline(file2, line2);
if (file2) cout << ">: " << line2 << endl;
}
// Clean up and return
file1.close(); file2.close();
return EXIT_SUCCESS;
}
Example 25-4A program using command-line arguments. In source code, the executable for g++ is named mydiff. Instructions on how to run and debug it follow the example
要从命令行运行它
在 Unix 中,键入./mydiff file1.txt file2.txt
。
对于 MinGW,键入mydiff file1.txt file2.txt
。
Visual Studio 用户应该首先将可执行文件从Debug/
、Release/
或x64/
中的某个地方复制到项目文件夹中,这样它就可以找到文本文件。它的名字是4-cmdLineArgs.exe
,所以打4-cmdLineArgs file1.txt file2.txt
。或者将其重命名为mydiff
并使用该命令。
有关在 Windows 中使用命令提示符的提示,请参阅第十三章的“在 Windows 中设置命令提示符”小节
在 Unix 中使用命令行参数进行调试
无论是在ddd
还是gdb
,在提示符下,输入set args file1.txt file2.txt
,你就准备好run
了。
在 Visual Studio 中使用命令行参数进行调试
如果在 Visual Studio 中启动程序,它会像没有参数一样运行,但实际上它没有。要解决这个问题,进入项目菜单➤属性➤配置属性➤调试,设置配置为所有配置和平台为所有平台,并添加您的参数到命令参数(图 25-4 )。
图 25-4
在 Microsoft Visual Studio 中设置命令参数
项目的命令行参数存储在.user
文件中。如果您删除它,您将不得不再次添加它们。
Exercises
写一个程序
myGrep
,一个 Unixgrep
实用程序的简化版本。它应该重复标准输入中包含给定单词的所有行。所以如果你有一个带线条的文件input.txt
alpha
beta
alphabet
然后命令
myGrep
(或./myGrep
)alpha < input.txt
应该在屏幕上打印出来:alpha
alphabet
在前面的练习中,添加一个选项
-n
,如果存在,它会指示grep
打印每行输出的行号。(以-
开头的选项在 Unix 命令中很常见。)给定练习 1 的输入,命令myGrep -n alpha < input.txt
应该打印出来1: alpha
3: alphabet
编写一个程序,在给定输入和列中的数字的情况下,将只包含指定列的版本打印到标准输出。例如,如果你给它参数
0 3
并得到这样的输入1900 -0.06 -0.05 -0.05 -0.08 -0.07 -0.07
1901 -0.07 -0.21 -0.14 -0.06 -0.2 -0.13
...
输出应该只显示第 0 和第 3 列:
1900 -0.05
1901 -0.14
...
我推荐用
stringstream
,format
也没坏处。
位操作:&
、|
、~
和<</>>
许多库,比如 SDL 和它的助手,要求你用单独的位 6 设置它们的一些特性,并以同样的方式报告特性。嵌入式系统、加密和文件压缩也受益于对单个位的操作。
要启动 SDL 图像库,调用IMG_Init
,它接受一个类型为int
的参数,告诉它支持什么图像格式。我们如何把它塞进一个int
?SDL_Image.h
提供标志(具有指定含义的位):IMG_INIT_JPG
为 1,IMG_INIT_PNG
为 2,IMG_INIT_TIF
为 4,以此类推。我们必须一点一点地将它们组合成一个int
,没有双关的意思(见图 25-5 )。
图 25-5
发送到IMG_Init
的int
是如何布局的:从右向左读,我们有位 1、位 2、位 4 等等。这个设置为支持 jpg 和 tiffs(位 1 和位 4)
有一些运算符——“按位”运算符——可以帮助我们处理位:
图 25-6
逐位算术
- 按位或,如
x|
y
:如果x
y
中的任一位为 1,x|y
中的每一位都为 1(图 25-6 (a))。
** 按位与,如x
&
y
:如果x
y
中的一位都为 1,那么x&y
中的一位将为 1(图 25-6 (b))。
** 按位非,如在~
x
中:所有的位都反转(图 25-6 (c))。
* 我们可以使用移位操作符`<<`和`>>`向左或向右移动位(现在为流 I/O 和位操作执行双重任务)。`x << 2`是否将`x`中的所有位左移两位(图 25-6 (d))。*
*(还有 xor,x^y
,这里不介绍,因为它不常用。)
现在我们可以看到如何构造发送给IMG_Init
的int
。int flags = IMG_INIT_JPG |
赋予我们IMG_INIT_TIF
IMG_INIT_JPG
: 000000001
IMG_INIT_TIF
: 000000100
flags
: 000000101
我们这样传递它:IMG_Init (flags);
。
为了更好地发挥这一点,我举了一个超级简化烤箱的例子(例子 25-5 , 25-6 )。你可以将它设置为烘焙和/或烧烤(每个 1 比特)。我想更好地控制上面的两个燃烧器,所以我让最右边的两位控制右边的燃烧器,接下来左边的两位控制左边的燃烧器。它们可以取值为 00 表示关,01 表示低,10 表示中,11 表示高。
我还有一个“火”的条件:如果两个燃烧器都在高温,烤箱设置为烘烤和烤。
// Program that controls a Super-Simple Demo Oven (SSDO) with flags
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cassert>
class SSDO // A Super-Simple Demo Oven
{
public:
static constexpr int
RIGHT = 0b00000011, // This is how to write in binary in C++:
LEFT = 0b00001100, // precede with 0b or 0B
BAKE = 0b00010000,
BROIL = 0b00100000;7
// Leftmost two bits are unused
static constexpr int
OFF = 0b00,
LO = 0b01,
MD = 0b10,
HI = 0b11;
// Right burner bits are at the right -- no offset
static constexpr int RIGHT_BURNER_OFFSET = 0;
// But the left burners' are offset two to the left
static constexpr int LEFT_BURNER_OFFSET = 2;
static constexpr int FIRE = 0b00111111;
Example 25-5Program that uses bit manipulation to set and use flags, part 1
为了打开烘焙或烧烤,我对一个成员变量flags_
使用了按位“或”|
(见图 25-7 (a)): flags_ = flags_ | BAKE;
。
图 25-7
在超级简单的烤箱中打开和关闭BAKE
钻头
要关闭它,我需要保持所有其他位不变,但将其设置为 0。这样做:得到一个所有位都是 1 的数,除了BAKE
;“和”与flags_
(图 25-7 (b)):
flags_ = flags_ & ~BAKE;
要设置燃烧器,我必须将该燃烧器的钻头更换为所需的Condition
–OFF
、LO
、MD
或HI
–移动到燃烧器的正确位置。为了将左燃烧器设置在HI
,比方说,我使用~
和&
清除左燃烧器位,就像我对BAKE
所做的那样(图 25-8 (a))。然后我取HI
,0b11
,用<<
左移两位得到0b1100
。我用按位或把它们放在一起。
图 25-8
打开LEFT
燃烧器;检查FIRE
状态
我通过说(flags() & FIRE) == FIRE
来确定烤箱是否着火——也就是说,是否所有的东西都开着,燃烧器是否在高处。如果我只说flags() == FIRE
,可能不行,因为我不知道最左边那两个没用的位是什么(图 25-8 (b))。防御性编程。
// We're still in class SSDO's public section...
// ctors and =
SSDO () { flags_ = '\0'; }
SSDO (const SSDO&) = delete;
SSDO& operator= (const SSDO&) = delete;
// the controls
void setBake() { flags_ |= BAKE; }
void clearBake() { flags_ &= ~BAKE; }
void setBroil() { flags_ |= BROIL; }
void clearBroil() { flags_ &= ~BROIL; }
void setLeftBurner (unsigned char c)
{
flags_ &= ~LEFT;
flags_ |= (c<< LEFT_BURNER_OFFSET);
}
void setRightBurner (unsigned char c)
{
flags_ &= ~RIGHT;
flags_ |= (c << RIGHT_BURNER_OFFSET);
}
void clearLeftBurner () { setLeftBurner (OFF);}
void clearRightBurner() { setRightBurner(OFF);}
// access functions
unsigned char flags () const { return flags_; }
bool isSelfCleaning () const // bake and broil -> self-cleaning mode
{
return (flags() & BAKE) && (flags() & BROIL);
}
bool isFireHazard () const // they're all on, high!
{
return (flags() & FIRE) == FIRE;
}
private:
unsigned char flags_; // unsigned char has at least 8 bits --
// that's plenty for us here
};
using namespace std;
int main ()
{
SSDO myOven;
// Turning the oven completely on; now it's in self-cleaning mode
myOven.setBake ();
myOven.setBroil();
assert(myOven.isSelfCleaning());
assert(myOven.flags() == 0b00110000);
// Playing with the right burner, checking the result...
myOven.setRightBurner (SSDO::LO);
myOven.clearRightBurner ();
assert ((myOven.flags() & SSDO::RIGHT) == 0);
// I probably shouldn't do this...
myOven.setRightBurner (SSDO::HI);
myOven.setLeftBurner (SSDO::HI);
if (myOven.isFireHazard())
cout << "Cut the power and call the fire department!\n";
return 0;
}
Example 25-6Program that uses bit manipulation to set and use flags, part 2
输出为Cut the power and call the fire department!
。所有断言都成功。
在最后的assert
,我需要在==
之前的()
。如果没有它们,它会将表达式解析为myOven.flags() & (SSDO::RIGHT == 0)
。这既奇怪又没用。
现在我们已经讨论了位操作,我们应该能够设置标志,将信息发送给使用它们的库,并获得这样的信息,或者在我们自己的库中使用它们。
如果 C++20 兼容,你的编译器还提供了一个头文件<bit>
,带有rotr
(“向右旋转”)——像>>
,除了最右边的位被复制到最左边:
unsigned char c = 0b00000001; //1 is in the rightmost bit
c = std::rotr (c, 1); //now it's in the leftmost
再加上rotl
(“向左旋转”)和其他我没怎么用的功能。在写作的时候,你可以在 en.cppreference.com/w/cpp/numeric
找到一个列表。
防错法
- 你得到了一个比特操纵表达式的错误答案,但是你没有看到如何。也许你用
&&
代表&
或者用||
代表|
。我做那件事。或者也许你需要一些()
。
Exercises
写一个函数,通过检查一个数的某一位来确定这个数是奇数还是偶数。
编写一个函数,通过打印每个单独的位来打印一个二进制数。你可能想要
sizeof
。写一个函数,找到一个
int
的日志 2 ,用>>
,不用/
。写一个函数来判断一个数中的位序列是否对称(像 11000011 但不是 11010011)。
二十六、秘籍(推荐)·续
更多的附加功能使你的程序更安全、更快捷、更容易编写。
默认的构造器和=
早先我避免使用构造器和 operator=的默认值,因为有时 C++ 的猜测是非常错误的;具体来说,它复制数组地址,而不是数组内容。
但有时候完全正确。默认值可以节省我们写第十九章的Card
类的时间(见例子 26-1 )。
class Card
{
public:
Card () : rank_(Rank(0)), suit_(Suit(0)) {}
Card (const Card& other) = default;
Card& operator= (const Card& other) = default;
...
private:
Rank rank_; Suit suit_;
};
Example 26-1Class Card, its constructors slightly altered from Chapter 19 for simplicity, and with defaulted constructor and = added
如果我们要使用 C++ 的默认值,我们最好知道它们是做什么的!是这样的:
在默认(无参数)构造器中,调用所有父类和所有部件的默认构造器。但是
Suit
和Rank
没有默认的构造器,所以默认的Card ()
不会做任何事情来初始化部件。我将让第一个构造器保持原样。在复制构造器中,复制父类和成员(对于类,复制构造器,对于基本类型,
=
)。In =,对父类和所有部分调用=。
=
的使用解释了为什么这不适用于数组,但对于许多其他事情却很好。
除了前面讨论的成员函数,您还可以默认或删除、移动操作符和析构函数。
constexpr
和static_assert
:将工作移至编译时间
C++ 大师们现在建议你尽可能多地使用constexpr
:这在运行时会更快(当然);占用内存少;它还防止了“未定义的行为”(不可预测的结果),因为带有未定义行为的东西不应该被编译。(如果这行得通,我们能对语言的其他部分也这样做吗?求你了。)
尽管名字如此,constexpr
实际上意味着“在编译时这样做”,尽管它也使事情保持不变。
我们也可以使用constexpr
函数,并在编译时使用它们来生成值,前提是这些函数的输入在编译时是已知的。
假设我们正在销售一种油漆,我们想要大量的颜色。每种颜色都有自己的 RGB 值以及可以通过这些值计算出来的东西,比如亮度(我将亮度定义为 R、G 和 B 的平均值)和补色(色轮上相反的颜色)。在编译时这样做可以加快运行时间,特别是如果我们有许多这样的函数和许多颜色。示例 26-2 显示了一个struct
及其相关的函数和变量。
// A struct Color and associated constants and functions
// -- from _C++20 for Lazy Programmers_
#ifndef COLOR_H
#define COLOR_H
#include "SSDL.h"
namespace Palette
{
struct Color
{
constexpr Color (double r = 0, double g = 0, double b = 0)
: red_(r), green_(g), blue_(b), brightness_((r+g+b)/3)
{
}
constexpr Color (const Color&) = default;
constexpr Color& operator= (const Color& c) = default;
constexpr Color complement() const
{
return Color (1.0 - red_, 1.0 - green_, 1.0 - blue_);
}
double red_, green_, blue_; // Each ranges 0.0-1.0\. More fine // shades than
// if we used ints 0-255
double brightness_;
};
// Function to convert a Color to an SSDL_Color
constexpr SSDL_Color color2SSDL_Color (const Color& c)
{
return SSDL_Color (int (c.red_ * 255),
int (c.green_ * 255),
int (c.blue_ * 255));
}
inline constexpr Color BLACK (0.0,0.0,0.0), RED (1.0,0.0,0.0),
GREEN (0.0,1.0,0.0), BLUE(0.0, 0.0, 1.0),
FUSCHIA (1.0,0.0,1.0);
inline constexpr
Color COLORS[] = {Color (0.80,0.53,0.60), // puce
Color (1.00,0.99,0.82), // cream
Color (0.94,0.92,0.84)};// eggshell
}
#endif //COLOR_H
Example 26-2struct Color, using defaulted ctors and constexpr out the wazoo
由于Color
的构造器是constexpr
,我可以把constexpr Color
s 做成类似BLACK
、RED
等等,或者把它们做成类似COLORS
的constexpr
数组。函数可以是构造器、其他成员、虚函数、非成员等等,只要这些函数在编译时足够简单。计算是可以的,文字值和其他constexpr
,但没有什么是直到运行时才知道的,也没有对函数的调用,无论是否内置,它们本身都不是constexpr
。所以在他们修好之前,没有sin
、sqrt
或strcpy
。 1
例 26-2 也有constexpr
功能color2SSDL_Color
。它调用SSDL_Color
的构造器。那能行吗?是的,因为SSDL_Color
的构造者也是constexpr
。
因为一个constexpr
函数必须能够在编译时完成它的工作,编译器必须知道,一旦它找到对它的调用,它是如何做的——就像内联函数一样。它需要放在.h
文件中,并且是隐式的inline
。
它是否真的在编译时工作取决于它必须处理什么。如果我们给它constexpr
s 和/或文字,它可以返回一个constexpr
值:
constexpr Color CREAM = COLORS[1];
constexpr
SSDL_Color SSDL_CREAM = color2SSDL_Color(CREAM); // done at compile time
但是如果我们给它一些运行时才存在的东西,比如这里的PUCE
,它就不能:
const Color PUCE = COLORS[0];
SSDL_Color SSDL_PUCE = color2SSDL_Color(PUCE);
// color2SSDL_Color is called at runtime because PUCE isn't constexpr
我们也有一个编译期版本的assert
用于constexpr
s。不像assert
,它不需要任何包含文件。
constexpr Color ONYX = CREAM.complement(); // CREAM's opposite
static_assert (ONYX. brightness_ < 0.10); // onyx isn't bright
static_assert (CREAM.brightness_ > 0.90, // but cream is
"Isn't cream supposed to be bright?");
在编译时完成它的工作。如果你愿意,你可以给它一些东西打印,如果它失败了,如图所示。有了 Visual Studio,你甚至不需要编译——在static_assert
上挥动你的鼠标指针,它会告诉你是否有问题。很好!
Extra
从我在网上看到的情况来看,有计划要在未来的标准中大大扩展编译时可以做的事情。这里有两个进一步推进编译时间的特性 edge,所以你的编译器可能还不喜欢它们。
constinit
Color favoriteColor = RED;
这不是一个常量,而是在编译时由某个常量初始化的。
为什么要这样做?假设在不同的.cpp
文件中有两个全局对象,其中一个依赖于另一个的初始值。如果它们以正确的顺序被初始化,那绝对是好运气,因为 C++ 没有指定哪个.cpp
文件的“静态”变量(全局变量和其他一些变量,包括static
类成员)首先被初始化。constinit
通过拒绝编译时未知的=
的任何权利来避免这种“静态初始化顺序的惨败”。
如果你想在一个函数中声明一个constinit
,在前面加上static
关键字。
还有“即时功能”:
consteval
int someFunction (...args...) {...}
这就像一个constexpr
函数,除了它不灵活:它必须在编译时执行。
Exercises
constexpr
能在Fraction
的都在,再算一些Fraction
(用+
、*
什么的),全部constexpr
。尽可能使用static_assert
来验证您的功能。constexpr
你能在Point2D
中的一切,并声明一些constexpr Point2D
在constexpr
表达式中使用。尽可能使用static_assert
来验证你的功能。
结构化绑定和tuple
s:一次返回多个值
在第七章,我可能给人的印象是一个函数只能返回一个东西。如果是的话…我撒谎了。
我们已经可以返回一个vector
或list
…但是这里有一种方法可以返回多个东西,而不会产生额外的开销。示例 26-3 展示了它的样子。
#包含<元组> **。**元组是值的序列,可能是不同的类型;这是我们要回报的。
让功能返回 自动。它实际上是返回
std::tuple
<firstType,secondType,...>
,但是为什么不让编译器来计算呢?用 Return STD::make _ tuple(value 1,value2,...);。
将返回值存储在“结构化绑定”中:
auto
[variable1, variable2, ...] = functionCall (...);
这将声明variable1
、variable2
等等,并从函数返回的内容中初始化它们。
// Program to calculate the quadratic formula
// using structured bindings and tuples
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cmath> // for sqrt
#include <tuple> // for tuple stuff // Step #1: #include <tuple>
#include <cassert>
using namespace std;
// If auto's going to work in main, we need the
// function body *above* main. Else there's no way main
// can know what type is to be returned
// #2: return auto
auto quadraticFormula (double a, double b, double c)
{
int numroots = 0;
double root1 = 0.0;
double root2 = 0.0;
double underTheRadical = b * b - 4 * a*c;
if (underTheRadical >= 0) // If we have to sqrt a neg #,
// no solution. Otherwise...
{
root1 = (-b + sqrt (underTheRadical)) / (2 * a);
root2 = (-b - sqrt (underTheRadical)) / (2 * a);
if (root1 == root2) numroots = 1; else numroots = 2;
}
// #3: return a tuple
return std::make_tuple (numroots, root1, root2);
}
int main ()
{
// Get user input
cout << "Enter the a, b, c from ax²+bx+c = 0: ";
double a, b, c; cin >> a >> b >> c;
// Get the results
// #4: store result in auto[...]
auto[howMany, r1, r2] = quadraticFormula (a, b, c);
// Print the results
switch (howMany)
{
case 0: cout << "No solution.\n"; break;
case 1: cout << "Solution is " <<r1<<endl; break;
case 2: cout << "Solutions are " <<r1<<' '<<r2<<endl; break;
default:cout << "Can't have " <<howMany<<" solutions!\n";
}
return 0;
}
Example 26-3Using structured bindings to get multiple values through a return statement
这种新的能力,加上我们之前所知道的,导致了第八章中关于这个主题的黄金法则的更新版本。
Golden Rule of Function Parameters and
return
(新版)
如果函数提供了一个值...
如果很小,就退掉。对于倍数,返回一个元组。
如果没有,就路过
&
。
如果它接受一个变量并改变它,则通过
&
。如果它接受它而不改变它,
如果是一个对象,作为
const TheClass&
对象传递。否则按值传递(否
&
)。
如果是移动
=
,则经过&&
。…除了数组作为参数传入之外,根据内容是否改变,有或没有
const
。
你也可以在其他地方使用元组,有点像pair
,只是元素数量不同。为了得到零件,无论是改变它们还是使用它们,你可以使用std::get
<>()
。将您想要的元素放在<>
和()
之间的元组之间:
std::tuple<int, double, double> myTuple = std::make_tuple (0, 2.0, 3.0);
assert(std::get<0> (myTuple) == 0); // check the 0th value
std::get<0> (myTuple) = 1; // set the 0th value
如果能节省你的时间就好。但是它甚至没有那个有auto [...]
的东西一半酷。
Exercises
在每个练习中,让main
使用auto [...]
来存储返回值:
编写一个函数
sortedTriple
,它接受一个由三个元素组成的元组,将它们按顺序排列,然后返回新的版本。从第十八章写一个版本的
factorial
,不仅返回n!
,还返回n
。(更难)写一个函数,给定一个
vector
,返回最大值、最小值、平均值和标准差,所有这些都在一个元组中。标准差有时定义为。
现在为一个
list
写一个做同样事情的。泛型编程,是的。
智能指针
C++ 最近更新背后的一个动机是防止指针错误破坏我们的代码。祝你好运!但是还是有进步的。
unique_ptr
主要的主力是std::unique_ptr
。它维护一个指针,让您使用它,并在它超出范围时自动删除它。是的,你可以打破它,但是你必须试一试。它通常用make_unique
初始化,它带参数来初始化你所指向的任何东西:
#include <memory
>
...
std::unique_ptr<int > p1 = std::make_unique<int>(27);
// new int ptr, value 27
std::unique_ptr<Date> pDate = std::make_unique<Date>(1, 1, 2000);
// Put the arguments for Date's constructor in
// and make_unique will take care of it
或者它取你想要创建的数组的大小:
std::unique_ptr<char[]> myChars = std::make_unique<char[]>(100);
之后,像平常使用指针一样使用它:
*p1 = 2; cout << *p1;
pDate->print(cout);
myChars[5] = 'A';
你可以用get()
拿到里面的指针。当传递给一个函数:strcpy (myChars.get(), "totally unique");
时,您可能需要它。
不需要记得清理;它会自动删除。而且对于谁做delete
也没有混淆,因为unique_ptr
不共享内存(因此有了单词“unique”)。
你能不能告诉它马上删除:
myChars.reset(); // the memory is deleted --
// myChars now thinks it's nullptr
或许可以重置成你想要的其他东西:
myChars.reset (new char [100]);2 // takes ownership of the new memory –-
// is responsible for deleting later
为什么要这么做?
错误预防:这样你就不会忘记初始化或删除,也不会忘记使用已经被删除的指针——它会自动设置为
nullptr
,所以你不能这样做。异常安全:当你离开一个函数时,它调用所有局部变量的析构函数。原始指针——我们一直在使用的那种——没有析构函数,所以它们的内存不会被抛出,但是
unique_ptr
s 会把它们的内存放回它们的析构函数中。因此,如果抛出异常,使用unique_ptr
可以防止内存泄漏。
我代码中的大多数指针都在类的私有部分,清理由析构函数处理,所以我认为它们是相当安全的。但是让我们看看unique_ptr
是否能为我们省去麻烦。
我将从示例 21-6 的奥林匹克标志程序开始(此处更新为示例 26-4 )。
// Program to show, and move, the Olympic symbol
// It uses Circle, and a subclass of Shape called Text
// Adapted to use unique_ptr
// -- from _C++20 for Lazy Programmers_
#include <vector>
#include <memory> // for unique_ptr
#include "circle.h"
#include "text.h"
int main (int argc, char** argv)
{
SSDL_SetWindowSize (500, 300); // make smaller window
// Create Olympic symbol
std::vector<std::unique_ptr<Shape>> olympicSymbol;
// with Circles
constexpr int RADIUS = 50;
olympicSymbol.push_back
(std::make_unique<Circle> ( 50, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (150, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (250, 50, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (100, 100, RADIUS));
olympicSymbol.push_back
(std::make_unique<Circle> (200, 100, RADIUS));
// plus a label
olympicSymbol.push_back
(std::make_unique<Text> (150,150,"Games of the Olympiad"));
// color those circles (and the label)
SSDL_Color olympicColors[] = { BLUE,
SSDL_CreateColor (0, 255, 255), // yellow
BLACK, GREEN, RED, BLACK };
for (unsigned int i = 0; i < olympicSymbol.size(); ++i)
olympicSymbol[i]->setColor (olympicColors [i]);
// do a game loop
while (SSDL_IsNextFrame ())
{
SSDL_DefaultEventHandler ();
SSDL_RenderClear (WHITE); // clear the screen
// draw all those shapes
for (const auto& i : olympicSymbol) i->draw ();
// ranged-based for-loops ftw!
// move all those shapes
for (const auto& i : olympicSymbol) i->moveBy (1, 1);
}
// No longer needed:
// for (auto i : olympicSymbol) delete i;
return 0;
}
Example 26-4The Olympic symbol program from Example 21-6, now using unique_ptr
我对我挽救的工作并不兴奋。让我们看看它在类Shape
中会做什么。
Shape
有一个char
指针description_
。在现实生活中,我只是使用了一个string
,但是这将帮助我们看到当一个类必须有一个指针时unique_ptr
为我们做了什么(例子 26-5 和 26-6 )。
// Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#include "shape.h"
// ctors
Shape::Shape(int x, int y, const char* text)
: description_(copy(text))
{
location_.x_ = x; location_.y_ = y;
}
Shape::Shape (const Shape& s) :
location_ (s.location()),
color_ (s.color ()),
description_(copy (s.description_.get()))
{
}
// I no longer have to write the move ctor
// ...or the move = operator
// operator=
Shape& Shape::operator= (const Shape& s)
{
location_ = s.location();
color_ = s.color ();
description_.reset (copy (s.description_.get()));
return *this;
}
// copy, used by = and copy ctor
char* Shape::copy (const char* text)
{
char* result = new char [strlen (text) + 1];
strcpy (result, text);
return result;
}
Example 26-6The Shape class from Example 21-1, now using unique_ptr
// Shape class, for use with the SSDL library
// -- from _C++20 for Lazy Programmers_
#ifndef SHAPE_H
#define SHAPE_H
#include <memory> // for unique_ptr
#include "SSDL.h"
struct Point2D // Life would be easier if this were a full-fledged class
{ // with operators +, =, etc. . . . but that
int x_, y_; // was left as an exercise.
};
class Shape
{
public:
Shape (int x = 0, int y = 0, const char* text = "");
Shape (const Shape& other);
Shape (Shape&&) = default;
virtual ~Shape() {} // No need to delete contents_ -- handled!
Shape& operator= (const Shape& s);
Shape& operator= (Shape&&) = default;
//...
const char* description() const { return description.get(); }
//...
private:
Point2D location_;
SSDL_Color color_;
std::unique_ptr<char> description_;
char* copy(const char*); // used for copying descriptions
// altered from original for clearer use
// with the new description_, but
// it's not really new stuff
};
#endif //SHAPE_H
Example 26-5The Shape class from Example 21-1, now using unique_ptr
我发现的主要优势是
无需在析构函数中编写任何代码;知道如何删除自己。我也不必在
operator=
或复制构造器中删除内存——这是一个容易出错的活动。我现在可以使用默认的移动功能。
location_
和color_
使用它们的复制扇区;description_
知道如何用它的 move 构造器复制自己。不编写 move 构造器和 move=
为我节省了大约八行代码。
shared_ptr
一个unique_ptr
拥有它的内存,其他任何人都不应该改变或释放它。
一个shared_ptr
让其他shared_ptr
拥有相同的内存。它记录了有多少个shared_ptr
正在使用它(这就是“引用计数”),只有当这个数字降到 0 时,内存才会被删除。
这里有一个可能的用途:我有一个可以从文件中加载的 3d 模型。这些东西往往很大。如果我有 20 个相同类型的怪物,我不想要所有图形数据的 20 个副本!
所以我将图形数据放在一个类型为GraphicsData
的对象中,并为怪物的每个实例创建一个Monster
对象。让Monster
s 分享他们的GraphicsData
。
class GraphicsData
{
...
private:
... lots of graphics info...
};
class Monster
{
...
Point3D location_;
shared_ptr<GraphicsData> _modelInfo;
};
如果GraphicsData
包含一个指向Monster
的指针,那么shared_ptr
的引用计数可能会遇到一个问题,因此引用的数量不会降到零,也不会删除任何东西。weak_ptr
,这里没有涉及,是处理那个问题的一种方式。
防错法
我发现智能指针的主要问题是忘记get()
:
strcpy (myChars, "totally unique"); // should be myChars.get()
。
Exercises
- 重写
String
类(使用第十八章,这样它会有移动复制和移动=
)来使用unique_ptr
。
static_cast
等。
哪里曾经说过newtype (value)
,哪里就可以说static_cast
<newtype> (value)
:
double dbl = double (intVal);
成为
double dbl = static_cast<double> (intVal);
和
((ChildClass*) (parentClassPtr))->childClassFunction ();
成为
(static_cast<ChildClass*> (parentClassPtr))->childClassFunction ();
为什么呢?所以当你想说
int* intArray = static_cast<int*> (myFloatArray); // huh?!
编译器不会让你这么做。指针安全。
Extra
我不推荐 C++ 提供的其他选角 运算符,如果你继续下一部分,我也不会责怪你。
还在吗?好的。以下是其他类型:
const_cast
<type>
:增加或减少const
ness:
((const_cast<const MyClass*> (someVar ))->print (cout);
// adds constness
((const_cast< MyClass*> (someConst))->alterMeInSomeWay();
// takes it away
…但是你不能安全地把它应用到最初声明的东西上const
。你可以将它应用于作为const
参数传入的东西。
我避免这样。如果是非const
,我能用const
做什么非const
做不了的事?不多。如果是const
,我真的不应该破坏那个安全。
dynamic_cast
<type>
:如果涉及到虚拟的话,这可以让你在继承层次中进行转换。我从来没用过。reinterpret_cast
<type>
:这个不太“什么都行”——不影响const
ness,也不能施放自己想不通的东西,但是可以做类似前文施放myFloatArray
到int*
的诡异事情。我也没用过。但是也许我在不知道的情况下用了最后一个,那时候我们只有更老更简单的演员阵容
<type>()
。当您这样做时,C++ 会按顺序尝试这些类型的转换:const_cast
static_cast
static_cast
然后const_cast
reinterpret_cast
reinterpret_cast
然后const_cast
找一个能用的。如果失败了,你就不能做演员了。
在我看来,我们使用的是
static_cast
,而不是老式的造型,主要是为了不让我们不知道它已经到达了reinterpret_cast
。
用户定义的文字:测量系统之间的自动转换
1999 年 9 月 23 日,美国太空探测器火星气候轨道器在火星附近失踪。该计划中有关进入轨道的部分有一些英制计算和一些公制计算。该计划失败了,美国国家航空航天局从未从其价值 3 . 27 亿美元的宇宙飞船中得到任何东西。哎呀。
我完全理解。每次用 trig 函数写程序,我都用度数,C++ 用弧度。
当 NASA 编写这个软件时,现代 C++ 还不存在,但是如果有,他们可以让计算机在系统之间自动转换。以下是如何:
Write an operator to convert from your unit to some unit you want the calculations done in. I’ll convert miles to meters:
long double operator"" _mi (long double mi) { return mi * 1609.344; // 1 mi = 1609.344 meters }
你需要主角
_
。
** 这样称呼它: 10_mi 。C++ 将此视为将 10 传递给"" _mi
操作符。
*
*在 BNF 中,操作符看起来是这样的(尽管您可以像constexpr
一样给它添加限定符):
<return-type> operator "" _<operator name> (<parameter list>)
{
<body>
}
这样用: <值> _
<运算符名称> ,无空格*。它只适用于文字——你不能用它来转换变量。*
示例 26-7 将此与constexpr
一起使用–为什么不呢?–进行一些简单的计算。
// Program to use user-defined literals
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <cmath>
using namespace std;
constexpr double PI = 3.14159;3
// "Literal" operators
constexpr long double operator"" _deg (long double degrees) // version for // double values
{
return degrees * PI/180;
}
constexpr long double operator"" _deg (unsigned long long degrees) // ...for int
{
return degrees * PI/180;
}
constexpr long double operator"" _m (long double m) { return m; }
// 1 m = 1 m (duh)
constexpr long double operator"" _mi (long double mi) { return mi * 1609.344;}
// 1 mi = 1609.344 m
int main ()
{
cout << "The speed of light is 186,000 miles per second.\n";
cout << "In metric, that's ";
cout << 186'000.0_mi4
<< " meters per second --\n"
<< " should be about " << 300'000'000.0_m << ".\n";
cout << "Oh, and sin (30 deg) is about 0.5
: " << sin (30_deg) << endl;
return 0;
}
Example 26-7A program using user-defined literals
输出:
The speed of light is 186,000 miles per second.
In metric, that's 2.99338e+008 meters per second --
should be about 3e+008.
Oh, and sin (30 deg) is about 0.5: 0.5
以下是关于参数的两个特性:
它不会在类型之间进行隐式转换(尽管它对像
unsigned
或long
这样的修饰符很灵活)。如果你给它186'000_mi
,它就会失败,因为它期待的是double
而不是int
。要么添加.0
,要么编写一个期望整数类型的运算符。参数表必须是表 26-1 中的一组。我大多用
long double
。
表 26-1
用户定义的文字运算符的可能参数列表
| `unsigned long long` | `long double` | | `char` | `const char*` | | | `const char*, std::size_t`5 | | `wchar_t` | `const wchar_t*, std::size_t` | | `char16_t` | `const char16_t*, std::size_t` | | `char32_t` | `const char32_t*, std::size_t` |Exercises
使用用户定义的文字,如果可能的话用constexpr
...
火星的气压约为 6.1 毫巴,地球的气压约为每平方英寸 14.7 磅。金星的大概是 9.3 MPa。在网上查找这些单位进行转换,计算金星的压力比地球大多少倍,地球比火星大多少倍。
使用砝码,用户提供三个物体,分别是磅、千克和石头,哪一个最重。
一次性使用的 Lambda 函数
STL 的函数sort
可以接受第三个参数,这是一个比较函数,如果认为第一个参数小于第二个参数,则返回true
。假设我们想按名字或按人口对城市进行排序:
bool lessThanByName (const City& a, const City& b)
{
return a.name() < b.name();
}
bool lessThanByPop (const City& a, const City& b)
{
return a.population() < b.population();
}
...
ranges::sort (cities, lessThanByName);6
ranges::sort (cities, lessThanByPop);
如果一个比较函数只使用一次,也许我懒得为它创建一个完整的函数。我可以这样做:
ranges::sort (cities, [](const City& a, const City& b)
{
return a.name() < b.name();
});
这被称为“lambda”函数,这个术语是通过 LISP 编程语言从数学中借用的。BNF 就像一个函数,除了返回类型和函数名被替换为[]
:
[] (<parameters, separated by commas>)
{
<thing to do -- variable declaration, action, whatever>*
}
[]
是一种表示“函数名放在这里,只是这次我没有考虑名字。”
λ捕获
越来越奇怪了。假设我想根据我的City
离某个特定地点的距离来排序。我不能把那个位置作为第三个参数传入-sort
需要一个两个参数的比较函数!但是我可以告诉 lambda,“把这个变量从外面带进来”:
const City LA ("Los Angeles", 3'900'000, { 34_deg, -118_deg }); // name, pop, location
ranges::sort (cities, &LA
{
return distance (LA, a) < distance (LA, b);
});
我在上面加了一个&
来说明,“提交一份参考资料,不要复制。”你不能说const &
,但既然LA
是const
,就不会改变它。
如果 lambda 不改变值并且复制成本不高,我可以省略&
:
// Find out if some bad letter is in my city's name
auto findBadLetter =
find_if (name, badLetter { return ch == badLetter; });
表 26-2 显示了我们可以放在[]
之间的东西。通常我们不需要任何东西。但是我们可以列出具体的变量,有或者没有&
。
表 26-2
λ捕捉。这些位于 lambda 函数的[]
之间,允许访问不是 lambda 函数参数的非静态局部变量。它们可以组合:【arg1, &arg2,
this
]
我们也可以用=
或&
来说,“让一切都进来。”这里的“一切”是指非全局变量(ack!)并且不是静态的。 7 全局变量和静态局部变量无论如何都可以被引用,而不需要在[]
中列出
我避免裸露的&
和=
——确切地指定什么可以进入函数更安全。
lambda 函数的示例
示例 26-8 展示了 lambda 函数和捕获,在程序中以各种方式对城市进行排序。
// Program that uses lambda functions to order cities by different criteria
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <vector>
#include <cassert>
#include <algorithm>
#include "globalLoc.h" // provides user-defined literal operators _deg and _mi,
// struct GlobalLoc, latitude, longitude, and
// a distance function that works on a globe
using namespace std;
class City
{
public:
City (const std::string& n, int pop, const GlobalLoc& loc) :
name_ (n), population_ (pop), location_ (loc)
{
}
City (const City&) = default;
City& operator= (const City&) = default;
const std::string& name () const { return name_; }
int population () const { return population_; }
const GlobalLoc& location() const { return location_; }
private:
std::string name_;
int population_;
GlobalLoc location_;
};
inline
double distance (const City& xCity, const City& yCity)
{
return distance (xCity.location(), yCity.location());
}
int main()
{
// Some prominent party spots
vector<City> cities =
{
{"London", 10'313'000, { 51_deg, -5_deg}},
{"Hamburg", 1'739'000, { 53_deg, 10_deg}},
{"Paris", 10'843'000, { 49_deg, 2_deg}},
{"Rome", 3'718'000, { 42_deg, 12_deg}},
{"Rio de Janiero", 12'902'000, {-22_deg, -43_deg}},
{"Hong Kong", 7'314'000, { 20_deg, 114_deg}},
{"Tokyo", 38'001'000, { 36_deg, 140_deg}}
};
// Print those cities in different orderings:
cout << "Some major cities, in alpha order : ";
ranges::sort (cities, [](const City& a, const City& b)
{
return a.name() < b.name();
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
cout << "Ordered by population: ";
ranges::sort (cities, [](const City& a, const City& b)
{
return a.population() < b.population();
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
cout << "Ordered by how far they are from LA: ";
const City LA ("Los Angeles", 3'900'000, { 34_deg, -118_deg });
// & would work here too -- but &LA is a little more secure
ranges::sort (cities, &LA
{
return distance (LA, a) < distance (LA, b);
});
for (const auto& i : cities) cout << i.name() << " / ";
cout << endl;
return 0;
}
Example 26-8A program to sort cities, using lambdas. Parts are omitted for brevity
Exercises
当然,在所有这些情况下,都要使用 lambda 函数。我引用了我在文中没有描述过的函数;需要的话在网上查查。
按纬度对前面示例中的
City
进行排序。使用
for_each
函数打印一个容器的每个元素,由/
分隔。也许你会用它来代替示例 26-8 中打印的for
。使用
for_each
将一个容器中的每一个柠檬大写。使用
count_if
函数查看容器中有多少整数是整数的平方。使用
all_of
来验证容器中的每个字符串都包含一些子字符串,由用户给出。
二十七、秘籍(不推荐)
我很少使用这些功能。这一章的部分原因是在那些罕见的情况下,它们是有用的。另一个是理解为什么他们不那么受欢迎。因为向前看很有趣,所以有两个 C20 特性的预览,它们还不是很有用,但是应该在 C23 中:模块和协程。
protected
章节,protected
传承
考虑类Phone
。Phone
有一个成员numCalls_
,它记录任何一个Phone
打过的所有电话。有一个函数可以改变它,但它是私有的,因为我们真的应该只在制作call ()
时更新numCalls_
。
class Phone
{
public:
void call() { /*do some stuff, and then */ incNumCalls(); }
static int numCalls() { return numCalls_; }
private:
void incNumCalls () { ++numCalls_; }
inline static int numCalls_ = 0;
};
但是现在我们已经到达了人类文明的黎明,出现了MobilePhone
s。他们用不同的方式打电话,使用信号塔,但是他们也需要增加那个数字。他们无法访问Phone::incNumCalls()
;这是隐私。我们有充分的理由决定不公开。我们还能做什么?
C++ 提供了另一个部分:受保护的(参见示例 27-1 )。外界看不到(像 private),但是子类看得见。
class Phone
{
public:
void call() { /*do some stuff, and then */ incNumCalls(); }
static int numCalls() { return numCalls_; }
protected:
void incNumCalls () { ++numCalls_; }
private:
inline static int numCalls_ = 0;
};
Example 27-1The Phone class, ready to share a family secret with its child classes
现在MobilePhone
将能够访问incNumCalls()
:
class MobilePhone : public Phone
{
public:
void call() { /* do stuff w cell towers, and */ incNumCalls(); }
};
应该用public
还是private
继承?比方说,当你打手机时,你需要一些额外的安全措施。所以在MobilePhone
中,我将抛弃旧的call
,增加一个新的功能secureCall
:
class MobilePhone : public /*?*/ Phone
{
public:
void secureCall()
{
makeSecure ();
/* do cell tower stuff */
incNumCalls();
}
void makeSecure (); // however that's done
};
有了公共继承(图 27-1 ,继承的成员在子类中就像在父类中一样是公共的。这对MobilePhone
不利:它让外界使用不安全的、继承的call
函数。也许私人继承,如图 27-2 ,会更好?看起来是这样。
图 27-2
带有受保护节的私有继承
图 27-1
带有受保护节的公共继承
现在我将添加一个MobilePhone
的子类:一个SatellitePhone
。它的叫声不同:
class SatellitePhone : public MobilePhone
{
public:
void secureCall()
{
makeSecure ();
/* do satellite stuff */
incNumCalls();
}
// makeSecure is inherited from MobilePhone
};
问题:SatellitePhone
不能用incNumCalls
。私人继承把它放在MobilePhone
的私人部分。
我们可以用保护 继承,如图 27-3 。这就像公共继承,除了继承的公共成员受到保护。
图 27-3
继承:一个解决问题的方法
现在,正如示例 27-2 中的程序可以验证的那样,子类是安全的,不会被旧的call
函数、和使用,所有的类都根据需要访问incNumCalls()
。
int main ()
{
Phone P; P.call();
MobilePhone MP; MP.secureCall();
SatellitePhone SP; SP.secureCall();
assert (Phone::numCalls() == 3); // If this assertion succeeds,
// incNumCalls got called // 3 times -- good!
return 0;
}
Example 27-2Verifying that no matter what kind of call we made, Phone::numCalls_ got updated
在拥有孙类之前,使用私有继承还是受保护继承根本无关紧要。即便如此,这可能也无关紧要。我几乎从不需要受保护的部分或受保护的继承。
为什么你不应该有
考虑一个使用地图的程序。它读入几个Area
秒,如图 27-4 所示。每个Area
都有一个名称和边界框(?? 向北、向南、向西和向东延伸多远)。然后它报告哪个Area
在最北边。示例 27-3 和 27-4 显示源代码,为简洁起见省略了一些代码。
图 27-4
四个Area
的地图,其中一个Area
上显示一个边界框
// Program to read in regions from a file, and tell which
// is furthest north.
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include <fstream>
#include <vector>
#include "area.h"
using namespace std;
int main ()
{
vector<Area> myAreas;
ifstream infile("regions.txt");
if (!infile)
{
cerr << "Can't open file regions.txt.\n"; return 1;
}
while (infile) // read in Areas
{
Area area; infile >> area;
if (infile) myAreas.push_back (area);
}
// find the northernmost Area
int northernmostIndex = 0;
for (unsigned int i = 1; i < myAreas.size(); ++i)
if (furtherNorthThan (myAreas[i],myAreas[northernmostIndex]))
northernmostIndex = i;
// print it
cout << "The northernmost area is "
<< myAreas [northernmostIndex]
<< endl;
return 0;
}
Example 27-4The map program, which identifies the Area furthest north
// Class Area
// Each Area is read in as
// <north bound> <south bound> <west bound> <east bound> <name>
// as in
// 8 2 1 4 Blovinia
// ...and that's what an Area contains
// -- from _C++20 for Lazy Programmers_
#ifndef AREA_H
#define AREA_H
#include <string>
#include <iostream>
class Area
{
public:
static constexpr int NORTH = 0,
SOUTH = 1,
EAST = 2,
WEST = 3;
static constexpr int DIRECTIONS = 4 ; // there are 4 directions
Area () {}
Area (const Area& other);
Area& operator= (const Area& other);
void read (std::istream& in );
void print (std::ostream& out) const { out << name_; }
private:
double boundingBox_[DIRECTIONS];
// the northernmost, southernmost, etc., extent of our Area
// bigger numbers are further north
// bigger numbers are further east
std::string name_;
};
inline
bool furtherNorthThan (const Area& a, const Area& b)
{
return a.boundingBox_[Area::NORTH] > b.boundingBox_[Area::NORTH];
}
#endif //AREA_H
Example 27-3area.h
我知道我已经在这里写了清晰的、注释良好的代码(我也很谦虚),所以我不会进一步解释。但是当furtherNorthThan
试图访问boundingBox_
时,编译器抱怨侵犯了隐私。没错:boundingBox_
是的私有。
C++ 的朋友可以解决这个问题。如果一个函数与一个类联系如此紧密,以至于它也可能是一个成员——但是不方便使它成为一个成员——你可以让它访问所有成员,包括私有成员,就像它是成员一样。下面是方法:在类Area
的某个地方(我把它放在顶部,所以它总是在同一个地方),放一个函数的friend
声明(例子 27-5 )。
class Area
{
// "friend" keyword plus prototype of the trusted function
friend bool furtherNorthThan (const Area& a, const Area& b);
...
Example 27-5A friend for Area
现在程序应该编译好了,并报告 Morgravia 在最北边。
也可以做一个 a 类friend
:
class Area
{
friend class OtherClassITrust;1
...
或者让其他类的成员函数成为朋友:
class Area
{
friend void OtherClassIPartlyTrust::functionIFullyTrust();
...
这是个好主意吗?
根据马歇尔·克莱恩和《C++ 超级常见问题解答》, 2 是的。他认为friend
函数是公共接口的一部分,就像公共成员函数一样。它不违反安全性,但只是安全性的另一部分。
我明白他的意思,但我想不出一个不能用另一种方法来做的例子。在这个例子中,我们可以用bool Area::furtherNorthThan (const Area& b) const;
代替bool furtherNorthThan (const Area& a, const Area& b);
。这就是我们对像<
这样的运营商所做的。为什么不也这样呢?
我曾经让流 I/O 操作符>>
和<<
成为他们打印/读取的类的朋友;现在我让它们调用成员函数print
和read
。使用friend
可能会更容易,但不会更容易。
如果你想要它,就像专家建议的那样使用它:用于与所讨论的类紧密相关的事物,因此它们可以被认为是类与世界的接口的一部分。我打赌不会经常发生。
用户定义的转换(转换运算符)
我们是否应该根据需要添加一种从String
隐式强制转换到const char*
的方法?有道理。许多内置函数都希望有一个char*
,你可能更喜欢myInFile.open (filename);
而不是myInFile.open (filename.c_str())
,尤其是在你输入第 100 次的时候。所以我们将这个运算符添加到String:
operator const char* () const { return c_str (); } // called implicitly as needed
。
对于对myInFile.open
的调用很有效。然后我们尝试一个简单的字符串比较:
if (str1 == "END")
cout << "Looks like we've reached the END.\n";
它不再编译——抱怨含糊不清或过多重载。
没错。现在有两种方法来匹配运算符==
的参数:隐式地将"END"
转换为另一个String
,并与String
的==
进行比较;用 cast 操作符将str1
隐式转换为char*
,并使用char*
的==
。
解决方法是在函数前面加上explicit
这个词(例如 26-7 )。 3
class String
{
public:
...
explicit operator const char* () const { return c_str(); }
// will cast from String to const char* if explicitly called
...
};
Example 27-6Giving String a user-defined cast operator
现在我们可以选角了,但是我们必须说我们想要选角:
myInputFile.open ((const char*) (filename)); //old-style explicit // cast -- OK
或者
myInputFile.open (static_cast<const char*>(filename)); //modern explicit // cast -- OK
它起作用了,但是我们从说filename.c_str()
中得到什么了吗?
我似乎从来没有找到一种既安全又省时的方法来使用这个功能。也许你会。
Exercises
在每个练习中,使用explicit
来避免歧义。
向
Fraction
类添加一个 cast-to-double 运算符。比如 1/2 的double
版本是 0.5(当然)。将 cast-to-double 运算符添加到前面练习中的
Point2D
类中。一个Point2D
的double
版本就是星等:。
模块
我以外的程序员开始担心加载那些越来越长的.h
文件所花费的时间。我们也回避了一些问题:你在一个.h
文件中包含的一个#define
可能会干扰另一个文件。我们试图通过命名这些定义的惯例来避免这种情况;我们可能会失败并得到可怕的错误信息。
模块是一个补丁。一个模块可以编译一次,而不是为每个使用它的.cpp
文件重新编译,不像一个.h
文件; 4 这样应该可以缩短编译时间。它还可以指定其作者希望与世界共享的内容,从而防止一些名称冲突。(一个.h
文件让所有东西对它的 includer 可见,一个.cpp
文件什么也不显示,但是一个模块可以选择。)和一个模块都可以在一个文件中——你不必在.cpp
和.h
文件中分开。
我相信会按计划进行的。但是标准本身并不完整:即将到来的 C++23 标准的“首要任务”之一是将标准库放在模块中,这意味着他们还没有这样做。 5 我会等,我也推荐你这么做。
但是我当然不能就此罢休——所以这里有一个在线的补充。
Online Extra: Using Modules Right Now
请参阅 github.com/Apress/cpp20-for-lazy-programmers,了解如何使用模块的最新演练,因为现在最好的编译器支持它们。
协同程序
一般来说,如果你第二次调用一个函数,它会从头开始。协程程序可以从中断的地方重新开始。
示例 27-6 使用协程来计算下一个阶乘。(关于阶乘的复习,参见第十八章的递归部分)是co_yield
让 C++ 将它识别为协程。6std::experimental::``generator``<int>
返回类型的意思是“设置这个,以便factorial
可以生成int
s。”
// Program to print several factorials using a coroutine
// -- from _C++ for Lazy Programmers_
#include <iostream>
#include <experimental/generator>
std::experimental::generator<int> factorial()
{
int whichOne = 0; // start with 0!
int result = 1; // 0! is 1
while (true)
{
co_yield result;
++whichOne; // go on to next one
result *= whichOne; // and calculate next result
}
}
int main ()
{
std::cout << "The first 8 factorials: \n";
for (int i : factorial())
{
static int whichOne = 0;
std::cout << whichOne << ": " << i << '\n';
++whichOne; // go on to next
if (whichOne > 8) break; // stop at 8
}
std::cout << std::endl;
return 0;
}
Example 27-7A program using a coroutine in Microsoft Visual Studio. g++ and the C++20 standard aren’t equipped for this yet
追踪它的动作:第一次调用它时,它将whichOne
——我们要返回的阶乘——设置为 0。0 的result
为 1。(您可以将源代码示例加载到ch26
文件夹中,并在调试器中跟踪它。我就是这么做的。)
它进入循环。它要做的第一件事是给调用者main
提供那个result
,和co_yield
,意思是“把result
给调用者,当再次被调用时,在这里继续执行。”因此控制返回到main
,它打印那个result
。
当main
再次调用它时,它从它停止的地方继续:在co_yield
处。它继续将whichOne
加 1(将whichOne
改为 1),将result
乘以whichOne
(再次得到 1),进入循环的下一次迭代,然后co_yield
就是result
。
当再次被调用时,它将再次增加whichOne
(得到 2),将result
乘以whichOne
(得到 2),然后co_yield
得到结果。
下一次,whichOne
会变成 3,result
会变成 6。等等。
main
被设置为在一个基于范围的 for 循环中一次又一次地调用这个函数,在 8 处中断(必须在某处停止)。
协程的一个优点是效率。每次我们调用factorial
,它所做的只是一个增量、一个乘法和一个返回。是 O(1)!第十八章的版本是 O(N)。程序员还报告说,对于一些问题,协程更直观,也更容易编写。
目前最大的劣势是支持。如你所见,Visual Studio 认为它的generator
模板是实验性的,而 g++ 根本没有。两个都支持协程——都有co_await
、co_result
和co_yield
——但是generator
不是标准的,我认为最好编写能在任何机器上工作的代码。在 g++ 中,你必须自己写,这并不容易。对于你可能想做的其他事情也是一样。我的希望是 C++23 能够解决这个问题,社区中也有关于这个 7 的讨论。
Exercises
改编示例 27-7 ,以便
factorial
不仅返回result
,还返回结构化绑定中带有whichOne
的result
—main
不必独立跟踪自己的whichOne
。这实际上不是用协程而是用结构化绑定来实现的,但我认为仍然值得一做。编写另一个返回下一个质数的
generator
函数,从 2 开始,和一个打印前 100 个质数的main
版本。
二十八、C
如果你懂 C++,你就差不多懂 C 了。使用 C 的经验给了你另一个吹牛的机会——一个字符长,所以应该合适!–在你的简历上。c 是操作系统和嵌入式系统的流行语言,里面有很多库。
c 基本上是我们在上课前所学的内容,不包括
SDL/SSDL
cin
和cout
&
参数bool
(用int
代替)constexpr
(用const
代替)
没有类、异常、重载运算符、模板或命名空间。存在,但没有成员函数或公共/私有/受保护部分(都是公共的)。
还有一些较小的差异,包括以下几点:
铸造长这样,
(int) f
,不是这样:int (f)
。struct
S {...};
确实声明了一个名为S
的struct
,但是声明该类型的变量需要一个额外的单词:struct
S myStruct;
cplusplus.com 和 cppreference.com,不管名字如何,都是 C 和 C++ 的好资源。
编译 C
在 Visual Studio 中,你不能选择“C 文件”作为添加到你的项目中的东西,但是你可以选择“C++ 文件”并将你的命名为<something>.c
。编译器会将其视为 C 文件。照常编译和运行。
在 Unix 或 MinGW 中,你可以将你的程序命名为<something>.c
并用gcc
命令进行编译,这就像g++
只适用于 C 文件。示例代码将这一点构建到了它的 Makefiles 中。
这是必须的“你好,世界!”
// Hello, world! -- again! This time in C.1
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
printf ("Hello, world!\n");
return 0;
}
Example 28-1“Hello, world!” in C
一些需要注意的事项:
Includ e 文件,无论是否属于系统,都以
.h
结尾。那些从 C 继承而来的 C++ 将首字母c
去掉了:stdlib.h
而不是cstdlib
;math.h
不是cmath
。我们使用
printf
打印–下一节将详细介绍。
Extra
如果您希望在同一个项目中同时包含 C++ 和 C 文件,这是可行的,但是需要一些技巧。您需要将main
放在一个 C++ 文件中,并且对于要在 C++ 文件中使用的任何 C include 文件,这样包装 include:
extern "C"
{
#include "mycheader.h"
}
如果你用的是 gcc/g++,那就用 g++ 链接。
关于这方面的更多内容,在写作的时候,参见 C++ 超级 FAQ, isocpp.org/wiki/faq/mixing-c-and-cpp
。
输入-输出
所有这些 I/O 功能都在stdio.h
中。
printf
代替cout >>
,C 有函数printf
(print-f,意为“带格式打印”):
printf ("Ints like %d, strings like %s, and floats like %f -- oh, my!\n",
12, "ROFL", 3.14159); // %d is for "decimal"
将打印
Ints like 12, strings like ROFL, and floats like 3.141590 -- oh, my!
%
序列是“格式字符串”("Ints like %d..."
)中的占位符,显示每个连续参数的位置。你可以有尽可能多的论点。最常见的%
序列有%d
表示十进制整数、%f
表示固定浮点、%s
表示字符串,即字符数组。%%
的意思是“仅仅是%
字符。”
你可以把修饰语放在里面。例如,%.2f
将小数点右边的两位数字。
scanf
和地址-of ( &
)运算符
scanf
(“扫描-f”)取代了cin >>
,如下所示:
scanf ("%f %s", &myDouble, myCharArray);
&
的意思是“记下…的地址”C 和 C++ 都有这个操作符,但是 C 一直在用。scanf
需要知道myDouble
在哪里,这样它就可以修改它(下一节将详细介绍)。它不需要myCharArray
的地址,因为myCharArray
是地址。
如果你使用本章中的scanf
或其他一些函数,Visual Studio 会给出一个警告,告诉你这个函数是不安全的,就像使用一些cstring
函数一样(见第十四章)。如果您想禁用警告,请将此行放在main
之前:
#pragma warning (disable:4996)
示例 28-2 演示printf
和scanf
。
// Program to test C's major standard I/O functions
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
float number; // number we'll read in and print out
int age; // your age
enum {MAXSTR = 80};2 // array size
char name [MAXSTR]; // your name
// A printf showing float, and use of % sign
printf ("%3.2f%% of statistics are made up on the spot!\n\n",
98.23567894);
// printfs using decimal, hex, and char array
// %02d means pad number to a width of 2 with leading 0's
printf ("%d is 0x%x in hexadecimal.\n\n", 16, 16);
printf ("\"%s\" is a $%d.%02d word.\n\n", "hexadecimal",
5, 0);
// scanf needs & for the variables it sets
printf ("Enter a floating-point number: ");
scanf ("%f", &number);
printf ("%g is %f in fixed notation and %e in scientific.\n",
number, number, number);
printf ("...in scientific with a precision
of 2: %.2e.\n\n",
number);
// ...except arrays, since they're already addresses
printf ("Enter your name and age: ");
scanf ("%s %d", name, &age);
printf ("%s is %d years old!\n\n", name, age);
return 0;
}
Example 28-2printf and scanf
输出可能是这样的:
98.24% of statistics are made up on the spot!
16 is 0x10 in hexadecimal.
"hexadecimal" is a $5.00 word.
Enter a floating-point number: 2
2 is 2.000000 in fixed notation and 2.000000e+000 in scientific.
Here it is in scientific with a precision of 2: 2.00e+000.
Enter your name and age: Linus 7
Linus is 7 years old!
表 28-1 包含 printf 和 scanf 格式代码的部分列表。有关更多详细信息,请参见(在编写本报告时)cplusplus . com/reference/CST dio/printf/和cplusplus . com/reference/CST dio/scanf/。
fprintf
和fscanf
、fopen
和fclose
C 中的文件 I/O 使用了printf
和scanf
的变体。考虑以下代码:
FILE* file = fopen ("newfile.txt", "w"); // open file
if (!file) { printf("Can't open newfile.txt!\n"); return 0; }
// did it work? if not, quit main
fprintf (file, "Avagadro's number is %.4e.\n", 6.023e+023);
// use it
fclose (file); // close it
为了打开文件进行写入,我们调用fopen
(“f-open”),给它一个文件名,"w"
表示“写入”(到一个输出文件)。文件信息存储在一个类型为FILE*
的指针中,如果fopen
失败的话,这个指针就是NULL
,相当于 C++ 的nullptr
。
关闭文件只是将文件指针发送到fclose
,如图所示。
在这两者之间,通过添加file
作为第一个参数,将printf
修改为fprintf
。
如果你想读而不是写,打开文件,用"r"
表示“读”,并类似地修改scanf
:
file = fopen ("newfile.txt", "r");
if (!file) { printf("Can't open newfile.txt!\n"); return 0; }
fscanf (file, "%s %s %s %e", word1, word2, word3, &number);
fclose (file);
如果成功,fscanf
和scanf
返回您给出的参数个数。如果号码不一样,那就是出了问题。可能您已到达文件末尾。诸如此类测试:
while (1) // while true
{
if (fscanf (file, "%d", number) != 1)
/* it didn't work -- handle that */;
//...
}
示例 28-3 演示了这些功能。
// Program to test C's major standard I/O functions
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
FILE* file; // a file to write to or read from
float number; // number we'll read in and print out
enum { MAXSTR = 80 }; // array size
char junk [MAXSTR]; // a char array for reading in (and thus
// discarding) a word
// printing to file. The number gets 4 digits of precision
file = fopen ("newfile.txt", "w");
if (!file)
{
printf ("Can't open newfile.txt for writing!\n"); return 0;
}
printf ( "Avagadro's number is %.4e.\n", 6.023e+023);
fprintf (file, "Avagadro's number is %.4e.\n", 6.023e+023);
fclose (file);
// reading from a file
file = fopen ("newfile.txt", "r");
if (!file)
{
printf("Can't open newfile.txt for reading!\n"); return 0;
}
fscanf (file, "%s %s %s %e", junk, junk, junk, &number);
// Read in 3 words, then the number we want
fclose (file);
printf ("Looks like Avagadro's number is still %.4e.\n", number);
return 0;
}
Example 28-3fprintf, fscanf, fopen, and fclose
该文件将包含Avagadro's number is 6.0230e+023.
,屏幕输出将为:
Avagadro's number is 6.0230e+023.
Looks like Avagadro's number is still 6.0230e+023.
sprintf
和sscanf
;fgets
、fputs
、puts
这里还有几个 I/O 函数。
sscanf
**(“s-scan-f”)**从字符数组中读取。如果myCharArray
是"2.3 kg"
,我们可以说sscanf (myCharArray, "%f %s", &myDouble, myWord); // myDouble gets 2.3, myWord gets "kg"
,在 C++ 中我们会说
sscanf (myCharArray, "%f %s", &myDouble, myWord); // myDouble gets 2.3, myWord gets "kg"
sprintf
打印到一个字符数组:sprintf (myCharArray, "%s %f", name, number);
在 C++ 中我们会说
sstream myStringStream; myStringStream << name << number; string myString = myStringStream.str();
fgets
从文件中读取一行文本:fgets (myCharArray, MAX_STRING, someFile); // read myCharArray, // which should // be no more than // MAX_STRING long, // from someFile
或者键盘:
fputs
打印一个char
数组到一个文件:
fgets (myCharArray, MAX_STRING, stdin);
fputs (myCharArray, someFile);
您可以将该文件命名为stdout
,但通常我们会使用一个更短的版本:
puts
(myCharArray); //sends to stdout
我们不会将fgets
缩写为gets
: gets
存在,但被认为是不安全的,行为不像fgets
,因此不被使用。
示例 28-4 演示了这些功能。
表 28-1
printf
和scanf
的格式代码
%序列
|
意义
%d |
十进制格式的整数。 |
%o |
无符号八进制整数(基数为 8)。 |
%x/%X |
无符号十六进制整数(基数为 16)。十六进制 1f 将显示为0x1f/0X1F 。 |
%c |
性格。 |
%s |
字符数组。 |
%f |
定点浮点。 |
%e/%E |
科学记数法浮点。如果你说%E ,那么E 将是大写的。 |
%g/%G |
默认浮点。如果你说%G ,那么E (如果有的话)将会是大写的。 |
%p |
指针。 |
%% |
% 人物本身。 |
// Program to test sprintf, sscanf, fgets, fputs, puts
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
int main ()
{
while (1) // forever, or until break...
{
enum {MAXLINE=256}; // array size for line
char line [MAXLINE]; // a line of text
enum {MAXSTR = 80}; // array size for word
char word [MAXSTR]; // your word
int number; // a number to read in
// get an entire line with gets; on end of file quit
printf("Enter a line with 1 word & 1 number, end of file to quit: ");
if (! fgets (line, MAXLINE, stdin)) break;
// repeat line with fputs
printf("You entered: ");
fputs (line, stdout); // You *can* use fputs with stdout; puts is // more usual
// Use char array as source for 2 arguments
if (sscanf (line, "%s %i", word, &number) != 2)
puts ("That wasn't a word and a number!\n");
else
{
// Print using sprintf and puts
sprintf(line, "The name was %s and the number was %i.\n",
word, number);
puts (line);
// If this weren't a demo of new functions, I'd say
// printf ("The name was %s and the number was %f.\n",
// name, number);
}
}
puts ("\n\nBye!\n");
return 0;
}
Example 28-4A program using sprintf, sscanf, fgets, and so on
样本输出:
Enter a line with 1 word and 1 numbers, end of file to quit: Mila 18
You entered: Mila 18
The name was Mila and the number was 18.
Enter a line with 1 word and 1 numbers, end of file to quit: Catch 22
You entered: Catch 22
The name was Catch and the number was 22.
Enter a line with 1 word and 1 numbers, end of file to quit: [Enter Ctrl-D or Ctrl-Z here]
Bye!
命令摘要
在表 28-2 中描述的函数中,如果我没有给出函数返回的含义,那是因为我们很少关心那个函数。有了fopen
、fgets
和scanf
家族,我们做到了。
表 28-2
C 语言中常见的stdio
函数
printf
和变体
int``printf``...); |
根据formatString 的规定,在formatString 之后打印屏幕参数。 |
int``fprintf``const char* formatString, ...); |
与printf 相同,但打印到file 。 |
int``sprintf``const char* formatString, ...); |
与printf 相同,但打印到str 。 |
scanf 和变体 |
|
int``scanf``...); |
按照formatString 的指定,读取formatString 之后的参数。如果在读取任何文件之前到达EOF ,则返回EOF (文件结束);成功读取的 else #个参数。 |
int``fscanf``const char* formatString, ...); |
与scanf 相同,但从file 读取。 |
int``sscanf``const char* formatString, ...); |
与scanf 相同,但从str 读取 |
打开/关闭文件 | |
FILE*``fopen``const char* fileMode); |
打开由filename 指定的文件并返回指针。常见的fileMode 有"r" (读)"w" (写)"a" (追加)。 |
int``fclose |
关闭文件。 |
读/写字符串 | |
int``puts``int``fputs``FILE* file); |
打印str /打印str 到file 。 |
char*``fgets``FILE* file); |
从file (可能是stdin )读取str ,最多读取max-1 个字符(所以str 的大小应该是max 或者更大)。失败时返回NULL 。 |
防错法
scanf
因缺少&:
而失败scanf ("%f %s", myDouble, myCharArray);
应该是
scanf ("%f %s",``&
很容易忘记
&
,编译器可能不会警告你。
用*
传递参数
C 没有&
参数,但和 C++ 一样,它认为一次函数调用不会改变其他参数。哦哦。
void swap (int arg1, int arg2)
{
int temp = arg1; arg1 = arg2; arg2 = temp;
}
int main ()
{
int x, y;
...
swap (x, y); // x, y will not be changed
...
}
c 希望您发送参数的地址:
int main ()
{
int x, y;
...
swap (&
x, &y); // x's and y's addresses are sent, not x and y
...
}
该函数获取该地址,并使用*
来引用它所指向的东西,其中可以被改变:
void swap (int* arg1, int* arg2)
{
int temp = *arg1; *arg1 = *arg2; *arg2 = temp;
}
有效!但是它很笨重,并且引入了一个令人恼火的常见错误:忘记*
的。
示例 28-5 展示了一个使用它的程序(并且不要忘记*
)。
// Program to do statistics on some strings
// from _C++20 for Lazy Programmers_
#include <stdio.h> // for printf, scanf
#include <string.h> // for strlen
void updateLineStats (char line[], unsigned int* length,
float* averageLineLength);
int main ()
{
printf ("Type in a line and I'll reply. ");
printf ("Type the end-of-file character to end.\n");
while (1) // forever (or until a break) ...
{
enum { MAXSTRING = 256 }; // max line length
char line [MAXSTRING]; // the line
int length; // its current length
float averageLineLength;
// get line of input
if (!fgets (line, MAXSTRING, stdin)) break;
// do the stats. We send addresses, not variables, using &
updateLineStats (line, &length, &averageLineLength);
// give the result
printf ("Length of that line, ");
printf ("and average so far: %d, %.2f.\n",
length, averageLineLength);
}
return 0;
}
void updateLineStats (char line[], unsigned int* length,
float* averageLineLength)
{
static int totalLinesLength = 0; // have to remember these
static int linesSoFar = 0; // for next time
// length is a pointer, so *length is the length
*length = (unsigned int) strlen (line);
// casting from size_t to unsigned int
// fgets included the final \n, but I won't count that:
--(*length);
++linesSoFar;
totalLinesLength += *length;
*averageLineLength = // and averageLineLength needs its *, too
totalLinesLength / ((float) linesSoFar);
}
Example 28-5Program using parameter passing with *’s in C
示例会话:
Type in a line and I'll reply. Type the end-of-file character to end.
alpha
Length of that line, and average so far: 5, 5.00.
bet
Length of that line, and average so far: 3, 4.00.
soup
Length of that line, and average so far: 4, 4.00.
防错法
- **" <变量>的间接级别不同"或"无法从<类型>转换为<类型> * "或"在不强制转换的情况下从整数生成指针。"**有各种各样的抱怨方式,但底线是很难记住在函数调用中放入
&
的,更难记住*
的 *every!时间到了!*你使用传入的变量。我从未找到解决办法。至少你知道这可能是出错的原因。
动态存储器
忘记new
、new []
、delete
和delete []
。c 的动态内存更简单,虽然更难看:
#include <stdlib.h> // for malloc, free
...
someType* myArray = malloc (myArraySize * sizeof (someType));
// allocate a myArraySize element array of some type
...use the array...
free (myArray); // throw it back
没有析构函数来帮助你记住释放东西——你只能靠自己了。
示例 28-6 将示例 14-3(一个使用动态数组的早期程序)改编为 c。
// Program to generate a random passcode of digits
// -- from _C++20 for Lazy Programmers_
#include <stdio.h>
#include <stdlib.h> // for srand, rand, malloc, free
#include <time.h> // for time
int main ()
{
srand ((unsigned int) time(NULL));// start random # generator
// NULL, not nullptr
int codeLength; // get code length
printf ("I'll make your secret passcode. "
"How long should it be? ");
scanf ("%d", &codeLength);
// allocate array
int* passcode = malloc (codeLength * sizeof(int));
for (int i = 0; i < codeLength; ++i)// generate passcode
passcode[i] = rand () % 10; // each entry is a digit
printf ("Here it is:\n"); // print passcode
for (int i = 0; i < codeLength; ++i)
printf ("%d", passcode[i]);
printf ("\n");
printf ("But I guess it's not secret any more!\n");
free (passcode); // deallocate array
return 0;
}
Example 28-6A C program using dynamic memory
Exercises
做第十三章和第十四章的练习,不包括那些用 SSDL 的。
二十九、继续 SDL
通过使用 SSDL,你已经朝着成为一名 SDL 程序员迈进了一大步。要坚持下去,你可以
甩掉 SSDL,找一个关于 SDL 的教程。你会看到很多你认识的东西。许多 SSDL 函数是 SDL 函数,前面有一个“S”(比如,
SDL_PollEvent
变成了SSDL_PollEvent
)。通常 SDL 函数需要多一个初始参数,通常是类型SDL_Window*
或SDL_Renderer*
,这两种类型你马上就能学会。您通常可以猜到需要什么(提示:名称中带有“Render”的函数可能需要SDL_Renderer*
)。或者,保留 SSDL,但扩展更多 SDL 功能,比如支持操纵杆。
无论哪种方式,看看 SSDL 背后隐藏的东西都是有用的。让我们从初始化和清理代码开始。
典型的 SDL 程序有一个版本的main
,看起来像示例 29-1 。
图 29-1
来自示例 29-1 、 29-2 和 29-3 的 SDL 程序。值得吗?
// An SDL program that does nothing of interest (yet)
// -- from _C++20 for Lazy Programmers_
#include <iostream>
#include "SDL.h"
#include "SDL_image.h"
#include "SDL_mixer.h"
#include "SDL_ttf.h"
int main(int argc, char** argv)
{
// initialization
constexpr int DEFAULT_WIDTH = 640, DEFAULT_HEIGHT = 480;
if (SDL_Init (SDL_INIT_EVERYTHING) < 0) return -1;
SDL_Window* sdlWindow
= SDL_CreateWindow ("My SDL program!",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
DEFAULT_WIDTH, DEFAULT_HEIGHT,
0); // flags are 0 by default
if (!sdlWindow) return -1; // nope, it failed
int rendererIndex = -1; //pick first renderer that works
SDL_Renderer* sdlRenderer
= SDL_CreateRenderer (sdlWindow, rendererIndex,
0); // flags are 0 by default
if (!sdlRenderer) return -1; // nope, it failed
SDL_ClearError(); // Initially, no errors
static constexpr int IMG_FLAGS // all available types
= IMG_INIT_PNG | IMG_INIT_JPG | IMG_INIT_TIF;
if (! (IMG_Init (IMG_FLAGS) & IMG_FLAGS)) // start SDL_Image
return -1;
if (TTF_Init() == -1) return -1; // ...and SDL_TTF
// ...and SDL_Mixer
int soundsSupported = Mix_Init
(MIX_INIT_FLAC|MIX_INIT_MOD|MIX_INIT_MP3|MIX_INIT_OGG);
if (!soundsSupported) return -1;
int soundInitialized = (Mix_OpenAudio(88020, MIX_DEFAULT_FORMAT,
MIX_DEFAULT_CHANNELS, 4096) != -1);
if (!soundInitialized) SDL_ClearError();
// if it failed, we can still do the program
// -- just forget the error
// STUFF YOU ACTUALLY WANT TO DO GOES HERE
// cleanup -- we're about to end the program anyway, but it's considered nice anyway
if (soundInitialized) { Mix_AllocateChannels(0); Mix_CloseAudio(); }
Mix_Quit();
TTF_Quit();
IMG_Quit();
SDL_DestroyRenderer(sdlRenderer);
SDL_DestroyWindow (sdlWindow);
SDL_Quit();
return 0;
}
Example 29-1A simple SDL program
在 SSDL,例 29-1 中的初始化代码是由SSDL_Display
和SSDL_SoundSystem
的构造器完成的。示例 29-1 有所简化。一个大问题是,我们不能在没有 SSDL 的情况下发射SSDL_Exception
(咄),所以我们用return -1;
来处理发射失败。
看它做什么:初始化 SDL(这个必须先做);创建窗口(好!);创建绘制或粘贴图像所需的“渲染器”;初始化图像和字体所需的 SDL 图像和 SDL TTF。如果出了什么问题,我们就放弃,因为没有这些东西你真的走不下去。
如果可以的话,它还通过初始化 SDL 混合器来支持声音。
清理代码关闭助手库,关闭窗口和渲染器,最后关闭 SDL。
显然,我更喜欢我的方式,因为它有条理、整洁,而且不必在每个新程序中键入所有代码;但是既然我们看到了这一切混乱的内部,我想我们会像游戏程序员经常做的那样把它留在main
里。至少我没有使用全局变量。
编写代码
那么我们能得到一个实际上能做些什么的程序吗?当然,但首先让我谈谈 SSDL 还隐瞒了什么:
许多 SSDL 类型代表 SDL 类型,通常是指针。
SSDL_Color
本质上是一个SDL_Color
。SSDL_Display
本质上是一个SDL_Renderer
*
和一个SDL_Window
*
。(如果您关心如何将它们传递给需要它们的 SDL 函数,请参见SSDL_Display
类定义,特别是两个用户定义的转换或转换操作符。)SSDL_Font
是一个TTF_Font
*
。SSDL_Image
是一个SDL_Texture
*
。SSDL_Music
是一个Mix_Music
*
。SSDL_Sound
是一个Mix_Chunk
*
和一个int
(针对频道)。SSDL_Sprite
是一个SDL_Texture*
加上许多字段,在一个复杂的调用中被发送到SDL_RenderCopyEx
(见SSDL_RenderSprite
)。这些类的存在主要是为了保护初学者免受指针的影响,并且每个人都不必自己进行动态分配和清理。
除了 RGB,
SDL_Color
和SSDL_Color
还有一个“alpha”成员,其范围也是从 0 到 255。0 表示完全透明,255 表示完全不透明。要使用它,你需要名字中带有“混合”的 SDL 函数。忘记
ssin
和sout
;你将使用TTF_RenderText_Solid
(见SSDL_Display::RenderTextLine
)。SDL 总是使用动态内存,但是你不能使用
new
和delete
。SDL 及其助手提供了自己的分配和释放函数,例如,SDL_CreateTexture
和SDL_DestroyTexture
;TTF_OpenFont
和TTF_CloseFont
。你必须使用它们。
好,让我们做点什么,看看 SSDL 是怎么作弊的。我会在屏幕上显示一个图像,然后等待有人按下一个键。呼-啊!
我将使用来自SSDL_LoadImage
和SSDL_RenderImage
的图像代码(在 SSDL 代码中搜索这些代码——它们一定在某个地方)。如果你在追寻自我,请这样做!–你会看到我省略了对SSDL_Display::Instance
的调用(这只是为了确保初始化代码首先被调用,我们已经这样做了)。我们不会拉伸图像,所以我将省略对stretchWidth
和stretchHeight
的引用,使用图像的实际大小。我根据需要重命名变量。再进行一点清理,我得到了示例 29-2 中的代码,它紧跟在示例 29-1 中的初始化代码之后。
// Draw an image
SDL_Surface* sdlSurface = IMG_Load("media/pupdog.png");
if (!sdlSurface) return -1;
SDL_Texture* image = SDL_CreateTextureFromSurface
(sdlRenderer, sdlSurface);
if (!image) return -1;
SDL_FreeSurface(sdlSurface);
SDL_Rect dst; // dst is where it's going on screen
dst.x = 0; dst.y = 0;
SDL_QueryTexture(image, nullptr, nullptr, &dst.w, &dst.h);
// get width and height of image
SDL_RenderCopy(sdlRenderer, image, nullptr, &dst);
Example 29-2Displaying an image in SDL
等待一把钥匙…我读了SSDL_WaitKey
,然后是它调用的东西,然后是它调用的东西*,最终可以构造出例子 29-3 中的怪物。它紧接在示例 29-2 中的图像显示代码之后。*
最后,我可以看到图 29-1 中显示的输出。
// Waiting for a response
SDL_Event sdlEvent;
SDL_RenderPresent(sdlRenderer); // display everything
bool isTimeToQuit = false;
while (!isTimeToQuit)
{
if (SDL_WaitEvent(&sdlEvent) == 0) return -1;
// handle quit messages
if (sdlEvent.type == SDL_QUIT) isTimeToQuit = true;
if (sdlEvent.type == SDL_KEYDOWN
&& sdlEvent.key.keysym.scancode == SDL_SCANCODE_ESCAPE)
isTimeToQuit = true;
if (sdlEvent.type == SDL_KEYDOWN)// Got that key? quit
isTimeToQuit = true;
}
Example 29-3Waiting for a keystroke in SDL
你知道吗,这很有效。而我只用了 100 行!
诚然,我在那里写了一些糟糕的代码:一切都在main
中。但是我已经在建造 SSDL 库的时候做了很好的编码工作(我希望如此)。如果我要写好的 ?? 代码,我会说
int main (int argc, char** argv)
{
const SSDL_Image PUPPY = SSDL_LoadImage("media/pupdog.png");
SSDL_RenderImage(PUPPY, 0, 0);
SSDL_WaitKey();
return 0;
}
游戏程序因糟糕的实践而臭名昭著:像这样的长函数、全局变量和指向 wazoo 的指针。当你开始为 SDL 编程时,你可以向每个人展示如何正确地编程。
防错法
有时我会在程序结束时崩溃:SDL 或一个助手库在它的部分清理中失败。我可能不应该这样做,但是我承认,我注释掉了清理代码。反正节目结束后就无所谓了。
收集
在 Unix 或 MinGW,为你的平台(MinGW 或 Unix)选择一个支持 SSDL 的Makefile
,并删除所有对 SSDL 的引用。
在 Microsoft Visual Studio 中,获取一个支持 SSDL 的项目(.vcxproj
,加上.vcxproj.filters
和.vcxproj.user
),加载它,并删除对 SSDL 的所有引用——也就是说,在“项目属性”“➤配置属性”下,针对所有平台(如果可能)和所有配置....
C/C++ ➤通用➤附加包含目录:取出 SSDL 包含的路径。
链接器➤通用➤附加库目录:取出 SSDL 库的路径。
链接器➤输入➤附加依赖:取出 ssdl
.lib
然后编译并运行,就像你处理 SSDL 项目一样。
更多资源
我认为最好的参考是 libsdl.org。关于 SDL 图像和其他的文档也在那里;你只需要找到它(我在网上搜索我想要的,它会带我去那里)。而且很难击败懒惰的福(lazyfoo.net)的教程。