C++程序设计(第3版)

978-7-115-51243-7
作者: [美]瑞克·莫瑟(Rick Mercer)
译者: 凌杰
编辑: 陈冀康
分类: C++

图书目录:

详情

本书是针对计算机科学专业领域的C++编程课程而编写的一本教科书,适合没有编程经验的学生,以及有其他语言编程经验的学习者。本书强调计算的基础,因为组织了和面向对象编程相关的内容。精选的内容都是传统教学中的重要主题,符合C++14及其以上标准,采用先讲对象的教学方法。

图书摘要

版权信息

书名:C++程序设计(第3版)

ISBN:978-7-115-51243-7

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。

著    [美] 瑞克·莫瑟(Rick Mercer)

译    凌 杰

责任编辑 陈冀康

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Simplified Chinese translation copyright ©2019 by Posts and Telecommunications Press

ALL RIGHTS RESERVED

Computing Fundamentals with C++, by Rick Mercer

Copyright © 2018 Franklin, Beedle & Associates Incorporated.

本书中文简体版由Franklin, Beedle & Associates公司授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式或任何手段复制和传播。

版权所有,侵权必究。


本书是以C++编程语言讲解计算基础知识和技能的实用教程。全书共13章。本书首先介绍了通过程序设计解决问题的思路和步骤,然后依次介绍了C++基础知识、函数的运用和实现、消息机制、成员函数、条件、循环、文件流、vector类、泛型容器和二维数组等技术及其C++编程实现技巧。在每一章中以及每章的最后,分别给出了自测题、练习题、编程技巧、编程项目等内容。附录部分给出了所有自测题的解答,供读者学习参考。

本书适合作为高等院校计算机专业程序设计等课程的教材,也适合专业程序员和想要学习C++编程的读者阅读参考。


《C++程序设计(第3版)》是一本用C++编程语言为计算机系学生讲解计算基础课程的教材,它的主要适用人群是没有任何编程经验以及有使用其他编程语言经验的学生。

《C++程序设计(第3版)》致力于利用面向对象编程的相关性和有效性来介绍计算基础概念。这本书中凝聚了我们数十年来的教学经验——我们知道如何才能最大限度地帮助学生学习他们在计算机专业中上的首门课程,如何将对象与类的关系解释得恰到好处,以及如何为学生的下一门课程打下坚实的基础。

作为一本教材,我们还为学生提供了很多教学上的特定安排,以帮助他们更好地学习编程、设计以及对象访问技术:

在第3版中,我们对要布置的编程项目做了大量改进,使它们更具趣味性和挑战性。这其中包括了我在亚利桑那大学开发的项目和课程测试的内容,事实上,这些“外来”的作业得到了学生们很高的评价。除此之外,第3版还在内容上做了缩减,我们移除了与继承、面向对象编程与设计、操作符重载以及递归相关的章节。因为这本书的使用者通常不会用到这些章节,而且我们也认为本书第2版的篇幅太过庞大了。在这一版本中,我们将把话题局限在CS1课程的传统范围内,并少量添加一些CS2课程的话题,比如带模板的泛型容器。

第3版还做了一些内容上的更新,使其相关内容能适应当前的C++14标准。我们在这一版中加入了一些C++的扩展,例如那个延误多时的关键字nullptr。当然,C++语言的大部分新增特性,比如线程之类的,就不在本书的讨论范围之内了。

如果想要编写一本内容扎实可靠的教材,学生和其他教师对它的反馈是至关重要的。在本书前两个版本的编写过程中,我有幸创办了一个小型讲座(规模为20到35名学生),并且与我所有的学生在实验室里共同工作了十年,我因此长期持续地跟踪了他们的学习进度并了解所遇到的问题,这些经历为我编写一部有的放矢的教材提供了莫大的帮助。为此,我必须要感谢我在宾夕法尼亚州的那些学生。

除此之外,我还有幸遇到了许多优秀的教育界人士,他们和我一样正在关心和思考这个问题,通过与他们的现场交流和在电子邮件上的探讨和辩论,我得到了不少新的想法,了解了不少情况,这些都为我编写一本高质量的教材提供了莫大的支持。为此,我必须把他们列出来一一致谢(若有遗漏,请原谅我的粗心大意),他们是:Gene Wallingford、Doug Van Weiren、David Teague、Marty Stepp、Dave Richards、Stuart Reges、Margaret Reek、Ken Reek、Rich Pattis、Allison Obourn、Linda Northrop、Zung Nguyen、John McCormick、Carolina McCluskey、Lester McCann、Mary Lynn Manns、Mike Lutz、David Levine、Patrick Homer、Jim Heliotis、Peter Grogono、Adele Goldberg、Michael Feldman、Ed Epp、Robert Duvall(这位是杜克大学的讲师,不是那位演员)、Ward Cunningham、Alistair Cockburn、Mike Clancy、Tim Budd、Barbara Boucher-Owens、Mike Berman、Joe Bergin、Owen Astrachan和Erzebet Angster。

最后,虽然多不胜数,但我还是要感谢一下对我30年职业生涯产生过各种影响的多位作者和推荐人。另外,我还要特别感谢一下Franklin Beedle&Associates的那些人:Jim Leisy(已故)、Jaron Ayres、Brenda Jones和Tom Sumner。

由于本书的审阅者不辞辛劳、仔细而严格地研读,我们得到了不少富有价值的批评和建议。当然,我也逐一对这些批评和建议做了认真的思考。在此,我要再次感谢本书所有审阅者对于这一版本和之前所有版本所做的无私奉献。

  Kristin Roberts

  大急流城社区学院

  Rich Pattis

  加州大学欧文分校

  Michael Berman

  罗文大学

  Seth Bergman

  罗文大学

  Robert Duvall

  杜克大学

  Tom Bricker

  威斯康星大学麦迪逊分校

  David Teague

  西卡罗来纳大学

  Ed Epp

  波特兰大学

  James Murphy

  加州大学奇科分校

  Jerry Weltman

  路易斯安那州立大学巴吞鲁日分校

  John Miller

  圣约翰大学

  Stephen Leach

  佛罗里达州立大学

  Alva Thompson

  南佛罗里达大学

  Norman Jacobson

  加州大学尔湾分校

  David Levine

  葛底斯堡学院

  H. E. Dunsmore

  普渡大学

  Howard Pyron

  密苏里大学罗拉分校

  Lee Cornell

  曼凯托州立大学

  Eugene Wallingford

  北爱荷华大学

  David Teague

  西卡罗来纳大学

  Clayton Lewis

  科罗拉多大学

  Tim Budd

  俄勒冈州立大学

  Jim Miller

  堪萨斯大学

  Art Farley

  俄勒冈大学

  Richard Enbody

  密歇根州立大学

  Van Howbert

  科罗拉多州立大学

  Joe Burgin

  得州理工大学

  Jim Coplien

  贝尔实验室

  Dick Weide

  俄亥俄州立大学

  Gene Norris

  乔治梅森大学

我在这里要特别感谢一下来自大急流城社区学院的Kristin Roberts。作为这本书的审阅者,她不仅对书稿提供了大量的反馈,还一直不断地鼓励我坚持完成本书的第3版。这是本书的一次重大更新,一些章节进行了重组,改进并增加了许多新的编程项目,总之,这本书现在可以说是焕然一新了。这一切很大程度上都要归功于Kristin。


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

本书提供如下资源:

要获得以上配套资源,请在异步社区本书页面中单击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。

如果您是教师,希望获得教学配套资源,请在社区本书页面中直接联系本书的责任编辑。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


本章提要

在本章中,我们将会介绍针对一个问题提出计算机解决方案需要做哪些事。首先,我们可能需要用一到两个段落来做一下问题的描述。然后,从理解这个问题的描述到具体实现一个可行的计算机解决方案,这个过程称为解决问题。总而言之,我们希望在学习完本章内容之后,你将能够理解:

解决问题的方法有很多种。在本章,我们首先要研究的是一个3步走策略,即分析、设计、实现策略。

步骤

具体活动

分析

理解待解决问题

设计

根据解决方案的概要设计出算法

实现

写出可执行程序的代码

接下来,我们将通过一个“计算课程成绩”的示例来逐一示范这个3步走策略中的各个步骤,看看它们在解决问题过程中所发挥的作用,并以此开始这门课程的学习。

程序的开发通常始于针对某个问题的研究或分析。这是很显然的,如果我们想要确定一个程序要执行哪些操作,当然先得理解该程序要解决的问题。如果该问题已经完成了书面化描述,我们就可以从阅读这个问题开始进入分析步骤了。

在分析一个问题的过程中,做好对程序所需信息数据的命名工作会是很有帮助的。例如,我们可能会被要求计算出特定飞机在特定气象条件(比如温度、风向等)下,在指定机场跑道上可以成功起飞时的最大重量。这时,我们就可以在分析问题时将这项要计算的信息命名为maximumWeight,并将计算该信息所需的信息命名为temperature、windDirection等。

虽然这些数据并不代表整个解决方案,但是它们的确表述了问题的某个重要部分。这些数据名称会是我们编写程序以及在程序中进行计算工作时要用到的符号,比如可能我们要计算的是飞机在temperature的值为19.0时的maximumWeight。总而言之,这些数据通常都要经过各种形式的操作或处理之后,才能得到我们所期待的结果。在这其中,有些数据得从用户那里获取,也有些数据得经过一些相乘或相加的运算,还有些数据得在计算机屏幕上显示。

在某些时候,这些数据的值会被存储在计算机的内存中。当程序运行时,相同内存位置上的值是会变化的。另外,这些数据值通常都会有一个类型,比如整数类型、浮点数类型、字符串类型或其他各种存储类型。对于这种用于在程序运行时存储这些可变值的内存区块,我们称之为变量

我们将会看到这些数据值施以某种特定行为意义的操作,这些特定的意义有助于我们将数据区分成由计算机显示的数据(输出),和计算出结果所需的数据(输入)。这些变量帮我们总结出了一个程序必须得做的事情。

通常情况下,我们都可以通过回答“给定输入能得到什么输出?”这个题目来更好地理解自己要解决的问题。因此,针对待解决的问题来进行举例往往是一个不错的思路。下面就是两个通过变量名的选择来精准描述其存储值的问题:

待解决的问题

变量

输入/输出

问题样例

每月还贷计算

amount
rate
months
payment

输入
输入
输入
输出

12500.00
0.08
48
303.14

计算莎士比亚的某指定剧本中某特定词的出现次数

theWork
theWord
howOften

输入
输入
输出

Muth Ado About Nothing
thee
74

现在来总结一下,我们在分析问题过程中需要:

1.阅读并理解待解决问题的书面说明。

2.定义用来表示问题答案的数据,以作为输出。

3.定义用户为获取问题答案必须要键入的数据,以作为输入。

4.创建一些问题样例,以作汇总之用(就像上面做的那样)。

当然,教材中的问题有时会提供清楚的变量名,以及输入/输出时用到的值类型(比如字符串、整数、浮点数等)。如果没有的话,它们识别起来也往往是相对比较容易的。但在现实中,对于相当规模的问题来说,分析问题这个步骤通常是需要花费大量精力的。

自测题

1-1.请基于英镑与美元之间的汇率转换问题,分别为用来存储用户输入值以及程序输出值的变量赋予有意义的命名。

1-2.针对“从拥有200张CD的播放器中选取一张CD来播放”这个问题,请分别设定用来表示所有CD以及表示用户所选择的那张CD的变量名。

问题分析示例

问题:请根据右侧的课程成绩估算表,用作业项目、期中考试和期末考试这三项的加权值计算出这一门课的成绩。

参考项

权重比

作业项目

50%

期中考试

20%

期末考试

30%

如前所述,问题分析的工作要从理解问题的书面描述开始,然后确定解决该问题所需要的输入和输出。在这里,先定义并命名输出的内容是一个不错的切入点。因为,输出内容中通常存储的就是这个待解决问题的答案,它会驱使我们去深入理解这个待解决的问题。

一旦我们定义好了解决问题所需的数据,并赋予它们有意义的变量名之后,就可以将注意力转向如何完成任务了。就这个特定的问题而言,它要输出应该就是实际的课程成绩,我们将这个要输出给用户的信息命名为courseGrade。然后为了让这个问题更具有通用性,我们要让用户自己输入产生计算结果所需的值。毕竟如果这个程序可以要求用户提供所需的数据,那么它以后就可以用来计算多名学生任何一门课程的成绩了。在这里,我们将需要用户输入的这些数据命名为projects、midterm和finalExam。这样一来,我们目前就已经完成了问题分析这一步骤中的前3个动作:

1.理解待解决的问题。

2.定义要输出的信息:courseGrade。

3.定义要输入的数据:projects、midterm和finalExam。

接下来需要有一个问题样例,它有助于我们创建一个测试用例(test case),以验证输入的数据和程序产生的输出结果。例如,当projects为74.0、midterm为79.0、finalExam为84.0时,其平均加权值应该为78.0:

(0.50 × projects) + (0.20 × midterm) + (0.30 × finalExam)
   (0.5 × 74.0)   +   (0.2 × 79.0)   +  (0.30 × 84.0)
      37.0        +      15.8        +      25.2
                         78.0

到这里,问题的分析步骤就算完成了,我们确定了用于输入/输出的变量,这有助于我们了解计算机解决方案需要做哪些事,同时还获得了一个现成的测试用例。

待解决的问题 变量 输入/输出 测试用例
计算某门课的成绩 projects 输入 74.0
midterm 输入 79.0
finalExam 输入 84.0
courseGrade 输出 78.0

自测题

1-3.请完成对下面问题的分析,这里你可能会需要用到一个准确的计算器。

问题:请基于某项投资的当前价值、投资期限(可能以年为单位)以及投资利率,估算出它的未来价值。在这里,投资利率和投资期限是步调一致的。也就是说,如果投资期限以年为单位,那么这里的投资利率就是年利率(例如8.5%,就是0.085);如果投资期限以月为单位,那么这里的投资利率就是月利率(例如,如果年利率是9%,那么月利率就是0.075)。其未来价值的计算公式如下:

future value = present value * (1 + rate)periods

设计这个概念背后所代表的是一系列动作,这其中包括为程序中的每个组件安排具有针对性的算法。而算法则是指我们在解决问题或达成某项目标的过程中所要完成的逐个步骤。一个好的算法必须要:

事实上,我们可以将烤制胡萝卜蛋糕的过程看成是一个算法:

如果这些步骤的顺序被改变了,厨师可能得到的就是一个滚烫的烤箱模具,里面放了一团鸡蛋与面粉的搅拌物。如果省去了其中的某一个步骤,那么厨师也不会烤成蛋糕,或许他只是点了一次火而已。当然,熟练的厨师通常是不需要这种算法的。但是,蛋糕制作原料的销售商可不能,也不该假设他们的客户都很熟练。总之,好的算法必须要按照恰当的顺序列出恰当的步骤,并且要详尽到足以完成任务。

自测题

1-4.烤制蛋糕的食谱通常会省略一个非常重要的动作,请指出上述算法中缺少的是什么动作。

通常情况下,算法中所包含的都是一些不涉及太多细节的步骤。例如,“在大碗中搅拌”并不是一个非常具体的动作描述,里面的食材配比是什么呢?如果我们现在的问题是要编写一个人类能够理解的蛋糕烤制算法,这个步骤就可以做进一步的改进,使其能指导厨师更好地安排食材配比。比如我们可以将该步骤改成“将牛奶倒入盛有鸡蛋与面粉的大碗中搅拌,直至其表面光滑”,或者为面包师将该步骤切分如下:

算法可以用伪代码来描述,甚至也可以用一种非程序员也能理解的语言来描述。由于伪代码面向的是人类,而不是计算机,因此用伪代码描述的算法在程序设计中是很有帮助的。

伪代码有极强的表达能力。一条伪代码通常可以表示多条计算机指令。另外,用伪代码来描述算法可以避免纠缠于标点错误或者与特定计算机系统相关的细节。用伪代码来描述解决方案允许我们将这些细节问题向后推,这可以让设计变得更容易一些。其实,写算法就相当于在做计划,程序开发者也可以用纸和笔来做这些设计,甚至有时可以直接在脑海中完成这些事。

解决问题通常需要用户完成一定的输入才能计算并显示出相应的信息。事实上,这种输入-处理-输出的动作流是如此的司空见惯,我们甚至可以把它视为一种模式,而且你们会发现这绝对是程序设计中最有用的几个算法模式之一。

模式可以是任何一种事物形式或设计,它的作用是将某些事物模型化或者提供某种行事指南。而算法模式就是一种用于辅助我们解决问题的指南。以下面的输入/处理/输出(Input/Process/Output,IPO)算法模式为例,我们可以用它来辅助解决第一个问题的设计,事实上,IPO模式可以辅助我们解决本书前5章中几乎所有程序的设计问题。

算法模式

输入/处理/输出

模式

输入/处理/输出(IPO)

问题

程序需要基于用户的输入来计算并显示我们所需的信息

纲要

1.获取输入数据
2.用某种有意义的方式处理数据
3.输出结果

代码示例如下:

int n1, n2, n3;
float average;

// Input
cout << "Enter three numbers: ";
cin >> n1 >> n2 >> n3;

// Process
average = (n1 + n2 + n3) / 3.0;

// Output
cout << "Average = " << average;

这是若干种算法模式中的第一种。在后面的章节中,我们会陆续看到Guarded Action、Alternative Action、Indeterminate Loop等其他算法模式。为了有效地使用一个算法模式,我们首先必须得熟记它。将IPO模式注册在心中,并在开发程序时能想起它,这样就会让我们的程序设计变得更容易。例如,如果你在数据中发现了无意义的值,有可能是你将程序的处理步骤放在了输入步骤之前,或者根本就跳过了输入步骤。

关于模式在解决其他类型问题时所能提供的帮助,我们可以参考Christopher Alexander在A Pattern Language[Alexander 77]这本书里的一段话:

每个模式描述的都是一个我们所在客观环境中反复出现的问题,及其解决方案的核心内容,通过这种方式构建的解决方案,可以让我们用上一百万次,无须用相同的方式构建两次解决方案。

尽管Alexander所描述的是用于设计家具、花园、大楼和城镇的模式,但他描述的模式也适用于计算领域问题的解决。在程序设计的过程中,IPO模式就是会反复出现,并指引着许多问题的解决方案。

IPO模式也可以用来指导我们解决之前那个课程成绩计算问题的算法设计:

3步骤模式

将模式应用到具体的算法中

1.输入

1.读取projects、midterm和finalExam三个变量

2.处理

2.计算出courseGrade的值

3.输出

3.显示courseGrade的值

当然了,算法的开发通常是一个迭代的过程,模式也只是提供了解决这个问题所必需的动作序列纲要。

自测题

1-5.在阅读上述算法的3个动作时,你发现其中缺失的动作了吗?

1-6.在阅读上述算法的3个动作时,你发现其中有什么不正常的动作吗?

1-7.如果对调上述算法中前两个动作的顺序,该算法还能正常工作吗?

1-8.上述算法的描述是否已经足够支持计算出courseGrade的值了?

很显然,我们在上面对计算课程成绩问题的处理步骤的描述是不够详细的,我们还需对它进行进一步的细化。具体来说就是,说清楚在处理过程中如何用输入数据计算出课程成绩。上面的算法中省略了我们在问题书面化描述中提到的加权值,所以我们在第二步中重新细化了处理步骤:

1.从用户那里获取projects、midterm、finalExam这3个数据值。

2.计算courseGrade = (projects × 50%) + (midterm × 20%) + (finalExam × 30%)。

3.显示courseGrade的值。

就像人们常说的那样,好的艺术家应该知道什么时候该放下画笔,并决定与此刻完成他的画作,这对他的成功是至关重要的。同样地,设计师也必须要知道什么时候该停止设计,那就是我们进入解决问题第三阶段——实现阶段的好时机。

现在,我们来总结一下到目前为止所取得的进展:

计算机本质上就是一种可编程的、用来存储、检索并处理数据的电子设备。事实上,程序员们也可以通过纸和笔来手动执行存储、检索与处理数据的动作,以此来模拟算法在电子设备中的执行过程。下面就是一个人工模拟的(非电子的)算法执行过程:

1.从用户那里检索到一些示例值并将它们存储起来:

    projects = 80
    midterm = 90
    finalExam = 100

2.再次检索这些值并用它们计算出courseGrade的值:

    courseGrade = (0.5 × projects) + (0.2 × midterm) + (0.3 × finalExam)
                    (0.5 × 80.0)   +   (0.2 × 90.0)  +  (0.3 × 100.0)
                         40.0      +      18.0       +     30.0
                                courseGrade = 88.0

3.将存储在courseGrade中的值显示成88% 。

下面,我们要带你预览一段完整的C++程序,由于对这里的许多编程语言上的细节问题,我们要到下一章中才会介绍,因此各位也不必期待自己能完全理解这段C++源代码。在此次此刻,我们只需要读懂这段源代码是对之前那个伪代码算法的实现就可以了。这里有projects、midterm、finalExam三个变量,代表的是用户的输入。另外,还有一个名为courseGrade的输出变量。这里的cout对象,发音是“see-out”,代表的是公共输出以及程序所产生的输出。输入部分用的则是cin对象,发音是“see-in”,代表的是公共输入。

/*
 * This program computes and displays a final course grade as a
 * weighted average after the user enters the appropriate input.
 *
 * File name: CourseGrade.cpp
 */
#include <iostream>   // for cin and cout
#include <string>     // for string
using namespace std;  // avoid writing std::cin std::cout std::string

int main() {
  // Explain what this program does.
  cout << "This program computes a weighted course grade." << endl;

  // Read in a string
  cout << "Enter the student's name: ";
  string name;
  cin >> name;

  // I)nput projects, midterm, and finalExam
  double projects, midterm, finalExam;

  cout << "Enter project score: ";
  cin >> projects;

  cout << "Enter midterm: ";
  cin >> midterm;

  cout << "Enter final exam: ";
  cin >> finalExam;

  // P)rocess
  double courseGrade = (0.5 * projects) +
                       (0.2 * midterm) +
                       (0.3 * finalExam);

  // O)utput the results
  cout << name << "'s grade: " << courseGrade << "%" << endl;
}

程序会话

下面是该程序计算一次加权课程成绩的过程:

Enter the student's name: Dakota
Enter project score: 80
Enter midterm: 90
Enter final exam: 100
Dakota's grade: 88%

测试这个重要的过程,可能,可以,并且也应该出现在我们解决问题的所有阶段中。这部分的实际工作量很小,但很值得做。只不过,在因为不做测试而遇到问题之前,你可能不会同意我这个观点。总而言之,测试相关的系列动作可以出现在程序开发的所有阶段中:

我们应该在针对问题编写程序之前(而不是之后)准备一个以上的测试用例,然后确定一下程序的输入值与预估输出值。比如,之前我们提到的输入值为80、90和100时,预估输出值是88%的情况,就属于这样的测试用例。当程序最终产生它的输出时,我们可以拿自己预估的结果与程序实际运行中的输出进行比对,如果预期输出与程序输出对不上,我们就要及时做出相关的调整,因为这种冲突表示该问题示例或程序输出有错,甚至有可能是两者都错了。

通过若干个测试用例的测试,我们可以有效地避免误认为只要程序能成功运行并产生输出,程序就是正确的。显然,输出本身也可能会出错!简单执行一下程序是无法确保程序正确的。测试用例的作用是确保程序的可行性。

然而,即使进行了详尽的测试,我们其实也未必能完全保证程序的正确性。E. W. Dijkstra就曾认为:测试只能证明程序中存在错误,无法证明其中不存在错误。毕竟,即使程序的输出是正确的,该程序本身也未必就一定正确。但测试还是有助于减少错误,并提高程序的可信度。

自测题

1-9.如果程序员预估当上述程序的3个输入都为100.0时,courseGrade的值也应为100.0,但程序显示的courseGrade的值却为75.0,请问是预估输出和程序输出中的哪一方出错了?还是双方都错了?

1-10.如果程序员预估当上述程序的输入projects为80.0、midterm为90.0、finalExam为100.0时,courseGrade的值应为90.0,但程序显示的courseGrade的值却为88.0,请问是预估输出和程序输出中的哪一方出错了?还是双方都错了?

1-11.如果程序员预估当上述程序的输入projects为80.0、midterm为90.0、finalExam为100.0时,courseGrade的值应为88.0,但程序显示的courseGrade的值却为90.0,请问是预估输出和程序输出中的哪一方出错了?还是双方都错了?

为了让输入的内容在程序中发挥作用,我们必须要在计算机内存中开辟一块“空间”来存储它们。关于这一点,C++之父Bjarne Stroustrup是这样说的:

我们将这样的一块“空间”称为一个对象。换而言之,对象就是内存中一块带有类型信息的区域,其类型规定的是这块“空间”内所能存储的信息种类,而被命名了的对象就叫作变量。例如,字符串要放在string变量中,整数要放在int变量中。大家可以将对象看作一个“盒子”,我们可以用它来存放该对象类型的值。

例如,在之前的程序中,我们就是用int类型来存储数字或整数的。在int变量上,我们可以执行包括加、减、乘、除在内的一系列操作。另外,这里需要提醒一下,C++中的乘法运算符是*(因为用x可能会带来某种混淆)。

double courseGrade = 0.5*projects + 0.2*midterm + 0.5*finalExam;

float和double这两个类型存储的是带有小数部分的数值(double是两倍大的float类型)。另外,C++的string类型中存储的是“Firstname I. Lastname”这样的字符序列,以及一个记录该字符串中字符数的整数。

对象是存在于计算机内存中的实体,我们可以通过一个对象所存储的值类型(它的属性)以及它所能执行的操作(它的行为)[Booch]来理解这个对象。也就是说,每个对象都应该有:

关于对象的名称、状态和操作这3个特征,我们在之前的课程成绩程序中其实都有说明。该程序用projects、midterm、finalExam这3个数字对象存储了来自键盘的输入。这些对象各自都存储了一个像79或90这样的整数[1]。并且这些对象可以执行输入、乘法和加法操作,以此计算出了courseGrade的值。另外,这些数字对象还用赋值操作完成了存储动作,用cout <<操作完成了输出动作,这样用户才能看到程序处理的结果。

首个程序中的对象特征:

名称 4个数字对象各自都有一个属于自己的名称做标识,比如其中的第一个对象名为projects
状态 projects的值是通过cin >>输入操作来设置的,而courseGrade的状态则是通过赋值操作(使用=操作符)来定义的。最后,courseGrade的状态又是在执行cout输出操作的过程中被检索的
操作 int对象上还可以执行加法(+)与乘法(*)这些其他操作[2]

在C++中,类型分为基本类型和复合类型两种。其中,基本类型所存储的是一个固定大小的、直接与硬件对应的值,这种类型确定的是其对象中可以存储什么值,以及可以在该对象上执行什么操作。对于int和double这样的数字类型来说,其对象所占的字节数在不同的计算机中是不一样的,这决定着该对象所能存储的取值区间。

数据类型

大小

通常情况下的取值区间(这是变化的)

short

2字节(16比特)

-32768到32767

unsigned short

2字节

0 到65535

int

4字节

-2147483648到2147483647

unsigned int

4字节

0 到 4294967295

unsigned long

8字节

0 到18446744073709551615

float

4字节

3.4E +/- 38(7位有效数字)

double

8字节

1.7E +/- 308(15位有效数字)

char

1字节

0 到 255

bool

1字节

true 或 false

复合类型是一种由其他类型来定义的类型,本书将会涉及的复合类型包括引用、函数、类、数组以及指针。举例来说,下面的string就是一个由字符和其他相关数据组成的引用类型,它可以找出某字符序列的长度,也可以从某一字符串中创建一个被指定了首尾索引的子字符串(在后续章节中,我们还会介绍更多相关的操作):

string aString = "A sequence of characters";   // Output:
cout << aString.length() << endl;              // 24
cout << aString.substr(2, 8) << endl;          // sequence

除了string类型之外,还有两个类型我们现在就已经使用到了,它们分别是:名为cin的istream对象——它的作用是从键盘和磁盘文件这样的输入源中读取数据;以及名为cout的ostream——它的作用是输出程序产生的内容。

自测题

1-12.请描述一下存储在double类型对象中的值。

1-13.请说出两个double对象的操作名称。

1-14.请描述一下存储在int类型对象中的值。

1-15.请说出两个int对象的操作名称。

1-16.请描述一下存储在string类型对象中的值。

1-17.上面哪种类型的对象中只存储一个值?

在这一章中,我们介绍了解决问题的分析、设计、实现3步走策略。下面我们用一张表来总结一下该策略的这3个阶段各自要执行的一些动作。除此之外,我们还添加了维护阶段,以补充这个3步走策略,使其成为一个完整的程序生命周期。毕竟,维护阶段的工作事实上占据了程序生命周期中大部分的时间、精力和金钱。

阶段任务

可执行的动作

分析

阅读并理解问题的书面说明,确认要用于输入/输出的对象,解决几个问题样例

设计

找出可用于指引算法开发方向的模式,写出一组解决问题所需要执行的算法步骤,在具体施行该算法的过程中进一步优化它

实现

将设计结果转换成编程语言,修复其中的错误,创建可执行的程序,测试该程序

维护

持续更新该程序,使其与时俱进,增强该程序;发现并纠正其中的bug

我们还介绍了一些用于分析和设计的工具:

我们还提供了示例程序,当然,我们要到下一章中才会介绍该程序中的许多细节,这里只是让读者了解一下C++中的基本类型和复合类型。

虽然测试很重要,但我们需要明白它不能证明程序中没有错误。当然,测试的确可以检测出部分错误,但这只能在某种程度上建构起我们对程序可行性的信心而已。

1.在分析问题的阶段,我们可以执行哪些动作?

2.一个好的算法应该具备哪些特征?

3.用于存储输出值的对象与用于存储用户输入值的对象之间有什么差异?

4.请列举出3个对象所具有的特征。

5.在设计程序的阶段,我们可以执行哪些动作?

6.怎样的设计成果是“可交付”的?

7.该用什么类型的对象来存储注册某一门课的学生人数?

8.该用什么类型的对象来存储π的值?

9.该用什么类型的对象来存储一部莎士比亚戏剧的剧本?

10.在程序开发中,实现阶段可交付的成果应该是怎样的?

11.该如何判断一个程序的运行是否正确?请证明你的判断。

12.请编写一个如何回到自己居住地的算法。

13.请编写一个可在电话簿中查找任意电话号码的算法。请问该算法始终能成功找到目标吗?

14.请编写一个能指引别人步行到你家的算法。

15.请设法获取你系统中能用于创建、编译、连接并执行一个C++程序所需的命令,这可能需要你登入自己的系统中,找出那些可用于基本编辑和编译程序的命令。在完成这件事之后,请你编写一个完整的算法,该算法要能指导一个新手完整地编写一个能通过测试的程序,你需要列出该过程中所有必要的步骤,比如“比对示例输入与程序输出”“创建新文件”“编译程序”等。

请编写一个算法,计算出3个权重相等的测试成绩的平均值。

请编写一个能根据以下权重比计算出课程成绩的算法:

成绩评估项    所占权重

小考平均分    20%

期中考试     20%

实验成绩     35%

期末考试     25%

假设我们碰巧知道了商家在出售CD播放机时通常要加价25%这个信息。在这种情况下,如果CD播放机的零售价(我们所支付的价格)是189.98美元,请问该商家进货时支付的价格(批发价)是多少?或者更一般地说,我们如何根据一个商品的零售价和商家对它的加价计算出该商品的批发价呢?请对该问题进行分析,并设计出一个能根据给定零售价和商家的加价计算出任意商品批发价的算法,你可以使用这个公式来计算批发价:retailPrice=wholesalePrice×(1+markup)。

请编写一个算法,使其能记录两列不同火车的出发时间(这里0代表凌晨零点、0700代表上午7:00、1314代表下午1:00后的第14分钟、2200代表的是晚上10点),并以小时加分钟的形式打印出这两个时间的差距。这里我们得假设双方的时间都在同一天,并且都得是有效时间。例如,1099不是一个有效时间,因为其最后两位数字代表的应该是分钟,它的取值范围应该是在00到59之间。同理,2401也不是一个有效时间,因为其前两位数字代表的是小时,它的取值范围必须在00到23之间。总之,在这种情况下,如果A列车是在1255出发,而B列车则是在1305出发,那么这两列火车的时间差应该就是0小时10分钟。

[1] 译者注:作者原文如此,实际上他用的是double类型的浮点数。

[2] 译者注:实际程序使用的是double对象,但并不影响这里的结论。


前章回顾

在第1章中,我们介绍了在实际程序开发中常见的分析、设计、实现3步走策略,并鼓励大家在编写C++代码之前先做一些分析与设计。然而,对于本书中的很多问题来说,实际上往往并没有什么具体的分析和设计可做。通常,所谓的分析可能只是“阅读问题”,而设计也可能只是“在脑海中构思一个算法”。

本章提要

在这一章中,我们将重点介绍如何用C++编程语言将算法转换成程序。而我们所键入的这份源代码将作为输入被传递给编译器,由编译器将该源代码转换成我们指定计算机所能理解的机器码。当然,编译器会要求源代码必须遵循某种精确的编程语言规范。因此,如果要想理解算法的伪代码是如何被转换成等效的编程语言的,我们就必须要了解组成一个程序的最小零件,并知道如何正确地将它们组建成相关的语句。另外,本章也会介绍许多对象可以执行的操作。我们希望在完成本章的学习之后,你将:

在原始状态下,C++程序不过只是存储在某种文件中的一段在字符序列而已,但这种文件的命名通常会以.cc、.c、.cp或.cpp结尾(比如first.cc、first.C或first.cpp),以此来表示该文件是一段C++程序。而某些编程环境往往需要或假定用户遵循这套文件命名约定,所以当我们为将某个算法转换成与之等效的C++编程语言创建相关文件时,也务必要使用这种约定的扩展名来命名文件。

而对于文件中所包含的文本,我们接下来也要引入某种C++程序的通用格式,这种通用格式是用来描述语法(语言使用规范)的,它也需要有符合编程语言结构的写法。和本书中其他所有地方一样,这种通用格式也将遵守以下约定。

1.以等宽字体印刷的元素可被原样使用。这其中既包括int maincoutcin这些关键字,也包括<<>>这样的符号。

2.以斜体印刷的这部分通用格式将必须由程序员来负责提供具体内容,比如expression表示程序员必须在该处提供一个有效的表达式。

3.以斜体印刷的实体项代表它在别的地方已被定义。

通用格式2.1:标准C++程序

// A comment
#include-directives
using namespace std;
int main() {
    statements
   return 0;
}

在上述通用格式中,以加粗字体显示的部分只需照原样编写即可。而statements所在的部分表示的是一个不同语句组成的集合。语句是程序所能执行的最小独立操作单元。事实上到目前为止,本章已经介绍到几条语句了。这里需要说明的是,虽然在C++标准中不是必需的,但本书的C++程序都会以“return 0;”结尾。另外,main函数的主体部分将会用一对花括号{ }框住,每个函数都有这样一个将其中所有代码视为一个整体的结构。

在进入更细节的讨论之前,我们先来看一段语法正确的标准C++程序。请以程序的形式运行一下这段代码,它必须要有一个名为main的函数。(提示:下面的std是standard的缩写。)

// This program prompts for a name and prints a friendly message
#include <iostream>   // for cout, cin, and endl
#include <string>     // for the string type

using namespace std;  // Allow programmers to write cin and cout
                      // rather than std::cin and std::cout
int main() {
  string name;
  cout << "What is your name: ";
  cin >> name;

  cout << "Hello " << name;
  cout << ", I hope you're feeling well." << endl;

  return 0;
}

程序会话

What is your name: Casey
Hello Casey, I hope you’re feeling well.

这段源代码将会被输入到编译器中,由编译器将这些源代码转换成机器码。在此过程中,编译器有可能会产生报错或警告信息。这些错误是编译器在扫描该程序的源代码以及该程序所有#include文件中的附加源代码时被检测到的。例如,在上述程序中,我们在int main()之前引入了一个名为iostream的文件,因此该文件中的源代码也成为这个程序的一部分。在这里,#include指令的作用就是用被#include文件的内容替换掉该指令所在的文本。

每个C++程序通常都会用到一两个由其他程序员所提供的源码文件。事实上,C++编译器本身就提供了大量的源码文件。下面,我们就来看看将其他文件中的源码加入到自己程序中的通用格式:

通用格式2.2:#include指令

#include <include-file>
          或
#include "include-file"

在这里,#include和尖括号< >或双引号" "的部分都只需照原样编写即可,只有include-file必须是已经存在了的文件名。例如,我们在程序中加入以下#include指令,为的就是让其提供cout、cin和endl这3个对象:

#include <iostream>

但是,这个#include指令实际上为我们提供的是std::cout、std::cin和std::endl。C++标准库(iostream只是它的一部分)是定义一个叫作std的名字空间的,为了避免频繁重复写std::,我们通常会在#include <iostream>以及其他#include指令后面加上下面这行代码:

using namespace std; // Can now write cout instead of std::cout

另外请注意,在< >或" "之间不能有任何空格。

#include <iostream >       // ERROR, space at end
#include " BankAccount.h"  // ERROR, space up front

通常,用尖括号< >所#include的文件应该必然属于系统的一部分,我们的系统应该可以自动找到这些文件。而被双引号" "所#include的文件则往往需要被存储到包含它们的程序所在的相同目录中。

在继续介绍对象初始化和语句的通用格式之前,我们希望先带读者来了解一下编程语言中那些用于构建起更大型结构的最小零件。这将有助于我们:

C++编译器读取源代码的过程,实际上就是它在逐一识别其中各种标记(token)的过程。标记是一个程序中最小的可识别组件,我们可以将其分成以下4类:

标记分类

具体示例

特殊符号

; ( ) << >>

关键字

return double int

标识符

main test2 firstName

字面常量

"Hello" 507 -2.1 true 'c' nullptr

特殊符号通常是一个由一到两个字符组成的序列,它往往代表着某一种特殊含义(有些也具有多重含义)。其中,有像“{”“;”“,”这样用来分割其他语言标记的特殊符号,也有像“+”“-”“<<”这种属于表达式操作符的。下面列出的是C++程序中一些被使用得比较频繁的单字符和双字符的特殊符号:

( )  .  +  -  /  *  =<  >=  //  {  }  ==  ;  <<  >>

标识符是我们给程序中各种事物赋予的名称,这些名称都要符合以下创建C++标识符的管理规则:

有效标识符

main cin incomeTax i MAX_SIZE
Maine cout employeeName x all_4_one
miSpelte string A1 n $motion$

无效标识符

1A 不能以数字开头
miles/Hour /是不可用字符
first Name 不能用空白符
pre-shrunk -代表的是减法操作符

C++有一个庞大的标准库,它们必然会占用掉一部分标识符。例如,名为cin的对象是用来获取用户键盘输入的,cout也是一个标准库标识符,它是终端输出对象的名称。下面所列出的这几个都是C++标准库所占用的标识符。(提示:下面的第一个标识符读作“end-ell”,作用是换行。)

endl  sqrt  fabs  pow  string  vector  width  precision  queue

程序员定义的标识符指的是创建该程序的程序员为后续的其他调用者和维护人员提供的标识符。例如,test1、finalExam、courseGrade这些是程序员定义的,也就是我们为自己所创建的标识符,所以请务必要用有明确含义的名称来表示它们的用途。

C++语言是严格区分大小写的,大写字母与小写字母代表的是不同的事物,“A”不等同于“a”。例如,每个完整的C++程序中都必须要包含main这个标识符,但MAIN或Main则不必。另外需要注意的是,程序员们在大小写的用法上会存在着一些约定俗成,有些程序员通常会尽量避免使用大写字母,有些程序员喜欢用大写来表示一些新的词汇。在本书中,我们将采用的是“camelBack”这种风格的写法,即将第一个单词之后每个单词的首字母设成大写。例如,我们将使用的是letterGrade,而不是lettergrade、LetterGrade或者letter_grade。对此,不同的程序员会有不同的风格。

关键字是一些具有特定用途的标识符,它们是语言标准所定义的保留字,比如像int和double这些都属于关键字。

C++关键字

break do for operator switch
case double if return typedef
char else int sizeof void
class float long struct while

C++区分大小写的特性也同样适用于关键字。例如double(这是关键字)与Double(这不是关键字)是不同的,C++的关键字始终为小写。

注释是程序中用于注解的一部分文本,我们对注释通常有以下预期(可能是其中任意一种,也可能是全部):

注释可以被添加在程序中的任何地方,可以是所有C++语句的右侧,也可以自行单独一行或若干行,它们通常先以/*这两个特殊字符开头,最后以*/收尾。

/*
  A comment may
  extend over
  several lines
*/

除此之外,注释的另一种形式是在相关的文本之前加上//,这种注释同样既可以是自行单独一行,也可以被附加在某一行的后面:

// A complete C++ program
int main() {
  return 0; // This program returns 0 to the operating system
}

在本书所涉及的这些程序中,我们对单行注释通常会采用// Comment而不是/* Comment */。原因是/*之后一直到*/之前的所有代码都会被视为一段注释,只要我们不慎忘记了在注释结尾加上一个*/,就会意外地让一大段代码变成注释。而单行注释就很难造成这种大段代码变成注释的情况。

这里需要提醒的是,我们添加注释的目的是为了澄清和记录源代码的用途,以便让程序更容易被理解、更容易被调试(纠正错误)以及更易于维护(并在必要时做一些修改)。很多时候,程序员们需要依靠这些注释来理解一些几天前、几周前、几个月前、几年前乃至于几十年前写的程序。

C++编译器可以自行识别字符串类型、整数类型、布尔类型(true/false)和浮点类型的字面常量。其中,字符串类型常量是由双引号括起来的0个或多个字符,并且所有字符都必须在同一行以内。

"Double quotes are used to delimit string constants."
"Hello, World!"

除此之外,整数类型常量是不带小数点的数字,浮点数类型常量是用小数点或科学计数法书写的常量(例如5e3 = 5 * 103 = 5000.0和1.23e-4 = 1.23 * 10-4 =0.000123),布尔类型常量即true和false。下面这些C++类型及其相应的常量示例是我们在本书会经常用到的常量对象。

类型

常量示例

int

0   1  999  -999  -2147483647  2147483647

char

'a'  '#'  '9'   '\t'(制表符)  '\n'(换行符)

double

1.23  0.5  .5  5.  2.3456e9  1e-12

bool

true  false

string

"Double quoted"  "Kim's"   "\n" ""(空字符串)

// Print a few C++ literals
#include <iostream>  // For cout and endl
using namespace std;

int main() {
  cout << 123 << endl;
  cout << 'a' << '\t' << 'm' << endl;
  cout << 1.23 << endl;
  // true prints as 1 and false as 0
  cout << true << " and " << false << endl;
  cout << "Hello \n world" << endl;

  return 0;
}

程序输出

123
a m
1.23
1 and 0
Hello
 world

自测题

2-1.在前面的程序中,我们使用了多少特殊符号?

2-2.请列出下面的有效标识符,并解释其余标识符无效的原因。

a.abc                 l.H.P.

b.123                 m.double

c.ABC                 n.55_mph

d.#include                o.sales Tax

e.my Age                 p.main

f.#define                q.a

g.Abc!                  r.å)

h.identifier                s.___1___

i.(identifier)              t.Mile/Hour

j.Double                  u.os

k.mispellted

2-3.请列举出两个单字符的特殊符号。

2-4.请列举出两个双字符的特殊符号。

2-5.请列举出两个属于标准库的标识符。

2-6.请创建两个由程序员定义的标识符。

2-7.对于以下标记:

'\n'  false  234  1.0  'H'  "'"  -123  1.0e+03  "H"  true

a.哪些属于有效的字符串类型常量?

b.哪些属于有效的整数类型常量?

c.哪些属于有效的浮点数类型常量?

d.哪些属于有效的布尔类型常量?

e.哪些属于有效的字符类型常量?

2-8.以下哪些属于有效的C++注释?

a.// Is this a comment?

b./ / Is this a comment?

c./* Is this a comment?

d./* Is this a comment? */

声明(declaration)语句的作用是将一个或多个对象的名称引入到程序中,而初始化(initialization)语句的作用除了将对象的名称引入到程序中之外,还附带着会按照程序员意图为该对象设置一个初始。而程序员们会在之后对它们的当前值有兴趣或者需要修改那些值时使用到这些变量名。下面我们就来看看声明或初始化一个基本类型和复合类型变量的通用格式:

通用格式 2.3:声明语句(某些类型自身会具有一个默认初始化状态)

type identifier ;

通用格式 2.4:初始化语句(声明一个变量并赋予它一个值)

type identifier = initial-state ;

这里的type既可以是一个浮点数类型double,也可以是一个像string这样的用于存储一组字符的复合类型(事实上,现有的其他复合类型还有很多)。

在下面的代码中,我们声明了一些变量,也初始化了一些变量。请注意,每一条语句都要以分号(;)结尾。

int credits;        // credits is some random integer
double points;      // points is some random floating point number
double GPA = 0.0;   // GPA is initialized to 0.0
bool boolOne;            // boolOne could be either true or false
bool boolTwo = true;     // boolTwo is true
string firstName;              // firstName is the empty string ""
string middleName = "James";    // middleName.length() is 5
string lastName = "Potter";     // lastName.length() is 6

这里需要指出的是,int、double、bool这些基本类型在行为上与string其他复合类型之间有着若干的不同之处。数字类型在被声明时,它们的初始值通常是未知的,而string对象在没有被显式初始化的情况下也会有一个空字符串""来充当其默认的初始值。

下面这张表总结了上述对象的初始状态,如你所见,其中有些对象属于未知状态。这些变量被声明了,但未被初始化。它们的值事实上就是该程序运行时其所在内存中的内容。也就是说,这些变量在不同的程序运行期间会有不同的值。

变量名称

对象状态

credits

未知

points

未知

boolOne

未知

boolTwo

true(可能还会打印出1)

GPA

0.0

fistName

""

middleName

"James"

lastName

"Potter"

程序与其用户之间的通信,通常是通过键盘输入和屏幕输出来完成的。虽说并不只局限于这两种方式,但是在本书中它们是我们许多编程项目的关键组成部分。

通用格式 2.5:cout语句

cout << expression-1 << expression-2 , . . . , << expression-n << endl;

在这里,对象名cout(读作“see-out”,是common output的简写形式)表示我们将把信息输出到控制台中。然后,从expression-1expression-n这部分格式具体既可以是GPA和firstName这样的对象名称,也可以是"Credits: "和99.5这样的常量。接下来是操作符<<,它代表了数据流动的方向。最后,每一条语句都要以一个分号(;)结尾。下面,我们来看一些使用了endl这个标识符(读作“end-ell”)的合法输出语句,如你所见,该标识符的作用是换行。

cout << 99.5 << endl;
cout << "Show me literally too" << endl;
cout << "First Name: " << firstName << endl;
cout << "Credits: " << credits << endl;

当一条cout语句被执行时,其相关的表达式就被插入到了一条导向计算机屏幕的数据流中。这些表达式的输出顺序与它们在语句中出现的顺序是相同的,也就是从左向右的顺序。当它们遇到endl这个表达式时,就会另起一行,因此后续的输出会从新的一行开始。

cout << 'A' << " line " << true << " " << 123 << 4.56 << endl;

程序输出

A line 1 1234.56

自测题

2-9.请初始化两个用来表示数字的对象,并将它们的初始值设为-1.5。

2-10.请声明一个可以用来表示街道地址的对象,并将其命名为address。

2-11.请编写一个完整的C++程序,用它来逐行显示你在程序中用过的所有名称。

赋值(Assignment)语句的作用是设置对象的状态,它会用=右边表达式的值替换掉=左边对象中的值。

通用格式 2.6:赋值语句

object-name = expression;

在这里,expression必须得是一个可以被存储到赋值操作符(=)左边对象中的值。例如,表达式产生的浮点数结果可以存储到一个数字类对象中,字符串表达式(一组用双引号""括住的字符)可以存储到字符串类型的对象中。下面让我们看几个赋值语句的示例:

double aNumber = -999.9;
string aString = "Initial state";

aNumber = 456.789;
aString = "Modified state";

在上面这4个赋值操作被执行完之后[1],两个对象的状态都被修改过了,它们当前的状态如下所示:

对象

状态

aNumber

456.789

aString

"Modified State"

另外,=右边的值必须要与左边的变量类型是赋值兼容(assignment-compatible)的,只有这样赋值操作才能正常执行。例如,一个string常量是不能被赋值给一个数字型变量的。

aNumber = "Ooooohhhh no, you can't do that"; // ERROR

同样的,一个double常量也不能被赋值给一个string对象。

aString = 12.34; // ERROR

通常情况下,编译器对上面这样的赋值语句是会报错的。但是,如果某个对象的类型可以被另一个对象的类型接受,编译器就会自动对其执行类型转换(type conversion)操作,这时就没有任何警告和报错信息了,只是相关变量可能会被赋予一个意外的值。

char c = 65;      // c becomes 'A'
bool b = 0;       // b becomes false
b = 42;           // b becomes true, actually 1
int n = b;        // n becomes 1, the integer for true
n = 5.9999;       // n becomes 5 due to truncation
double x = n;     // x becomes 5.0, but prints as 5
long l = n;       // l becomes 5, int promotes to long

自测题

2-12.针对在上述代码中被初始化的变量,以下哪些赋值操作会被报错?

a.b = -123;

b.n = 123.495678;

c.x = 123;

d.l = x;

e.c = 66;

f.ui = "abcde";

我们需要对那些没有意义的对象值保持警戒,因为这些值可能会带来一些不可预知的错误,我们要确保自己定义的所有对象都经过了初始化、赋值或键盘输入的设置。另外,我们一样要对类型转换保持谨慎,因为一旦转换出错,就会产生完全不同的结果值,这种情况在无符号类型与有符号类型混合使用时经常发生,所以最好不要这样做。总而言之,如果想在程序中正确地使用一个对象,我们的操作必须兼顾以下3个特征:

为了让程序具有更好的通用性(比如现在我们要查找任意一名学生的课程成绩),其对象状态通常就需要交由键盘输入来设置。这样就可以让用户输入任何他所需的数据了,这种输入是由名为cin(读作“see-in”,是common input的简写形式)的输入流对象及其操作符>>来提供的。例如,在下面的语句中,我们将两个对象的状态修改成了由用户提供的数据:

cin >> firstName;     // User must input a string
cin >> credits;       // User must input a number

下面我们来看一下cin输入语句的通用格式:

通用格式 2.7:cin语句

cin >> object-name;
        或
cin >> object-name-1 >> object-name-2 >> object-name-n;

这里的object-name必须是一个可接受键盘输入值的类实体。这本书中的许多对象定义(当然,不是所有)采用的就是这种形式,我们会用cin输入来定义int、double、string这些类型的对象。

当一条cin语句被执行时,程序就会暂停执行,等用户输入完相关的值并按下回车键之后再继续。如果一切顺利,这些被输入的值将会被转换成相对应的机器形态,并被存储为相关对象的状态。

除了回车键之外,被输入数据也会被一个或多个空格符分隔开来。这就会让程序在读取带空格符的字符串时遇到一些麻烦,比如说,对于一个人全名和地址,我们通常会这样编写代码:

string name;
cout << "Enter your name: ";
cin >> name;

然后我们会与该程序进行如下会话:

Enter your name: Kim McPhee

这时被存入name的是Kim,而不是我们所期望的Kim McPhee。Kim后面的空格符终止了这一次的输入内容。所以,如果我们想读取一行中所有的字符(包括空格符),这里需要执行的是getline操作:

getline(cin, name);

我们也可以编写可依次输入多个对象的cin语句。当然,在这样做的时候,我们必须要假设用户知道如何用空格符(敲空格键)、换行符(敲回车键)或制表符(敲TAB键)将这些对象分隔开。下面我们来演示一下各种分隔输入数据的方式:

#include <iostream>
using namespace std;

int main() {
  int a, b, c, d;
  cout << "Enter four integers: ";

  // Just need to separate input by a space, tab, or new line.
  cin >> a >> b >> c >> d;
  cout << a << endl;
  cout << b << endl;
  cout << c << endl;
  cout << d << endl;
  return 0;
}

以下是该程序3种可能的会话过程:

Enter four integers:

Enter four integers:

Enter four integers: 1

1 2 3 4

1 2

2

1

 

3

2

3

4

3

 

1

4

4

2

 

1

3

 

2

4

 

3

 

 

4

 

让这件事简单化的替代方案就是为每一次输入单独写一条cin语句。

本章的许多问题事实上都是在让我们编写算术表达式。算术表达式通常由运算符和操作数两部分组成。其中,运算符通常指的就是C++那些特殊符号+、-、/、*中的某一个;而操作数既可以是像之前test1这样的数字类对象,也可以是像0.25这样的数字常量。下面我们假设x是一个double类型的实体,那么在以下表达式的操作数就是x和4.5,运算符就是+。

x + 4.5

运算符和操作数将共同决定该算术表达式的值。

算术表达式最简单的形式就单纯一个数字常量或数字类对象的名称,不过它也可以是两个操作数加一个运算符的形式(如下所示):

算术表达式的形式

具体示例

数字类对象

x

数字类常量

10099.5

表达式 + 表达式

x + 2.0

表达式 - 表达式

x - 2.0

表达式 * 表达式

x * 2.0

表达式 / 表达式

x / 2.0

( 表达式 )

(x + 2.0)

上面定义的这些表达式还可以有更复杂的算术表达式,例如:

1.5 * ((x - 99.5) * 1.0 / x)

由于算术运算符的编写通常会用到多个常量、数字类对象的名称和运算符,因此其执行规则得要符合一般性表达式求值的需要。下面列出5个C++算术运算符以及它们操作数字类对象的顺序。

二元算术运算符

运算符

处理规则

*、/、%

在没有括号的情况下,乘法、除法以及取模(%)这3种运算符的求值是先于加法与减法的。换句话说,就是*、/和%(对int取模)的优先级要高于+和-,如果这些运算符在同一个表达式中出现了不止一个,则从最左边的那个开始求值

+、-

在没有括号的情况下,+与-这两种运算符的求值要在所有的*、/、%运算完成之后才会执行,顺序也是从左边开始。当然,括号可以覆盖掉以上这些处理规则

比如,以下表达式作用在操作数上的操作应该依次是:/、+、-。

2.0 + 5.0 - 8.0 / 4.0    // Evaluates to 5.0

下面我们用括号来改变一下该表达式在操作数上的顺序。

(2.0 + 5.0 - 8.0) / 4.0  // Evaluates to -0.25

在加了括号之后,/运算符是最后一个被求值的,而不再是第一个了。

以上这些处理规则针对的只是二元运算符。二元运算符的左右两侧通常各有一个操作数。下面我们来看只在右侧有操作数的一元运算符,这里先看一个同时包含了二元运算符(*)和一元负值运算符的表达式(-):

3.5 * -2.0 // Evaluates to -7.0

如你所见,一元运算符的求值要先于二元运算符(*):3.5乘以负2.0(-2.0),结果为负7.0(-7.0)。

当然,算术表达式通常是以对象名为操作数的,但C++对一个double对象的表达式进行求值时,这些对象名会被它们的状态所替代,请看下面这段代码:

double x = 1.0;
double y = 2.0;
double z = 3.0;
double answer = x + y * z / 4.0;

当程序运行时,被存储在变量中的值会被检索出来,我们得到的实际上是下面这个等效的表达式:

double answer = 1.0 + 2.0 * 3.0 / 4.0; // store 2.5 into answer

自测题

2-13.请对以下算术表达式进行求值:

double x = 2.5;
double y = 3.0;

a.x * y + 3.0
b.0.5 + x / 2.0
c.1 + x * 3.0 / y
d.1.5 * (x - y)
e.y + -x
f.( x - 2) * (y - 1)

在C++语言所提供的几种数字类型中,double和int或许是最常用的两种类型了。int对象表示的是一个有限范围内的整数。在某些时候,int可能是一个比double更正确的选择。int对象可以执行的操作与double基本相同(+、*、-、=、<<、>>),但也略有些不同。例如,小数部分不能存储在int对象中。在下面的赋值语句中,小数部分将会被丢失:

int anInt = 1.999;     // The state of anInt is 1, not 1.999

除此之外,/运算符在int和double这两种类型的操作数上所呈现的意义也是不尽相同的,比如3.0 / 4.0等于0.75,而3 / 4的结果却是0。也就是说,两个整数类型的操作数执行/运算的结果会是一个整数,而不是一个浮点数。这会发什么情况呢?情况就是两个整数相除得到的商还是一个整数,例如3除以4得到的商等于0。这就是相同的运算符(比如这里的/)在两个整数类型的操作数上所呈现的不同意义。

int对象的另一个不同就是它支持%运算符所代表的取模运算。例如,18 % 4的结果应该是18整除4之后的余数,等于整数2。下面我们用一段程序来说明这些差异,具体演示一下整数表达式中的%和/运算以及浮点数表达式中的/运算。在这个示例中,我们将会使用整小时、整分钟的整数形式来表示结果,而不再采用与之等效的小数形式了。

// This program provides an example of int division with '/' for
// the quotient and '%' for the remainder
#include <iostream>
using namespace std;

int main() {
  // Declare objects that will be given meaningful values later
  int totalMinutes, minutes, hours;
  double fractionalHour;

  // Input
  cout << "Enter total minutes: ";
  cin >> totalMinutes;

  // Process
  fractionalHour = totalMinutes / 60.0;
  hours = totalMinutes / 60;
  minutes = totalMinutes % 60;

  // Output
  cout << totalMinutes << " minutes can be rewritten as "
       << fractionalHour << " hours " << endl;
  cout << "or as " << hours << " hours and "
       << minutes << " minutes" << endl;

  return 0;
}

程序会话

Enter total minutes: 254
254 minutes can be rewritten as 4.23333 hours
or as 4 hours and 14 minutes

上面这段程序说明了即便int对象与double对象如此相似,但有时double类型依然会比int更合适,有时则正好相反。基本上,当我们需要带小数部分的数字对象时就选择double类型,而当我们需要进行纯整数操作时则应选择int类型。另外,在选择好类型之后,就必须要充分考虑一些算术运算符之间的差异。例如,尽管+、-、/、*这些运算在double类型的操作数上都可使用,但%运算就只能作用在两个int操作数上。

自测题

2-14.nickel中存储的是什么值?

int change = 97;
int nickel = 0;
nickel = change % 25 % 10 / 5;

2-15.当change被初始化成下列值时,nickel中存储的分别是什么值?

a.4
b.5
c.10
d.15
e.49
f.0

每当整数与浮点数的值分别出现在一个算术运算符两侧时,整数类型的操作数就会升格为等效的浮点数(例如3变成3.0),该表达式的结果会是一个浮点数。同样的规则也适用于一个操作数是int对象、另一个操作数是double对象的情况。

// Display the value of an expression with a mix of operands
#include <iostream>
using namespace std;

int main() {
  int n = 10;
  double sum = 567.9;

  // n will be promoted to a double and use the floating point/
  cout << (sum / n) << endl;

  return 0;
}

程序输出

56.79

自测题

2-16.请对以下表达式执行求值运算:

a.5 / 9
b.5.0 / 9
c.5 / 9.0
d.2 + 4 * 6 / 3
e.(2 + 4) * 6 / 3
f.5 / 2

在通常情况下,对象的状态在程序执行的过程中是可以被修改的。但在某些时候,让数据值在程序执行期间无法被修改可能会更方便一些。为了满足这方面的需求,C++为我们提供了const关键字。创建const对象的方法就是在为相关值指定标识符的同时加上const关键字前缀。实质上,这就是一个状态不能被赋值或流提取操作改变的对象。初始化一个const对象的通用格式就是在一般初始化语句前面加上const关键字。另外,const对象的名称通常会用大写字母来表示。

通用格式 2.8:初始化const对象

const type IDENTIFIER = expression;

例如,存储在const对象PI中的值是一个浮点数3.1415926,而对象TAX_RATE中的值是7.51%。

const double PI = 3.1415926;
const double TAX_RATE = 0.0751;
const string PAUSE_MESSAGE = "Press any key to continue . . .";

由于这些const对象所代表的值在程序执行过程中是不能被修改的,所以像“PI = PI * r * r;”这样的语句就会因为PI被声明成了const而报错。同样的,该值也不会被“cin >> PI;”这样的输入语句所修改。

我们在程序中要想从用户那里获取相关的值,通常是输出与输入操作一起使用的。因为程序必须要先用输出语句通知用户,然后才能执行输入操作设置相关对象的状态。这一动作序列太常见了,常见到成为一种固定模式。这种先提示再输入的算法模式主要由两个动作组成:

1.要求用户输入一个值(提示)。

2.获取相关对象的值(输入)。

算法模式

先提示再输入

模式

先提示再输入

问题

用户必须要输入的内容

纲要

1.提示用户要输入的内容
2.获取输入

代码示例

cout << "Enter your first name: ";
cin >> firstName;

如果将提示部分省略,程序就可能会变得很诡异,用户将无从知道他到底要输入什么内容。所以无论什么时候要求用户进行输入,我们都必须要先做好提示,通过编写代码精确地告诉用户我们想要的。总之,先输出提示,再获取用户的输入。

下面来看一下先提示后输入这个模式的具体实例:

cout << "Enter test #1: ";
cin >> test1;

另一个实例:

cout << "Enter credits: ";
cin >> credits;

通常情况下,我们只需要告诉用户他们需要提供值,然后用cin读取他们的输入即可。

cout << "the prompt for the_object: ";
cin >> the_object;

在下面这段程序中,我们使用了4次先提示再输入模式。除此之外,它还带我们回顾了对象初始化、赋值、输入、输出这些操作。该程序所描述的是一个更通用的计算课程平均成绩的方法。通过让用户输入数据,我们可以反复使用到不同的输入集,以产生不同的结果。另外,读者也可以自行留意一下这段实现中我们是否用到了IPO模式。

// This program uses input statements to produce a meaningful
// result that can be used in a variety of examples
#include <iostream>  // For input and output
#include <string>    // For the string class
using namespace std;

int main() {
  // 0. Initialize some objects
  double credits = 0.0;
  double points = 0.0;
  double GPA = 0.0;
  string firstName;
  string lastName;

  // 1. Input
  cout << "Enter first name: ";
  cin >> firstName;
  cout << "Enter last name: ";
  cin >> lastName;
  cout << "Enter credits: ";
  cin >> credits;
  cout << "Enter points: ";
  cin >> points;

  // 2. Process
  GPA = points / credits;

  // 3. Output
  cout << "Name    : " << firstName << " " << lastName << endl;
  cout << "Credits : " << credits   << endl;
  cout << "Points  : " << points    << endl;
  cout << "GPA     : " << GPA       << endl;

  return 0;
}

程序会话

Enter first name: Pat
Enter last name: McCormick
Enter credits: 97.5
Enter points: 323.75
Name    : Pat McCormick
Credits : 97.5
Points  : 323.75
GPA     : 3.32051

在输入数字类数据时必须要小心仔细一些,一旦我们输入某个非数字,形成了无效的数字输入,输入对象(cin对象)的“良好”状态就会被破坏,这可能会导致后续所有的cin语句被忽略掉。

自测题

2-17.请写出下面每段程序会话中的GPA值。

// This program uses input statements to produce a
// meaningful result that can be used for a variety of examples
#include <iostream>  // For cin, cout, and endl
#include <string>    // For the string class
using namespace std;

int main() {
  // 0. Initialize some numeric objects
  double c1 = 0.0;
  double c2 = 0.0;
  double g1 = 0.0;
  double g2 = 0.0;
  double GPA = 0.0;
  // 1. Input
  cout << "Credits for course 1: ";
  cin >> c1;
  cout << "  Grade for course 1: ";
  cin >> g1;
  cout << "Credits for course 2: ";
  cin >> c2;
  cout << "  Grade for course 2: ";
  cin >> g2;
  // 2. Process
  GPA = ( (g1*c1) + (g2*c2) ) / (c1+c2);
  // 3. Output
  cout << "GPA: " << GPA << endl;
  return 0;
}

程序会话1

Credits for course 1: 2.0
  Grade for course 1: 2.0
Credits for course 2: 3.0
  Grade for course 2: 4.0

a. ________GPA

程序会话2

Credits for course 1: 4.0
  Grade for course 1: 1.5
Credits for course 2: 1.0
  Grade for course 2: 3.5

b. ________GPA

程序会话3

Credits for course 1: 1.0
  Grade for course 1: 2.0
Credits for course 2: 4.0
  Grade for course 2: 3.0

c. ________GPA

在解决问题的阶段中,我们在程序实现过程中会遇到以下几种类型的错误和警告:

每一门编程语言都需要有一套用户严格遵守的语法规则。相信读者们一定已经注意到了,当我们要将算法转换成等效的编程语言时,一不小心就会违反这些语法规则,只要少一个“{”或“;”就会把事情完全搞砸。在C++编译器将源代码转换成计算机可以执行代码的过程中,编译器会:

当编译器认为语法规则被违反时,它就会报出编译时错误。在一个程序中的所有编译时错误被清除之前,我们是无法创建其相应的机器代码的。如果机器代码无法被创建,连接器自然也就无法创建可执行程序了。编译器在读取源代码的过程中会报出许多奇奇怪怪的错误信息。不幸的是,解读这些编译时错误信息是需要我们付出大量的训练、耐心并充分掌握C++这门编程语言才行的。为了让这种状况改善一些,我们在下表中为你列出了一些常见的编译时错误及其相关示例,并对它们各自的修正方法做了说明。(请注意:编译器所产生的错误信息可能会有少许的不同。)

编译器所检测到的错误

错误代码示范

正确代码示范

变量名被(空白符)拆分了

int Total Weight;

int totalWeight;

相关名称拼写错误

integer sum = 0 ;

int sum ;

缺失了一个分号

double x

double x;

相关字符串没有被关闭

cout << "Hello;

cout << "Hello";

相关变量没有被声明

cin >> testScore;

double testScore; cin >> testScore;

忽略了大小写区分

double X; X = 5.0; [2]

double x; x = 5.0;

忘了写函数参数

cout << sqrt;

cout << sqrt(x);

参数类型用错了

cout << sqrt("12");

cout << sqrt(12.0);

用了过多的参数

cout << sqrt(1.2, 3);

cout << sqrt(1.2);

忘了声明名字空间std

// cout is unknown

using namespace std;

编译器会产生许多错误信息,但这些错误是真真切切来自于我们的源代码的。所以每当编译器跟你唠唠叨叨的时候,请务必要记住:编译器是在尽心尽责地帮助你纠正错误,它是一个好朋友。

在下面的代码中,我们为你演示了几种编译器应该检测出来并报告的错误。由于编译器生成的错误信息是因系统而异的,因此我们在代码中用注释对错误的原因做了说明,这些说明并不对应任何特定编译器产生的编译时错误信息(况且编译器也有很多),你们各自的系统一定会产生不同的错误信息。

// This attempt at a program contains many errors--over a
// dozen. Add #include <iostream>, and there are only eight.
using namespace std;

int main { // 1. No () after main.
           // 2. Every cin and cout will generate an error
           //    because #include <iostream> is missing.
  int pounds;

  cout << "Begin execution" << endl    // Missing ; after endl
  cout >> "Enter weight in pounds: ";  // >> should be <<
  cin << pounds;                       // << should be >>
  cout << "In the U.K., you";          // Extra ;
       << " weigh " << (Pounds / 14)   // Pounds is not declared
       << " stone. " << endl           // Missing ;
  return 0;                            // Missing right brace }

编译器所产生的错误信息通常都会有一些隐晦难懂。当我们用某个特定的编译器编译上述程序时,可能报出的是6个错误(其他编译器可能是7个或两个),所有被报告的错误都会有一个预置的类型名称。但到了其他系统中,它们又会产生一批不同的错误,另一种UNIX编译器可能报出的是8个完全不同的错误。无论如何,对于编译时的错误信息,我们是需要花一点时间来熟悉的,并需要耐心观察这些编译时错误发生的位置。这些错误通常就位于这些错误信息所报告的行中,当然,有时候我们可能还要修复之前行中的错误。另外,请永远要记得先修复第一个错误,比如在这里,到23行才报出的错误,实际上可能只是第4行缺失的那个分号。

下面我们来看一下上述代码被纠正后没有错误情况下的样子,以及它被执行之后的程序会话:

// There are no compile time errors in this program
#include <iostream>
using namespace std;

int main() {
  int pounds;

  cout << "Begin execution" << endl;
  cout << "Enter your weight in pounds: ";
  cin >> pounds;
  cout << "In the U.K., "
       << "you weigh " << (pounds/14.0) << " stone." << endl;

  return 0;
}

程序会话

Begin execution
Enter your weight in pounds: 162
In the U.K., you weigh 11.5714 stone.

在这里,我们还应该注意到一个小小的编译时错误可能会导致一连串的错误。例如,如果我们在main()之后漏掉了一个“{”,就会导致clang的C++编译器报出11个错误。

#include <iostream>   // For cin and cout
#include <string>     // For the string class
using namespace std;

int main() // <- Without the left curly brace, there were 11 errors!
  double x;
  string str;
  cout << "Enter a double: ";
  cin >> x;
  cout << "Enter a string: ";
  cin >> str;
  return 0;
}

某次编译产生的编译时错误信息:

main.cpp:5:11: error: expected ';' after top level declarator
main.cpp:9:3: error: unknown type name 'cout'
main.cpp:9:8: error: expected unqualified-id
main.cpp:10:3: error: unknown type name 'cin'
main.cpp:10:7: error: expected unqualified-id
main.cpp:11:3: error: unknown type name 'cout'
main.cpp:11:8: error: expected unqualified-id
main.cpp:12:3: error: unknown type name 'cin'
main.cpp:12:7: error: expected unqualified-id
main.cpp:13:3: error: expected unqualified-id
main.cpp:14:1: error: expected external declaration

SunOS的C++编译器虽然只报一个错误,但是这似乎更难以理解:

 "{" expected not double

因此,修复了第一个错误可能就等于修复了许多错误。当然,修复了一个错误也有可能会导致编译器发现新的错误。总之,我们应该试着将精力放在编译器报告的第一个错误上。编译器通常(但也不总是如此)会将源代码中最接近错误的位置报告给我们,但错误可能会在被报告位置的前一行甚至是前几行。

另外,请记住代码中的所有语句必须要以分号“;”结尾,一旦我们漏掉了这个语句终止符,或者将它放在了不适当的地方,就必然会导致编译时错误。而且编译器在读完某一行之前,通常是不会发现这一行有分号缺失错误的,所以这类错误往往位于编译器所报告位置的前一条语句。

编译器也会产生一些警告信息,这些信息会帮助程序员们避免一些日后可能会发生的错误。比如,请看下面这段代码:

#include <iostream>
using namespace std;

int main() {
  double x, y;
  y = 2 * x;
  cout << y << endl;
}

自测题

2-18.请问上述程序会输出什么?

上述程序中存在一个错误,但编译器是捕获不了这种错误的。编译器会很愉快地将这段源代码转换成机器码,并用连接器将其建构成可执行程序。执行两次这个程序,我们会得到两个相当让人困惑的数字:第一次是1.09087e+82,第二次是1.39064e-309。如果我们换一个编译器,可能输出又变成了0。不过好在,有些编译器会给出如下警告:

Warning: Possible use of 'x' before definition in main()
Warning: 'x' is used uninitialized in this function

这段警告信息告诉我们,x这个变量在定义(实际上应该说初始化)之前就已经被使用了。这是一个很有帮助的、不可忽视的警告。上述程序没有对x初始化,这会让它处于某种未知状态,在某些情况下就等于是一个垃圾变量。不幸的是,不是所有的编译器都能对这个潜在的错误提出警告。

当然,声明变量时没有做初始化并没有违反任何C++的语法规则,但语法上没有问题,并不代表可以忽视这一类警告。我们应该要认真读这段警告信息,并确保在算术表达式使用到x之前这个对象已经完成初始化。

这只是我们会看到的其中一种警告,将来还会有更多的警告出现。我们可能会忽略很多警告信息,但警告基本上是在对程序中可能的内容错误和可能会出错的地方做出提示。所以如果我们的程序给出了不正确的结果,就请回头看看这些警告信息吧——或许能得到这个错误来源的线索。

计算机系统是用连接器将多段机器代码合并起来构建可执行程序的。除此之外,连接器还必须负责一些细节解析,比如定位main标识符所在的文件,如果在连接过程中没有找到main,连接器就会报告main这个符号没有定义的错误信息。如果遇到了这种情况,我们就必须要去确认一下自己的程序是否以int main()函数为起点了。

int main() {
  // . . .
}

请确保这里的main没有被输入成mane、Main或MAIN。

另外,当我们有两个文件中同时存在int main()时也一样会导致连接时错误。例如,我们可能在拥有两个程序的文件夹中构建编程项目。下面这段连接时错误信息就告诉我们,在名为src这个目录中,initials.cpp和average.cpp两个文件中都存在int main()函数。

ld: duplicate symbol main () in ./src/initials.o and ./src/average.o

解决这个问题的一种方案就是不要在它们之间进行连接。如果我们使用的是Eclipse、Visual Studio、Xcode之类的集成开发环境,那就得自己确保项目中只有一个main函数了。

在我们排除了所有编译时错误、顺利用连接器创建可执行程序之后,就可以执行该程序了,但程序在运行过程也是会发生错误的。一个运行时错误会导致程序提前终止运行,通常是由于计算机遇到了一些它无法处理的事件。

例如,如果我们的程序需要用户输入一个整数,而他却输入了一个浮点数,这时候cin输入流就会被破坏。下面,我们可以来试着将同一个程序执行两次:第一次是良性输入;第二次是“破坏性”输入,比如用浮点数1.2来替代整数。

#include <iostream>
using namespace std;

int main() {
  int anInt, anotherInt;

  cout << "Enter anInt: ";
  cin >> anInt;
  cout << "anInt: " << anInt << endl;

  cout << "Enter anotherInt: ";
  cin >> anotherInt;
  cout << "anotherInt: " << anotherInt << endl;
  return 0;
}

在良性输入时,用户输入的是两个整数:

Enter anInt: 7
anInt is 7
Enter anotherInt: 9
anotherInt is 9

再来看看非整数输入,由于1.2是无法赋值给一个int变量的,因此它不会执行。然后第二次输入就不会再被允许执行了,用户即使输入了数字也无济于事(anotherInt会输出0,因为它事实上处于未定义状态)。

Enter anInt: 1.2
anInt: 1
Enter anotherInt: anotherInt: 0

即使在既没有编译时错误也没有运行时错误的情况下,程序也是有可能会执行不正常的。因为一个程序即便能正常运行到结束,也可能会得到不正确的结果。下面,让我们来对之前的程序做一点小小的修改,将其变成一个不正确的程序。

cout << "Average: " << (n / sum);

现在该程序的交互会话过程应该如下:

Enter sum: 291
Enter n: 3
Average: 0.010309

如你所见,该程序在执行输入操作时发生了意向性错误,这显然不符合它的原本意向。不幸的是,编译器是无法定位意向性错误的,毕竟n/sum在语法上是完全正确的,编译器不可能会知道程序员原本想写的是sum/n。

意向性错误通常是最诡异也最难纠正的一种错误,而且也往往很难被检测出来。用户、测试者、程序员都很难察觉到它们的存在。

自测题

2-19.假设我们现在有一个能根据给定集合的总和值以及集合元素的个数来求取平均值的程序,它的执行会话过程如下,你是否能从中看出什么导致意向性错误的线索?

Enter sum: 100
Number   : 4
Average  : 0.04

2-20.假设产生上述会话的是下面这段代码,该意向性错误该如何纠正?

cout << "Enter sum: ";
cin >> n;
cout << "Number :";
cin >> sum;
average = sum / n;
cout << "Average : " << average << endl;

2-21.请列出当我们将上述程序中的相关语句修改成下列语句时它们各自会产生的错误类型(编译时错误、连接时错误、运行时错误或意向性错误)和警告:

a.cout << "Average: " << "sum / n";
b.cout << "Average: ", sum / n;
c.cout << "Average: " << sum / n

即便进入了自动化阶段,开发人员已经按照工作顺序将产品发布给了客户之后,错误也依然会存在。因为有许多软件虽然可以工作了,但是它并没有做到该做的事,可能是该程序没有达到问题陈述的要求。会发生这种情况,通常是因为该软件的开发人员没有理解客户的问题陈述,他们可能遗漏了什么或者误解了什么。另外,当客户没有正确描述相关问题时,我们还会遇到一些相关方面的错误。会发生这种情况,通常是因为需求方无法确定需求,他们的需求说明可能过于琐碎或者出现了严重的纰漏。况且,有些需求方还经常会在我们开始解决问题之后改变他们的想法。

在大多数情况下,本书在每一章的末尾都会布置一个编程项目,并会有一份相应的问题说明。如果这些说明中有纰漏或者不容易理解的地方,请直接提出来,千万不要犹豫。在进入问题解决的设计和实现阶段之前,请务必要理解并掌握自己要解决的是什么问题。尽管不会有人故意为之,但问题说明经常会存在不正确或不完整的情况,这在现实世界中是很常见的。

1.请列出3种可作用于double这种数字类型的操作。

2.请描述一下string对象所存储的值。

3.请列出3种可作用于任何字符串对象的操作。

4.请列出4种C++标记的类型,并针对每个类型举出两个例子。

5.请指出以下标识符中的有效标识符。

a.a-one             b.R2D2

c.registered_voter         d.BEGIN

e.1Header              f.$money

g.1_2_3              h.A_B_C

i.all right            j.'doubleObject'

k.{Right}            l.Mispelt

6.请声明一个名为totalPoints、可用于存储一个数字的对象。

7.请写一条语句,将totalPoints的状态设置为100.0。

8.请写出以下程序在终端输入为5.2和6.3时所产生的会话过程。你必须要完整地写出用户提供的输入和程序输出的提示。

#include <iostream>
using namespace std;
int main() {
  double x = 0.0;
  double y = 0.0;
  double answer = 0.0;
  cout << "Enter a number: ";
  cin >> x;
  cout << "Enter another number: ";
  cin >> y;
  answer = x * (1.0 + y);
  cout << "Answer: " << answer << endl;
  return 0;
}

9.请编写一段C++代码,声明一个名为tolerance的数字类型对象,将值设置为0.001,并令其在程序执行过程中不可被修改。

10.请写一条语句,显示出一个名为total的对象中的值。

11.请根据以下两条初始化语句,写出存储到各对象中的值,或报告操作错误的信息。

string aString;
double aNumber = 0.0;

a.aString = "4.5";
b.aNumber = "4.5";
c.aString = 8.9;
d.aNumber = 8.9;

12.请用纸和笔编写一个完整的C++程序,先提示用户输入一个0.0到1.0之间的数字,然后将获得的输入值存储到一个名为relativeError的数字类型对象中,并回显这个输入(输出用户的输入)。整个程序的会话过程如下:

Enter relativeError [0.0 through 1.0]: 0.341
You entered: 0.341

13.假设x = 5.0、y = 7.0,请计算出下列表达式的值:

a.x / y
b.y / y
c.2.0 - x * y
d.(x*y)/(x+y)

14.请预测一下这两段程序会产生什么输出:

a

#include <iostream>
using namespace std;
int main() {
  double x = 1.2;
  double y = 3.4;
  cout << (x + y) << endl;
  cout << (x - y) << endl;
  cout << (x * y) << endl;
  cout << (x / y) << endl;
  return 0;
}

b

#include <iostream>
using namespace std;
int main() {
  double x = 0.5;
  double y = 2.3;
  double answer = 0.0;
  answer = x * (1 + y);
  cout << answer << endl;
  answer = x / (1 + y);
  cout << answer << endl;
  return 0;
}

15.当change分别被初始化下列值时,quarter的值各是什么?

a.0
b.74
c.49
d.549
int change = (0、74、49、549 这4个数字中的其中一个);
int quarter = change % 50 / 25;

16.下面这段代码是正确的吗?

const double EPSILON = 0.000001;
EPSILON = 999999.9;

17.请编写一段会产生运行时错误的C++代码,并说明产生错误的理由。

18.下面这段代码中的错误会在什么时候被检测到?

#include <iostream>
using namespace std;
int Main() {
  cout << "Hello world";
  return 0;
}

19.请详细说明以下各行代码中的错误该如何修复:

a.cout << "Hello world"
b.cout << "Hello world";
c.cout "Hello World";
d.cout << "Hello World;

20.请详细说明下面代码中的错误:

int main() {
  cout << "Hello world";
  return 0;
}

21.请解释一下意向性错误这个词。

22.请问“double average = x + y + z / 3.0;”这句代码是否能计算出x、y、z这3个double对象的平均值?

23.请计算出下列表达式的值,并用科学计数法来分别表示整数和浮点数。

a.5 / 2

b.5 / 2.0

c.101 % 2

d.5.0 / 2.0

e.1.0 + 2.0 - 3.0 * 4.0

f.100 % 2

24.请写出以下程序会产生的输出:

a

#include <iostream>
using namespace std;
int main() {
  const int MAX = 5;
  cout << (MAX / 2.0) << endl;
  cout << (2.0 / MAX) << endl;
  cout << (2 / MAX) << endl;
  cout << (MAX / 2) << endl;
  return 0;
}

b

#include <iostream>
using namespace std;
int main() {
  int j = 14;
  int k = 3;
  cout << "Quotient: "
       << (j / k) << endl;
  cout << "Remainder: "
       << (j % k) << endl;
  return 0;
}

c

#include <iostream>
using namespace std;
#include <string>
int main() {
  const string pipe = " ¦ ";
  cout << pipe << (1 + 5.5)
       << pipe << (3 + 3 / 3)
       << pipe << (1 + 2) / (3 + 4)
       << pipe << (1 + 2 * 3 / 4);
  return 0;
}

d

#include <iostream>
using namespace std;
int main() {
  int j = 11;
  cout << " " << (j % 2)
       << " " << (j / 2)
       << " " << ((j - j) / 2);
  return 0;
}

1.以分号终止语句:请确保每条语句都以“;”终止,但#include语句和int main()后面除外。[3]

#include <iostream>;  // Error found on this line
int main() ;          // Error found on this line
{

2.优先修复第一个错误:我们在编译时通常会收到很多错误信息,这时候不要惊慌,请先试着修复第一个错误,它可能会连带修复许多其他错误。当然,有时候修复一个错误也可能会导致其他错误出现。因为编译器在一个错误被修复之后才能检测到其他之前没有发现的错误。

3.对于一些学生来说,整数在算术运算中的行为常常出乎他们的预料。整数之间的除法运算产生的一定是一个整数,因此5 / 2的结果是2,而不是你大脑和计算直觉认为更正确的2.5。

4.%算术运算符返回的是一个int类型的余数。从教学经验来看,有很多学生不太理解%这个运算符,或者至少他们在期末考试中仍然会给出错误的答案。表达式a % b计算的是a被b整除之后得到的余数,它是一个整数。

99 % 50 = 49                101 % 2 = 1
99 % 50 % 25 = 24           102 % 2 = 0
4 % 99 = 4                  103 % 2 = 1

5.如果我们没有在代码中加入“using namespace std;”这行语句,那么在每次使用cin、cout、endl这些对象时都必须在它们之前加上std::这个前缀。

#include <iostream>    // For cout, cin, and endl
// using namespace std; Without this, prepend with std::

int main() {
  std::string name;
  std::cin >> name;
  std::cout << "Hello" << std::endl;
  std::cout << name << std::endl;
}

在AT&T设计C语言时,Dennis Ritchie曾建议将显示“Hello World! ”作为该语言的第一个程序,从那时起许多语言的第一个程序都将“Hello World!”作为一个传统延续了下来。现在,我们可以创建一个名为hello.cpp的文件,然后在其中输入以下代码。在保存该文件之后,我们就可以用自己所安装的工具编译、连接、运行这个程序了。

// Programmer: Firstname Lastname
// This programs displays a simple message.
#include <iostream>   // For cout
using namespace std;  // Allow cout instead of std::cout

int main() {
  cout << "Hello World!" << endl;
  return 0;
}

一个小小的编码错误往往会在编译时导致大量的报错信息——这常常会形成误导。例如,一个分号的遗漏可能会导致整个程序出现数十个错误。所以请记住,我们应始终优先修复第一个错误,从修复源代码中最先被检测到的错误开始入手。请你一字不差地输入下面这段代码,观察一下遗漏了一个左花括号之后会发生什么情况:

// Observe how many errors occur when { is missing
#include <iostream>    // For cout
using namespace std;   // To make cout known

int main() // <- Leave off {
  double x = 2.4;
  double y = 4.5;
  cout << "x: " << x << endl;
  cout << "y: " << y << endl;
  return 0;
}

1.请编译这段代码,然后写出产生的错误数量。

2.在int main()后加上“{”之后再次进行编译,然后将代码修改至无错状态。

3.移除“#include <iostream>”这条#include指令,看看编译后会产生多少错误信息。

4.复原“#include <iostream>”指令,然后移除main后面的(),看看编译后会产生多少错误信息。

5.注释掉“using namespace std;”,看看编译后会产生多少错误信息。

6.如有必要,你还可以继续编辑并编译这段代码,排除其中所有的错误,然后将其连接成程序并执行它。

请编写一段C++程序,用大型字母在屏幕上显示你的姓名缩写。该程序无须设置输入和处理步骤,只做单纯的输出就好。例如,假设你的姓名缩写是E. T. M.,那么它似乎应该就是由5条cout语句来输出的。

EEEEE      TTTTTTT          M     M
E             T             M M M M
EEEEE         T             M  M  M
E             T             M     M
EEEEE o       T      o      M     M o

请编写一段C++程序,先从用户那里获取任意3个字符串,然后将它们反序输出,彼此之间用空格符隔开。(提示:这个程序没有处理步骤,只有先输入再输出。)

Enter string one: happy
Enter string two: am
Enter string three: I
I am happy

请实现一个能根据下面加权比例计算课程平均分的C++程序,并测试它。

评估项目

加权比重

测验平均成绩

20%

期中考试成绩

20%

实验室成绩

35%

期末考试成绩

25%

该程序应该有如下会话过程:

Enter Quiz Average: 90.0
Enter Midterm: 90.0
Enter Lab Grade: 90.0
Enter Final Exam: 90.0
Course Average = 90

请编写一个读取秒值的程序,它会将读取的输入转换成小时、分钟、秒数的形式。下面是它的两个会话样例:

Enter seconds: 32123          Enter seconds: 61
8:55:23                       0:1:1

请编写一个C++程序,提示用户输入一个整数,以代表要找还给一个美国客户的金额(以美分为单位)。然后按照50美分、25美分、10美分、5美分、1美分的顺序依次输出各种币种在找还指定金额过程中所需的最小数量。(提示:你可以根据表达式的增长,动态运用/和%这两种运算符来计算出各币种所需的数量,或者也可以先用/算出所需的钱币总数,再用%算出剩余的找还金额。)然后请用各种输入验证该程序是否能正常工作,下面是我们提供的两个会话样例。

Enter change [0...99]: 83         Enter change [0...99]: 14
Half(ves)  : 1                    Half(ves)  : 0
Quarter(s) : 1                    Quarter(s) : 0
Dime(s)    : 0                    Dime(s)    : 1
Nickel(s)  : 1                    Nickel(s)  : 0
Penny(ies) : 3                    Penny(ies) : 4

据说Albert Einstein很自豪自己曾经出了一道谜题难倒了他的朋友们,该题目的要求是这样的:

如果一切顺利,这个游戏的观众应该会觉得很惊奇:一开始写下的这个1089,始终会与这个数学游戏的最终结果相同。现在请你将这个游戏复述成一个C++程序,然后在程序会话中看看用户输入为541时的结果。

Enter a 3 digit number ( first and last digits must differ): 541

541 -- original
145 -- reversed
396 -- difference
693 -- reverse of the difference
1089 -- difference + reverse of the difference

在该程序中,我们无须检查输入的3位数是否有错,就直接假设输入的数是在100到998之间,它的第一个数与最后一个数不相同,像101、252、989这样的数字是不会产生1089这个结果的。(提示:为了计算出两个数的差值,我们在这里需要调用abs这个求绝对值的函数。该函数的参数是一个执行两数相减运算的表达式。当然,要使用这个函数,我们还必须加上“#include <cstdlib>”这条指令。)

#include <cstdlib>   // A new include
#include <iostream>
using namespace std;
int main() {
  // abs is a new function that can return the difference
  // between two numbers by subtracting one from the other.
  cout << abs(541 - 145) << endl; // 396
  cout << abs(145 - 541) << endl; // 396
  return 0;
}

请编写一个C++程序,该程序会记录两列不同火车的出发时间(这里0代表凌晨零点、0700代表上午7:00、1314代表下午1:00后的第14分钟、2200代表的是晚上10点),并以小时加分钟的形式打印出这两个时间的差距。这里我们得假设双方的时间都在同一天,并且都得是有效时间。例如,1099不是一个有效时间,因为其最后两位数字代表的应该是分钟,它的取值范围应该是在00到59之间。同理,2401也不是一个有效时间,因为其前两位数字代表的是小时,它的取值范围必须在00到23之间。总之,在这种情况下,如果A列车是在1255出发,而B列车则是在1305出发,那么这两列火车的时间差应该就是0小时10分钟。我们在下面提供了一个该程序的会话样例。当然,你可以多试几组测试用例。

Train A departs at: 1255
Train B departs at: 1305

Difference: 0 hours and 10 minutes

[1] 译者注:原文如此,但事实上前两句执行的应该是初始化操作,在后续章节中我们应该会了解到,初始化操作调用的是对象的构造函数,而赋值操作调用的是operator=(),两者是完全不同的语法元素。

[2] 译者注:这里两处用的都是大写X,并没有错。作者可能是想定义一个小写的_x_变量,即"double x; x=5.0"。

[3] 译者注:前者为宏指令,后者是函数定义的一部分。


相关图书

代码审计——C/C++实践
代码审计——C/C++实践
CMake构建实战:项目开发卷
CMake构建实战:项目开发卷
C++ Templates(第2版)中文版
C++ Templates(第2版)中文版
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
计算机图形学编程(使用OpenGL和C++)(第2版)
Qt 6 C++开发指南
Qt 6 C++开发指南

相关文章

相关课程