5.1 for循环

循环是编程中的基本控制结构,它允许我们重复执行一段代码,直到满足某个条件为止。for 循环是 C++ 中最常用的循环结构之一,特别适用于已知循环次数或需要按特定步长迭代的情况。

5.1.1 for循环的组成部分

for 循环的头部包含三个由分号 ; 分隔的部分,控制着循环的执行流程:

  1. 初始化 (Initialization):
    • 在循环开始前执行一次
    • 通常用于声明和/或初始化循环控制变量(计数器)。
    • 可以包含多条语句,用逗号分隔(见 5.1.11)。
    • 也可以为空。
  2. 测试条件 (Test Condition / Condition):
    • 每次循环迭代开始前进行求值。
    • 结果必须是一个布尔值 (truefalse) 或可以转换为布尔值(非零为 true,零为 false)。
    • 如果条件为 true,则执行循环体。
    • 如果条件为 false,则循环终止,程序继续执行循环后面的语句。
    • 也可以为空,空条件被视为 true,形成无限循环(需要其他方式跳出,如 break)。
  3. 更新 (Update / Increment / Decrement):
    • 每次循环迭代结束时(执行完循环体之后,下次测试条件之前)执行。
    • 通常用于修改循环控制变量(例如,递增或递减计数器)。
    • 可以包含多条语句,用逗号分隔。
    • 也可以为空。

语法:

1
2
3
4
5
6
7
8
9
for (initialization; test_condition; update) {
// 循环体 (statement(s) to be executed repeatedly)
statement1;
statement2;
// ...
}
// 或者如果循环体只有一条语句,可以省略花括号
for (initialization; test_condition; update)
single_statement;

执行流程:

  1. 执行 initialization
  2. 计算 test_condition
  3. 如果 test_conditionfalse,跳出循环,执行循环后面的代码。
  4. 如果 test_conditiontrue,执行循环体中的语句。
  5. 执行 update
  6. 回到步骤 2。

示例:

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

int main() {
// 打印数字 0 到 4
for (int i = 0; // 1. 初始化: 声明并初始化计数器 i 为 0
i < 5; // 2. 测试条件: 只要 i 小于 5 就继续
i = i + 1) // 4. 更新: 每次循环后将 i 增加 1
{
// 3. 循环体
std::cout << "i = " << i << std::endl;
}

std::cout << "Loop finished." << std::endl;

return 0;
}

5.1.2 回到for循环

for 循环提供了一种非常结构化的方式来编写计数循环。上面的例子展示了一个典型的从 0 开始计数到某个值之前的循环。

基本计数循环示例:

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

int main() {
// 计算 1 到 10 的和
int sum = 0;
for (int i = 1; i <= 10; i = i + 1) { // 从 1 开始,包含 10
sum = sum + i;
}
std::cout << "Sum of 1 to 10 is: " << sum << std::endl; // 输出 55

// 倒序打印 5 到 1
std::cout << "Countdown:" << std::endl;
for (int count = 5; count > 0; count = count - 1) {
std::cout << count << "..." << std::endl;
}
std::cout << "Liftoff!" << std::endl;

return 0;
}

5.1.3 修改步长

for 循环的更新部分不一定总是加 1 或减 1。你可以根据需要指定任何有效的更新表达式。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>

int main() {
// 以步长 2 递增,打印偶数
std::cout << "Even numbers less than 10:" << std::endl;
for (int i = 0; i < 10; i = i + 2) {
std::cout << i << " ";
}
std::cout << std::endl; // 输出 0 2 4 6 8

// 以步长 5 递减
std::cout << "Counting down by 5s:" << std::endl;
for (int n = 50; n >= 0; n = n - 5) {
std::cout << n << " ";
}
std::cout << std::endl; // 输出 50 45 40 35 30 25 20 15 10 5 0

// 使用乘法作为步长 (注意避免无限循环)
std::cout << "Powers of 2 less than 100:" << std::endl;
for (int p = 1; p < 100; p = p * 2) {
std::cout << p << " ";
}
std::cout << std::endl; // 输出 1 2 4 8 16 32 64

return 0;
}

5.1.4 使用for循环访问字符串

for 循环是遍历字符串(无论是 C 风格字符串还是 std::string 对象)中每个字符的常用方法。

示例 (C 风格字符串):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstring> // 为了 strlen()

int main() {
char message[] = "Hello";
int len = strlen(message); // 获取字符串长度 (不包括 \0)

std::cout << "Characters in \"" << message << "\":" << std::endl;
for (int i = 0; i < len; i = i + 1) { // 索引从 0 到 len-1
std::cout << "Index " << i << ": " << message[i] << std::endl;
}

// 也可以使用指针和空字符判断
std::cout << "Characters using pointer:" << std::endl;
for (char *p = message; *p != '\0'; p = p + 1) {
std::cout << *p << " ";
}
std::cout << std::endl;

return 0;
}

示例 (std::string):

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

int main() {
std::string word = "World";

std::cout << "Characters in \"" << word << "\":" << std::endl;
// 使用 size() 获取长度,size_t 通常是合适的索引类型
for (size_t i = 0; i < word.size(); i = i + 1) {
std::cout << "Index " << i << ": " << word[i] << std::endl;
}

// C++11 基于范围的 for 循环 (更简洁,见 5.4)
// std::cout << "Characters using range-based for:" << std::endl;
// for (char ch : word) {
// std::cout << ch << " ";
// }
// std::cout << std::endl;

return 0;
}

5.1.5 递增运算符(++)和递减运算符(–)

C++ 提供了两个非常有用的运算符来简化变量加 1 或减 1 的操作:

  • 递增运算符 (++): 将操作数的值增加 1。
  • 递减运算符 (--): 将操作数的值减少 1。

它们可以用于整数类型、浮点类型和指针类型。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

int main() {
int count = 5;
double value = 10.5;

// 递增
count++; // 等价于 count = count + 1; 现在 count 是 6
++value; // 等价于 value = value + 1; 现在 value 是 11.5

std::cout << "Count after increment: " << count << std::endl;
std::cout << "Value after increment: " << value << std::endl;

// 递减
count--; // 等价于 count = count - 1; 现在 count 是 5
--value; // 等价于 value = value - 1; 现在 value 是 10.5

std::cout << "Count after decrement: " << count << std::endl;
std::cout << "Value after decrement: " << value << std::endl;

// 在 for 循环中使用
std::cout << "Loop using ++:" << std::endl;
for (int i = 0; i < 3; ++i) { // 使用 ++i 或 i++ 效果相同 (作为独立语句)
std::cout << "i = " << i << std::endl;
}

return 0;
}

5.1.6 副作用和顺序点

副作用 (Side Effect): 指的是修改变量的值或执行 I/O 操作等改变程序状态的行为。递增 (++) 和递减 (--) 运算符都具有副作用,因为它们会修改操作数的值。

顺序点 (Sequence Point): 是程序执行过程中的一个时间点,在该点之前的所有副作用都已完成,并且后续的副作用尚未发生。C++ 标准定义了一些顺序点,例如:

  • 分号 ; (语句结束处)
  • 完整表达式结束时(如 if 条件、while 条件、for 循环的三个部分之后)
  • 函数调用之前(所有参数的副作用完成)
  • 某些运算符(如 &&, ||, , 逗号运算符)的特定位置

重要性: 在两个顺序点之间,不要对同一个变量进行多次修改,或者既修改它又读取它(除了读取它的值以计算要写入的值之外)。否则,行为是**未定义的 (Undefined Behavior)**,编译器可能产生任何结果。

示例 (未定义行为):

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

int main() {
int x = 5;
int y;

// 未定义行为: 在同一表达式中多次修改 x,且没有顺序点分隔
// y = (x++) * (x++); // 不要这样写! 结果不可预测
// y = x + (++x); // 不要这样写! 结果不可预测
// std::cout << x << (++x); // 不要这样写! 输出顺序和 x 的最终值不可预测

// 安全的写法: 使用顺序点分隔副作用
y = x++; // y 获取 x 的原始值 5, 然后 x 变为 6 (副作用在分号处完成)
std::cout << "y = " << y << ", x = " << x << std::endl; // 输出 y = 5, x = 6

y = ++x; // x 先变为 7, 然后 y 获取新值 7 (副作用在分号处完成)
std::cout << "y = " << y << ", x = " << x << std::endl; // 输出 y = 7, x = 7

return 0;
}

结论: 避免在单个表达式中对同一变量产生复杂的、依赖于副作用顺序的操作。将它们分解成多个语句通常更安全、更清晰。

5.1.7 前缀格式和后缀格式

递增 (++) 和递减 (--) 运算符都有两种使用形式:

  1. 前缀 (Prefix): 运算符放在操作数之前 (++x, --x)。
    • 行为: 修改操作数的值(加 1 或减 1),然后使用修改后的值作为整个表达式的结果。
  2. 后缀 (Postfix): 运算符放在操作数之后 (x++, x--)。
    • 行为: 使用操作数的原始值作为整个表达式的结果,然后再修改操作数的值(加 1 或减 1)。

区别在于表达式的值:

表达式 行为描述 表达式的值 操作数最终值
++x 先将 x 加 1 x 的新值 新值
x++ 先使用 x 的原始值 x 的原始值 新值
--x 先将 x 减 1 x 的新值 新值
x-- 先使用 x 的原始值 x 的原始值 新值

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <iostream>

int main() {
int a = 5, b = 5;
int result_a, result_b;

// 前缀递增
result_a = ++a; // a 先变成 6, 然后 result_a 被赋值为 6
std::cout << "Prefix: result_a = " << result_a << ", a = " << a << std::endl; // 输出 6, 6

// 后缀递增
result_b = b++; // result_b 先被赋值为 b 的原始值 5, 然后 b 变成 6
std::cout << "Postfix: result_b = " << result_b << ", b = " << b << std::endl; // 输出 5, 6

// 在 for 循环更新部分,通常前缀和后缀效果相同
// 但在复杂表达式中,它们的区别很重要

int arr[] = {10, 20, 30};
int index = 0;

// 使用后缀获取值并移动索引
int val1 = arr[index++]; // val1 = arr[0] (10), index 变为 1
std::cout << "val1 = " << val1 << ", index = " << index << std::endl;

// 使用前缀移动索引并获取值
index = 0; // 重置 index
int val2 = arr[++index]; // index 先变为 1, val2 = arr[1] (20)
std::cout << "val2 = " << val2 << ", index = " << index << std::endl;

return 0;
}

性能建议: 对于内置类型(如 int, double, 指针),前缀和后缀的性能差异通常可以忽略。但对于用户定义的类类型(迭代器等),前缀形式 (++it) 通常比后缀形式 (it++) 效率更高,因为后缀形式需要创建一个临时对象来保存原始值。因此,在不需要使用原始值的情况下,养成优先使用前缀递增/递减的习惯是好的。

5.1.8 递增/递减运算符和指针

++-- 运算符可以应用于指针,其效果是使指针指向内存中的下一个或上一个元素。编译器会自动根据指针指向的数据类型的大小来调整地址。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>

int main() {
double arr[3] = {1.1, 2.2, 3.3};
double *ptr = arr; // ptr 指向 arr[0]

std::cout << "Initial pointer: " << ptr << ", value: " << *ptr << std::endl; // 指向 1.1

// 前缀递增指针
++ptr;
std::cout << "After ++ptr: " << ptr << ", value: " << *ptr << std::endl; // 指向 2.2

// 后缀递增指针
ptr++;
std::cout << "After ptr++: " << ptr << ", value: " << *ptr << std::endl; // 指向 3.3

// 前缀递减指针
--ptr;
std::cout << "After --ptr: " << ptr << ", value: " << *ptr << std::endl; // 指向 2.2

// 后缀递减指针
ptr--;
std::cout << "After ptr--: " << ptr << ", value: " << *ptr << std::endl; // 指向 1.1

// 结合解引用 (注意优先级)
ptr = arr; // 重置
double val;

// *ptr++ : 获取 ptr 当前指向的值,然后 ptr 指向下一个元素 (后缀 ++ 优先级高于 *)
val = *ptr++;
std::cout << "val = *ptr++ : val = " << val << ", ptr now points to value " << *ptr << std::endl; // val=1.1, ptr 指向 2.2

// *++ptr : ptr 先指向下一个元素,然后获取新指向的值 (前缀 ++ 优先级高于 *)
ptr = arr; // 重置
val = *++ptr;
std::cout << "val = *++ptr : val = " << val << ", ptr now points to value " << *ptr << std::endl; // val=2.2, ptr 指向 2.2

// ++*ptr : 获取 ptr 指向的值,然后将该值加 1 (解引用 * 优先级高于前缀 ++)
ptr = arr; // 重置
++*ptr; // 将 arr[0] 的值从 1.1 增加到 2.1
std::cout << "After ++*ptr : arr[0] = " << arr[0] << ", ptr points to value " << *ptr << std::endl; // arr[0]=2.1, ptr 指向 2.1

// (*ptr)++ : 获取 ptr 指向的值,然后将该值加 1 (括号强制先解引用)
ptr = arr; // 重置
arr[0] = 1.1; // 恢复 arr[0]
(*ptr)++; // 将 arr[0] 的值从 1.1 增加到 2.1
std::cout << "After (*ptr)++: arr[0] = " << arr[0] << ", ptr points to value " << *ptr << std::endl; // arr[0]=2.1, ptr 指向 2.1

return 0;
}

优先级: 解引用 * 和前/后缀 ++/-- 的优先级相同,结合性是从右到左。为了清晰起见,当结合使用时,使用括号 () 是个好主意,例如 (*ptr)++

5.1.9 组合赋值运算符

C++ 提供了一组组合赋值运算符,将算术运算和赋值运算合并为一个运算符,使代码更简洁。

运算符 示例 等价于
+= x += y x = x + y
-= x -= y x = x - y
*= x *= y x = x * y
/= x /= y x = x / y
%= x %= y x = x % y
&= x &= y x = x & y
` =` `x
^= x ^= y x = x ^ y
<<= x <<= y x = x << y
>>= x >>= y x = x >> y

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

int main() {
int score = 100;
int bonus = 10;
int penalty = 5;
int factor = 2;

score += bonus; // score = score + bonus; score 变为 110
std::cout << "Score after bonus: " << score << std::endl;

score -= penalty; // score = score - penalty; score 变为 105
std::cout << "Score after penalty: " << score << std::endl;

score *= factor; // score = score * factor; score 变为 210
std::cout << "Score after factor: " << score << std::endl;

score /= 3; // score = score / 3; score 变为 70 (整数除法)
std::cout << "Score after division: " << score << std::endl;

score %= 8; // score = score % 8; score 变为 6 (70 除以 8 余 6)
std::cout << "Score after modulo: " << score << std::endl;

// 在 for 循环更新中使用
int total = 0;
for (int i = 1; i <= 5; ++i) {
total += i; // 累加
}
std::cout << "Total (1-5): " << total << std::endl; // 输出 15

return 0;
}

组合赋值运算符通常更易读,并且可能比分开写稍微高效一些。

5.1.10 复合语句(语句块)

复合语句 (Compound Statement)语句块 (Block) 是由一对花括号 {} 括起来的零条或多条语句。

作用:

  • 语法需要: 在 C++ 语法要求只能出现一条语句的地方(例如 if, else, for, while 的循环体),可以使用语句块来包含多条语句。
  • 创建作用域: 语句块会创建一个新的**局部作用域 (Local Scope)**。在块内声明的变量(自动存储变量)只在该块内部可见,并在块结束时销毁。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

int main() {
int x = 10;

if (x > 5) { // if 后面需要一条语句,这里使用语句块
std::cout << "x is greater than 5." << std::endl;
int y = x * 2; // y 只在 if 块内部可见
std::cout << "Double x is: " << y << std::endl;
} // y 在这里销毁

// std::cout << y; // 错误! y 在此作用域不可见

std::cout << "Loop with block:" << std::endl;
for (int i = 0; i < 2; ++i) { // for 循环体使用语句块
std::cout << " Outer loop i = " << i << std::endl;
int j = i + 10; // j 只在 for 循环的当前迭代块内可见
std::cout << " Inner variable j = " << j << std::endl;
} // 每次迭代结束时 j 销毁

return 0;
}

5.1.11 其他语法技巧——逗号运算符

逗号运算符 (,) 是 C++ 中优先级最低的运算符。它允许将两个表达式连接成一个表达式。

行为:

  1. 先计算逗号左侧的表达式。
  2. 丢弃左侧表达式的计算结果。
  3. 然后计算逗号右侧的表达式。
  4. 整个逗号表达式的结果是右侧表达式的值和类型。

主要用途:

  • for 循环的初始化和更新部分: 允许在这些部分执行多个操作,而不需要语句块。
  • (较少见) 在需要单个表达式的地方执行多个有副作用的操作。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

int main() {
// 在 for 循环中使用逗号运算符
int i, j;
std::cout << "Using comma in for loop:" << std::endl;
for (i = 0, j = 10; // 初始化: 初始化 i 和 j
i < j; // 条件
++i, --j) // 更新: 递增 i, 递减 j
{
std::cout << "i = " << i << ", j = " << j << std::endl;
}

// 逗号表达式的值
int x;
int result = (x = 5, x + 10); // x 先被赋值为 5, 然后计算 x+10 (15)
// 整个表达式的结果是 15
std::cout << "Result of comma expression: " << result << std::endl; // 输出 15
std::cout << "x after comma expression: " << x << std::endl; // 输出 5

// 优先级最低
int a = 1, b = 2, c = 3;
int value = a++, b += a, c += b; // 逗号优先级低于赋值
// 这实际上等价于: value = a++; (value=1, a=2) 然后计算 b+=a (b=2+2=4), 然后计算 c+=b (c=3+4=7)
std::cout << "value=" << value << ", a=" << a << ", b=" << b << ", c=" << c << std::endl; // 输出 value=1, a=2, b=4, c=7

// 如果想按顺序执行并取最后结果,需要括号
value = (++a, b += a, c += b); // a=3, b=4+3=7, c=7+7=14. value=14
std::cout << "value=" << value << ", a=" << a << ", b=" << b << ", c=" << c << std::endl; // 输出 value=14, a=3, b=7, c=14

return 0;
}

虽然逗号运算符提供了这种能力,但过度使用可能降低代码的可读性。在 for 循环的初始化和更新部分是其最常见且合理的用途。

5.1.12 关系表达式

关系表达式 (Relational Expression) 使用关系运算符来比较两个操作数的值,其结果是一个布尔值 (truefalse)。

关系运算符:

  • < : 小于 (Less than)
  • > : 大于 (Greater than)
  • <=: 小于或等于 (Less than or equal to)
  • >=: 大于或等于 (Greater than or equal to)
  • ==: 等于 (Equal to)
  • !=: 不等于 (Not equal to)

这些运算符的优先级低于算术运算符,但高于赋值运算符。==!= 的优先级低于其他四个关系运算符。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

int main() {
int score = 85;
int passing_score = 60;
double temp1 = 36.5, temp2 = 37.0;

bool isPassing = (score >= passing_score); // 比较 score 是否大于等于 passing_score
bool isFever = (temp1 > temp2); // 比较 temp1 是否大于 temp2
bool isEqual = (score == 85); // 比较 score 是否等于 85
bool isNotEqual = (temp1 != temp2); // 比较 temp1 是否不等于 temp2

std::cout << std::boolalpha; // 使 bool 输出为 true/false
std::cout << "Score: " << score << ", Passing Score: " << passing_score << std::endl;
std::cout << "Is passing? " << isPassing << std::endl; // 输出 true
std::cout << "Is fever? " << isFever << std::endl; // 输出 false
std::cout << "Is score 85? " << isEqual << std::endl; // 输出 true
std::cout << "Temps not equal? " << isNotEqual << std::endl; // 输出 true

// 在循环条件中使用
int count = 0;
while (count < 3) { // 当 count 小于 3 时循环
std::cout << "Count is " << count << std::endl;
count++;
}

return 0;
}

5.1.13 赋值、比较和可能犯的错误

一个非常常见的 C++ 编程错误是将赋值运算符 (=) 误用在需要比较运算符 (==) 的地方,尤其是在 ifwhile 的条件语句中。

  • if (x = 5): 这不是比较 x 是否等于 5。它的作用是:

    1. 将 5 赋给变量 x
    2. 整个赋值表达式 (x = 5) 的结果是赋给 x 的值,即 5。
    3. if 条件中,非零值被视为 true
      因此,这个 if 语句总是会执行其代码块(除非 5 被视为 false,这在 C++ 中不会发生),并且还会意外地将 x 的值修改为 5。
  • if (x == 5): 这才是正确的比较,检查 x 的当前值是否等于 5,结果为 truefalse,并且不会修改 x 的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <iostream>

int main() {
int score = 0;

std::cout << "Enter your score: ";
std::cin >> score;

// 错误: 使用了赋值 =
if (score = 100) { // 总是 true,并且 score 被改为 100
std::cout << "Mistake: You entered 100 (or maybe not, score is now 100)." << std::endl;
} else {
std::cout << "Mistake: This part will likely never execute." << std::endl;
}
std::cout << "Score after mistaken if: " << score << std::endl; // score 总是 100

// 正确: 使用了比较 ==
std::cout << "\nEnter your score again: ";
std::cin >> score;
if (score == 100) { // 正确比较
std::cout << "Correct: Perfect score!" << std::endl;
} else {
std::cout << "Correct: Score is not 100." << std::endl;
}
std::cout << "Score after correct if: " << score << std::endl; // score 保持用户输入的值

return 0;
}

如何避免:

  • 仔细检查条件语句中的 ===
  • 一些编码风格建议将常量放在比较运算符的左边(”Yoda conditions”),例如 if (100 == score)。这样如果意外写成 if (100 = score),编译器会报错,因为不能给常量赋值。

5.1.14 C风格字符串的比较

对于 C 风格字符串(char 数组或 char* 指针),不能直接使用关系运算符(==, !=, <, > 等)来比较字符串的内容

当对两个 char* 指针使用 ==!= 时,比较的是指针存储的内存地址,而不是它们指向的字符串内容。

要比较 C 风格字符串的内容,需要使用 C 字符串库 <cstring> (或 C 的 <string.h>) 中提供的函数,主要是 strcmp()

  • strcmp(str1, str2):
    • 如果 str1 按字典序等于 str2,返回 0。
    • 如果 str1 小于 str2,返回负值。
    • 如果 str1 大于 str2,返回正值。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <cstring> // 为了 strcmp()

int main() {
char word1[] = "apple";
char word2[] = "apply";
char word3[] = "apple";
const char *p1 = "banana";
const char *p2 = "banana"; // 编译器可能将相同的字面值存储在同一地址
const char *p3 = word1;

// 错误: 比较地址
if (word1 == word3) { // 比较两个不同数组的地址,结果通常是 false
std::cout << "Mistake: word1 == word3 (comparing addresses)" << std::endl;
} else {
std::cout << "Mistake: word1 != word3 (comparing addresses)" << std::endl;
}

if (p1 == p2) { // 可能 true (如果编译器优化),也可能 false
std::cout << "Info: p1 == p2 (compiler might optimize literals)" << std::endl;
} else {
std::cout << "Info: p1 != p2 (compiler might not optimize literals)" << std::endl;
}

if (p3 == word1) { // true, p3 指向 word1 的起始地址
std::cout << "Info: p3 == word1 (same address)" << std::endl;
}

// 正确: 使用 strcmp() 比较内容
if (strcmp(word1, word3) == 0) { // 比较内容是否相等
std::cout << "Correct: strcmp(word1, word3) == 0 (contents are equal)" << std::endl;
} else {
std::cout << "Correct: strcmp(word1, word3) != 0" << std::endl;
}

if (strcmp(word1, word2) < 0) { // 比较 word1 是否小于 word2
std::cout << "Correct: strcmp(word1, word2) < 0 (\"apple\" < \"apply\")" << std::endl;
}

return 0;
}

5.1.15 比较string类字符串

与 C 风格字符串不同,C++ 的 std::string重载了所有的关系运算符(==, !=, <, >, <=, >=)。

这意味着你可以直接使用这些运算符来比较两个 std::string 对象的内容(按字典序进行比较)。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <string>

int main() {
std::string s1 = "apple";
std::string s2 = "apply";
std::string s3 = "apple";
std::string s4 = "Banana";

// 直接使用 == 比较内容
if (s1 == s3) {
std::cout << "s1 == s3 is true (contents are equal)" << std::endl;
} else {
std::cout << "s1 == s3 is false" << std::endl;
}

// 直接使用 !=
if (s1 != s2) {
std::cout << "s1 != s2 is true" << std::endl;
} else {
std::cout << "s1 != s2 is false" << std::endl;
}

// 直接使用 <, > 等进行字典序比较
if (s1 < s2) {
std::cout << "s1 < s2 is true (\"apple\" < \"apply\")" << std::endl;
}

if (s1 < s4) { // 比较 "apple" 和 "Banana"
std::cout << "s1 < s4 is true (\"apple\" > \"Banana\" due to case)" << std::endl;
// 注意:比较是区分大小写的,'a' 的 ASCII 值大于 'B'
} else {
std::cout << "s1 < s4 is false (\"apple\" > \"Banana\" due to case)" << std::endl;
}

// 也可以和 C 风格字符串字面值比较
if (s1 == "apple") {
std::cout << "s1 == \"apple\" is true" << std::endl;
}

return 0;
}

使用 std::string 进行字符串比较比使用 C 风格字符串和 strcmp() 更直观、更安全。

5.2 while循环

while 循环是 C++ 中另一种重要的循环结构。与 for 循环不同,while 循环在结构上更简单,它只包含一个**测试条件 (Test Condition)**。只要该条件为 true,循环体就会一直执行。

语法:

1
2
3
4
5
6
7
8
9
while (test_condition) {
// 循环体 (statement(s) to be executed repeatedly)
statement1;
statement2;
// ...
}
// 或者如果循环体只有一条语句
while (test_condition)
single_statement;

执行流程:

  1. 计算 test_condition
  2. 如果 test_conditionfalse,跳出循环,执行循环后面的代码。
  3. 如果 test_conditiontrue,执行循环体中的语句。
  4. 回到步骤 1。

关键点:

  • while 循环是一种**入口条件循环 (Entry-Condition Loop)**,即在每次执行循环体之前检查条件。如果第一次检查条件就为 false,则循环体一次也不会执行。
  • 循环体内部必须有能够影响 test_condition 的语句(例如修改用于判断的变量),否则如果条件初始为 true,循环将永远不会停止,形成**无限循环 (Infinite Loop)**。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>

int main() {
int count = 0;

// 使用 while 循环打印数字 0 到 4
std::cout << "Counting with while (0-4):" << std::endl;
while (count < 5) { // 1. 测试条件
std::cout << "count = " << count << std::endl; // 2. 循环体
count++; // 3. 更新条件变量 (非常重要!)
}
std::cout << "Loop finished." << std::endl;

// 示例:等待用户输入特定字符
char response;
std::cout << "\nEnter 'y' to continue: ";
std::cin >> response;

while (response != 'y' && response != 'Y') {
std::cout << "Invalid input. Please enter 'y' to continue: ";
std::cin >> response;
}
std::cout << "Continuing..." << std::endl;

return 0;
}

5.2.1 for与while

for 循环和 while 循环在很多情况下是可以互换的,因为它们都可以用来实现基于条件的重复执行。

转换关系:

一个典型的 for 循环:

1
2
3
for (initialization; test_condition; update) {
body;
}

可以等价地转换为 while 循环:

1
2
3
4
5
initialization; // 初始化移到循环之前
while (test_condition) { // 测试条件保持不变
body;
update; // 更新移到循环体的末尾
}

选择依据:

  • for 循环:
    • 优点: 将初始化、测试和更新逻辑集中在循环头部,结构清晰,特别适用于计数循环(循环次数已知或易于计算)或需要按固定步长迭代的情况。
    • 适用场景: 遍历数组、按索引处理字符串、执行固定次数的操作。
  • while 循环:
    • 优点: 结构更简单,只关注循环条件,适用于循环次数不确定,依赖于某个事件或状态改变的情况。
    • 适用场景: 等待用户输入、读取文件直到结束、处理链表、当循环条件比计数器更新更重要时。

示例 (两种循环实现相同功能):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

int main() {
// 使用 for 循环计算 1 到 5 的和
int sum_for = 0;
for (int i = 1; i <= 5; ++i) {
sum_for += i;
}
std::cout << "Sum using for: " << sum_for << std::endl;

// 使用 while 循环计算 1 到 5 的和
int sum_while = 0;
int i_while = 1; // 初始化
while (i_while <= 5) { // 测试条件
sum_while += i_while;
i_while++; // 更新
}
std::cout << "Sum using while: " << sum_while << std::endl;

return 0;
}

虽然两者可以转换,但选择更自然地表达循环意图的结构可以提高代码的可读性。

5.2.2 等待一段时间:编写延时循环

有时我们需要让程序暂停执行一段时间。虽然有更精确、更现代的方法(如 C++11 <chrono><thread> 库),但可以使用循环来实现简单的、基于处理器时间的**延时循环 (Delay Loop)**。

这种方法不精确且不推荐用于实际的精确延时,因为它:

  • 依赖于处理器速度: 在快的 CPU 上执行时间短,在慢的 CPU 上执行时间长。
  • 受编译器优化影响: 编译器可能会识别出循环体为空或无副作用,并将其完全优化掉。
  • 浪费 CPU 资源: 循环在空转,消耗 CPU 时间,无法执行其他有用任务。

基本思路: 执行一个已知需要一定时间的空循环或简单操作的循环。

示例 (使用 <ctime> 库):

<ctime> (或 C 的 <time.h>) 库提供了一些与时间相关的函数,可以用来实现稍微好一点(但仍不理想)的延时。

  • clock(): 返回程序启动以来所用的时钟计时单元 (clock ticks) 数。
  • CLOCKS_PER_SEC: 一个常量,表示每秒包含的时钟计时单元数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <ctime> // 为了 clock() 和 CLOCKS_PER_SEC

int main() {
std::cout << "Starting delay..." << std::endl;

float delay_seconds = 2.5f; // 期望延时 2.5 秒

// 获取开始时间
clock_t start_time = clock();

// 计算目标结束时间 (以 clock ticks 为单位)
clock_t target_ticks = delay_seconds * CLOCKS_PER_SEC;

// 循环直到经过了足够多的 clock ticks
while (clock() < start_time + target_ticks) {
// 循环体可以为空,或者执行一些轻量操作
// ; // 空语句
}

std::cout << "Delay finished after approximately " << delay_seconds << " seconds." << std::endl;

// 另一个简单的空循环延时 (非常不精确)
// long wait = 0;
// while (wait < 100000000) { // 循环次数需要根据机器调整
// wait++;
// }
// std::cout << "Simple loop delay finished." << std::endl;


return 0;
}

再次强调: 对于需要精确延时或暂停执行而不浪费 CPU 的场景,应使用 C++11 及更高版本提供的 <chrono><thread> 中的 std::this_thread::sleep_for()std::this_thread::sleep_until()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <chrono> // 为了时间单位 (e.g., seconds, milliseconds)
#include <thread> // 为了 std::this_thread::sleep_for

int main() {
std::cout << "Starting modern delay..." << std::endl;

// 延时 2 秒 500 毫秒
std::chrono::seconds sec(2);
std::chrono::milliseconds ms(500);

std::this_thread::sleep_for(sec + ms); // 线程休眠,不消耗 CPU

std::cout << "Modern delay finished." << std::endl;

return 0;
}

这个现代方法更精确、可移植性更好,并且不会浪费 CPU 周期。

5.3 do while循环

do while 循环是 C++ 提供的第三种循环结构。它与 while 循环非常相似,但有一个关键区别:do while 循环是**出口条件循环 (Exit-Condition Loop)**,而 while 循环是入口条件循环。

这意味着 do while 循环会先执行一次循环体然后再检查测试条件。只要条件为 true,循环就会继续执行。

语法:

1
2
3
4
5
6
do {
// 循环体 (statement(s) to be executed repeatedly)
statement1;
statement2;
// ...
} while (test_condition); // 注意这里的条件后面必须有分号 ;

或者如果循环体只有一条语句(虽然不常见,且为了清晰通常还是用花括号):

1
2
3
do
single_statement;
while (test_condition);

执行流程:

  1. 执行循环体中的语句。
  2. 计算 test_condition
  3. 如果 test_conditiontrue,回到步骤 1。
  4. 如果 test_conditionfalse,循环终止,执行循环后面的代码。

关键点:

  • 至少执行一次: 由于条件是在循环体执行之后检查的,do while 循环的循环体至少会执行一次,即使条件初始就为 false
  • 分号: while (test_condition) 后面必须有一个分号 ;
  • 适用场景: 当你需要确保循环体中的代码至少执行一次时,do while 循环是理想的选择。例如,获取用户输入并验证,至少需要获取一次输入才能进行验证。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>

int main() {
int number;

// 示例:要求用户输入一个正数
// 循环体至少执行一次以获取输入
do {
std::cout << "Enter a positive number: ";
std::cin >> number;
if (number <= 0) {
std::cout << "Invalid input. Please try again." << std::endl;
}
} while (number <= 0); // 条件在输入之后检查

std::cout << "You entered the positive number: " << number << std::endl;

// 比较: 如果使用 while,需要先获取一次输入
// int number_while;
// std::cout << "\nEnter a positive number (using while): ";
// std::cin >> number_while;
// while (number_while <= 0) {
// std::cout << "Invalid input. Please try again." << std::endl;
// std::cout << "Enter a positive number (using while): ";
// std::cin >> number_while;
// }
// std::cout << "You entered: " << number_while << std::endl;


// 即使条件初始为 false,循环体也执行一次
int count = 5;
std::cout << "\nStarting do-while with count = 5 (condition count < 5 is false):" << std::endl;
do {
std::cout << " Inside do-while loop, count = " << count << std::endl; // 这行会执行
count++;
} while (count < 5); // 第一次检查时 count 是 6, 条件为 false
std::cout << "After do-while loop, count = " << count << std::endl; // count 变为 6

return 0;
}

whilefor 的比较:

  • while: 入口条件,可能一次都不执行。
  • for: 通常用于计数或已知迭代次数,结构包含初始化、条件、更新。
  • do while: 出口条件,保证至少执行一次。

根据循环逻辑选择最合适的循环结构可以使代码更清晰、更易于理解。如果需要确保操作至少发生一次(如菜单选择、输入验证),do while 是一个很好的选择。

5.4 基于范围的for循环(C++11)

C++11 引入了一种更简洁、更易读的 for 循环语法,称为基于范围的 for 循环 (Range-Based for Loop)增强 for 循环 (Enhanced for Loop)**。它专门用于遍历一个序列(或范围)中的所有元素**,例如数组、STL 容器(如 vector, array, string)、初始化列表等。

目的: 简化遍历操作,减少手动管理索引或迭代器的代码,避免常见的差一错误 (off-by-one errors)。

语法:

1
2
3
4
5
for (declaration : range_expression) {
// 循环体
// 使用 declaration 访问当前元素
statement;
}
  • declaration: 声明一个变量,其类型应与 range_expression 中元素的类型兼容(或可以转换)。在每次循环迭代中,该变量会被初始化为范围中的当前元素。
    • 通常使用 auto 让编译器自动推断类型。
    • auto variable: variable 会成为当前元素的副本。修改 variable 不会影响原始序列中的元素。
    • auto& variable: variable 会成为当前元素的引用。修改 variable 修改原始序列中的元素。用于需要修改元素或避免复制大型对象开销的情况。
    • const auto& variable: variable 会成为当前元素的常量引用。不能通过 variable 修改元素,但可以避免复制开销。用于只读访问。
  • :: 用于分隔声明和范围表达式。
  • range_expression: 一个可以表示序列的表达式。这通常是:
    • 数组名。
    • STL 容器对象(如 std::vector, std::array, std::string, std::list 等)。
    • 初始化列表 { ... }
    • 任何定义了 begin()end() 成员函数或可以通过全局 begin()end() 函数获取迭代器的对象。

执行流程:

循环会自动遍历 range_expression 中的每一个元素。在每次迭代中:

  1. 从序列中获取下一个元素。
  2. 将该元素的值(或引用)赋给 declaration 中声明的变量。
  3. 执行循环体。
  4. 重复此过程,直到遍历完序列中的所有元素。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <vector>
#include <string>
#include <array> // 为了 std::array

int main() {
// 1. 遍历数组
double prices[] = {19.99, 25.50, 9.75, 100.0};
std::cout << "Prices (array):";
for (double price : prices) { // price 是每个元素的副本
std::cout << " " << price;
}
std::cout << std::endl;

// 2. 遍历 std::vector
std::vector<int> numbers = {1, 2, 3, 4, 5};
std::cout << "Numbers (vector):";
for (int num : numbers) {
std::cout << " " << num;
}
std::cout << std::endl;

// 3. 遍历 std::string
std::string message = "Hello";
std::cout << "Characters (string):";
for (char c : message) {
std::cout << " " << c;
}
std::cout << std::endl;

// 4. 遍历初始化列表
std::cout << "Initializer list:";
for (int x : {10, 20, 30, 40}) {
std::cout << " " << x;
}
std::cout << std::endl;

// 5. 使用 auto 简化声明
std::array<std::string, 3> fruits {"Apple", "Banana", "Cherry"};
std::cout << "Fruits (array with auto):";
for (auto fruit : fruits) { // fruit 是 std::string 的副本
std::cout << " " << fruit;
}
std::cout << std::endl;

// 6. 使用引用修改元素 (auto&)
std::vector<int> scores = {70, 85, 90};
std::cout << "Original scores:";
for (int score : scores) std::cout << " " << score;
std::cout << std::endl;

for (auto& score_ref : scores) { // score_ref 是元素的引用
score_ref += 5; // 给每个分数加 5
}

std::cout << "Scores after adding 5:";
for (int score : scores) std::cout << " " << score; // 输出修改后的分数
std::cout << std::endl;

// 7. 使用常量引用进行只读访问 (const auto&)
std::cout << "Reading scores (const auto&):";
for (const auto& score_cref : scores) {
// score_cref += 1; // 错误! 不能通过常量引用修改
std::cout << " " << score_cref;
}
std::cout << std::endl;

return 0;
}

优点:

  • 简洁: 代码更短,意图更清晰(“对范围中的每个元素做某事”)。
  • 安全: 避免了手动管理索引或迭代器可能导致的错误(如越界访问、迭代器失效等)。
  • 通用: 适用于所有定义了 begin()end() 的标准容器以及内置数组和初始化列表。

局限性:

  • 无法直接获取索引: 如果在循环中需要知道当前元素的索引,基于范围的 for 循环本身不提供这个信息。需要额外维护一个计数器变量。
    1
    2
    3
    4
    5
    6
    std::vector<int> data = {100, 200, 300};
    int index = 0;
    for (int val : data) {
    std::cout << "Index " << index << ": " << val << std::endl;
    index++;
    }
  • 遍历整个范围: 它总是从头到尾遍历整个范围。如果需要更复杂的遍历模式(如反向、跳跃、只遍历部分范围),传统的 for 循环或 while 循环配合迭代器可能更合适。
  • 修改容器大小: 在循环体内修改容器的大小(例如,在 vectorpush_backerase)通常是不安全的,可能导致迭代器失效和未定义行为。基于范围的 for 循环不适合这种情况。

总结:

基于范围的 for 循环是 C++11 提供的一个非常有用的特性,极大地简化了对序列中所有元素的遍历操作。在不需要索引且需要遍历整个序列的情况下,它通常是比传统 for 循环更优选、更安全、更易读的选择。

5.5 循环和文本输入

循环结构在处理文本输入时非常有用,特别是当我们需要逐个字符或逐行读取数据,直到满足某个条件(如遇到特定字符、文件结束或达到一定数量)时。本节将探讨使用 cin 及其相关方法进行文本输入的常见模式和技巧。

5.5.1 使用原始的cin进行输入

我们已经知道,使用 cin >> variable 可以从标准输入读取数据。当用于读取文本(如 charstring)时,cin >> 的行为特点是:

  1. 跳过空白: 它会自动忽略输入流中开头的任何空白字符(空格、制表符、换行符)。
  2. 读取直到空白: 它会读取非空白字符,直到遇到下一个空白字符为止。
  3. 空白符留在流中: 停止读取时遇到的那个空白字符会留在输入流(输入缓冲区)中,等待下一次读取操作。

这使得 cin >> 适合读取单个单词或以空白分隔的数据项,但不适合读取包含空格的整行文本或精确地逐个字符处理(包括空格)。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>

int main() {
char ch;
std::string word;

std::cout << "Enter some characters (e.g., 'a b c'): ";

// 读取第一个非空白字符
std::cin >> ch;
std::cout << "First char read: '" << ch << "'" << std::endl; // 如果输入 " a b c", 这里会读到 'a'

// 读取下一个单词
std::cin >> word;
std::cout << "Next word read: \"" << word << "\"" << std::endl; // 会读到 "b"

// 再次读取字符
std::cin >> ch;
std::cout << "Next char read: '" << ch << "'" << std::endl; // 会读到 'c'

// 循环读取单词直到输入结束 (例如按 Ctrl+Z/Ctrl+D)
std::cout << "\nEnter words (Ctrl+Z/D to stop):" << std::endl;
while (std::cin >> word) { // cin >> word 在成功读取时返回 true
std::cout << "Read word: " << word << std::endl;
}
// 注意:循环结束后,cin 可能处于失败状态

return 0;
}

while (std::cin >> word) 是一种常见的读取模式,它利用了 cin 对象在成功读取时可以被转换为 true 的特性。当读取失败(例如到达文件末尾或遇到无效输入)时,cin 对象会转换为 false,循环终止。

5.5.2 使用cin.get(char)进行补救

cin >> 跳过空白并停止于空白的行为有时不是我们想要的,特别是当我们需要读取包括空格在内的每一个字符时。cin.get(char& ch) 成员函数提供了解决方案。

  • cin.get(char& ch):
    • 尝试从输入流中读取下一个字符(无论它是什么,包括空格、制表符、换行符)。
    • 如果成功读取,将该字符存储在参数 ch 中,并返回 cin 对象本身(可以转换为 true)。
    • 如果到达文件末尾或发生错误,不修改 ch,并将 cin 置于失败状态(转换为 false)。

这使得 cin.get(char) 非常适合在循环中逐个读取所有字符。

用法与示例:

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

int main() {
char ch;
int count = 0;

std::cout << "Enter text (Ctrl+Z/D to stop):" << std::endl;

// 循环读取每一个字符,包括空白符
while (std::cin.get(ch)) { // 尝试读取一个字符到 ch
// 成功读取,处理字符 ch
std::cout << ch; // 逐个字符回显
count++;
}

std::cout << "\n--- End of input ---" << std::endl;
std::cout << "Total characters read: " << count << std::endl;

return 0;
}

这个循环会读取并回显用户输入的所有字符,包括空格和换行,直到遇到文件结束符。

5.5.3 使用哪个cin.get()

istream 类(cin 是其对象)实际上提供了几个名为 get 的成员函数(函数重载):

  1. cin.get(char& ch): (已在 5.5.2 讨论)

    • 读取下一个字符到参数 ch 中。
    • 返回 cin 对象。
    • 适合在 while 条件中直接使用 while(cin.get(ch))
  2. cin.get(): (无参数版本)

    • 读取下一个字符。
    • 返回该字符的整数 ASCII 码(或 wchar_t 对应的值)。
    • 如果到达文件末尾或发生错误,返回特殊值 EOF (End Of File,通常定义为 -1,在 <iostream><cstdio> 中定义)。
    • 不直接将字符存入变量,需要接收其返回值。

选择依据:

  • cin.get(char& ch): 当你需要将读取的字符直接存储到一个 char 变量中,并且想利用 cin 对象在 while 条件中的布尔转换特性时,这是最常用的选择。
  • cin.get() (无参数): 当你需要显式地检查文件结束符 EOF 时,或者当你需要获取字符的整数值时,这个版本更合适。返回值需要与 EOF 进行比较。

用法与示例 (cin.get() 无参数版本):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <cstdio> // 为了 EOF (虽然 iostream 通常也包含)

int main() {
int ch_int; // 注意类型是 int,以接收 EOF
int count = 0;

std::cout << "Enter text (Ctrl+Z/D to stop):" << std::endl;

// 循环读取,直到遇到 EOF
while ((ch_int = std::cin.get()) != EOF) { // 读取字符的 int 值并与 EOF 比较
// 成功读取 (不是 EOF)
std::cout << static_cast<char>(ch_int); // 将 int 值转回 char 进行输出
count++;
}

std::cout << "\n--- End of input ---" << std::endl;
std::cout << "Total characters read: " << count << std::endl;

return 0;
}

两种 get 方法都可以用于逐字符读取,选择哪种取决于你喜欢的判断循环结束的方式(检查 cin 状态还是检查 EOF 返回值)。

5.5.4 文件尾条件

当从输入流(如 cin 或文件流)读取数据时,最终会到达输入的末尾,这被称为**文件尾 (End-of-File, EOF)**。程序需要能够检测到 EOF 条件以正常终止读取循环。

有几种方法可以检测 EOF:

  1. 检查 cin 状态:

    • cin 对象本身可以转换为布尔值。当读取操作成功时,它转换为 true;当遇到 EOF 或其他错误导致读取失败时,它转换为 false。这是 while (cin >> word)while (cin.get(ch)) 能够工作的原因。
    • cin.eof(): 如果流是因为到达文件末尾而失败,此函数返回 true注意: eof() 只有在尝试读取并失败后才会变为 true。不能用它来预测下一次读取是否会到达 EOF。
    • cin.fail(): 如果发生了非 EOF 的 I/O 错误(例如读取了无效数据类型)或到达 EOF,此函数返回 true
    • cin.good(): 如果流处于正常状态(没有设置 eofbit, failbit, badbit),返回 true
  2. 检查 cin.get() 的返回值:

    • 无参数的 cin.get() 在到达 EOF 时返回特殊值 EOF。这是 while ((ch = cin.get()) != EOF) 能够工作的原因。

EOF 的触发:

  • 键盘输入: 通常通过按下特定的组合键来模拟 EOF:
    • Unix/Linux/macOS: Ctrl+D (通常需要在行首按)
    • Windows: Ctrl+Z (通常需要在一行结束后按 Enter,然后再按 Ctrl+Z 再按 Enter)
  • 文件输入: 当读取操作尝试越过文件的最后一个字节时,会触发 EOF。

示例 (使用 cin 状态):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>

int main() {
int number;
int sum = 0;
int count = 0;

std::cout << "Enter numbers (non-number or Ctrl+Z/D to stop):" << std::endl;

// 循环读取,直到 cin 失败 (EOF 或类型不匹配)
while (std::cin >> number) {
sum += number;
count++;
}

std::cout << "\n--- Input finished ---" << std::endl;

// 检查循环结束的原因
if (std::cin.eof()) {
std::cout << "Reason: End-of-File reached." << std::endl;
} else if (std::cin.fail()) {
// fail() 在 eof() 时也可能为 true,但这里我们排除了 eof
// 如果是因为类型不匹配(例如输入了字母),fail() 为 true, eof() 为 false
std::cout << "Reason: Invalid input (non-number)." << std::endl;
// 可能需要清除错误状态并忽略无效输入以继续
// std::cin.clear();
// std::cin.ignore(10000, '\n');
} else if (std::cin.bad()) {
std::cout << "Reason: Unrecoverable stream error." << std::endl;
}

if (count > 0) {
std::cout << "Read " << count << " numbers. Sum = " << sum << std::endl;
} else {
std::cout << "No valid numbers were entered." << std::endl;
}

return 0;
}

理解如何检测 EOF 对于编写能正确处理输入结束的循环至关重要。最常用的方法是利用 cin 对象或 cin.get()while 条件中的行为。

5.5.5 另一个cin.get()版本

除了读取单个字符的 get() 函数外,istream 还提供了用于读取 C 风格字符串(字符数组)的 get() 版本:

  • cin.get(char* buffer, int size, char delimiter = '\n'):
    • 从输入流中读取字符,并将它们存储到 buffer 指向的字符数组中。
    • 最多读取 size - 1 个字符(为末尾的空字符 \0 留出空间)。
    • 如果在读取 size - 1 个字符之前遇到 delimiter 字符,则停止读取。
    • delimiter 字符本身不会被读取到 buffer 中,而是会留在输入流中。 (这是与 getline 的主要区别之一)。
    • 读取结束后,总会在 buffer 的末尾添加一个空字符 \0
    • 返回 cin 对象。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>

const int BUFFER_SIZE = 20;

int main() {
char name[BUFFER_SIZE];
char address[BUFFER_SIZE];

std::cout << "Enter your name (max " << BUFFER_SIZE - 1 << " chars): ";
// 读取最多 BUFFER_SIZE - 1 个字符,或直到遇到换行符
std::cin.get(name, BUFFER_SIZE);

// 检查读取是否成功以及是否还有剩余字符(换行符)
if (std::cin) { // 检查流状态是否良好
std::cout << "Name entered: " << name << std::endl;

// 问题:换行符 '\n' 仍然留在输入流中!
// 如果直接调用下一个 get 或 getline,它会立即读到换行符

// 处理残留的换行符
// 方法1: 读取并丢弃单个字符 (如果是换行符)
if (std::cin.peek() == '\n') { // peek() 查看下一个字符但不读取
std::cin.ignore(); // 读取并丢弃一个字符
}
// 方法2: 读取并丢弃直到换行符 (更通用)
// std::cin.ignore(10000, '\n');

std::cout << "Enter your address (max " << BUFFER_SIZE - 1 << " chars): ";
std::cin.get(address, BUFFER_SIZE);

if (std::cin) {
std::cout << "Address entered: " << address << std::endl;
} else {
std::cout << "Error reading address or EOF reached." << std::endl;
}

} else {
std::cout << "Error reading name or EOF reached." << std::endl;
}


return 0;
}

getline(cin, string) 的比较:

  • cin.get(buffer, size):
    • 用于 C 风格字符数组。
    • 需要指定缓冲区大小以防止溢出。
    • 不读取分隔符,分隔符留在流中。
    • 需要手动处理留在流中的分隔符。
  • getline(cin, str):
    • 用于 std::string 对象。
    • 自动管理内存,无需担心缓冲区溢出。
    • 读取并丢弃分隔符(默认为 \n)。
    • 通常更方便、更安全。

由于 cin.get(buffer, size) 不读取分隔符并将其留在流中,这常常导致后续输入出现问题。因此,在现代 C++ 中,当需要读取整行文本时,**强烈推荐使用 getline(cin, std::string)**。cin.get(buffer, size) 主要用于需要与 C 风格字符串 API 交互或有特定限制的场景。

5.6 嵌套循环和二维数组

嵌套循环 (Nested Loop) 是指一个循环结构完全包含在另一个循环结构的循环体内部。外层循环每执行一次,内层循环会完整地执行一遍(从开始到结束)。嵌套循环常用于处理具有多维结构的数据,例如表格、矩阵或图像的像素。

二维数组 (Two-Dimensional Array) 是数组的一种扩展,可以看作是“数组的数组”。它在概念上像一个表格或网格,有行 (row) 和列 (column)。二维数组是使用嵌套循环处理的典型数据结构。

嵌套循环示例:

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

int main() {
const int ROWS = 3;
const int COLS = 4;

std::cout << "Nested loop example (printing coordinates):" << std::endl;

// 外层循环控制行
for (int i = 0; i < ROWS; ++i) {
// 内层循环控制列
for (int j = 0; j < COLS; ++j) {
// 对于外层循环的每次迭代 (i),内层循环会完整执行一遍 (j 从 0 到 COLS-1)
std::cout << "(" << i << "," << j << ") ";
}
std::cout << std::endl; // 每行结束后换行
}

return 0;
}

输出:

1
2
3
4
Nested loop example (printing coordinates):
(0,0) (0,1) (0,2) (0,3)
(1,0) (1,1) (1,2) (1,3)
(2,0) (2,1) (2,2) (2,3)

5.6.1 初始化二维数组

声明二维数组需要指定两个维度的大小:第一个是行数,第二个是列数

声明语法:

1
typeName arrayName[numberOfRows][numberOfColumns];

初始化方法:

可以使用嵌套的花括号 {} 来初始化二维数组。外层花括号代表整个数组,内层花括号代表每一行。

  1. 完整初始化: 提供所有行的初始化列表。
    1
    2
    3
    4
    5
    int matrix[3][4] = { // 3 行 4 列
    {1, 2, 3, 4}, // 初始化第 0 行
    {5, 6, 7, 8}, // 初始化第 1 行
    {9, 10, 11, 12} // 初始化第 2 行
    };
  2. 部分初始化: 如果提供的初始化值不足,剩余元素会被自动初始化为 0(对于数值类型)。
    1
    2
    3
    4
    5
    6
    7
    8
    int partial[3][4] = {
    {1, 2}, // 第 0 行: {1, 2, 0, 0}
    {5} // 第 1 行: {5, 0, 0, 0}
    // 第 2 行: {0, 0, 0, 0} (未提供初始化列表)
    };

    // 将整个二维数组初始化为 0
    int allZeros[10][20] = {0}; // 或 C++11: int allZeros[10][20] {};
  3. 省略行数 (但不能省略列数): 如果在声明时提供了初始化列表,可以省略第一个维度(行数),编译器会根据初始化列表推断行数。但第二个维度(列数)必须指定。
    1
    2
    3
    4
    int inferredRows[][4] = { // 列数必须是 4
    {1, 1, 1, 1},
    {2, 2, 2, 2}
    }; // 编译器推断行数为 2
  4. C++11 列表初始化: 可以省略等号 =
    1
    2
    int matrix_cpp11[2][3] { {1, 2, 3}, {4, 5, 6} };
    int zeros_cpp11[5][5] {}; // 所有元素初始化为 0

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

int main() {
// 完整初始化
int data[2][3] = {
{10, 20, 30},
{40, 50, 60}
};

// 部分初始化
float coords[3][2] = {
{1.1f, 2.2f},
{3.3f} // {3.3f, 0.0f}
// 第三行为 {0.0f, 0.0f}
};

// 省略行数
char messages[][10] = { // 列数必须指定
"Hello",
"World"
}; // 推断为 2 行 10 列

// C++11 初始化
int table[2][2] { {1}, {3, 4} }; // {{1, 0}, {3, 4}}

std::cout << "data[1][1]: " << data[1][1] << std::endl; // 输出 50
std::cout << "coords[1][1]: " << coords[1][1] << std::endl; // 输出 0
std::cout << "messages[0]: " << messages[0] << std::endl; // 输出 Hello
std::cout << "table[0][1]: " << table[0][1] << std::endl; // 输出 0

return 0;
}

5.6.2 使用二维数组

访问二维数组的元素需要提供两个索引:第一个是行索引,第二个是列索引。索引同样从 0 开始。

访问语法:

1
arrayName[rowIndex][columnIndex]

使用嵌套循环处理二维数组:

嵌套循环是处理二维数组所有元素的标准方法。通常,外层循环遍历行,内层循环遍历列。

用法与示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <iostream>

const int NUM_CITIES = 3;
const int NUM_MONTHS = 4; // 假设只记录 4 个月的数据

int main() {
// 存储 3 个城市 4 个月的平均气温
double temperatures[NUM_CITIES][NUM_MONTHS] = {
{10.5, 12.1, 15.3, 18.0}, // City 0
{8.2, 9.5, 13.0, 16.5}, // City 1
{12.0, 14.5, 17.8, 21.2} // City 2
};

std::cout << "Monthly average temperatures:" << std::endl;

// 使用嵌套循环遍历并打印所有温度
for (int city = 0; city < NUM_CITIES; ++city) { // 外层循环遍历城市 (行)
std::cout << "City " << city << ": ";
for (int month = 0; month < NUM_MONTHS; ++month) { // 内层循环遍历月份 (列)
std::cout << temperatures[city][month] << "\t"; // 使用行和列索引访问元素
}
std::cout << std::endl;
}

// 计算 City 1 的总温度和平均温度
double city1_total = 0.0;
int city_index = 1; // 要计算的城市索引
for (int month = 0; month < NUM_MONTHS; ++month) {
city1_total += temperatures[city_index][month];
}
double city1_average = city1_total / NUM_MONTHS;
std::cout << "\nAverage temperature for City " << city_index << ": " << city1_average << std::endl;

// 计算所有城市第一个月的平均温度
double month0_total = 0.0;
int month_index = 0; // 要计算的月份索引
for (int city = 0; city < NUM_CITIES; ++city) {
month0_total += temperatures[city][month_index];
}
double month0_average = month0_total / NUM_CITIES;
std::cout << "Average temperature for Month " << month_index << " across all cities: " << month0_average << std::endl;

return 0;
}

内存布局:
在内存中,二维数组通常是按行主序 (Row-Major Order) 存储的。这意味着第一行的所有元素连续存储,然后是第二行的所有元素,依此类推。例如,matrix[3][4] 的内存布局看起来像:
matrix[0][0], matrix[0][1], matrix[0][2], matrix[0][3], matrix[1][0], matrix[1][1], ... , matrix[2][3]

理解这一点对于将二维数组传递给函数(通常需要知道列数)或进行某些指针操作很重要。

5.7 总结

本章重点介绍了C++中的循环结构关系表达式,它们是控制程序流程和处理重复任务的基础。

我们学习了三种主要的循环语句:

  1. for 循环: 这是一种入口条件循环,其头部包含了初始化、测试条件和更新三个部分,结构清晰,特别适用于计数或已知迭代次数的情况。我们探讨了其组成部分、如何修改步长、使用它访问字符串(C风格和std::string),并详细学习了递增 (++) 和递减 (--) 运算符(包括前缀和后缀形式及其区别、副作用和顺序点问题、在指针上的应用)。此外,还介绍了组合赋值运算符(如 +=, -=)和逗号运算符在 for 循环中的应用。
  2. while 循环: 这也是一种入口条件循环,但结构更简单,只包含一个测试条件。它适用于循环次数不确定、依赖于某个条件持续满足的情况。我们比较了 forwhile 的适用场景,并了解了如何使用循环(虽然不推荐)以及现代 C++ 的 <chrono><thread> 库来实现延时。
  3. do while 循环: 这是一种出口条件循环,其特点是循环体至少执行一次,然后在每次迭代结束时检查条件。它适用于需要确保操作至少发生一次的场景,如用户输入验证。

C++11 引入的基于范围的 for 循环提供了一种更简洁、更安全的遍历序列(如数组、vectorstring、初始化列表)中所有元素的方式。我们学习了其语法、如何使用 auto、引用 (&) 和常量引用 (const &) 来声明循环变量,以及它的优点和局限性(如无法直接获取索引)。

关系表达式使用关系运算符(<, >, <=, >=, ==, !=)来比较值,结果为布尔值 truefalse,常用于循环和分支语句的条件判断。我们特别强调了将赋值运算符 (=) 误用为比较运算符 (==) 的常见错误及其后果。对于字符串比较,我们了解到 C 风格字符串需要使用 <cstring> 中的 strcmp() 函数来比较内容,而 std::string 类则可以直接使用重载的关系运算符进行内容的字典序比较。

本章还深入探讨了循环与文本输入的结合。我们分析了 cin >> 读取单词(跳过并停止于空白)的行为,以及如何使用 cin.get(char) 和无参数的 cin.get() 来逐个读取字符(包括空白符)。我们学习了如何检测文件尾 (EOF) 条件以正确终止输入循环,包括检查 cin 流状态和 cin.get() 的返回值。最后,我们了解了读取 C 风格字符串的 cin.get(buffer, size) 版本及其与 getline 的区别(分隔符处理)。

最后,我们学习了嵌套循环的概念,即一个循环包含在另一个循环内部,以及如何使用嵌套循环来处理二维数组(数组的数组)。我们了解了二维数组的初始化方法和如何使用双重索引 [row][col] 配合嵌套循环来访问和处理其所有元素。

通过本章的学习,我们掌握了 C++ 中控制重复执行和进行比较的核心工具,为编写更复杂、更强大的程序奠定了基础。

评论