这一章在干嘛?

计算机的本事就是不知疲倦地重复。要算 16 个阶乘、把数组每个元素加一遍、逐个字符读入一篇文章,总不能把同一条语句抄 20 遍。本章讲 C++ 的三种循环(for、while、do while)、配合循环的递增递减运算符和关系表达式,以及一个特别实用的场景——用循环逐字符处理文本输入。学完这章,配合上一章的数组,你的程序才算真正”能干活”。

5.1 for 循环

5.2 表达式与语句

5.3 递增与递减运算符

5.4 组合赋值运算符与代码块

5.5 逗号运算符

5.6 关系表达式

5.7 while 循环

5.8 类型别名与延时循环

5.9 do while 循环与范围 for

5.10 循环与文本输入

5.11 嵌套循环与二维数组

5.1 for 循环

程序清单 5.1 用最简单的 for 循环打印 5 行字:

// forloop.cpp -- for 循环入门
#include <iostream>
int main()
{
    using namespace std;
    int i; // 计数器
    // 初始化;测试;更新
    for (i = 0; i < 5; i++)
        cout << "C++ knows loops.\n";
    cout << "C++ knows when to stop.\n";
    return 0;
}
C++ knows loops.
C++ knows loops.
C++ knows loops.
C++ knows loops.
C++ knows loops.
C++ knows when to stop.

循环先把 i 设为 0(初始化),然后测试 i < 5:为真就执行循环体,再用 i++(更新,把 i 加 1)完成一轮;回到测试,如此往复,直到 i 变成 5、测试失败,程序才走向循环后面的语句。i++ 里的 ++ 叫递增运算符,作用是把操作数加 1。

for 循环的完整配方:

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

三个要点:

  • 初始化只在开头执行一次,通常用来设起始值;
  • 测试表达式决定循环体是否执行。它不必是 true/false 比较——任何表达式都会被转换成 bool:0 变 false 结束循环,非 0 变 true 继续。程序清单 5.2 用 i 本身当测试条件,倒计数到 0 自动停:
// num_test.cpp -- 在 for 循环中使用数值测试
#include <iostream>
int main()
{
    using namespace std;
    cout << "Enter the starting countdown value: ";
    int limit;
    cin >> limit;
    int i;
    for (i = limit; i; i--) // i 为 0 时退出
        cout << "i = " << i << "\n";
    cout << "Done now that i = " << i << "\n";
    return 0;
}
Enter the starting countdown value: 4
i = 4
i = 3
i = 2
i = 1
Done now that i = 0
  • for 是入口条件循环:每次循环前先测试。如果一开始条件就是假(比如输入 0),循环体一次都不执行——“先看再跳”,这种态度能让程序少踩坑。

▲ 图 5.1 for 循环的设计

在初始化部分声明变量是 C++ 的常见写法:for (int i = 0; i < 5; i++)——这样的 i 只在 for 语句内存在,出了循环就没了。循环体按语法只是一条语句,想放多条就得用花括号包成块(见 5.4)。

程序清单 5.4 用循环算阶乘,顺带展示了 for 与数组的黄金搭档关系:

// formore.cpp -- 更多 for 循环
#include <iostream>
const int ArSize = 16;    // 外部声明的例子
int main()
{
    long long factorials[ArSize];
    factorials[1] = factorials[0] = 1LL;
    for (int i = 2; i < ArSize; i++)
        factorials[i] = i * factorials[i-1];
    for (int i = 0; i < ArSize; i++)
        std::cout << i << " != " << factorials[i] << std::endl;
    return 0;
}
0! = 1
1! = 1
2! = 2
3! = 6
4! = 24
5! = 120
6! = 720
7! = 5040
8! = 40320
9! = 362880
10! = 3628800
11! = 39916800
12! = 479001600
13! = 6227020800
14! = 87178291200
15! = 1307674368000

数组下标从 0 到 ArSize-1,所以测试写 i < ArSize 正好停在下标越界之前。把数组大小定义为 const 符号常量(ArSize),改规模时只改一处。

程序清单 5.5 说明更新表达式可以是任意表达式(这里每步加用户指定的步长):

// bigstep.cpp -- 按指定步长计数
#include <iostream>
int main()
{
    using std::cout;    // using 声明
    using std::cin;
    using std::endl;
    cout << "Enter an integer: ";
    int by;
    cin >> by;
    cout << "Counting by " << by << "s:\n";
    for (int i = 0; i < 100; i = i + by)
        cout << i << endl;
    return 0;
}
Enter an integer: 17
Counting by 17s:
0
17
34
51
68
85

注意:测试用 i < 100 而不是 i == 100——i 从 85 直接跳到 102,等于 100 的时刻根本不存在,等值测试会永远错过。范围测试优于等值测试。

程序清单 5.6 用 for 逐字符倒序打印一个单词:

// forstr1.cpp -- for 与 string 搭配
#include <iostream>
#include <string>
int main()
{
    using namespace std;
    cout << "Enter a word: ";
    string word;
    cin >> word;
 
    // 倒序显示字母
    for (int i = word.size() - 1; i >= 0; i--)
        cout << word[i];
    cout << "\nBye.\n";
    return 0;
}
Enter a word: animal
lamina
Bye.

word.size() 给出字符数,i 从最后一个下标开始、用 -- 每轮减 1、用 >= 测试是否到达第一个字符。

5.2 表达式与语句

C++ 中任何值或值与运算符的合法组合都是表达式,且每个表达式都有值。 22 + 27 的值是 49;x = 20 也是表达式——赋值表达式的值被定义为左侧成员的值,即 20。正因如此,maids = (cooks = 4) + 3; 会让 maids 得到 7;同样地,x = y = z = 0; 能一口气把三个变量清零(赋值从右向左结合)。关系表达式 x < y 的值是 bool 的 true/false。

程序清单 5.3 验证这些说法:

// express.cpp -- 表达式的值
#include <iostream>
int main()
{
    using namespace std;
    int x;
 
    cout << "The expression x = 100 has the value ";
    cout << (x = 100) << endl;
    cout << "Now x = " << x << endl;
    cout << "The expression x < 3 has the value ";
    cout << (x < 3) << endl;
    cout << "The expression x > 3 has the value ";
    cout << (x > 3) << endl;
    cout.setf(ios_base::boolalpha); // 让 cout 显示 true/false 字样
    cout << "The expression x < 3 has the value ";
    cout << (x < 3) << endl;
    cout << "The expression x > 3 has the value ";
    cout << (x > 3) << endl;
    return 0;
}
The expression x = 100 has the value 100
Now x = 100
The expression x < 3 has the value 0
The expression x > 3 has the value 1
The expression x < 3 has the value false
The expression x > 3 has the value true

cout 默认把 bool 转成 1/0 显示,cout.setf(ios_base::boolalpha) 让它显示 true/false 字样。

求值改变内存中的数据叫副作用(side effect):求值 x = 100 的副作用是 x 变成 100;x + 15 不改任何东西,++x + 15 会。表达式加个分号就成了语句:age = 100 是表达式,age = 100; 是表达式语句。反过来的推论不成立——并非语句去掉分号就是表达式:int toad; 是声明语句,int toad 不是表达式没有值,所以 eggs = int toad * 1000; 非法。for 循环本身也不是表达式,不能赋给变量。

顺带一提:C++ 语法经过调整,允许在 for 的初始化部分放声明语句(如 for (int i = 0; ...)),这样声明的变量只属于这个循环。

5.3 递增与递减运算符

++ 和 — 各有两种形态,对操作数的最终效果相同,差别在生效时机:

  • 前缀 ++x:先加 1,再在表达式中使用新值;
  • 后缀 x++:先在表达式中使用当前值,再加 1。

就像修剪草坪先付钱还是后付钱——钱包最终一样,时点不同。程序清单 5.7:

// plus_one.cpp -- 递增运算符
#include <iostream>
int main()
{
    using std::cout;
    int a = 20;
    int b = 20;
 
    cout << "a = " << a << ": b = " << b << "\n";
    cout << "a++ = " << a++ << ": ++b = " << ++b << "\n";
    cout << "a = " << a << ": b = " << b << "\n";
    return 0;
}
a = 20: b = 20
a++ = 20: ++b = 21
a = 21: b = 21

用代码总结:

int x = 5;
int y = ++x; // 先改 x 再赋给 y:y 是 6,x 是 6
int z = 5;
int y = z++; // 先赋给 y 再改 z:y 是 5,z 是 6

常见坑:同一条语句里对同一变量多次递增/递减

x = 2 * x++ * (3 - ++x); 这类语句 C++ 不定义正确行为,不同系统结果可能完全不同。一条语句最多让一个变量自增/自减一次。

背后的原理是副作用与序列点:分号是序列点,语句中所有副作用(赋值、++、—)必须在进入下一条语句前完成。while (guests++ < 10) 的测试是完整表达式,C++ 保证比较用旧值、进入 cout 前 guests 已加 1。但 y = (4 + x++) + (6 + x++); 里两个子表达式都不是完整表达式,C++ 不保证 x 在子表达式之间被递增,所以结果因编译器而异——避开这种写法就好。

如果表达式的值没被使用(比如单独一条 x++; 或 for 的更新部分),前缀后缀效果相同。但对类类型(如迭代器)而言前缀版本效率略高(后缀要先存一份副本),习惯上值得留意。

与指针搭配

指针的 ++ 遵循指针算术规则:++pt 让 pt 指向下一个元素。* 和 ++ 组合时,看运算符的位置和优先级:

double arr[5] = {21.1, 32.8, 23.4, 45.2, 37.4};
double *pt = arr;   // pt 指向 arr[0],即 21.1
++pt;               // pt 指向 arr[1],即 32.8
 
double x = *++pt;   // 先 ++ 指针再解引用:arr[2],即 23.4
++*pt;              // 先解引用再 ++ 值:23.4 变 24.4,pt 不动
(*pt)++;            // 括号强制先解引用:24.4 变 25.4,pt 不动
x = *pt++;          // 后缀优先级高:取 arr[2] 的值 25.4,然后 pt 指向 arr[3]

口诀:前缀 *++pt 先动指针;++*pt 与 (*pt)++ 动的是值;后缀 *pt++ 取原位置的值然后指针后移。

5.4 组合赋值运算符与代码块

i = i + by 可以简写成 i += by。每种算术运算符都有对应的组合赋值形式:

运算符效果(L=左操作数,R=右操作数)
+=把 L + R 赋给 L
-=把 L - R 赋给 L
*=把 L * R 赋给 L
/=把 L / R 赋给 L
%=把 L % R 赋给 L

左侧必须是可赋值的东西(变量、数组元素、结构成员、解引用的指针):k += 3;、pa[4] += 6;、*(pa + 4) += 7; 都行,34 += 10; 就错了。

代码块(复合语句)

for 的循环体按语法只有”一条语句”,但用成对花括号可以把多条语句打包成一个复合语句(块),整体算一条。程序清单 5.8 的循环体要提示、读入、累加三件事:

// block.cpp -- 使用块语句
#include <iostream>
int main()
{
    using namespace std;
    cout << "The Amazing Accounto will sum and average ";
    cout << "five numbers for you.\n";
    cout << "Please enter five values:\n";
    double number;
    double sum = 0.0;
    for (int i = 1; i <= 5; i++)
    {   // 块从这里开始
        cout << "Value " << i << ": ";
        cin >> number;
        sum += number;
    }   // 块到这里结束
    cout << "Five exquisite choices indeed! ";
    cout << "They sum to " << sum << endl;
    cout << "and average to " << sum / 5 << ".\n";
    cout << "The Amazing Accounto bids you adieu!\n";
    return 0;
}
The Amazing Accounto will sum and average five numbers for you.
Please enter five values:
Value 1: 1942
Value 2: 1948
Value 3: 1957
Value 4: 1974
Value 5: 1980
Five exquisite choices indeed! They sum to 9801 and average to 1960.2.
The Amazing Accounto bids you adieu!

如果保留缩进却漏掉花括号,编译器不理会缩进——循环体只剩第一条 cout,循环只会打印 5 次提示,读入和累加都发生在循环结束之后。

块还影响变量寿命:块内定义的变量只在块内存在,出了块就被释放。外层块的变量在内层仍可见;但如果内层声明了同名变量,在内层它会遮蔽外层的同名变量,出块后外层变量恢复可见。

通关标准:

能背出五种组合赋值运算符;能解释”循环体只有一条语句”时如何放多条(花括号);能说出块内同名变量遮蔽外层变量的行为。

5.5 逗号运算符

块解决的是”一条语句的位置放多条语句”,逗号运算符解决的是”一个表达式的位置放多个表达式”。比如循环里想让 j 每轮加 1、i 每轮减 1,更新部分却只能放一个表达式:

++j, --i // 两个表达式在语法上算一个

注意区分:int i, j; 里的逗号只是列表分隔符,不是运算符。程序清单 5.9 用逗号运算符把字符串真正倒转(清单 5.6 只是倒着打印,这里原地交换字符):

// forstr2.cpp -- 反转数组
#include <iostream>
#include <string>
int main()
{
    using namespace std;
    cout << "Enter a word: ";
    string word;
    cin >> word;
 
    // 物理修改 string 对象
    char temp;
    int i, j;
    for (j = 0, i = word.size() - 1; j < i; --i, ++j)
    {   // 开始块
        temp = word[i];
        word[i] = word[j];
        word[j] = temp;
    }   // 结束块
    cout << word << "\nDone\n";
    return 0;
}
Enter a word: stressed
desserts
Done

初始化部分 j = 0, i = word.size() - 1 和更新部分 --i, ++j 都各塞了两个表达式进一个位置。

▲ 图 5.2 反转一个字符串

测试条件 j < i 让循环在到达中点时停止——继续换下去会把换好的字符再换回去。逗号运算符还有两个性质:保证先算左边再算右边(它是序列点,i = 20, j = 2 * i 安全且 j 得 40);整个逗号表达式的值是右边部分的值。它的优先级最低:cats = 17, 240; 被读成 (cats = 17), 240;,cats 是 17;而 cats = (17, 240); 里括号优先,cats 得 240。

5.6 关系表达式

计算机的决策能力建立在比较之上。C++ 有六个关系运算符,结果都是 bool 的 true/false:

运算符含义
<小于
<=小于或等于
==等于
>大于
>=大于或等于
!=不等于

关系运算符比算术运算符优先级低,所以 x + 3 > y - 2 就是 (x + 3) > (y - 2)。

= 与 == 的世纪大坑

musicians == 4 是比较(值为 true/false);musicians = 4 是赋值(整个表达式值为 4,非 0 即真)。如果把 == 手滑写成 =,代码依然合法,测试永远为真。程序清单 5.10 故意演示后果: