1.4 控制流
1.4.1 while语句
while语句的形式为
while(condition)
statement
while语句的执行过程是交替地检测condition条件和执行关联的语句statement,直至condition为假时停止。所谓条件(condition)就是一个产生真或假的结果的表达式。
例如,
while(val <= 10) {
sum += val;
++val;
}
其中条件中使用了小于等于运算符(<=)来比较val的当前值和10。
所谓的语句块,就是用花括号包围的零条或多条语句的序列。语句块是语句的一种,在任何要求使用语句的地方都可以使用语句块。在本例中,语句块的第一条语句使用了复合赋值运算符(+=)。将右侧的运算对象加到左侧运算对象上,将结果保存在左侧运算对象中。它本质上与一个加法结合一个赋值(assignment)是相同的:
sum = sum + val;
使用前缀递增运算符(++)。递增运算符将运算对象的值增加1。++val等价于val = val + 1。
prog1.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1;
while(val <= 10) {
sum += val;
++val;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
return 0;
}
prog2.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1;
while(val <= 10) {
sum += val;
val = val + 1;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
return 0;
}
我们看一下,编译器对于独立语句++val和val = val + 1的处理有什么不同:
etc@ruc-etc:~/cpp/ch1/4$ g++ prog1.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ g++ prog2.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ ls
prog1 prog1.cpp prog1.s prog2.cpp prog2.s
etc@ruc-etc:~/cpp/ch1/4$ diff prog1.s prog2.s
1c1
< .file "prog1.cpp"
---
> .file "prog2.cpp"
由上述结果看出,编译器对独立语句++val和val = val + 1的处理并没有什么不同。同理,我们再测试一下val++和val += 1。
prog3.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1;
while(val <= 10) {
sum += val;
val++;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
return 0;
}
prog4.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1;
while(val <= 10) {
sum += val;
val += 1;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
return 0;
}
测试结果如下:
etc@ruc-etc:~/cpp/ch1/4$ g++ prog3.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ g++ prog4.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ diff prog3.s prog4.s
1c1
< .file "prog3.cpp"
---
> .file "prog4.cpp"
etc@ruc-etc:~/cpp/ch1/4$ diff prog1.s prog3.s
1c1
< .file "prog1.cpp"
---
> .file "prog3.cpp"
结论是当++val,val++,val = val + 1和 val += 1作为一条独立的语句存在时,当前编译器对它们的处理是相同的。
我们对程序做简单的修改后,如下:
prog1.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1, n;
while(val <= 10) {
sum += val;
n = ++val;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
cout << "n : " << n << endl;
return 0;
}
prog2.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1, n;
while(val <= 10) {
sum += val;
n = val = val + 1;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
cout << "n : " << n << endl;
return 0;
}
prog3.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1, n;
while(val <= 10) {
sum += val;
n = val++;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
cout << "n : " << n << endl;
return 0;
}
prog4.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1, n;
while(val <= 10) {
sum += val;
n = val += 1;
}
cout << "Sum of 1 to 10 inclusive is "
<< sum << endl;
cout << "n : " << n << endl;
return 0;
}
测试结果如下:
etc@ruc-etc:~/cpp/ch1/4$ g++ prog1.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ g++ prog2.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ g++ prog3.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ g++ prog4.cpp -S
etc@ruc-etc:~/cpp/ch1/4$ diff prog1.s prog2.s
1c1
< .file "prog1.cpp"
---
> .file "prog2.cpp"
etc@ruc-etc:~/cpp/ch1/4$ diff prog1.s prog3.s
1c1
< .file "prog1.cpp"
---
> .file "prog3.cpp"
26d25
< addl $1, 24(%esp)
28a28
> addl $1, 24(%esp)
etc@ruc-etc:~/cpp/ch1/4$ diff prog1.s prog4.s
1c1
< .file "prog1.cpp"
---
> .file "prog4.cpp"
我们发现,当++val,val = val + 1和val += 1作为右值时,当前编译器对它们的处理相同;val++作为右值的时候,当前编译器对其处理与上述三者不同。不难看出执行addl $1, 24(%esp)动作的时机不一样。我们不妨看一下程序运行的结果:
etc@ruc-etc:~/cpp/ch1/4$ g++ prog1.cpp -o prog1 -Wall -g
etc@ruc-etc:~/cpp/ch1/4$ g++ prog3.cpp -o prog3 -Wall -g
etc@ruc-etc:~/cpp/ch1/4$ ./prog1
Sum of 1 to 10 inclusive is 55
n : 11
etc@ruc-etc:~/cpp/ch1/4$ ./prog3
Sum of 1 to 10 inclusive is 55
n : 10
前缀递增运算符和后缀递增运算符作为右值时,其运行结果也不同。
练习 1.9
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 50;
while(val <= 100) {
sum += val;
++val;
}
cout << "Sum of 50 to 100 inclusive is "
<< sum << endl;
return 0;
}
练习 1.10
#include <iostream>
using namespace std;
int main()
{
int val = 10;
while(val >= 0) {
cout << val << endl;
--val;
}
return 0;
}
练习 1.11
#include <iostream>
using namespace std;
int main()
{
int v1, v2;
cin >> v1 >> v2;
if(v1 > v2) {
int tmp = v1;
v1 = v2;
v2 = tmp;
}
while(v1 <= v2) {
cout << v1 << endl;
++v1;
}
return 0;
}
1.4.2 for语句
在while循环中检测变量、在循环体中递增变量的模式使用非常频繁,以至于C++专门定义了第二种循环语句——for语句,来简化符合这种模式的语句。
for语句的形式为
for(init-statement; condition; expression)
statement
其执行流程如下:
-
先执行初始化语句init-statement;
-
再对条件语句condition进行判断。如果condition为真,那么执行statement,否则退出循环;
-
执行expression;
-
重复第2步的条件检测,只要为真就继续执行剩余步骤。
练习 1.12
功能:求-100到100之间的整数之和。sum的终值应该是0。
练习 1.13
exercise1_13_1.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0;
for(int val = 50; val <= 100; ++val)
sum += val;
cout << "Sum of 50 to 100 inclusive is "
<< sum << endl;
return 0;
}
exercise1_13_2.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0;
for(int val = 10; val >= 0; --val)
sum += val;
cout << "Sum of 10 to 0 inclusive is "
<< sum << endl;
return 0;
}
exercise1_13_3.cpp:
#include <iostream>
using namespace std;
int main()
{
int v1, v2;
for(cin >> v1 >> v2; v1 <= v2; ++v1)
cout << v1 << endl;
return 0;
}
练习 1.14
对于for循环适用于循环次数已知的情况,while循环适用于循环次数未知的情况的说法,我不敢苟同。举一个栗子,带头节点的单链表的遍历。对于带头结点的单链表,我们是无法预知其节点个数的,那么按照上述的说法用while循环比较适合?实际上并不是这样:
struct list_node {
void *data;
struct list_node *next;
};
void list_traverse(struct list_node *head, void (*visit)(void *))
{
#if 0
struct list_node *p = head->next;
while(p) {
visit(p->data);
p = p->next;
}
#else
for(struct list_node *p = head->next; p; p = p->next)
visit(p->data);
#endif
}
不难看出for循环的形式比较清楚简洁。
实际上,for循环和while循环两者之间通常是可以互相替换的。对于while循环,我们能找到一个几乎等价的for循环:
while(condition)
statement
for( ; condition; )
statement
for( ; condition; statement)
;
同理,对于for循环,我们也能找到一个几乎等价的while循环:
for(init-statement; condition; expression)
statement
init-statement
while(condition) {
statement
expression
}
实际上,上述等价转换还有一些细微的差异,比如:
for循环的()中init-statement、condition、expression三者都是可以省略的。
for( ; ; )
for循环的init-statement、condition、expression都省略是经典的“死循环”,等价于
while(1)
while循环中的condition是不能够省略的,下面的写法会编译报错。
while()
另外,for循环和与其几乎等价的while循环在处理statement中continue语句的时候也颇为不同:for循环在执行完continue后,执行流会转到expression,而while循环会跳过expression。
例如,求1-100不能被5整除的数之和。我们可以用continue关键字,也可以单纯地使用if-else,这里特意使用continue关键字就是为了感受一下它在两种循环中的差异。
exercise1_14_1.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0;
for(int val = 1; val <= 100; ++val) {
if(0 == val % 5)
continue;
sum += val;
}
cout << "Sum = " << sum << endl;
return 0;
}
exercise1_14_2.cpp:
#include <iostream>
using namespace std;
int main()
{
int sum = 0, val = 1;
while(val <= 100) {
if(0 == val % 5)
continue;
sum += val;
++val;
}
cout << "Sum = " << sum << endl;
return 0;
}
运行结果如下:
etc@ruc-etc:~/cpp/ch1/4$ g++ exercise1_14_1.cpp -o exe14_1 -Wall -g
etc@ruc-etc:~/cpp/ch1/4$ g++ exercise1_14_2.cpp -o exe14_2 -Wall -g
etc@ruc-etc:~/cpp/ch1/4$ ./exe14_1
Sum = 4000
etc@ruc-etc:~/cpp/ch1/4$ ./exe14_2
^C
exe14_1可以正确执行,exe14_2会死循环。原因是当0 == val % 5满足后执行continue就会跳过后面expression(++val),即val值不会更新。又由于val值没更新condition必然为真,再执行循环体statement的时候会一直满足0 == val % 5执行continue…从而陷入死循环。
对于个人而言,更喜欢使用for循环。因为它清楚明了,多数情况下可以在一行中表示出循环的运作机制。对于while循环,可能需要将condition中涉及到的变量加上高亮,以方便观察这些变量更新的时机(充当左值/自增等),从而弄清楚循环的运行机制。
练习 1.15
略。
1.4.3 读取数量不定的输入数据
std::cin >> value
此表达式从标准输入读取下一个数,保存在value中。输入运算符返回其左侧运算对象。当我们使用一个istream对象作为条件时,其效果是检测流的状态。如果流是有效的,即流末遇到错误,那么检测成功。当遇到文件结束符,或者遇到一个无效输入时(例如读入的值的类型不匹配),istream对象的状态会变为无效。处于无效状态的istream对象会使条件变为假。
在Windows系统中,输入文件结束符的方法是敲Ctrl+Z(按住Ctrl键的同时按住Z键),然后按Enter或Return键。
在UNIX系统中,包括Mac OS X系统中,文件结束符输入使用Ctrl+D。
修正编译错误的原则:
对于编译时产生的错误信息,按照报告的顺序来逐个修正错误,是一种好习惯。因为一个单个错误常常会具有传递效应,导致编译器在其后报告比实际数量多得多的错误信息。另一个好习惯是在每修正一个错误后就立即重新编译代码,或者最多是修正了一小部分明显的错误后就重新编译。这就是所谓的“编辑-编译-调试”(edit-compile-debug)周期。
练习 1.16
#include <iostream>
using namespace std;
int main()
{
int sum = 0, value = 0;
while(cin >> value)
sum += value;
cout << "Sum is " << sum << endl;
return 0;
}
1.4.4 if语句
if语句的形式为
if(condition)
statement1
else
statement2
如果condition为真,那么执行statement1;否则执行statement2。
例如,统计输入中每个值连续出现了多少次,prog5.cpp:
#include <iostream>
using namespace std;
int main()
{
int cur_value = 0, value = 0;
if(cin >> cur_value) {
int cnt = 1;
while(cin >> value) {
if(cur_value == value)
++cnt;
else {
cout << cur_value << " occurs " << cnt << " times " << endl;
cur_value = value;
cnt = 1;
}
}
cout << cur_value << " occurs " << cnt << " times " << endl;
}
return 0;
}
C++用=进行赋值,用==作为相等运算符。一般而言,赋值运算符相对于相等运算符出现得更频繁,因此字符数较少的=就被赋予了更常用的含义——赋值操作。这个逻辑类似于电文传输编码(如Huffman编码)总是希望电文总长尽可能地短。此外,两个运算符都可以出现在条件中。一个常见的错误是想在条件中使用==(相等判断),却误用了=。为了规避上述的错误,对于常量和变量的相等比较,我们应该把常量写在变量前面,这是一个好的编码习惯。当我们少打一个=的时候,编译器就会报错,因为常量是不能作为左值的。
test.cpp:
#include <iostream>
using namespace std;
int main()
{
int a = 5;
if(3 = a)
cout << "a = 3" << endl;
else
cout << "a != 3" << endl;
return 0;
}
编译结果:
etc@ruc-etc:~/cpp/ch1/4$ g++ test.cpp -o test -Wall -g
test.cpp: In function ‘int main()’:
test.cpp:9:9: error: lvalue required as left operand of assignment
当变量之间进行相等比较的时候,在编译选项中加入-Wparentheses,并且注意编译警告就显得尤为重要了。
test.cpp:
#include <iostream>
using namespace std;
int main()
{
int a = 5, b = 4;
if(a = b)
cout << "a = b" << endl;
else
cout << "a != b" << endl;
return 0;
}
不加相关编译选项的编译结果:
etc@ruc-etc:~/cpp/ch1/4$ g++ test.cpp -o test -g
etc@ruc-etc:~/cpp/ch1/4$
加上相关编译选项后的编译结果:
etc@ruc-etc:~/cpp/ch1/4$ g++ test.cpp -o test -g -Wparentheses
test.cpp: In function ‘int main()’:
test.cpp:9:10: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
当然,确实存在需要对变量进行赋值并且检查该变量的新值是否为0的情形。为了避免来自该类编译器警告,我们不应该简单地关闭警告选项,而是显式地进行比较。如使用ch暂存getchar()得到的标准输入的字符,直到收到’\0’(即0,字符串的结束标识)为止。不应写作:
while(ch = getchar())
do somthing;
而是写作:
while((ch = getchar()) != '\0')
do somthing;
这种写法也使得代码的意图一目了然,方便阅读者理解。
练习 1.17
如果输入的所有值都是相等的,程序会输出输入值以及输入该值的个数;如果没有重复值,每个输入值都会输出一行,且输出的cnt值为1。
练习 1.18
运行结果如下:
etc@ruc-etc:~/cpp/ch1/4$ g++ prog5.cpp -o prog5 -Wall -g
etc@ruc-etc:~/cpp/ch1/4$ vi case
etc@ruc-etc:~/cpp/ch1/4$ cat case
42 42 42 42 42
etc@ruc-etc:~/cpp/ch1/4$ ./prog5 < case
42 occurs 5 times
etc@ruc-etc:~/cpp/ch1/4$ vi case
etc@ruc-etc:~/cpp/ch1/4$ !c
cat case
42 43 44 45 46
etc@ruc-etc:~/cpp/ch1/4$ !./
./prog5 < case
42 occurs 1 times
43 occurs 1 times
44 occurs 1 times
45 occurs 1 times
46 occurs 1 times
练习 1.19
修改的程序应该是练习 1.11。
#include <iostream>
using namespace std;
int main()
{
int v1, v2;
cin >> v1 >> v2;
while(v1 <= v2) {
cout << v1 << endl;
++v1;
}
return 0;
}
C++程序的缩进和格式
对于C++程序缩进和格式风格的重要性在1.3 注释简介中有过说明。此外,我们要牢记一件重要的事情:当我们选择一种格式风格时,思考一下它会对程序的可读性和易理解性有什么影响,而一旦选择了一种风格,就要坚持使用。