C++学习之九、循环和关系表达式
程序通常使用关系表达式和逻辑表达式来控制其行为。
表达式
表达式定义与值
任何值或任何有效的值和运算符的组合都是表达式,C++ 中每个表达式都有值。
1 | 10 // 10 是值为 10 的表达式 |
1 | // express.cpp -- 表达式 |
表达式副作用(side effect)
当表达式的操作改变了内存中数据的值时,我们说表达式有副作用(side effect)。如 x = 100
这个表达式改变了 x
的值,有副作用,而 x + 15
将计算出一个新的值且不会修改 x
的值,没有副作用。
顺序(sequence)
指程序执行过程中的一个点,在这里,进入下一步之前将确保对所有的副作用都进行了评估。
- 语句中的分号是一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。
1 | y = (4 + x++) + (6 + x++); // 4 + x++ 不是一个完整的表达式,因此,C++ 不保证 x 的值在计算子表达式 4 + x++ 后立刻增加 1,只保证在分号顺序点时,x 被执行了再次加 1,由于中间的过程的不确定性,所以应避免使用这样的表达式 |
- 任何完整的表达式(不是另一个更大表达式的子表达式)末尾都是一个顺序点。如表达式语句中的表达式部分以及用作 while 循环中检测条件的表达式。
1 | while (guests++ < 10) // guests 值首先用于判断是否小于 10,由于 while 循环中检测条件的表达式是一个顺序点,所以此语句将保证 guests 的值被加 1 |
表达式语句
从表达式到表达式语句的转变很容易,只要加上分号即可。
1 | age = 100 // 表达式 |
非表达式语句
表达式语句去掉分号就是表达式。但不是所有的语句都是这样。很多种情况语句去掉分号并不是表达式,此为非表达式语句。
1 | int a; // 这是一条语句,但 int a 并不是一条表达式,因此它没有值,不能用于直接运算 |
关系表达式
C++ 提供了 6 种关系运算符来对数字进行比较。
操作符 | 含义 |
---|---|
< | 小于 |
<= | 小于或等于 |
== | 等等于 |
> | 大于 |
>= | 大于或等于 |
!= | 不等于 |
Tips: 不要混淆 == 与赋值运算符 = 。前者用于判断,后者则是赋值。当使用赋值表达式作为判断时,值为等号左边的值,非零值为 true,零值为 false
C——风格字符串比较
数组名是数组的地址,同样,用引号括起的字符串常量也是其地址。因此,使用 == 只能比较两个字符串的地址。
应使用 C-风格字符串库中的 strcmp()
函数来比较。该函数接受两个字符串地址作为参数,这意味着参数可以是指针、字符串常量或字符数组名。比较时,如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序在第二个字符串之前,则 strcmp() 将返回一个负数值;如果第一个字符串按字母顺序排在第二个字符串之后,则 strcmp() 将返回一个正数值,字符是根据字符的系统编码来进行比较的,所以按照排列顺序,大写字母将位于小写字母之前。
1 |
|
比较 string 类字符串
string 类重载了运算符,可以直接使用关系运算符
1 |
|
复合语句(语句块)
两个花括号括起来的部分将形成一条复合语句,也即代码块,将视为一条语句。
复合语句有一种特性,如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完语句块后,变量将被释放。
然而,如果一个变量既在代码块外面有声明,内部也有声明的时候,使用的时候是哪个呢?这个时候遵循就近原则。代码块内部使用时,离内部声明较近,使用的是内部的,执行完代码块后,代码块内的变量释放,外部使用的是外部声明的变量。
1 |
|
for 循环
很多情况下都需要程序执行重复的任务,如依次打印数组中所有的元素,从 1 累加到 100 等,for 循环可以轻松完成这样的重复性任务。
for 语句的句法如下:
for (for-init-statement condition; expression)
statement
for
括号中包含三部分 for-init-statement
语句,condition
表达式,expression
表达式。这里 for-init-statement
视为一条语句,有自己的分号。在 for-init-statement
语句中可以让我们进行一些初始化的操作。
使用 for 循环进行计算
1 | // 打印整数的阶乘 |
使用 for 循环访问字符串
1 |
|
Tips: 省略 for 循环中的测试表达式时,测试结果将为 true,因此下面的循环将一直运行下去:
1 | for (; ; ;) |
C++11 新增基于范围的 for 循环
基于范围的 for 循环简化了一种常见的循环任务:对数组(或容器类,如 vector 和 array)的每个元素执行相同的操作,如下所示:
1 | double price[5] = {4.99, 20, 2.33, 8.888, 6.87}; |
还可以通过这种方法修改数组的元素。
1 | for (double &x : price) // &x 表明 x 是一个引用变量 |
while 循环
语法:
1 | while (test-condition) |
while 与 for 循环本质上是一样的,使用哪一个只是风格上的问题。通常,使用 for 循环来为循环计数,在无法预先知道循环将执行的次数时,常使用 while 循环。
Tips: 在设计循环时,记住下面几条指导原则。
- 指定循环终止的条件。
- 在首次测试之前初始化条件。
- 在条件被再次测试之前更新条件。
while 循环编写延时循环
ANSI C 和 C++ 库中有一个函数有助于完成延时工作。这个函数名为 clock()
,返回程序开始执行后所用的系统时间。但这有两个复杂的问题,首先,clock()
返回时间的单位不一定是秒;其次,该函数的返回类型在某些系统上可能是 long,在另一些系统上可能是 unsigned long 或其他类型。
但头文件 ctime(较早的实现中为 time.h)提供了这些问题的解决方案。首先,它定义了一个符号常量-CLOCK_PER_SEC,该常量等于每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,可以得到秒数。或者将秒数乘以 CLOCK_PER_SEC,可以得到以系统时间为单位的时间。其次,ctime 将 clock_t 作为 clock() 返回类型的别名,这意味着可以将变量声明为 clock_t 类型,编译器将把它转换为 long、unsigned int 或适合系统的其他类型。
1 | // 程序以系统时间为单位计算延时时间,避免了在每轮循环中将系统时间转换为秒 |
类型别名
C++ 为类型建立别名的方式有两种。
使用预处理方式
#define BYTE char // BYTE 作为 char 的别名,预处理器将在编译程序时用
char
替换所有的BYTE
,编译时将只是做最基本的字符替换操作,所以有些情况下不适合使用这种方式,如给指针类型取别名时1
2#define float_pointer float *;
float_point pa, pb; // 预处理器将转换为 float * pa, pb; 此时 pb 是 float 类型,非指针类型,使用 typedef 不会有这样的问题使用关键字
typedef
格式为:typedef typeName aliasName; // 注意和 #define 的定义顺序相反
typedef char byte;
// 将 byte 作为 char 的别名,注意 typedef 并不会创建新类型,只是别名
do while 循环
出口条件循环,即这种循环首先执行循环体。
1 | do |
循环和文本输入
使用原始 cin 进行输入
1 | // 读取一行输入,遇到 # 字符时停止 |
下面是程序的运行情况:
1 | 请输入字符,将在 # 号时结束:this is a right select.#not read |
由于 cin 在读取字符时将忽略空格和换行符,所以程序在输出时省略了空格,计数中也没有包括空格。而且,发送给 cin 的输入被缓冲,只有用户按下回车键后,输入的内容才会被发送给程序。
使用 cin.get(char) 进行输入
cin.get(char) 将读取输入中的下一个字符(包括空格、制表符和换行符)并赋值给括号中的形参。替换上面的程序可以修补空格及计数的问题。
1 | // 读取一行输入,遇到 # 字符时停止 |
在 C 语言中,要修改变量的值,必须将变量的地址传递给函数。但上面程序 cin.get(ch) 传递的是一个 char 类型,不是一个地址,在 C 语言中这样的代码无效。但在 C++ 中有效,只要函数将参数声明为引用即可,头文件 iostream 将 cin.get(ch) 的参数声明为引用类型,因此该函数可以修改其参数的值。
读取文件输入
如果输入来自于文件,则可以使用一种功能更强大的技术-检测文件尾(EOF)。C++ 输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。
读取输入能通过读取文件操作来实现,是因为很多操作系统都支持重定向,允许用文件替换键盘输入。<
符号是 Unix 和 Windows 命令提示符模式的重定向运算符。其次,很多操作系统都允许通过键盘来模拟文件尾条件。在 Unix 中,可以在行首按下 Ctrl+D
来实现;在 Windows 命令提示符模式下,可以在任意位置按 Ctrl+Z
和 Enter
。
如果编程环境能够检测 EOF,可以在程序中使用重定向的文件,也可以使用键盘输入,并在键盘输入中模拟 EOF。
检测到 EOF 后,cin 将两位(eofbit 和 failbit)都设置为 1。可以通过成员函数 eof() 来查看 eofbit 是否被设置;如果检测到 EOF 后,则 cin.eof() 将返回 bool 值 true,否则返回 false。同样,如果 eofbit 或 failbit 被设置为 1,则 fail() 成员函数返回 true,否则返回 false。注意,eof() 和 fail() 方法报告最近读取的结果,也就是说,它们在事后报告,而不是预先报告。因此应将 cin.eof() 或 cin.fail() 测试放在读取后。实际中使用 fail() 更多。
1 |
|
程序的运行情况如下:
The green bird sings in the winter.<ENTER>
The green bird sings in the winter.
Yes, but the crow flies in the dawn.<ENTER>
Yes, but the crow flies in the dawn.
<CTRL>+<z><ENTER> // Windows 7 下 Ctrl+z 模拟文件尾
73 characters read
EOF 结束输入
前面指出过,cin 方法检测到 EOF 时,将设置 cin 对象中一个指示 EOF 条件的标记。设置这个标记后,cin 将不读取输入,再次调用 cin 也不管用。对于文件输入,这是有道理的,因为程序不应该读取超出文件尾的内容。然而,对于键盘输入,有可能使用模拟 EOF 来结束循环,但后续还需要读取其它输入。cin.clear() 方法可以清除 EOF 标记,使输入继续进行。这将在后续详细介绍,不过要记住的是,在有些系统中,按 Ctrl+z 实际上将结束输入和输出,而 cin.clear() 将无法恢复输入和输出。
常见的字符输入做法
每次读取一个字符,直到遇到 EOF 的输入循环的基本设计如下:
1 | cin.get(ch); // 首先先读取一个字符 |
上面判断条件可以简化为:!cin.fail()
。
方法 cin.get(char)
的返回值是一个 cin
对象。然而 istream
类提供了一个可以将 istream
对象(如 cin
)转换为 bool 值的函数;此 cin
出现在需要 bool 值的地方(如在 while 循环的测试条件中)时,该转换函数将被调用。另外,如果最后一次读取成功了,则转换得到的 bool 值为 true;否则为 false。这意味着可以将上述 while 测试简化为这样:
1 | while (cin) // 当读取成功时 |
这比 !cin.fail()
或 !cin.eof()
更通用,因为它可以检测到其他失败原因,如磁盘故障。
最后,由于 cin.get(char)
返回值为 cin
,因此可以将循环简化成如下格式:
1 | while (cin.get(ch)) // cin.get(ch) 读取一个字符,并返回 cin 对象,cin 出现在 while 条件语句中,将转换为 bool 类型,读取成功则返回 true,否则返回 false |
这样,cin.get(char) 只被调用一次,而不是两次:循环前一次,循环结束后一次。
另一个 cin.get() 版本
C 语言用户可能喜欢 C 语言中的字符 I/O 函数——getchar() 和 putchar(),它们仍然可以在 C++ 中使用,只要像 C 语言中那样包含头文件 stdio.h(或新的 cstdio)即可。也可以使用 istream 和 ostream 类中类似功能的成员函数,来看看这种方式。
不接受任何参数的 cin.get()
成员函数返回输入中的下一个字符。也就是说,可以这样使用它:
ch = cin.get();
该函数的工作方式与 C 语言中的 getchar() 相似,将字符编码作为 int 值返回;而 cin.get(ch) 返回一个对象,而不是读取的字符。同样,可以使用 cout.put 函数来显示字符:
cout.put(ch);
该函数的工作方式类似 C 语言中的 putchar(),只不过其参数类型为 char,而不是 int。
Tips: 最初,put() 成员只有一个原型——put(char)。可以传递一个 int 参数给它,该参数将被强制转换为 char。C++ 标准还要求只有一个原型。然而,有些 C++ 实现都提供了 3 个原型:put(char)、put(signed char) 和 put(unsigned char)。在这些实现中,给 put() 传递一个 int 参数将导致错误消息,因为转换 int 的方式不止一种。使用显式强制类型转换的原型(如 cin.put(char(ch)))可使用 int 参数。
为成功地使用 cin.get(),需要知道其如何处理 EOF 条件。当该函数到达 EOF 时,cin.get() 将返回一个用符号常量 EOF 表示的特殊值,该常量在头文件 iostream 中定义,值为 -1。
1 | int ch; |
cin.get() 两个版本的区别
属性 | cin.get(ch) | ch=cin.get() |
---|---|---|
传递输入字符的方式 | 赋给参数 ch | 将函数返回值赋给 ch |
用于字符输入时函数的返回值 | istream 对象(执行 bool 转换后为 true) | int 类型的字符编码 |
到达 EOF 时函数的返回值 | istream 对象(执行 bool 转换后为 false) | EOF |
使用 cin.get(ch) 进行输入时,将不会导致任何类型方面的问题,且该函数在到达 EOF 时,不会将一个特殊值赋给 ch,在这种情况下,它不会将任何值赋给 ch,ch 不会被用来存储非 char 值。对于 cin.get() 返回是的 int 类型的值,如果要输出为字符,则需要强制转换。
那么应使用 cin.get() 还是 cin.get(char) 呢?使用字符参数的版本更符合对象方式,因为其返回值是 istream 对象。这意味着可以将它们拼接起来。如
cin.get(ch1).get(ch2);
// 这个是可行的,因为 cin.get(char) 返回的是 cin 对象