1.3 开始认识C++
这一节会提供足够多的信息来支持后面章节中的例子。关于这些细节你肯定会有疑问,但是接下来的章节会一一回答,不要慌!
1.3.1 C++类型系统
C++是一门面向对象的语言。对象是关于状态和行为的抽象。想象一下真实世界的对象,例如电灯开关。我们可以把开关的各种属性描述成状态,例如:它开着还是关着?额定电压是多少?在哪个房间?我们也可以描述开关的行为,例如:它是否从一个状态(开)切换到另一个状态(关)?是不是一个变光开关,在开和关之间还有很多别的状态?
行为和状态的集合被用来描述对象,我们称之为类型。C++是一门强类型语言,这意味着每一个对象都有一个预先定义好的数据类型。
C++有一个内建整数类型,即int。int对象可以存储整数(状态),并且也支持许多数学运算(行为)。
要使用int类型来做一些有意义的任务,需要创建一些int对象,并且对它们命名。命名对象被称为变量。
1.3.2 声明变量
要声明变量,我们需要先提供类型,接着提供名称,然后以分号结束。以下例子声明了一个名为the_answer的int变量:
类型int❶后面便是变量名,即the_answer❷。
1.3.3 初始化变量的状态
当声明变量时,我们同时会初始化它们。对象初始化会建立对象的初始状态,例如设置它的值。我们将在第2章深入研究初始化的细节。目前,我们可以在变量声明时在变量名后用等号(=)来设置变量的初始值。例如,我们可以用一行代码实现变量the_answer的声明和赋值:
这行代码执行之后,我们会得到一个名为the_anwser的变量,其类型是int,初始值是42。当然,我们也可以将数学表达式的结果设置为变量的值,例如:
这行代码会对表达式the_anwser / 6求值,然后把结果赋给lucky_number。int类型还支持许多其他运算,例如加法(+)运算、减法(-)运算、乘法(*)运算和模(%)运算。
注意 如果你不熟悉模运算或者好奇两个数相除有余数时会发生什么的话,那么你问对了问题。这些问题将在第7章得到详细的解答。
1.3.4 条件语句
条件语句允许在程序中做决定。这些决定取决于布尔表达式,布尔表达式的结果为真(true)或假(false)。例如,我们可以使用比较运算符,如“大于”或者“不等于”来构造布尔表达式。
一些和int有关的基本比较运算符见代码清单1-2的程序。
代码清单1-2 使用比较运算符的程序
这个程序不产生输出(可以编译和运行代码清单1-2的程序来验证这一点)。尽管这个程序不产生任何输出,但编译它可以让我们检验C++程序是否合法。为了生成一些更有趣的程序,可以使用条件语句,如if语句。
if条件语句包含一个布尔表达式以及一条或多条嵌套语句。根据条件到底是真还是假,程序可以选择执行哪一条嵌套语句。if语句的形式有好几种,但是基本用法如下:
如果布尔表达式❶为真,则嵌套语句❷会执行;否则不执行。
有时候,我们需要运行一组语句而不是单条语句。这样的语句组称为复合语句。要声明复合语句,可以直接将这一组语句包含在大括号{}中。if语句中可以使用复合语句,例如:
如果布尔表达式❶为真,则复合语句❷中的所有语句都会执行;否则都不执行。
我们也可以用else if和else语句来扩充if语句。使用它们可以描述更复杂的分支行为,如代码清单1-3所示。
代码清单1-3 带有else if和else分支的if语句
首先,对boolean-expression-1❶求值。如果boolean-expression-1为真,则执行statement-1,然后整个if语句停止执行。如果boolean-expression-1为假,则对boolean-expression-2求值❷,并且如果为真,则执行statement-2,否则执行statement-3❸。请注意,statement-1、statement-2和statement-3是互斥的,它们一起覆盖了if语句所有可能的输出,只有其中一个会被执行。
else if子句可以有零个或者多个。包括开始的if语句在内,每个else if的布尔表达式都会按顺序求值。当其中一个布尔表达式为真时,就会停止求值,并执行相应的语句。如果没有任何一个else if语句为真,则执行else子句的statement-3。和else if一样,else也是可选的。
请考虑代码清单1-4,它使用if语句来判断打印哪条语句。
代码清单1-4 具有条件行为的程序
编译这段程序并且运行它。结果应该是Zero。如果改变x的值,这段程序会打印什么?
注意 代码清单1-4中的main函数忽略了return语句。这是因为main函数是个特殊函数,return语句不是必需的。
1.3.5 函数
函数是一种代码块,它接受任意数量的输入对象——称为参数,同时可以将输出对象返回给调用者。
我们可以按照代码清单1-5中显示的通用语法来声明函数。
代码清单1-5 C++函数的通用语法
函数声明的第一部分是返回变量的类型❶,比如int。当函数返回一个值时❺,返回值的类型必须与返回类型return-type匹配。
在声明返回类型之后,需要声明函数的名称❷。函数名后的圆括号内包含函数所需的参数,它们是以逗号分隔的输入参数。每个参数都有一个类型和一个名称。
代码清单1-5中的函数有两个参数。第一个参数的类型为par-type1,名称为par_name1❸;第二个参数的类型为par-type2,名称为par_name2❹。参数代表传递给函数的对象。
后面的大括号内是函数体。这是一条复合语句,其中包含了函数的逻辑。在这个逻辑中,函数可能会决定向调用者返回一个值。返回值的函数可以有一条或多条return语句。一旦函数返回,它就停止执行,程序流程回到调用该函数的地方。我们来看一个例子。
1.示例:阶跃函数
出于演示的目的,这里将展示如何构建数学函数step_function,该函数对于所有负参数返回-1,对于零值参数返回0,对于所有正参数返回1。代码清单1-6显示了编写step_function的方法。
代码清单1-6 阶跃函数,当参数为负数时返回-1,为零时返回0,为正数时返回1
step_function接受一个参数x❶。result变量被声明并且被初始化为0❷。如果x小于0,if语句将result设置为-1❸。如果x大于0,if语句就将result设置为1❹。最后,result被返回给调用者❺。
2.调用函数
要调用一个函数,需要使用函数的名称、大括号,以及一系列逗号隔开的必需参数。编译器会从头到尾读取文件内容,所以函数的声明必须出现在它第一次被使用之前。
请考虑代码清单1-7中的程序,它调用了step_function。
代码清单1-7 一个调用step_function的程序(该程序没有任何输出)
代码清单1-7调用了step_function三次,每次调用都使用不同的参数,结果分别赋给了变量value1、value2和value3。
如果能把这些值打印出来不是更好吗?所幸我们可以使用printf函数来打印不同变量的输出。技巧就是使用printf格式指定符。
1.3.6 printf格式指定符
除了打印字符串常量(如代码清单1-1中的Hello, world!)以外,printf还可以将多个值组成一个格式良好的字符串。它是一种特殊的函数,可以接受一个或者多个参数。
printf的第一个参数一直是格式化字符串。格式化字符串为要打印的字符串提供模板,并且它可以包含任意数量的特殊格式指定符(format specifier)。格式指定符告诉printf如何解释和格式化跟在格式化字符串后面的参数。所有格式指定符都以%开头。
例如,int的格式指定符是%d。当printf在格式化字符串中看到%d时,它就知道格式指定符后面的参数是int参数。然后,printf就用参数的实际值来替换格式指定符。
注意 printf函数最初来自BCPL的writef,BCPL是Martin Richards于1967年设计的一门编程语言(已过时)。writef函数使用%H、%I和%O指定符,最终会通过WRITEHEX、WRITED和WRITEOCT函数来生成十六进制和八进制输出。目前仍然不清楚%d来自哪里(可能是WRITED的D?),但是我们也只能用它了。
考虑以下printf调用,它将打印字符串Ten 10, Twenty 20, Thirty 30:
第一个参数"Ten%d, Twenty%d, Thirty%d"是格式化字符串。注意,这里有三个字符串指定符%d❶❷❸。因此,也有三个参数跟在格式化字符串后面❹❺❻。当printf构建输出时,它会用❹替换❶,用❺替换❷,用❻替换❸。
iostream、printf和输入/输出教学法
人们对教给C++新手哪种标准输出方法有非常强烈的意见。一种方法是printf,它的血统可以追溯到C语言。另一种方法是cout,它是C++标准库的iostream库的一部分。本书两者都教:第一部分主要介绍printf,第二部分主要介绍cout。这就是原因。
本书将引导你逐渐构建C++知识体系。每一章都是按顺序设计的,所以你不需要跃跃欲试地去理解代码示例。你会清楚地知道每一行代码在做什么。因为printf是相当初级的,所以学完第3章,你就有足够的知识来了解它的具体工作原理。
相比之下,cout涉及一大堆C++概念,只有学完第一部分,才会有足够的背景知识来理解它的工作原理(什么是流缓冲区?什么是operator<<?什么是方法?flush()是如何工作的?cout会在析构函数中自动刷新吗?什么是析构函数?setf是什么?格式化标志又是什么?是BitmaskType吗?什么是操纵符?等等)。
当然,printf也有问题,一旦你学会了cout,你应该会更喜欢它。使用printf很容易导致格式指定符和参数不匹配,进而导致奇怪的行为,还可能导致程序崩溃,甚至出现安全漏洞。使用cout意味着不需要格式化字符串了,所以也就不需要记住格式指定符了,因此永远不会出现格式化字符串和参数不匹配的情况。iostream也是可扩展的,这意味着我们可以将输入和输出功能集成到自己的类型中。
本书直接教授现代C++,但在这个特殊的主题上,做出了一些妥协,这使得它看起来不那么现代,目的是使介绍过程更直接顺畅。作为一个附带的好处,你会碰到printf指定符,这很可能在你编程生涯的某个阶段发生。大多数语言,如C、Python、Java和Ruby,都有printf指定符,C#、JavaScript等语言也有类似的功能。
1.3.7 重新审视step_function
我们来看另一个调用step_function的例子。代码清单1-8包含了变量声明、函数调用和printf格式指定符。
代码清单1-8 打印对几个整数调用step_function的结果的程序
因为程序使用了printf,所以包含了cstdio❶。step_function❷被定义了,这样我们就可以在程序的后面使用它,而main❸建立了定义的入口点。
注意 本书中的一些代码清单是相互依存的。为了节约空间,常使用--snip--符号来表示对复用的部分不做任何修改。
在main中,我们初始化一些int类型,如num1❹。接下来,我们将这些变量传递给step_function,并初始化结果变量以存储返回的值,如result1❺。
最后,调用printf来打印返回的值。每个调用都以一个格式化字符串开始,如"Num1:%d, Step:%d\n"❻。每个格式化字符串中都嵌入了两个格式指定符%d。根据printf的要求,格式化字符串后面有两个参数,即num1和result1,它们分别对应这两个格式指定符。
1.3.8 注释
注释是人类可读的内容,它可以放到源代码中。在代码中添加注释时,使用的符号是//或/**/。//告诉编译器要忽略从第一个斜杠到下一个换行符间的所有内容,这意味着可以把注释和代码放在一起,也可以把注释放在单独的行中。
注释以/*开头,以*/结尾(斜杠之间的星号是可选的,但通常都会使用)。什么时候使用注释是一个永远争论不休的问题。一些编程专家认为,代码应该具有很强的表现力和自我解释能力,因而注释在很大程度上是没有必要的。他们认为描述性的变量名、简短的函数和良好的测试通常就是所需的所有文件。有些程序员则很喜欢用注释。
你可以培养自己的注释习惯。编译器将完全无视你所做的一切,因为它从不解释注释。