像计算机科学家一样思考C++(txt+pdf+epub+mobi电子书下载)


发布时间:2020-08-03 23:15:33

点击下载

作者:[美] Allen B. Downey 著

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

像计算机科学家一样思考C++

像计算机科学家一样思考C++试读:

译者简介

黄鑫,毕业于西安交通大学。多年软件开发经验,对设计大型分布式系统有独到的见解。对将更多的开源项目引入Windows平台有浓厚的兴趣。目前致力于推广持续交付这一实践。希望通过自己的努力,让技术改变世界。微博:http://weibo.com/dummyone

夏思雨,1987年8月出生于陕西西安。2009年毕业于华中科技大学,2012年毕业于北京邮电大学。现就职于思特沃克软件技术(西安)有限公司,从事软件开发。微博:http://weibo.com/evainexia

作者简介

Allen B. Downey是美国Olin工程学院的计算机科学副教授。他曾经在Wellesley College、Colby College和U.C. Berkeley教授计算机科学课程。他在MIT获得学士和硕士学位,并且从U.C. Berkeley获得计算机科学博士学位。Allen基于自己教授计算机程序设计课程的经验,开创了“像计算机科学家一样思考(How to Think Like a Computer Scientist)”的教学理念和方法,并由此编写了几本程序设计语言的图书。其中,《Think Python》、《Think Complexity》由O’Reilly出版;《Think Java》、《Think C++》也广受关注和好评。第1章编程方式

本书旨在教会你如何像计算机专家一样思考。我喜欢计算机专家的思考方式,因为他们综合了数学、工程和自然科学的最佳特性。计算机专家像数学家一样,运用形式语言来表达思想(尤其是计算指令);又像工程师一样进行设计,将组件装配到系统里并对可替换的部件进行评估权衡;还像自然科学家一样,观察复杂系统的行为,形成假设并通过实验来证明预测。

解决问题是一个计算机专家应该具备的最重要的单一技能。该技能包括明确表述问题的能力,有创意地思考解决方案以及清楚准确地表述解决方案。人们后来发现,学习编程的过程是练习解决问题技巧的一个相当好的机会。这就是为什么本章叫做“编程方式”。1

同时,本书的另一目的是帮助你准备计算机科学AP考试。尽管我们可能并没有直接实现这一目标。比如,本书并没有很多类似AP考试题的练习。但从另一个角度说,如果你完全理解了本书中的概念和C++编程的细节,你就可以在考试中有一个良好的表现。1

注释:编者注:AP考试全称AdvancedPlacement,是美国大学预修课程。由美国大学理事会主持,AP成绩不但可以抵扣成功申请美国大学的同学入学后相应课程的学分,而且AP成绩也是美国各大学录取学生的重要依据。1.1 什么是编程语言

你即将学习的编程语言是C++。自1998年起的AP考试都以C++为基础。在这之前,采用的是Pascal。C++和Pascal都是高级编程语言,你可能听说过的其他高级语言有Java、C和FORTRAN。

你可能从“高级编程语言”这个名字中得知还有低级编程语言。低级编程语言一般指的是机器语言或者汇编语言。一般来说,计算机只能执行用低级语言编写的程序。因此,高级语言编写的程序需要先转换成低级语言再执行。高级语言的一个小缺点就是这一转换过程需要耗费一些时间。

但是,高级语言具有巨大的优势。首先,用高级语言编程要容易得多,这意味着该程序的编程时间较短,简明易读,正确性较高。其次,高级语言具有可移植的优势。这意味着用高级语言编写的程序只要经过略微的修改就可以在不同的计算机操作系统上运行。而用低级语言编写的程序只能在某一种计算机系统上运行,若要在另一种系统上运行,则需要重新编写代码。

鉴于这些优势,几乎所有的程序都是用高级语言编写。低级语言只应用在少数特殊场景中。

有两种将高级语言翻译成低级语言的方式:解释或者编译。解释器就是一个读取高级程序并执行的程序。实际上,解释器逐行翻译程序,交替读取代码行及执行命令,如图1-1所示。图1-1

编译器则会在执行命令前,一次性地将全部高级程序代码翻译成机器语言。通常可以将编译程序作为一个单独的步骤,稍后再执行编译后的代码。在这种情况下,高级程序称为源代码;编译后的程序称为目标代码或者可执行代码。

以下面这种情况为例,假设你用C++编写程序。你可能选择一个文本编辑器来编写程序(文本编辑器就是一个简单的文字处理器)。当程序编写完成时,可以将它保存为 program.cpp。program 是你自己命名的文件名,后缀.cpp 则表示文件为 C++源代码。

然后,根据编程环境,可以关闭文本编辑器,运行编译器。编译器会读取源代码,编译源代码并创建一个包含目标代码的新文件 program.o ,或者可执行文件program.exe,如图1-2所示。图1-2

下一个步骤就是运行程序,这一步需要程序的执行器。程序的执行器需要加载可运行程序(从硬盘复制到内存)并让计算机开始执行程序。

尽管这一过程看起来有点复杂,但是好消息是在绝大多数的编程环境(有时称为开发环境)中,这些步骤已经能够自动执行。一般来说,只需要编写一段程序,输入一条命令就可以完成编译和运行过程。另一方面,了解程序执行过程中有哪些步骤在后台运行是很有用的,这样在出错的时候你可以很快发现问题所在。1.2 什么是程序

程序就是详细说明如何进行一次计算的一个指令序列。该计算可能是数学计算,比如,解方程组或者计算多项式的根;也可能是符号计算,比如,在文件中搜索和替换文本或者编译一个程序(够奇怪了)。

不同编程语言中的指令(命令或者描述)看起来都不一样,但是每种语言都有一些基本的功能。

输入:从键盘或者其他设备读取数据和文件。

输出:向显示器或者其他设备输入数据,或将数据写入文件。

数学计算:完成基本数学运算,如,加法和乘法等。

测试:检查特定条件并按适当序列执行指令。

复现:在有一定可变性下重复执行某些动作。

不管你相信与否,这几乎是一个程序所有的功能。你所使用过的每个程序,不管多复杂,都是由或多或少类似这样的功能组成的。因此,描述程序的一个方法就是将大而复杂的任务划分成尽可能小的子任务,直到这些小的子任务可以用这些基本功能中的某一个完成。1.3 什么是调试

编程本身是一个复杂的过程,并且由人类而不是机器完成,所以经常会发生一些错误。由于一些奇怪的原因,程序中的错误称为bug,而追踪定位bug并且将其修正的过程则称为调试(Debug)。

程序中发生的错误有不同的种类,知道如何分辨不同的错误可以更快速地定位bug的位置。1.3.1 编译时错误

编译器只能编译语法正确的程序,否则会导致编译过程失败,无法运行程序。语法指的是程序结构以及与该结构相关的规则。

以英语语法为例,一个句子必须以大写字母开头,句号结尾。诸如“this sentence contains a syntax error.”和“So does this one”这样的两个句子都包含语法错误。

对大多数读者来说,少量语法错误并不是什么大问题。这就是为什么我们可以毫无障碍地阅读E.E.卡明斯的诗歌。

但是编译器并不是如此的宽容。如果你的程序中出现一处语法错误,编译器就会输出错误消息并且退出,而你就无法再运行自己的程序。

更糟糕的是,C++中具有比英语更多的语法规则,并且大多数时候你从编译器得到的错误消息都没有太大帮助。在你刚开始学习编程的时候,你很可能会花费大量的时间查找语法错误。不过随着你经验日益丰富,发生和查找错误需要的时间都会越来越少。1.3.2 运行时错误

第二种错误是运行时错误。将其称为运行时错误,是因为该错误只有在程序运行时才会发生。

接下来的几周我们要写的各种程序中,运行时错误很少发生。所1以你可能需要一段时间才会遇到。1

注释:这不是个好事情吗?——译者注1.3.3 逻辑和语义错误

第三种错误是逻辑和语义错误。如果程序中出现逻辑和语义错误,计算机不会产生任何错误消息,编译和运行过程都会成功。但是程序并没有做它应该做的,而是做了其他的事。只有在极少情况下它才会做你让它做的。

问题在于你写的程序不是你本意想写的程序,程序的意义(语义)有错误。识别逻辑错误是一件很棘手的事情,因为它需要你回头查看程序的输出并且尝试发现哪里出错了。1.3.4 实验调试

在本书的学习过程中,你应当获得的最重要的技能之一就是调试。尽管错误的出现让你沮丧,但是调试是编程过程中最需要脑力、最富有挑战性而且最有趣的部分了。

在某些方面调试就像侦察。你需要面对线索,推断出具体过程和事件,这些过程和事件能够得到你所看到的结果。

同时,调试又像是科学实验。一旦你想出来哪里可能出错了,你就会修改你的程序再次尝试。如果你的假设成立,你可以预测到修改后的结果并离可工作的程序更近一步。如果假设失败了,你需要提出一个新的假设。正如福尔摩斯所说,“当你排除了一切不可能的因素之后,剩下的无论看起来有多么不合理,也一定是事实。”(来自柯南道尔的《四个人的签名》)。

对一些人来说,编程和调试是同一件事情。也就是说,编程的过程就是逐步调试直到程序完成你想要的功能的过程。这种观点表明,任何时候你都应该从一个可以正常运行的程序入手,然后进行小的改动并调试通过,这样你的程序可以一直工作。

比如,Linux操作系统包含成千上万行代码,但是它最开始也只是 Linux Torvalds用于探索英特尔 80386芯片的简单程序。Larry Greenfield说:“Linus早期的工程之一就是一段在输出AAAA和BBBB之间切换的程序。然后进化成了Linux。”(来自Linux用户指导测试版1)。

在稍后的章节里,我会提出关于调试和编程练习的更多建议。1.4 形式语言和自然语言

自然语言是指人类表达的语言,比如,英语、西班牙语和法语。自然语言不是由人类设计(尽管人类尝试对其强加某些命令)的,而是通过自然演化的。

形式语言则是由人类为了某些特殊应用而设计的语言。例如,数学中使用的记号法就是一种特别擅长表示数字和符号间关系的形式语言。化学家使用某种形式语言来表示分子间的化学结构。而最重要的是:

编程语言是用于表达计算过程的形式语言。

正如我之前提到的,形式语言具有严格的语法规则。例如,3+3=6是一个语法正确的数学表达式,但是3=+6$就不是。同样,HO是一个语法正确的化学名词,但是Zz就不是。22

语法规则有两种:与标识有关的规则或者与结构有关的规则。标识是语言的基本元素,如单词、数字以及化学元素。3=+6$的问题之一是$不是数学里合法的标识(至少据我所知是这样)。类似地,因为化学里没有缩写为Zz的元素,所以 Zz也是不合法的。2

第二种语法错误是表达式结构的问题。所谓结构,就是标识的顺序。表达式3=+6$在结构上就不合法,因为不能在等号之后直接使用加号。类似地,分子表达式需要在元素名之后添加下标而不是之前。

当你阅读一句英文或者形式语言的一条语句,你需要找到它的结构(尽管这一行为在阅读自然语言时是无意识的)。这一过程称为语法分析。

举个例子,当你听到一句话:“另一只鞋掉了,”你会知道“另一只鞋”是主语而“掉”是动词。一旦你解析了一个句子的语法,你会了解它是什么意思,就是句子的语义。假设你知道鞋是什么意思和掉了是什么意思,你就会明白这句话的大致含义。

尽管形式语言和自然语言有很多共同的特性,但是他们在标识、结构、语法以及语义上有很多不同。

二义性:自然语言充满了二义性,需要人们根据上下文线索和其他信息理解。而形式语言几乎没有二义性,即形式语言的任何表达式都仅有一个含义,无关上下文。

冗余性:为了弥补歧义和减少误解,自然语言引入了很多冗余,结果自然语言通常都很啰嗦。形式语言则更简明扼要。

文学性:在自然语言中有很多习语和暗喻。如果我说另一只鞋掉了,很有可能指的不是鞋,也没有什么东西掉了。而形式语言则精确地描述了它们表达的意思。

习惯于自然语言的人们(每个人)要适应形式语言通常都很艰难。在某些方面形式语言和自然语言的差别就像是诗歌和散文,但是更甚。

诗歌:词汇的发音和意义都十分重要,而整首诗歌创造某种效果或者情感回应。诗歌中随处可见精心设计的双关语。

散文:相较于诗歌,散文中文字本身的含义更为重要,同时结构也具有更大的意义。散文虽然也有二义性,但是比诗歌容易分析。

程序:计算机程序的含义是纯字面且无歧义的,并且可以通过分析标识和结构将其完整理解。

关于阅读程序(还有其他一些形式语言)的几条建议如下。首先,形式语言比自然语言难以理解得多,需要花费更长时间来阅读。其次,形式语言的结构很重要。所以从头到尾的完整阅读并不是一个好方法。应该学会先将程序解析,识别标识和解释结构。最后,记住细节决定成败。像自然语言中无关大碍的错误拼写和标点符号等,在形式语言中可能产生很大的影响。1.5 第一个程序

按照惯例,人们用新语言编写的第一个程序叫做“Hello World”。因为它所做的所有事情就是输出“Hello,World”。在C++中,这个程序是这样的:

#include

// main: generate some simple output

void main ()

{

  cout << "Hello, world." << endl;

  return 0

}

有些人根据“Hello World”程序的简洁程度评判编程语言的质量。按照这个标准, C++做得相当不错。即便如此,这个简单的程序里依然包含着很难对编程新手解释的某些语言特性。现在,我们会先忽略其中一部分,比如第一行。

第二行以“//”开头,代表注释。注释是指在程序中插入的用于解释程序行为的一些文字。当编译器看到“//”时,它会忽略从该位置开始到行尾的整行内容。

第三行,你暂时可以忽略void,但是请注意main。main是指示程序入口点的特殊命名。当一个程序开始执行时,它从main中的第一条语句按顺序执行直到末尾,然后退出。

main中的语句数目没有限制,但是该例子只包含一条。这是一条基本的输出语句,表示在屏幕上输出或者显示一条消息。

cout 是由系统提供的允许你把内容输出到屏幕的特殊对象。<<符号是一个操作符,表示将一个字符串应用于cout。这会使该字符串显示在屏幕上。

endl也是一个特殊符号,代表一行的结束。当发送endl给cout时,屏幕上的光标会移动到下一行。所以,当下一次输出时,新的内容会在下一行显示。

像所有语句一样,输出语句也是以分号结尾。

你还需要注意这个程序中的其他一些小符号。首先,C++使用花括号(“{”和“}”)对内容分组。在这种情况下,输出语句包含在花括号里,意味着它在main的定义内部。同时,注意语句的缩进,它可以直观地显示该定义的内部都有哪些行。

现在,你可以坐在电脑前自己编译并运行这个程序。具体实现的细节取决于你的编程环境,但是从现在开始,我假定你知道应该如何做。

如前所述,C++编译器对语法检查很严格。当你编写程序时出现任何错误,编译都很有可能不成功。比如,你拼写错了iostream,你可能会得到以下错误消息:

hello.cpp:1: oistream.h: No such file or directory

这一行包含大量隐蔽密集的信息,并不容易读懂。一个更友好的编译器可能会这样表述:“hello.cpp源代码文件第一行,尝试引用头文件oistream.h。找不到该文件,只找到文件iostream.h。这是否可能是你需要的?”

遗憾的是,很少有编译器这么友好。编译器并不智能,大多数情况下你得到的错误消息只是一个关于错误的提示。学会解析编译器的消息需要花一些时间。

尽管如此,编译器依然是学习语言语法规则的有力工具。从一个可以正常运行的程序入手(比如hello.cpp),用不同的方法修改它并查看结果。如果你得到错误消息,记住错误消息的内容以及导致错误的原因,这样下次再看到的时候就能够知道它是什么意思。1.6 术语

问题解决(problem-solving):表述问题,找到解决方案并描述该解决方案的过程。

高级语言(high-level language):类似C++这样,为了便于人类读写而设计的编程语言。

低级语言(low-level language):为了便于机器执行而设计的编程语言。也称为机器语言或者汇编语言。

可移植性(portability):程序可以在不同计算机操作系统上运行的属性。

形式语言(formal language):人类设计的用于特殊目的语言,比如,用于表达数学思想或者计算机程序。所有编程语言都是形式语言。

自然语言(natural language):人类所说的经过自然进化得到的各种语言。

解释(interpret):逐句翻译高级语言编写的源程序,边翻译边运行。

编译(compile):一次性将高级语言程序翻译为低级语言,形成目标代码,为之后的执行做准备。

源代码(source code):用高级语言编写的未经过编译的程序。

目标代码(object code):编译器编译程序后的输出。

可执行程序(executable):可执行的目标代码。

算法(algorithm):解决同一类型问题的一般过程。

bug:程序中发生的错误。

语法(syntax):程序的结构。

语义(semantics):程序的含义。

解析(parse):检查一个程序并分析其语法结构。

语法错误(syntax error):程序中无法完成语法解析的错误(因此也无法编译)。

运行时错误(run-time error):在程序执行时导致程序失败的错误。

逻辑错误(logical error):程序中发生的导致程序偏离编程本意的错误。

调试(debugging):发现并解决三种错误的过程。第2章变量和类型2.1 输出更多

正如第1章提到的,在main函数中,可以按照意愿放任意多的语句。例如,可以输出更多行:

#include

// main: generate some simple output

void main ()

{

  cout << "Hello, world." << endl; // output one line

  cout << "How are you?" << endl; // output another

}

在行尾放置注释是合法的,把注释放到单独的一行也是合法的。

出现在双引号之间的短语是字符串,它由一个字符序列组成。实际上,字符串可以是任何字符、数字、标点符号和特殊字符的组合。

通常将多条输出语句输出的内容显示到一行是很实用的。可以通过把第一个endl移除的方式达到这个目的:

void main ()

{

  cout << "Goodbye, ";

  cout << "cruel world!" << endl;

}

在这个例子中,输出内容显示成一行,如“Goodbye, cruel world!”。值得注意的是,程序中的输出命令中“Goodbye,”和第二个双引号之间有一个空格。因为这个空格会出现在输出内容里,所以它是可以影响程序的行为的。

程序中出现在双引号之外的空格一般是不会影响程序行为的。例如,可以这样写:

void main ()

{

cout<<"Goodbye, ";

cout<<"cruel world!"<

}

这个程序会像之前的程序一样编译和运行。在每行之后的换行符(新行)也不会影响到程序的行为,所以也可以这样写:

void main(){cout<<"Goodbye, ";cout<<"cruel world!"<

尽管你会注意,程序变得越来越难读,但是它是可以工作的。新行和空格对于组织程序结构是十分有帮助的,它可以使程序更加便于阅读和定位语法错误。2.2 值

值是程序操作的一种基本单位,例如,字符和数字。目前为止,我们操作过的唯一值是我们输出的字符串,例如,“Hello,World.”。你(和编译器)能够分辨一个字符串值是因为它包含在双引号中。

还有一些其他类型的值,包括整数和字符。整数是一个完整的数字,例如,1或者17。可以用同样的方式输出整数值:

cout << 17 << endl;

字符值是一个字母或者数字或者出现在单引号之中的标点符号,例如,‘a’或者‘5’可以用相同的方式输出字符值:

cout << '}' << endl;

在这个例子中,一个单独的大括号会以单独一行的方式输出。

我们很容易混淆不同类型的值,例如,“5”、‘5’和5,但是如果能注意一下标点符号,你就会很清楚地知道,第一个值是字符串类型,第二个值是字符类型,第三个值是整数类型。稍后你就会明白这个区别如此重要的原因。2.3 变量

编程语言最强大的一个功能就是操作变量的能力。变量是指一个存储值的命名区域。

就像有不同类型的值(整数、字符)一样,变量也有不同的类型。当创建新的变量时,必须声明这个变量的类型。例如,在C++中称字符类型为char。下面的语句创建了一个类型为char名字为fred的新变量。

char fred;

这种类型的语句称为声明语句。

变量的类型表明了这个变量可以存储什么类型的值。char类型的变量可以存储字符,由此我们顺理成章地知道int类型的变量可以存储整数。

在C++中,有几种变量都可以存储字符串值,但现在我们暂时跳过这部分(参阅第7章)。

创建整型变量的语法如下:

int bob;

bob 是你随意为变量指定的一个名字。通常来说,你希望为变量指定一个更有意义的名字,这个名字可以指示你计划用这个变量来做什么。比如,你看到了这些变量的声明:

char firstLetter;

char lastLetter;

int hour, minute;

你可以很容易猜到这些变量会存储什么值。这个例子也同时演示了声明多个相同类型变量的语法:hour和minute都是整数(int类型)。2.4 赋值

既然已经创建了一些变量,就可以通过赋值语句用它们存储一些值。

firstLetter = 'a'; // give firstLetter the value 'a'

hour = 11; // assign the value 11 to hour

minute = 59; // set minute to 59

这个例子展示了三条赋值语句,注释用三种不同的方式展示了我们通常习惯用到的赋值语句。这里的词汇可能含混不清,但是思想简单明了:

 当声明变量时,创建了一个命名的存储位置。

 当对变量应用赋值语句时,给它一个值。

一种通常用来在纸上展示一个变量的方式是画一个方框,将变量的名字写在方框外面,把变量的值写在方框内部,这种方式叫做状态图,因为它可以展示各变量所处的状态(你可以想象它就是变量的内部状态)。图 2-1 用状态图的方式展示了三条赋值语句所表达的意思。图2-1

有时候,我会用不同的形状标识不同类型的变量,这些形状能够提醒你注意,在C++中,有这样一条规则:只能赋予变量与其类型相同的值。例如,不能用一个int类型的变量存储一个字符串。下面的语句会产生一个编译器错误。

int hour;

hour = "Hello."; // WRONG !!

有时候,这个规则会成为混乱之源。因为有很多种方式可以把一个值从一种类型转化成另一种类型,有时候C++甚至会进行自动转化。但是就现在而言,你应该记住这条常规,即作为一个通则,变量和它的值应该有相同的类型,对于特殊的情况我们稍后会提及。

另一个混乱之源是一些字符串看起来像整数,但是它们不是整数。例如,字符串“123”是由字符1、2和3组成的,它和数字123是不一样的。这个赋值语句是非法的:

minute = "59"; // WRONG!2.5 输出变量

可以用与输出简单值相同的命令输出变量的值:

int hour, minute;

char colon;

hour = 11;

minute = 59;

colon = ':';

cout << "The current time is ";

cout << hour;

cout << colon;

cout << minute;

cout << endl;

这个程序创建了两个整型变量,分别叫做hour和minute,同时还创建一个字符变量,叫做 colon。接着为这些变量赋予了合适的值,最后用一系列输出语句生成了如下输出:

The current time is 11:59

当我们谈论“输出变量”时,我们实际是在说输出变量的值。如果你想输出变量的名字,你就必须要把变量放到双引号之间。例如:cout<<"hour";

就像我们之前看到的,在一条输出语句中,可以包含多个变量,这可以令之前的程序看起来更加简洁明了:

int hour, minute;

char colon;

hour = 11;

minute = 59;

colon = ':';

cout << "The current time is " << hour << colon << minute << endl;

这个程序在一行里输出了一个字符串、两个整数、一个字符和一个特殊的值endl。印象很深刻吧!2.6 关键字

之前章节曾提到过,可以给变量取任何名字,但这并不是完全正确的。有些单词在C++中是保留的,编译器会用这些单词解析程序的结构,如果你把它们用作变量的名字,它就会导致歧义。我们称这些单词为关键字,包括int、char、void、endl等。

在C++标准中,已经包含了一个完整的关键字清单。国际标准化组织(International Organization for Standardization,ISO)于 1998年9月1日将该清单收录于官方语言定义中。可以从http://www.ansi.org/下载电子版。

比起强行记住这个清单,我更想建议你利用开发环境提供的功能:代码高亮。随着你键入代码,程序的不同部分会显示成不同颜色。例如,关键字可能会显示为蓝色,字符串会显示为红色,其他代码为黑色。如果你键入了一个变量名字却显示为蓝色,你要当心了!你也许会看到编译器产生了一些奇怪的行为。2.7 运算符

运算符是一类特殊的标示符,用来表示简单的计算,像加法和乘法。在C++中,大多数运算符的功能和你期望的一样,因为它们是常规的数学符号。例如,用于把两个整数相加的运算符是+。

以下是C++中所有合法的表达式,它们所表达的意思或多或少都是显而易见的:

1+1hour-1 hour*60 + minute minute/60

表达式既可以包含变量名也可以包含整数值。在每个例子中,当进行计算之前,变量名将会被它所代表的值替换掉。

此外,减法和乘法与你期望的行为一致,但是对于除法,你会感到奇怪。例如,下面的程序:

int hour, minute;

hour = 11;

minute = 59;

cout << "Number of minutes since midnight: ";

cout << hour*60 + minute << endl;

cout << "Fraction of the hour that has passed: ";

cout << minute/60 << endl;

将会生成如下输出:

Number of minutes since midnight: 719

Fraction of the hour that has passed: 0

第一行和我们的期望一致,但是第二行很奇怪。minute变量的值是59,59 除以60是 0.98 333,不是 0。出现这个差异的原因是C++采用整数除法。

当两个操作数(操作数是指运算符作用的数)都是整数时,结果必然是整数,同时根据定义,整数除法是向下取整的,即使在这个结果更接近向上的整数的例子里。

在这个例子里,一种可能的替代办法是计算一个百分数而不是分数:

cout << "Percentage of the hour that has passed: ";

cout << minute*100/60 << endl;

结果是:

Percentage of the hour that has passed: 98

同样的结果向下取整了,但是至少答案接近于准确。为了获得一个更加准确的答案,可以用一个不同类型的变量,这种变量称为浮点型,它可以存储小数值。我们将在第3章中使用这种类型的变量。2.8 计算顺序

当一个表达式包含多个运算符时,运算的顺序取决于优先级规则。对于优先级的完整解释是非常复杂的,但是为了你更快上手,我把它简化如下:

 乘法和除法先于加法和减法。所以2*3−1等于5而不是4,同样的,2/3−1等于−1,而不是1(记住整数除法2/3是0)。

 如果运算符既有相同的优先级,则运算的顺序是从左到右。所以在表达式minute*100/60中,先计算乘法,结果等于5900/60,最后等于98。如果计算顺序是从右到左,则结果会是59*1等于59,答案是错误的。

 当你想覆写优先级规则时(或者你不确定优先级是什么样的),你可以用圆括号。在圆括号中的表达式会先计算,所以 2*(3−1)是 4。也可以通过用圆括号的方式让表达式更易读懂,例如,表达式(minute*100)/60,结果并不会发生变化。2.9 字符类型的运算符

有趣的是,整型的数学运算同样也可以应用在字符类型上。例如:

char letter;

letter = 'a' + 1;

cout << letter << endl;

输出的是字符b。尽管在语法上字符类型相乘是合法的,但是这么做几乎没有任何意义。

如前所述,只能把整型的值赋给整型变量,把字符型的值赋给字符型变量,但这并不完全正确。在某些情况下,C++会自动完成类型之间的转换。例如,下面的语句也是合法的。

int number;

number = 'a';

cout << number << endl;

结果是 97,C++用这个数字来内部表示字母‘a’。但是,把字符当作字符对待和把整数当作整数对待,并在真正需要的时候再作类型转化是一个不错的方式。

自动类型转化是设计编程语言的一个常规问题。因为在设计的几个形式之间是有冲突的。设计编程语言的目标是形式语言应该有简单的规则和较少的异常情况,更具便利性,同时它还要容易使用。

通常,便利性会取胜,这通常对专家级程序员来说是个好消息,这会使专家避免深陷严格而笨重的形式泥潭。但对新手来说,它却不怎么好,他们通常对复杂的规则和大量的异常情况感到匪夷所思。在本书中,我会试图通过强调规则忽略大多数异常的方式让事情变得更加简单化。2.10 组合

至此,我们分别讲解了编程语言中的诸多元素——变量、表达式和语句,但却没有讨论过如何把这些元素组合到一起。

编程语言的一个很有用的功能就是把简单的积木进行组合。例如,我们知道如何把

整数相乘也知道如何输出值,结果表明可以在同一时刻做这两件事:

cout << 17 * 3;

实际上,我不应该说“同一时刻”,因为事实上,乘法先于输出发生,但是我想说的是任何表达式,包括数字、字符和变量,都可以用在输出语句中,我们已经看到过这样一个例子了:

cout << hour*60 + minute << endl;

也可以把任何表达式放到赋值语句的右边:

int percentage;

percentage = (minute * 100) / 60;

目前这个功能似乎并不那么令人印象深刻,但是我们将看到一些其他的例子,这些例子展示了组合会让复杂的计算变得简洁明快。

警告:不同的表达式都有各自的局限性;最显著的是赋值语句的左面一定是变量名,而不能是表达式。这是因为左面表明结果应该存储在什么位置。表达式不能代表存储位置,只能代表值。所以下面这条语句是非法的:minute+1 = hour;。2.11 术语

变量:有名字的值存储区。所以的变量都有类型,表明该类型可以存储什么值。

值:字母或者数字或者其他可存储在变量里的东西。

类型:值的集合。我们已经见过的类型有整型(C++中的int)和字符型(C++中char)。

关键字:保留的单词,编译器用来解析程序。我们看到的例子包括int、void和endl。

语句:一行用来表达一个命令或动作的代码。至此我们见过的语句有声明语句、赋值语句和输出语句。

声明:用来创建新变量并且标识变量类型的语句。

赋值:将值赋给变量的语句。

表达式:变量、运算符和值的联合体,代表单个结果值。表达式也有类型,这个类型由运算符和操作数决定。

运算符:特殊的符号,代表一个简单的数学运算,例如,加法或乘法。

操作数:运算符作用的值。

优先级:运算发生的先后顺序。

组合:为了简洁地表达复杂计算而将简单表达式和语句结合成复合语句和表达式。第3章函数3.1 浮点数

在第2章中,在遇到一些处理非整型数据问题时,我们采用百分比代替分数来解决。但是更常用的方法是使用既可以表示分数又可以表示整数的浮点数。在C++中,有float和double两种浮点类型。而本书只使用double类型。

可以创建一个浮点类型的变量并且用给其他类型赋值的语法给它赋值。例如:

double pi;

pi = 3.14159;

在声明变量的同时赋值给它也是合法的:

int x = 1;

String empty = "";

double pi = 3.14159;

事实上,这一语法很常见。声明变量和给变量赋值同时进行的方式称为变量的初始化。

尽管浮点数很有用,但是它们经常造成困扰。因为浮点数和整型数看起来似乎有重叠的部分。比如,对于数值1,它是一个整型数,还是浮点数,还是二者都是?

严格地说,C++认为整数 1 和浮点数 1.0 是不一样的,尽管它们看起来是同一个数值。浮点数和整型数属于不同的类型,从严格意义上来说,不能在不同类型的数值间进行赋值。比如,下面这个表达式就是错误的:

int x=1.1;1

因为表达式左边操作的变量是int类型,而右边的是double类型。因为C++在某些情况下会自动进行类型转换,所以这一点很容易忘记。例如:1

注释: 可以称为左操作数。——译者注

double y=1;

技术层面来讲不合法,但是C++允许从int到double的自动转换。这种转换很方便,但是容易引起问题。

例如:

double y=1/3;

你可能期待的结果是一个合法的浮点值 0.333 333,但是实际得到的却是 0.0。这是因为右边的表达式是两个整型数据的比值,所以C++进行了整数的除法得到结果0。转换为浮点数,结果为0.0。

解决这个问题的一个方法(一旦你发现了问题在哪里)是使右边成为一个浮点数的表达式:

double y=1.0/3.0;

这样y的值就是期望的 0.333 333。

你可能想知道的一点是我们所看到的全部运算符,加减乘除,在浮点数运算上的内在实现是完全不同的。事实上,大多数处理器都有专门为浮点数运算存在的特殊硬件。3.2 从double转换为int

之前提到过,C++在必要时候会自动将int转换为double,因为转换过程中没有信息丢失。相反,从double到int则需要截断。C++并不会自动实现这一操作,以确保程序员了解转换过程中会丢失数据的小数部分。

将float转换为int最简单的方法是类型转换(typecast)。称为类型转换是因为它允许你将一种类型的数值“转换”为另一种类型(塑造或者转型的意思,不是扔掉)。

类型转换的语法类似函数调用。例如:

double pi = 3.14159;

int x = int (pi);

int 函数返回一个整数,所以 x 等于 3。转换为整数总是向下舍入,即使小数部分是 0.99 999 999。

C++中的每种类型都有对应的函数将其参数转换成相应的类型。3.3 数学函数

在数学中,你可能见过类似sin和log的函数,也学过如何求类似sin(π/2)和log(1/x)表达式的值。首先,计算小括号中的表达式,这个表达式称为函数的参数。比如,

π/2近似于1.571,1/x(如果x=10)为0.1。

然后可以通过查表或者进行各种计算求函数本身的值。1.571的sin函数值为1,而0.1的log值为−1(假设log是以10为底的对数)。

这一过程可以应用于更复杂的表达式,如log(1/sin(π/2))上。首先计算最里面的函数值,然后中间函数的值,最后最外层,层层递推。

C++提供了一系列内置函数的集合,包含了你能想到的绝大多数数学运算。数学函数的调用类似于数学标示符的语法:

double log = log (17.0);

double angle = 1.5;

double height = sin (angle);

第一个例子设置log是以e(自然数,近似值为2.718281828)为底17的对数。还有一个函数log10()代表以10为底的对数。

第二个例子计算一个变量angle的sine值。C++假定用于sin或者其他三角函数(cos.tan)计算的值是弧度值。可以将角度值除以360乘以2π转换为弧度。

如果你不知道π的15位数字,你可以用acos函数计算出来。−1的arccosine(cosine的反函数)为π,因为π的cosine值为−1。

double pi = acos(−1.0);

double degrees = 90;

double angle = degrees * 2 * pi / 360.0;

在使用任意数学函数之前,需要在代码文件中包含math头文件。头文件包含编译器需要在程序之外定义的函数信息。比如,在“hello,world!”程序中用 include包含了一个iostream.h头文件:

#include

iostream.h包含输入输出流的信息,包括cout对象。

类似地,数学计算的头文件包含了数学函数的信息。可以在程序的开头和iostream.h一起包含进来。

#include 3.4 复合表达式

像数学函数一样,C++函数也可以组合应用,这意味着你可以把一个表达式作为另一个表达式的一部分。例如,任意表达式都可以作为函数的参数:

double x = cos (angle + pi/2);

这个表达式用pi的值除以2,加上angle变量的值。总和作为参数传给cos函数。

同样可以用将一个函数的返回值作为参数传递给另一个函数:

double x = exp (log (10.0));

参数中的表达式计算出以e为底的10的对数值,然后计算e的方幂。整个函数的值赋给x。我想你知道结果是多少(数学计算)。3.5 添加新的函数

目前为止我们只使用了C++的内建函数,但是同样也可以添加新的函数。在实际应用中,我们已经看到过一个函数定义:main。main函数是一个特殊的函数,因为它指示了函数从哪里开始执行。但是main函数的语法与其他函数定义一样:

void NAME( LIST OF PARAMETERS ) {

 STATEMENTS

}

可以给自己的函数取任何的名字,除了main和任何其他C++关键字。参数列表描述了调用函数需要的信息。如果有参数,就需要提供相应的值来调用新的函数。

main函数定义中的小括号为空,意味着它不接收任何参数。我们准备写的第一组函数也没有参数,所以语法如下所示:

void newLine () {

  cout << endl;

}

函数名为newLine,它只包含一条语句,该语句输出特殊值endl使光标移动到新行。

在main函数中我们像调用C++内置的命令一样调用这个新的函数:

void main ()

{

  cout << "First Line." << endl;

  newLine ();

  cout << "Second Line." << endl;

}

该程序的输出为:

First line.

Second line.

注意到两行之间的额外空行。如果行间需要更多的空间该怎么做?可以重复调用同一个函数:

void main ()

{

  cout << "First Line." << endl;

  newLine ();

  newLine ();

  newLine ();

  cout << "Second Line." << endl;

}

或者可以写一个新的函数,命名为threeLine。该函数输出三个新行:

void threeLine ()

{

  newLine (); newLine (); newLine ();

}

void main ()

{

  cout << "First Line." << endl;

  threeLine ();

  cout << "Second Line." << endl;

}

关于这个程序,应该注意以下几点:

可以重复调用同一个程序段。事实上,这一点很有用并且经常用到。

可以在一个函数中调用另一个函数。这意味着,main函数调用threeLine函数,threeLine又调用newLine。同样,这也是普遍使用的一点。

在 threeLine 函数中,在同一行中描述三条语句是合法的(记住空格和新行一般并不改变一个程序的含义)。但是,一般情况下每条语句独立成行是比较好的方法,这可以使程序更具有可读性。在本书中我经常为了节省空间而打破这一规则。

目前为止,你也许并不清楚这么费力创建这些新的函数的价值所在。事实上,这有很多原因,上一个例子只说明了两点:(1)可以给一组语句命名。函数从两个角度简化程序,用一个简单的命名隐藏一系列复杂的计算以及用英文描述代替晦涩的代码。比如,newLine和cout<

把之前所有的代码片段集合在一起,整个程序如下所示:

#include

void newLine ()

{

  cout << endl;

}

void threeLine ()

{

  newLine (); newLine (); newLine ();

}

void main ()

{

  cout << "First Line." << endl;

  threeLine ();

  cout << "Second Line." << endl;

}

该程序包含三个函数定义:newLine、threeLine和main函数。

在 main 函数定义里,调用了 threeLine 方法。类似地,threeLine 调用了newLine方法三次。注意一点,每个函数定义都出现在该函数使用之前。

在C++中这一点是必需的。一个函数的定义必须出现在第一次调用该函数之前。你可以尝试编译按不同函数定义和调用顺序排列的程序,看看会得到怎样的错误消息。3.7 多函数程序

当人们阅读一个包含多个函数的类的定义,一般会选择从头到位的阅读方式。但是这种方式很容易引起困惑,因为这并不是程序执行的顺序。

程序一般都是从main函数的第一行开始执行,不管main函数在程序中的哪个位置(一般都在末尾)。按顺序每次执行一条语句,直到执行函数调用。函数调用类似执行过程中的一个迂回。当执行调用函数时,不会按原先顺序执行下一条语句,而是进入被调用函数内部从第一行开始,执行被调用函数内部所有的语句,然后回到调用函数的位置继续执行。

这听起来很简单,但是需要记住哪个函数调用了哪个函数。因此,当程序运行到main函数中间,我们需要进入一个分支执行threeLine中的语句。但当程序在执行threeLine函数的时候,需要中断三次并执行newLine。

幸运的是,C++很擅长追踪程序运行的轨迹。每次当newLine运行完毕,程序回到threeLine中调用newLine的地方,最终回到main函数,程序执行完毕。

这个事例想说明什么呢?当你阅读一个程序的时候,不要从头读到尾,而是按照程序执行的顺序阅读。3.8 形参和实参

我们已经用过的一些内置函数都有形参,需要我们提供相应的值来调用函数。例如,如果你想要计算一个数值的 sin 值,就需要提供具体的数字。而 sin 函数接收double类型的数值作为形参。

有些函数有多个形参,比如pow函数(幂函数),接收两个double类型的值,底数和指数。

需要注意,在每种情形下不但需要描述有多少个参数,还需要明确定义参数的类型。所以当描述一个类的定义时,参数列表就表明了每个参数的类型。例如:

void printTwice (char phil) {

  cout << phil << phil << endl;

}

printTwice函数只有一个参数phil,其类型为char。不管这个形参具体是什么(这个时候我们也无法知道它是什么),它会输出两次,然后另起新行。选择phil这个名字是为了说明参数的命名是由程序员自己决定的,但是一般来说应该选择一个比phil这样的单词更形象的描述。

为了调用这样的函数,需要提供一个char类型的值。比如,main函数可能是这样的:

void main () {

  printTwice ('a');

}

调用函数所用的char值叫做实参,将实参传递给函数进行调用。这个时候把‘a’的值传递给printTwice,在内部打印两次。

另外,如果有一个char变量,可以将它作为实参传递给函数:

void main () {

  char argument = 'b';

  printTwice (argument);

}

注意一个很重要的问题:再次强调,作为实参传递给调用函数的变量名(argument)与函数形参的命名(phil)无关:

作为实参传递给调用函数的变量名与函数本身形参的命名无关。

形参和实参命名可以一样也可以不一样,但是重要的是认识到它们是不一样的事物,除非它们恰好有一样的值(这个例子中的字符b)。

用来调用函数的实参的类型必须与函数的形参类型一致。这条规则至关重要。但是C++在某些时候自动进行的类型转换容易引起误解。目前需要了解一般性的规则,稍后再来学习例外的一些情况。3.9 形参和局部变量

形参和变量都只存在于定义它们的函数内部。在main函数内部,没有phil这样的变量。如果你尝试使用这样的变量,编译器会报错。类似地,在 printTwice函数里也没有像argument这样的变量。

像这样的变量都是局部变量。为了追踪形参和局部变量是如何转换的,绘制一张栈图是很有用的。像状态图一样,栈图显示每个变量的值,但是变量包含在一个较大的方框中,该方框表示所属的函数。

例如,printTwice的状态图如图3-1所示。图3-1

任何时候调用一个函数,程序会创建出这个函数的一个新的实例。每个实例都包含这个函数的全部形参和局部变量。在状态图中,函数的实例用一个方框代表,方框外面标注函数名,方框内部标明形参和变量。

在示例中,main函数有一个局部变量和argument,没有形参。PrintTwice没有局部变量,只有一个形参phil。3.10 多参数函数

声明和调用多参数函数的语法经常导致各种常见错误。首先,记住,必须声明每个参数的类型。例如:

void printTime (int hour, int minute) {

  cout << hour;

  cout << ":";

  cout << minute;

}

第一行很容易写成write(int hour, minute),但是这样的语法只对变量声明合法,对参数声明不合法。

另一个容易引起混淆的地方是调用函数时不必声明参数的类型,下面这个例子是错误的!

int hour = 11;

int minute = 59;

printTime (int hour, int minute); // 错误示例!

在这种情况下,编译器可以可以通过查看函数声明来判断hour和minute的类型。将变量作为实参传递给函数时,包含变量类型是没必要而且非法的。正确的语法应该是printTime(hour,minute)。3.11 带返回值的函数

你可能注意到了,目前为止所使用到的一些函数,比如main函数,都产生某种结果。而另一些函数,比如 newLine,执行了某种操作但是不返回数值。这使得我们提出一些问题:

如果调用一个函数并且不对函数返回的结果做任何操作会发生什么(即,你并没有将函数返回的值赋给变量或者将函数作为一个更大的表达式的一部分)?

如果使用一个没有返回值的函数作为表达式的一部分会怎么样?比如newLine()+7。

我们能不能写一些产生结果的函数,还是我们就停留在类似 newLine 和printTwice这样的函数上?

对于第三个问题的答案是肯定的,你可以写带有返回值的函数。我们会在接下来的几章学习。我将另两个问题留给你通过尝试来回答。任何时候你提出关于C++中语法是否合法的问题,一个好的解决方法是通过编译器检查。

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载