C++学习之九、循环和关系表达式

程序通常使用关系表达式和逻辑表达式来控制其行为。

表达式

表达式定义与值

任何值或任何有效的值和运算符的组合都是表达式,C++ 中每个表达式都有值。

1
2
3
4
10  // 10 是值为 10 的表达式
28 * 10 // 值为 280 的表达式
x = 20 // 两个值和一个赋值运算符组成的表达式。C++ 将赋值表达式的值定义为左侧成员的值,因此这个表达式的值为 20
x < 30 // 表达式的值为 true 或 false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// express.cpp -- 表达式

#include <iostream>

int main()
{
using namespace std;
int x;
cout << "The expression x = 100 has the value ";
cout << (x = 100) << endl; // 赋值表达式的值为左侧成员的值,此时 x 的值为 100,所以输出为 100
cout << "Now x = " << x << endl;
cout << "The expression x < 3 has the value ";
cout << (x < 3) << endl; // 默认情况下,cout 在显示 bool 值之前将它们转换为 int,所以此输出为 0
cout << "The expression x > 3 has the value ";
cout << (x > 3) << endl; // 同上,此时输出为 1
cout.setf(ios_base::boolalpha); // C++ 的一个新功能,此函数设置了一个标记,该标记命令 cout 对判断语句显示 true 和 false,而不是 1 和 0
cout << "The expression x < 3 has the value ";
cout << (x < 3) << endl; // 输出为 false
cout << "The expression x > 3 has the value ";
cout << (x > 3) << endl; // 输出为 true
}

表达式副作用(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
2
while (guests++ < 10)   // guests 值首先用于判断是否小于 10,由于 while 循环中检测条件的表达式是一个顺序点,所以此语句将保证 guests 的值被加 1
cout << guests << endl; // guests 在条件语句中已经加 1,这里将输出加 1 后的值

表达式语句

从表达式到表达式语句的转变很容易,只要加上分号即可。

1
2
age = 100   // 表达式
age = 100; // 语句,更准确地说,这是一条表达式语句

非表达式语句

表达式语句去掉分号就是表达式。但不是所有的语句都是这样。很多种情况语句去掉分号并不是表达式,此为非表达式语句。

1
int a;  // 这是一条语句,但 int a 并不是一条表达式,因此它没有值,不能用于直接运算

关系表达式

C++ 提供了 6 种关系运算符来对数字进行比较。

操作符 含义
< 小于
<= 小于或等于
== 等等于
> 大于
>= 大于或等于
!= 不等于

Tips: 不要混淆 == 与赋值运算符 = 。前者用于判断,后者则是赋值。当使用赋值表达式作为判断时,值为等号左边的值,非零值为 true,零值为 false

C——风格字符串比较

数组名是数组的地址,同样,用引号括起的字符串常量也是其地址。因此,使用 == 只能比较两个字符串的地址。

应使用 C-风格字符串库中的 strcmp() 函数来比较。该函数接受两个字符串地址作为参数,这意味着参数可以是指针、字符串常量或字符数组名。比较时,如果两个字符串相同,该函数将返回零;如果第一个字符串按字母顺序在第二个字符串之前,则 strcmp() 将返回一个负数值;如果第一个字符串按字母顺序排在第二个字符串之后,则 strcmp() 将返回一个正数值,字符是根据字符的系统编码来进行比较的,所以按照排列顺序,大写字母将位于小写字母之前。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <cstring> // 包含 strcmp() 原型
int main()
{
using namespace std;
char word[5] = "?ate";
for (char ch = 'a'; strcmp(word, "mate"); ch++) // 比较两个字符串,如果字符不相等,则值为非零(true),相等则值为零(false)
{
cout << word << endl;
word[0] = ch;
}
cout << "After loop ends, word is " << word << endl;
return 0;
}

比较 string 类字符串

string 类重载了运算符,可以直接使用关系运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string> // 包含 string 类
int main()
{
using namespace std; // string 包含在命名空间 std 中
string word = "?ate";
for (char ch = 'a'; word != "mate"; ch++)
{
cout << word << endl;
word[0] = ch;
}
cout << "After loop ends, word is " << word << endl;
return 0;
}

复合语句(语句块)

两个花括号括起来的部分将形成一条复合语句,也即代码块,将视为一条语句。

复合语句有一种特性,如果在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完语句块后,变量将被释放。

然而,如果一个变量既在代码块外面有声明,内部也有声明的时候,使用的时候是哪个呢?这个时候遵循就近原则。代码块内部使用时,离内部声明较近,使用的是内部的,执行完代码块后,代码块内的变量释放,外部使用的是外部声明的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

int main()
{
using namespace std;
int x = 20;
{
cout << x << endl; // 使用的外部变量 x = 20
int x = 100;
cout << x << endl; // 使用的上面定义的变量 x = 100
}
cout << x << endl; // 内部定义的变量被释放,这时候使用的还是外部 x = 20
return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 打印整数的阶乘
#include <iostream>
const int ArSize = 16;
int main()
{
long long factorials[ArSize];
factorials[0] = factorials[1] = 1LL;
for (int i = 2; i < ArSize; i++) // 包含 ArSize 个元素的数组的下标从 0 到 ArSize-1,这里也可以使用 i<=ArSize-1,但它看上去没有前面的表达式好
{
factorials[i] = i * factorials[i-1];
}
for (int i = 0; i < ArSize; i++)
{
std::cout << i << "!=" << factorials[i] << std::endl;
}
return 0;
}

使用 for 循环访问字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

int main()
{
using namespace std; // string 类包含在命名空间 std 中
cout << "Enter a word: ";
string word;
getline(cin, word); // 通过 cin 读入一行字符到 word 中
for (int i = word.size() - 1; i >= 0; i--) // 反向打印 word
{
cout << word[i];
}
cout << endl;
return 0;
}

Tips: 省略 for 循环中的测试表达式时,测试结果将为 true,因此下面的循环将一直运行下去:

1
2
for (; ; ;)
body // 如果循环体只有一条语句,可以不加大括号

C++11 新增基于范围的 for 循环

基于范围的 for 循环简化了一种常见的循环任务:对数组(或容器类,如 vector 和 array)的每个元素执行相同的操作,如下所示:

1
2
3
4
5
double price[5] = {4.99, 20, 2.33, 8.888, 6.87};
for (double x : price) // x 将依次迭代取得 price 中的值
{
cout << x << std::endl;
}

还可以通过这种方法修改数组的元素。

1
2
3
4
for (double &x : price) // &x 表明 x 是一个引用变量
{
x *= 0.8;
}

while 循环

语法:

1
2
while (test-condition)
body

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 程序以系统时间为单位计算延时时间,避免了在每轮循环中将系统时间转换为秒
#include <iostream>
#include <ctime> // 包含 clock() 原型

int main()
{
using namespace std;
float sec;
cout << "Please input the time(s) you wait to wait: ";
cin >> sec;
clock_t delay = sec * CLOCKS_PER_SEC; // 将秒钟转换为系统时钟
clock_t start = clock(); // 使用系统时钟计算开始时间
while (clock() - start < delay)
{

}
cout << "done \a\n";
return 0;
}

类型别名

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
2
3
do
body
while (test-expression);

循环和文本输入

使用原始 cin 进行输入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 读取一行输入,遇到 # 字符时停止
#include <iostream>

int main()
{
using namespace std;
char ch;
cout << "请输入字符,将在 # 号时结束:";
int count = 0;
cin >> ch; // 循环中直接先判断是否为 #,在循环之前应该先读取一个字符到 ch 中
while (ch != '#')
{
count ++;
cout << ch;
cin >> ch;
}
cout << endl;
cout << "Have read " << count << " chars" << endl;
}

下面是程序的运行情况:

1
2
3
请输入字符,将在 # 号时结束:this is a right select.#not read
thisisarightselect.
Have read 19 chars

由于 cin 在读取字符时将忽略空格和换行符,所以程序在输出时省略了空格,计数中也没有包括空格。而且,发送给 cin 的输入被缓冲,只有用户按下回车键后,输入的内容才会被发送给程序。

使用 cin.get(char) 进行输入

cin.get(char) 将读取输入中的下一个字符(包括空格、制表符和换行符)并赋值给括号中的形参。替换上面的程序可以修补空格及计数的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 读取一行输入,遇到 # 字符时停止
#include <iostream>

int main()
{
using namespace std;
char ch;
cout << "请输入字符,将在 # 号时结束:";
int count = 0;
cin.get(ch); // 读取一个字符赋值给 ch,这将改变 ch 的值
while (ch != '#')
{
count ++;
cout << ch;
cin.get(ch);
}
cout << endl;
cout << "Have read " << count << " chars" << endl;
}

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

读取文件输入

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

读取输入能通过读取文件操作来实现,是因为很多操作系统都支持重定向,允许用文件替换键盘输入。< 符号是 Unix 和 Windows 命令提示符模式的重定向运算符。其次,很多操作系统都允许通过键盘来模拟文件尾条件。在 Unix 中,可以在行首按下 Ctrl+D 来实现;在 Windows 命令提示符模式下,可以在任意位置按 Ctrl+ZEnter

如果编程环境能够检测 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
int main()
{
using namespace std;
char ch;
int count = 0;
cin.get(ch); // 先读取一个字符,因为读取文件尾是“事后报告”,需要先执行读取操作
while (cin.fail() == false)
{
cout << ch;
++ count;
cin.get(ch);
}
cout << endl << count << " characters read\n";
return 0;
}

程序的运行情况如下:

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
2
3
4
5
6
cin.get(ch);    // 首先先读取一个字符
while (cin.fail() == false) // 判断上次的读操作是否读到的是文件尾
{
...
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
2
3
4
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
2
3
4
5
6
7
8
int ch;
ch = cin.get();
while (ch != EOF)
{
cout.put(ch);
++count;
ch = cin.get();
}

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 对象