C 编程/程序流程控制
很少有程序严格遵循一条控制路径并显式地执行每条指令。为了有效地进行编程,有必要了解如何根据用户输入或其他条件更改程序执行的步骤,如何用几行代码执行某些步骤多次,以及如何使程序看起来像是具备了基本的逻辑理解能力。C 语言中称为条件语句和循环的结构赋予了这种能力。
从这一点开始,有必要理解通常所说的“块”的含义。块是一组相关联并旨在作为一个单元执行的代码语句。在 C 语言中,代码块的开头用 { (左大括号) 表示,代码块的结尾用 } (右大括号) 表示。在代码块的结尾不需要加分号。块可以为空,例如 {}。块也可以嵌套,也就是说,可以在较大的块内包含代码块。
几乎所有有意义的程序都会在一定程度上体现计算机的基本决策能力。事实上,可以说,没有哪种有意义的人类活动不包含某种形式的决策,无论是本能的还是其他形式的。例如,当驾驶汽车并接近交通灯时,人们不会想“我会继续驶过交叉路口”。相反,人们会想“如果灯是红色我会停下来,如果灯是绿色我会走,如果灯是黄色我会在距离交叉路口一定距离并且以一定速度行驶时才会走。”这些过程可以通过 C 语言中的条件语句模拟。
条件语句是一种指令,它指示计算机仅在满足特定条件时才执行某些代码块或更改某些数据。最常见的条件语句是 If-Else 语句,条件表达式和 Switch-Case 语句通常用作更简化的方式。
在理解条件语句之前,有必要先了解 C 语言如何表达逻辑关系。C 语言将逻辑视为算术。值 0 (零) 表示假,而 *所有其他值* 表示真。如果选择某个特定值来表示真,然后将值与它进行比较,那么你的代码迟早会在你假定的值(通常为 1)被证明是错误时失效。由对 C 语言不太熟悉的人编写的代码通常可以通过使用 #define 来定义一个“TRUE”值来识别。 [1]
由于 C 语言中的逻辑是算术运算,因此算术运算符和逻辑运算符是相同的。但是,有一些运算符通常与逻辑相关联。
- a < b
- 如果 **a** 小于 **b**,则为 1,否则为 0。
- a > b
- 如果 **a** 大于 **b**,则为 1,否则为 0。
- a <= b
- 如果 **a** 小于或等于 **b**,则为 1,否则为 0。
- a >= b
- 如果 **a** 大于或等于 **b**,则为 1,否则为 0。
- a == b
- 如果 **a** 等于 **b**,则为 1,否则为 0。
- a != b
- 如果 **a** 不等于 **b**,则为 1,否则为 0
新手程序员需要注意的是,“等于”运算符是 ==,而不是 =。这是许多编码错误的原因,并且通常是一个难以发现的错误,因为表达式 (a = b)
将 a
设置为等于 b
,然后评估为 b
;而表达式 (a == b)
(通常是想要表达的) 检查 a
是否等于 b
。需要指出的是,如果你将 = 和 == 混淆,编译器通常不会提醒你错误。例如,语句 if (c = 20) {}
被语言认为是完全有效的,但它总是将 20 赋给 c
并评估为真。避免此类错误的一个简单技巧(在许多情况下,并非所有情况下)是将常量放在前面。这会导致编译器在 == 被错误地拼写为 = 时发出错误。
请注意,C 语言没有像其他许多语言那样专用的布尔类型。0 表示假,其他任何值表示真。因此,以下两种写法是等价的
if (foo()) {
// do something
}
和
if (foo() != 0) {
// do something
}
通常使用 #define TRUE 1
和 #define FALSE 0
来弥补布尔类型的缺失。这是一种不好的做法,因为它做出了不成立的假设。更好地做法是表明你实际上希望从函数调用中获得什么结果,因为根据具体情况,有很多不同的方法可以表示错误条件。
if (strstr("foo", bar) >= 0) {
// bar contains "foo"
}
这里,strstr
返回子字符串 foo 找到的位置索引,如果未找到则返回 -1。注意:这将在前一段中提到的 TRUE
定义下总是失败。如果我们省略 >= 0
,它也不会产生预期的结果。
还需要注意的是,关系表达式的评估方式与数学文本中不同。也就是说,表达式 myMin < value < myMax
的评估方式可能与你预期的不同。在数学上,这会测试 *value* 是否在 *myMin* 和 *myMax* 之间。但在 C 语言中,首先将 *value* 与 *myMin* 进行比较。这将产生 0 或 1。这个值将与 myMax 进行比较。例如
int value = 20;
/* ... */
if (0 < value < 10) { // don't do this! it always evaluates to "true"!
/* do some stuff */
}
由于 *value* 大于 0,因此第一次比较产生值 1。现在将 1 与 10 进行比较,结果为真,因此执行 if 语句中的代码。这可能不是程序员预期的结果。适当的代码应该是
int value = 20;
/* ... */
if (0 < value && value < 10) { // the && means "and"
/* do some stuff */
}
- a || b
- 当 **a** 或 **b** 为真(或两者都为真)时,结果为 1,否则结果为 0。
- a && b
- 当 **a** 和 **b** 都为真时,结果为 1,否则结果为 0。
- !a
- 当 **a** 为真时,结果为 0;当 **a** 为 0 时,结果为 1。
以下是一个较大的逻辑表达式的示例。在语句中
e = ((a && b) || (c > d));
如果 a 和 b 不为零,或者 c 大于 d,则将 e 设置为 1。在所有其他情况下,将 e 设置为 0。
C 语言使用逻辑表达式的短路评估。也就是说,一旦能够确定逻辑表达式的真假,就不会再进行进一步的评估。这在以下情况下非常有用
int myArray[12]; .... if (i < 12 && myArray[i] > 3) { ....
在代码片段中,首先对 i 与 12 进行比较。如果比较结果为 0(false),则 **i** 将会超出 **myArray** 的索引范围。在这种情况下,程序永远不会尝试访问 **myArray[i]**,因为表达式的真假已经确定为假。因此,如果已经知道 i 大于或等于零,我们无需担心尝试访问超出范围的数组元素。类似的事情也会发生在使用或 || 运算符的表达式中。
while (doThis() || doThat()) ...
如果 doThis() 返回非零(true)值,则永远不会调用 doThat()。
If-Else 语句
[edit | edit source]If-Else 提供了一种方法,可以让计算机仅在满足特定条件的情况下才执行代码块。If-Else 结构的语法如下:
if (/* condition goes here */) {
/* if the condition is non-zero (true), this code will execute */
} else {
/* if the condition is 0 (false), this code will execute */
}
如果紧随 if 之后的括号中的条件计算结果为非零(true),则第一个代码块将执行;否则,第二个代码块将执行。
else 和其后的代码块是完全可选的。如果不需要在条件不满足时执行代码,则可以省略它。
另外,请注意,if 可以直接跟随在 else 语句之后。虽然这在某些情况下可能有用,但以这种方式将两个或三个以上的 if-elses 链起来被认为是不好的编程习惯。我们可以使用后面介绍的 Switch-Case 结构来解决这个问题。
还需要注意两个关于通用语法的事项,您也会在其他控制结构中看到它们:首先,请注意 if 或 else 后面没有分号。可以有分号,但代码块(用 { 和 } 括起来的代码)取代了分号。其次,如果您只打算执行一条语句作为 if 或 else 的结果,则不需要使用花括号。但是,许多程序员认为在这种情况下插入花括号也是良好的编码习惯。
以下代码将变量 c 设置为两个变量 a 和 b 中较大的值,如果 a 和 b 相等则设置为 0。
if (a > b) {
c = a;
} else if (b > a) {
c = b;
} else {
c = 0;
}
考虑一下这个问题:为什么不能直接忽略 else 并像下面这样编写代码呢?
if (a > b) {
c = a;
}
if (a < b) {
c = b;
}
if (a == b) {
c = 0;
}
对此有几个答案。最重要的是,如果您的条件不是互斥的,则可能执行 两个 代码块,而不是只有一个。如果代码不同,并且 a 或 b 的值在某个代码块中发生了变化(例如:在比较之后,将 a 和 b 中较小的值重置为 0)?您最终可能会调用多个 if 语句,这不是您的意图。此外,评估 if 条件需要处理时间。如果您使用 else 来处理这些情况,在上面的示例中,假设 (a > b) 为非零(true),则程序将免于评估额外的 if 语句。最重要的是,通常最好为所有情况下插入 else 子句,在这些情况下,条件将不会计算为非零(true)。
条件表达式
[edit | edit source]条件表达式是通过比 If-Else 更简短的方式有条件地设置值的一种方法。语法如下:
(/* logical expression goes here */) ? (/* if non-zero (true) */) : (/* if 0 (false) */)
逻辑表达式将被评估。如果它是非零(true),则整个条件表达式将计算为 ? 和 : 之间的表达式,否则它将计算为 : 之后的表达式。因此,上面的示例(稍作修改,使 c 在 a 和 b 相等时设置为 b)将变为:
c = (a > b) ? a : b;
条件表达式有时可以澄清代码的意图。通常应避免嵌套条件运算符。最好仅在 a 和 b 的表达式很简单时才使用条件表达式。此外,与初学者常见的误解相反,条件表达式不会使代码更快。虽然假设代码行数越少,执行速度越快很诱人,但实际上并没有这种关联。
Switch-Case 语句
[edit | edit source]假设您编写了一个程序,用户输入一个 1-5 的数字(对应于学生成绩,A(表示为 1)-D(4)和 F(5)),将其存储在一个名为 **grade** 的变量中,程序会响应地在屏幕上打印相应的字母等级。如果您使用 If-Else 来实现这一点,您的代码看起来会像这样:
if (grade == 1) {
printf("A\n");
} else if (grade == 2) {
printf("B\n");
} else if /* etc. etc. */
长长的 if-else-if-else-if-else 链对于程序员和任何阅读代码的人来说都是一种痛苦。幸运的是,有一个解决方案:Switch-Case 结构,其基本语法如下:
switch (/* integer or enum goes here */) {
case /* potential value of the aforementioned int or enum */:
/* code */
case /* a different potential value */:
/* different code */
/* insert additional cases as needed */
default:
/* more code */
}
Switch-Case 结构接受一个变量(通常是 int 或 enum),将其放在 switch 后面,并将其与 case 关键字后面的值进行比较。如果变量等于 case 后面指定的 value,则该结构将“激活”,或者开始执行 case 语句后面的代码。一旦该结构“激活”,就不会再评估 case。
Switch-Case 在语法上很“奇怪”,因为与 case 关联的代码不需要使用花括号。
非常重要:通常,每个 case 的最后一个语句都是 break 语句。这会导致程序执行跳转到 switch 语句的结束括号后面的语句,这通常是人们希望发生的。但是,如果省略 break 语句,则程序执行将继续执行下一个 case 的第一行(如果有)。这称为 fall-through。当程序员需要此操作时,应该在语句块的末尾放置一个注释,说明需要 fall through。否则,维护代码的另一个程序员可能会认为省略 'break' 是错误,并无意间“纠正”问题。以下是一个示例:
switch (someVariable) {
case 1:
printf("This code handles case 1\n");
break;
case 2:
printf("This prints when someVariable is 2, along with...\n");
/* FALL THROUGH */
case 3:
printf("This prints when someVariable is either 2 or 3.\n" );
break;
}
如果指定了 default case,则如果其他 case 都不匹配,则会执行与之关联的语句。default case 是可选的。以下是一个与上面的 if - else if 语句序列相对应的 switch 语句。
回到上面的示例。以下是它作为 Switch-Case 的样子:
switch (grade) {
case 1:
printf("A\n");
break;
case 2:
printf("B\n");
break;
case 3:
printf("C\n");
break;
case 4:
printf("D\n");
break;
default:
printf("F\n");
break;
}
一组要执行的语句可以与多个变量值分组,如下面的示例所示。(fall-through 注释在这里不需要,因为预期的行为很明显)
switch (something) {
case 2:
case 3:
case 4:
/* some statements to execute for 2, 3 or 4 */
break;
case 1:
default:
/* some statements to execute for 1 or other than 2,3,and 4 */
break;
}
Switch-Case 结构在与用户定义的 enum 数据类型结合使用时特别有用。某些编译器能够警告未处理的 enum 值,这对于避免错误可能会有所帮助。
循环
[edit | edit source]在计算机编程中,经常需要执行某个操作特定次数,或者直到满足某个条件。简单地将某个语句或语句组重复输入很多次既不切实际也不方便,更不用说这种方法过于灵活且不直观,无法依靠它在发生特定事件时停止。作为一个现实世界的类比,有人问餐厅里的洗碗工他整晚都在做什么。他会回答,“我整晚都在洗碗。”他不太可能回答,“我洗了一个碗,然后又洗了一个碗,然后又洗了一个碗……”。使计算机能够执行某些重复任务的结构称为循环。
While 循环
[edit | edit source]While 循环是最基本的循环类型。它将一直运行,直到条件为非零(true)。例如,如果您尝试以下操作,程序将似乎锁死,您需要手动关闭程序。条件永远不会满足退出循环的条件的情况称为无限循环。
int a = 1;
while (42) {
a = a * 2;
}
以下是一个 While 循环的另一个示例。它打印出所有小于 100 的 2 的幂。
int a = 1;
while (a < 100) {
printf("a is %d \n", a);
a = a * 2;
}
所有循环的流程也可以由 **break** 和 **continue** 语句控制。break 语句将立即退出封闭循环。continue 语句将跳过块的剩余部分,并从控制条件语句处重新开始。例如:
int a = 1;
while (42) { // loops until the break statement in the loop is executed
printf("a is %d ", a);
a = a * 2;
if (a > 100) {
break;
} else if (a == 64) {
continue; // Immediately restarts at while, skips next step
}
printf("a is not 64\n");
}
在这个示例中,计算机照常打印 a 的值,并打印一条通知,说明 a 不等于 64(除非它被 continue 语句跳过了)。
与上面的 If 类似,如果 While 循环关联的代码块只包含一条语句,则可以省略花括号,例如:
int a = 1;
while (a < 100)
a = a * 2;
这将简单地增加 a 的值,直到 a 不小于 100。
当计算机到达 While 循环的末尾时,它始终会回到循环顶部的 while 语句处,在那里它会重新评估控制条件。如果该条件在那一刻为“true”(即使它在循环内部的几条语句中暂时为 0),则计算机将再次开始执行循环内部的语句;否则计算机将退出循环。计算机在执行 While 循环期间不会“持续检查”控制条件。它只在每次到达循环顶部的 while
时“查看”一下控制条件。
请务必注意,一旦 While 循环的控制条件变为 0(false),循环将不会终止,直到代码块执行完毕并且需要重新评估条件。如果您需要在达到某个条件时立即终止 While 循环,请考虑使用 **break**。
一个常见的习惯用法是编写:
int i = 5;
while (i--) {
printf("java and c# can't do this\n");
}
这将执行 While 循环中的代码 5 次,i 的值从 4 降至 0(在循环内部)。方便的是,这些是访问包含 5 个元素的数组的每个项目的所需值。
For 循环通常看起来像这样
for (initialization; test; increment) { /* code */ }
初始化语句只执行一次 - 在第一次评估测试条件之前。 通常,它用于为某个变量分配初始值,尽管这不是严格必需的。 初始化语句也可以用于声明和初始化循环中使用的变量。
每次在 for 循环中的代码执行之前,都会评估测试表达式。 如果该表达式在检查时评估为 0(false)(即表达式不为真),则不会(重新)进入循环,并且执行将继续正常地在 FOR 循环之后的代码处继续。 如果表达式为非零(true),则执行循环大括号内的代码。
在每次循环迭代之后,都会执行增量语句。 这通常用于增加循环索引,即在初始化表达式中初始化并在测试表达式中测试的变量。 在此语句执行之后,控制权将返回到循环顶部,测试操作将在那里发生。 如果在 for 循环中执行continue语句,则增量语句将是下一个执行的语句。
for 语句的每个部分都是可选的,可以省略。 由于 for 语句的自由格式性质,它可以完成一些相当奇特的事情。 通常,for 循环用于循环遍历数组中的项目,每次处理一个项目。
int myArray[12];
int ix;
for (ix = 0; ix < 12; ix++) {
myArray[ix] = 5 * ix + 3;
}
上面的 for 循环初始化 myArray 的 12 个元素中的每一个。 循环索引可以从任何值开始。 在以下情况下,它从 1 开始。
for (ix = 1; ix <= 10; ix++) {
printf("%d ", ix);
}
它将打印
1 2 3 4 5 6 7 8 9 10
您最常使用从 0 开始的循环索引,因为数组在零处索引,但您有时也会使用其他值来初始化循环索引。
增量操作可以执行其他操作,例如减量。 因此,这种类型的循环很常见
for (i = 5; i > 0; i--) {
printf("%d ", i);
}
它产生
5 4 3 2 1
这是一个测试条件只是一个变量的示例。 如果变量的值为 0 或 NULL,则循环退出,否则执行循环体中的语句。
for (t = list_head; t; t = NextItem(t)) {
/* body of loop */
}
WHILE 循环可以用来完成与 FOR 循环相同的事情,但是 FOR 循环是执行一组重复次数的更简洁的方式,因为所有必要的信息都在一行语句中。
FOR 循环也可以没有条件,例如
for (;;) {
/* block of statements */
}
这被称为无限循环,因为它将永远循环,除非 for 循环的语句中包含 break 语句。 空测试条件实际上评估为 true。
在 for 循环中使用逗号运算符来执行多个语句也很常见。
int i, j, n = 10;
for (i = 0, j = 0; i <= n; i++, j += 2) {
printf("i = %d , j = %d \n", i, j);
}
在设计或重构条件部分时,应特别注意,尤其是使用 < 或 <=,是否应将开始和停止修正 1,以及前缀和后缀符号的情况。( 在一条 100 码的林荫道上,每 10 码有一棵树,总共有 11 棵树。)
int i, n = 10;
for (i = 0; i < n; i++)
printf("%d ", i); // processed n times => 0 1 2 3 ... (n-1)
printf("\n");
for (i = 0; i <= n; i++)
printf("%d ", i); // processed (n+1) times => 0 1 2 3 ... n
printf("\n");
for (i = n; i--;)
printf("%d ", i); // processed n times => (n-1) ...3 2 1 0
printf("\n");
for (i = n; --i;)
printf("%d ", i); // processed (n-1) times => (n-1) ...4 3 2 1
printf("\n");
DO-WHILE 循环是后检查 while 循环,这意味着它在每次运行后检查条件。 因此,即使条件为零(false),它也会至少运行一次。 它遵循以下形式
do {
/* do stuff */
} while (condition);
注意终止分号。 这是正确语法所必需的。 由于这也是一种 while 循环,因此循环内的 break 和 continue 语句相应地起作用。 continue 语句会导致跳转到条件测试,而 break 语句则退出循环。
值得注意的是,Do-While 和 While 在功能上几乎完全相同,只有一个重要区别:Do-While 循环始终保证至少执行一次,但 While 循环如果它们的条件在第一次评估时为 0(false),则根本不会执行。
goto 是一种非常简单且传统的控制机制。 它是一个用于立即无条件地跳转到另一行代码的语句。 要使用 goto,您必须在程序中的某个点放置一个标签。 标签由一个名称后跟一个冒号 (:) 组成,位于单独的一行。 然后,您可以在程序中所需的点键入“goto label;”。 然后,代码将从 label 开始继续执行。 这看起来像
MyLabel:
/* some code */
goto MyLabel;
由 goto 启用的控制流转移能力非常强大,因此,除了简单的 if 之外,所有其他控制结构都可以使用 goto 而不是 if 来编写。 在这里,我们可以让“S”和“T”是任何任意的语句
if (''cond'') {
S;
} else {
T;
}
/* ... */
相同的语句可以使用两个 goto 和两个标签来完成
if (''cond'') goto Label1;
T;
goto Label2;
Label1:
S;
Label2:
/* ... */
在这里,第一个 goto 以“cond”的值为条件。 第二个 goto 是无条件的。 我们可以对循环执行相同的转换
while (''cond1'') {
S;
if (''cond2'')
break;
T;
}
/* ... */
可以写成
Start:
if (!''cond1'') goto End;
S;
if (''cond2'') goto End;
T;
goto Start;
End:
/* ... */
正如这些情况所示,通常程序执行的结构通常可以在不使用 goto 的情况下表达出来。 当更具惯用性的替代方案(例如 if-else 或 for 循环)可以更好地表达您的结构时,不加约束地使用 goto 会创建难以阅读、难以维护的代码。 从理论上讲,goto 结构并不总是需要使用,但在某些情况下,它可以提高可读性,避免代码重复或使控制变量变得不必要。 您应该首先考虑掌握惯用的解决方案,仅在必要时使用 goto。 请记住,许多(如果不是大多数)C 风格的指南严格禁止使用 goto,唯一的常见例外是以下示例。
goto 的一种用途是跳出深度嵌套的循环。 由于 break 无法工作(它只能退出一个循环),因此可以使用 goto 完全跳出循环。 在不使用 goto 的情况下跳出深度嵌套的循环始终是可能的,但通常涉及创建和测试额外的变量,这可能会使生成的代码的可读性远不如使用 goto 时那么好。 使用 goto 可以轻松地以有序的方式撤消操作,通常是为了避免未能释放已分配的内存。
另一个公认的用途是创建状态机。 不过,这是一个相当高级的话题,通常不需要。
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int years;
printf("Enter your age in years : ");
fflush(stdout);
errno = 0;
if (scanf("%d", &years) != 1 || errno)
return EXIT_FAILURE;
printf("Your age in days is %d\n", years * 365);
return 0;
}