Skip to content

Latest commit

 

History

History
336 lines (196 loc) · 19.6 KB

chapter05.md

File metadata and controls

336 lines (196 loc) · 19.6 KB

第五章 循环和关系表达式

本章内容包括:

  • for 循环;
  • 表达式和语句;
  • 递增运算符和递减运算符: ++ 和 --;
  • 组合赋值运算符;
  • 复合语句(语句块);
  • 逗号运算符;
  • 关系运算符:>、>=、==、<=、< 和 !=;
  • while 循环;
  • typedef 工具;
  • do while 循环;
  • 字符输入方法 get() ;
  • 文件尾条件;
  • 嵌套循环和二维数组。

5.1 for 循环

5.1.1 for 循环初始化

for循环为执行重复的操作提供了循序渐进的步骤。组成部分完成下面这些步骤:

  • 设置初始值;
  • 执行测试,判断循环是否应当继续进行;
  • 执行循环操作;
  • 更新用于测试的值。

初始化、测试和更 新操作构成了控制部分,这些操作由括号括起。其中每部分都是一个表 达式,彼此由分号隔开。控制部分后面的语句叫作循环体,只要测试表 达式为true,它便被执行:

for (initialization; test-expression; update-expression)
    body;

循环体如果大于一条语句,需要加花括号:

for (initialization; test-expression; update-expression) {
    body;
}

C++语法将整个for看作一条语句—虽然循环体可以包含一条或多条 语句。循环只执行一次初始化。

for 是一个 C++ 关键字,编译器不会将 for 视为一个函 数,这还将防止将函数命名为 for

image-20210727003613922

【编程风格 Tips】C++ 常用的方式是,在 for 和括号之间加上一个空格,但省略函数名与括号之间的空格。

看这个表达式赋值逻辑:

x = y = z = 0;

赋值运算符是从右向左结合的,因此首先将0赋给z,然 后将z = 0赋给y,依此类推。

C++在C循环的基础上添加了一项特性,要求对for循环句法做一些 微妙的调整。

这是原来的句法:

for (expresson; expression; expression)
    statement;

但,C++循环允许像下面这样做:

for (int i=0; i<5; ++i)

也就是说,可以在for循环的初始化部分中声明变量。这很方便,但并不适用于原来的句法,因为声明不是表达式。这个变量的生存时间只存在于for语句中,也就是说,当程序离开循环后,i 这个变量将消失。

5.1.2 回到for循环

很简单,略。

5.1.3 修改步长

循环示例每一轮循环都将循环计数加1或减1。可以通 过修改更新表达式来修改步长,例如改为表达式 i = i + by,其中 by 是用户选择的步长值。

5.1.4 使用for循环访问字符串

很简单,略。

5.1.5 递增运算符(++)和递减运算符(−−)

这两个运算符执行两种极其常见 的循环操作:将循环计数加1或减1。这两个运算符都有两种变体。前缀(prefix)版本位于操作数前 面,如++x;后缀(postfix)版本位于操作数后面,如x++。两个版本对操作数的影响是一样的,但是影响的时间不同。这就像对于钱包来说, 清理草坪之前付钱和清理草坪之后付钱的最终结果是一样的,但支付钱 的时间不同。

粗略地讲,a++意味着使用a的当前值计算表达式,然后将a的值加 1;而++b的意思是先将b的值加1,然后使用新的值来计算表达式。 递减操作符也同理。

image-20210727010134791

5.1.6 副作用和顺序点

C++就递增运算符何时生效的哪些方面做了规 定,哪些方面没有规定。首先,副作用(side effect)指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改;顺序点 (sequence point)是程序执行过程中的一个点,在这里,进入下一步之 前将确保对所有的副作用都进行了评估。在C++中,语句中的分号就是 一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。

另外,任何完整的表达式末尾都是一个顺序点。

何为完整表达式呢?它是这样一个表达式:不是另一个更大表达式 的子表达式。完整表达式的例子有:表达式语句中的表达式部分以及用 作while循环中检测条件的表达式。

image-20210727010733540

5.1.7 前缀格式和后缀格式

如果变量被用于某些目的(如用作函数参数或给变量赋 值),使用前缀格式和后缀格式的结果将不同。

然而,虽然选择使用前缀格式还是后缀格式对程序的行为没有影 响,但执行速度可能有细微的差别。对于内置类型和当代的编译器而 言,这看似不是什么问题。然而,C++允许您针对类定义这些运算符, 在这种情况下,用户这样定义前缀函数:将值加1,然后返回结果;但后缀版本首先复制一个副本,将其加1,然后将复制的副本返回。因 此,对于类而言,前缀版本的效率比后缀版本高。

5.1.8 递增/递减运算符和指针

可以将递增运算符用于指针和基本变量。将递增 运算符用于指针时,将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减。

前缀递增、前缀递减和解引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。

前缀运算符的从右到到结合规则意味着 *++pt 的含义如下:现将 ++ 应用于 pt(因为 ++ 位于 * 的右边),然后将 * 应用于被递增后的 pt。 另一方面,++*pt 意味着先取得 pt 指向的值,然后将这个值加 1,pt 依然指向原来的地址;

而这个组合 (*pt)++ 圆括号指出,首先对指针解引用,得到值。然后,运算符++将这个值递增 1,但 pt 仍然指在原来的地址。

5.1.9 组合赋值运算符

+= 运算符将两个操作数相加,并将结果赋给左边的操作数。这意味着左边的操作数必须能够被赋值,如变量、数组元素、结构体成员或通过对指针解引用来标识的数据。

image-20210727012040437

image-20210727012104165

5.1.10 复合语句(语句块)

只有这个地方要注意:假设对循环体进行了缩进,但省略了花括号,编译器将忽略缩进,因此只有第一条语句位于循环中。

image-20210727012230040

5.1.11 其他语法技巧—逗号运算符

语句块允许把两条或更多条语句放到按C++句法 只能放一条语句的地方。逗号运算符对表达式完成同样的任务,允许将 两个表达式放到C++句法只允许放一个表达式的地方。如:

++j, --i你  // 没有分号,合法,但不能独立运行,要在另外语句中,如 for 里

image-20210727012641834

但逗号并不总是逗号运算符。 如:

int i, j; // 个声明中的逗号将变量列 表中相邻的名称分开

到目前为止,逗号运算符最常见的用途是将两个或更多的表达式放 到一个for循环表达式中。不过C++还为这个运算符提供了另外两个特 性。首先,它确保先计算第一个表达式,然后计算第二个表达式(换句 话说,逗号运算符是一个顺序点)。

i = 20, j = 2 * i   // 这是合法的,这里 y 是 40

在所有运算符中,逗号运算符的优先级是最低的。

image-20210727013030988

5.1.12 关系表达式

image-20210727013059937

5.1.13 赋值、比较和可能犯的错误

很简单,略。

5.1.14 C-风格字符串的比较

假设要知道字符数组中的字符串是不是mate。如果word是数组名, 下面的测试可能并不能像我们预想的那样工作:

word == "mate"

请记住,数组名是数组的地址。同样,用引号括起的字符串常量也 是其地址。因此,上面的关系表达式不是判断两个字符串是否相同,而 是查看它们是否存储在相同的地址上。两个字符串的地址是否相同呢? 回答是否定的,虽然它们包含相同的字符。

由于C++将C-风格字符串视为地址,因此如果使用关系运算符来比 较它们,将无法得到满意的结果。需要用 C 字符串库中的 strcmp() 函数来比较。 该函数接受两个字符串地址作为参数。这意味着 参数可以是指针、字符串常量或字符数组名。如果两个字符串相同,该 函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前, 则strcmp( )将返回一个负数值;如果第一个字符串按字母顺序排在第二 个字符串之后,则strcpm( )将返回一个正数值。实际上,“按系统排列顺 序”比“按字母顺序”更准确。这意味着字符是根据字符的系统编码来进 行比较的。例如,使用ASCII码时,所有大写字母的编码都比小写字母 小,所以按排列顺序,大写字母将位于小写字母之前。因此,字符 串“Zoo”在字符串“aviary”之前。

存储在不同长度的数组 中的字符串彼此不相等。但是C-风格字符串是通过结尾的空值字符定义 的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存 储在长度不同的数组中,也可能是相同的。

image-20210727014019373

5.1.15 比较string类字符串

如果使用string类字符串而不是C-风格字符串,比较起来将简单些。直接通过运算符比较:>, <, == 等。

5.2 while 循环

while 循环是一个没有初始化和更新部分的 for 循环,它只有测试条件和循环体,与for循环一样,循环体也由一条语句或两个花括号定义的语句块组成。句法如下:

while (test-condition)
    body

如果循环体有多条语句,需要花括号:

while (test-condition) {
    body
}

首先,程序计算圆括号内的测试条件(test-condition)表达式。如 果该表达式为true,则执行循环体中的语句。执行完循环体后,程序返 回测试条件,对它进行重新评估。如果该条件为非零,则再次执行循环 体。测试和执行将一直进行下去,直到测试条件为false为止。

while 循环和 for 循环一样,也是一 种入口条件循环。因此,如果测试条件一开始便为false,则程序将不会 执行循环体。

image-20210727121115868

5.2.1 for 与 while

在C++中,for 和 while 循环本质上是相同的。

image-20210727121739245

但它们之间存在三个差别:

  • for 循环中省略了测试条件时,将认为条件为 true
  • for 循环中,可使用初始化语句声明一个局部变量,但在 while 循环中不能这样做;
  • 如果循环体中包括 continue 语句,情况将稍有不同,continue 语句将在第6章讨论。

通常,程序员使用 for 循环来为循环计数,因为 for 循环格式允许将所有相关的信息—初始值、终止值和更新计数器的方法—放在同一个地方。在无法预先知道循环将执行的次数时,程序员常使用 while 循环。

5.2.2 等待一段时间:编写延时循环

函数 clock() 可以返回程序开始执行后所用的系统时间。但,首先,clock( )返回时间的单位不一定是秒;其次,该函数的返回类 型在某些系统上可能是long,在另一些系统上可能是unsigned long或其 他类型。

头文件 ctime 提供了这些问题的解决方案。首先,它定义了一个符号常量—CLOCKS_PER_SEC,该常量等于 每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,可以得 到秒数。或者将秒数乘以 CLOCK_PER_SEC,可以得到以系统时间单位 为单位的时间。其次,ctimeclock_t 作为 clock() 返回类型的别名,这意味着可以将变量声明为 clock_t 类型,编译器将把它转换为long、unsigned int 或适合系统的其他类型。

image-20210727133448815

do while 循环

它 不同于另外两种循环,因为它是出口条件(exit condition)循环。这意 味着这种循环将首先执行循环体,然后再判定测试表达式,决定是否应 继续执行循环。如果条件为false,则循环终止;否则,进入新一轮的执 行和测试。这样的循环通常至少执行一次,循环体是一条语句或用括号括起的语句块。句法:

do {
    body
} while (test-condition);

image-20210727135803904

5.4 基于范围的for循环(C++11)

C++11新增了一种循环:基于范围(range-based)的for循环。这简化了一种常见的循环任务:对数组(或容器类,如vector和array)的每 个元素执行相同的操作。

5.5 循环和文本输入

5.5.1 使用原始的cin进行输入

为什么程序在输出时省略了空格呢?原因 在cin。读取char值时,与读取其他基本类型一样,cin将忽略空格和换行 符。因此输入中的空格没有被回显,也没有被包括在计数内。

更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按 下回车键后,他输入的内容才会被发送给程序。这就是在运行该程序 时,可以在#后面输入字符的原因。按下回车键后,整个字符序列将被 发送给程序,但程序在遇到#字符后将结束对输入的处理。

5.5.2 使用cin.get(char)进行补救

通常,逐个字符读取输入的程序需要检查每个字符,包括空格、制 表符和换行符。cin 所属的 istream 类(在 iostream 中定义)中包含一个能 够满足这种要求的成员函数。具体地说,成员函数cin.get(ch)读取输入 中的下一个字符(即使它是空格),并将其赋给变量ch。使用这个函数 调用替换cin>>ch,可以修补程序清单5.16的问题。

image-20210727161504704

在C语言中, 要修改变量的值,必须将变量的地址传递给函数。但程序清单5.17调用 cin.get( )时,传递的是ch,而不是&ch。在C语言中,这样的代码无效, 但在C++中有效,只要函数将参数声明为引用即可。引用是C++在C语 言的基础上新增的一种类型。头文件iostream将cin.get(ch)的参数声明为 引用类型,因此该函数可以修改其参数的值。

5.5.3 使用哪一个cin.get( )

函数重载允许创建多个同名函数,条件是它们 的参数列表不同。例如,如果在C++中使用 cin.get(name,ArSize), 则编译器将找到使用 char*int 作为参数的 cin.get() 版本;如果使用 cin.get(ch),则编译器将使用接受一个 char 参数的版本;如果没有提 供参数,则编译器将使用不接受任何参数的cin.get() 版本。函数重载允 许对多个相关的函数使用相同的名称,这些函数以不同方式或针对不同类型执行相同的基本任务。

5.5.4 文件尾条件

如果输入来自于文件,则可以使用一种功能更强大的技 术—检测文件尾(EOF)。C++输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。

检测到EOF后,cin将两位(eofbit和failbit)都设置为1。可以通过 成员函数eof( )来查看eofbit是否被设置;如果检测到EOF,则cin.eof( )将 返回bool值true,否则返回false。同样,如果eofbit或failbit被设置为1, 则fail( )成员函数返回true,否则返回false。

image-20210727162259773

5.5.5 另一个cin.get( )版本

为成功地使用cin.get( ),需要知道其如何处理EOF条件。当该函数 到达EOF时,将没有可返回的字符。相反,cin.get( )将返回一个用符号 常量EOF表示的特殊值。该常量是在头文件iostream中定义的。EOF值 必须不同于任何有效的字符值,以便程序不会将EOF与常规字符混淆。 通常,EOF被定义为值−1,因为没有ASCII码为−1的字符,但并不需要知道实际的值,而只需在程序中使用EOF即可。

image-20210727162823543

image-20210727163014040

另一方面,使用cin.get(ch)(有一个参数)进行输入时,将不会导 致任何类型方面的问题。前面讲过,cin.get(char)函数在到达EOF时,不 会将一个特殊值赋给ch。事实上,在这种情况下,它不会将任何值赋给 ch。ch不会被用来存储非char值。

image-20210727163117901

那么应使用cin.get( )还是cin.get(char)呢?使用字符参数的版本更符 合对象方式,因为其返回值是istream对象。这意味着可以将它们拼接起 来。例如,下面的代码将输入中的下一个字符读入到ch1中,并将接下 来的一个字符读入到ch2中:

cin.get(ch1).get(ch2);

5.6 嵌套循环和二维数组

C++没有提供二维数组类型,但用户可以创建每个元素本身都是数 组的数组。可以这样声明数组:

int maxtemps[4][5];

image-20210727163540362

image-20210727163623327

image-20210727163905902

假设要打印数组所有的内容,可以用一个for循环来改变行,用另一 个被嵌套的for循环来改变列:

for (int row = 0; row < 4; ++row) {
    for (int col=0; col < 5; ++col) {
        cout << maxtemps[row][col] << "\t";
    }
    cout << endl;
}

实际上,我更喜欢用 vector 来创建动态二维数据,只要创建 vector 的vector 就行,用起来更简单。

5.7 总结

C++提供了3种循环:for循环、while循环和do while循环。如果循环 测试条件为true或非零,则循环将重复执行一组指令;如果测试条件为 false或0,则结束循环。for循环和while循环都是入口条件循环,这意味 着程序将在执行循环体中的语句之前检查测试条件。do while循环是出 口条件循环,这意味着其将在执行循环体中的语句之后检查条件。

关系表达式对两个值进行比较,常被用作循环测试条件。关系表达 式是通过使用6种关系运算符之一构成的:<<===>=>!=。 关系表达式的结果为bool类型,值为true或false。