这一章在干嘛?
计算机的本事就是不知疲倦地重复。要算 16 个阶乘、把数组每个元素加一遍、逐个字符读入一篇文章,总不能把同一条语句抄 20 遍。本章讲 C++ 的三种循环(for、while、do while)、配合循环的递增递减运算符和关系表达式,以及一个特别实用的场景——用循环逐字符处理文本输入。学完这章,配合上一章的数组,你的程序才算真正”能干活”。
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、用 >= 测试是否到达第一个字符。
自测:输入起始值为 0 时,清单 5.2 的循环体执行几次?
零次。for 是入口条件循环,第一次测试
i(值为 0,即 false)就失败了,直接跳到循环后面的语句。
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 truecout 默认把 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。
自测:
int j = 0, i = word.size() - 1;里的逗号是逗号运算符吗?不是,它是声明语句里分隔变量列表的分隔符。这也是清单 5.9 把 i、j 的声明提到循环外的原因——不能用逗号运算符合并两个声明。
5.6 关系表达式
计算机的决策能力建立在比较之上。C++ 有六个关系运算符,结果都是 bool 的 true/false:
| 运算符 | 含义 |
|---|---|
| < | 小于 |
| <= | 小于或等于 |
| == | 等于 |
| > | 大于 |
| >= | 大于或等于 |
| != | 不等于 |
关系运算符比算术运算符优先级低,所以 x + 3 > y - 2 就是 (x + 3) > (y - 2)。
= 与 == 的世纪大坑
musicians == 4 是比较(值为 true/false);musicians = 4 是赋值(整个表达式值为 4,非 0 即真)。如果把 == 手滑写成 =,代码依然合法,测试永远为真。程序清单 5.10 故意演示后果: