跳转到内容

C 编程/程序流程控制

来自维基教科书,自由的教科书,开放的世界

很少有程序完全遵循一条控制路径,并且每个指令都明确声明。为了有效地编程,有必要了解如何根据用户输入或其他条件改变程序执行的步骤,如何用几行代码多次执行某些步骤,以及如何使程序看起来能够表现出基本的逻辑能力。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(假),则 _**i**_ 将超出作为 _**myArray**_ 的索引的范围。在这种情况下,程序永远不会尝试访问 _**myArray[i]**_,因为表达式的真值已知为假。因此,我们不需要担心这里试图访问超出范围的数组元素,如果已经知道 i 大于或等于零。对于包含或 || 运算符的表达式,也会发生类似的事情。

while (doThis() || doThat()) ...

如果 doThis() 返回非零(真)值,则永远不会调用 doThat()。

If-Else 语句

[编辑 | 编辑源代码]

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**_ 之后的括号中的条件计算为非零(真),则执行第一个代码块;否则,执行第二个代码块。

_**else**_ 和后面的代码块是完全可选的。如果不需要在条件不满足时执行代码,则将其省略。

此外,请记住,_**if**_ 可以直接跟在 _**else**_ 语句之后。虽然这在某些情况下可能有用,但以这种方式将两个或三个以上的 if-elses 连接起来被认为是不好的编程习惯。我们可以使用后面介绍的 Switch-Case 构造来解决这个问题。

需要指出两个其他的一般语法注意事项,您在其他控制结构中也会看到它们:首先,请注意在ifelse之后没有分号。可以有,但块(用 { 和 } 包含的代码)代替了它。其次,如果您只打算执行一个语句作为ifelse的结果,则不需要花括号。但是,许多程序员认为在这种情况下插入花括号是一种良好的编码习惯。

以下代码将变量 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) 不为零(为真),则程序可以避免评估其他if语句的开销。最重要的是,通常最好为所有条件将不评估为非零(真)的情况插入else子句。

条件表达式

[edit | edit source]

条件表达式是在比 If-Else 更简洁的方式下有条件地设置值的一种方法。语法是

(/* logical expression goes here */) ? (/* if non-zero (true) */) : (/* if 0 (false) */)

逻辑表达式将被评估。如果它不为零(为真),则整个条件表达式将评估为放置在 ? 和 : 之间的表达式,否则,它将评估为 : 之后的表达式。因此,上面的示例(稍微更改其功能,以便在 a 和 b 相等时将 c 设置为 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之后指定的值,则该结构将“激活”或开始执行case语句之后的代码。一旦该结构“激活”,将不会进一步评估case

Switch-Case 在语法上很“奇怪”,因为与case关联的代码不需要花括号。

非常重要:通常,每个 case 的最后一条语句都是 break 语句。这会导致程序执行跳到 switch 语句的结束括号后面的语句,这是通常希望发生的情况。但是,如果省略了 break 语句,程序执行将继续进行下一个 case 的第一行(如果有)。这称为穿透。当程序员希望采取此操作时,应在语句块的末尾放置一个注释,表明希望穿透。否则,维护代码的另一位程序员可能会认为省略“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;
 }

一组要执行的语句可以与变量的多个值一起分组,如下面的示例所示。(这里不需要穿透注释,因为预期行为是显而易见的)

 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;
 }

当与用户定义的enum数据类型结合使用时,Switch-Case 结构特别有用。一些编译器能够警告未处理的枚举值,这可能有助于避免错误。

循环

[edit | edit source]

在计算机编程中,通常需要执行某个操作一定次数,或者直到满足某个条件为止。简单地多次键入某个语句或语句组是不切实际且繁琐的,更不用说这种方法过于死板且不直观,无法保证在发生特定事件时停止。作为一个现实世界的类比,有人问一家餐厅的洗碗工他整晚都做了什么。他会回答,“我整晚都在洗碗”。他不太可能回答,“我洗了一个盘子,然后又洗了一个盘子,然后又洗了一个盘子……”。使计算机能够执行某些重复任务的结构称为循环。

While 循环

[edit | edit source]

while 循环是最基本的循环类型。只要条件不为零(为真),它就会运行。例如,如果您尝试以下操作,程序将看起来像锁定,您将不得不手动关闭程序。条件永远不会变为真的循环退出情况称为无限循环。

 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;
 }

所有循环的流程也可以通过breakcontinue语句来控制。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 语句,在那里它重新评估控制条件。如果该条件在那一刻为“真”(即使它在循环内部的几个语句中暂时为 0),那么计算机将再次开始执行循环内部的语句;否则计算机将退出循环。计算机不会在执行 while 循环期间“持续检查”while 循环的控制条件。它只在每次到达循环顶部的while时“查看”控制条件。

非常重要的是要注意,一旦 While 循环的控制条件变为 0(假),循环将不会终止,直到代码块完成并且重新评估条件的时间到来。如果您需要在达到某个条件时立即终止 While 循环,请考虑使用break

一个常见的习惯用法是写

 int i = 5;
 while (i--) {
   printf("java and c# can't do this\n");
 }

这将在 while 循环中执行代码 5 次,i 的值从 4 降至 0(在循环内)。方便的是,这些是访问包含 5 个元素的数组中每个项目的所需值。

For 循环

[edit | edit source]

For 循环通常看起来像这样

for (initialization; test; increment) {
  /* code */
}

初始化语句只执行一次 - 在第一次评估测试条件之前。通常,它用于将初始值分配给某个变量,尽管这不是严格必要的。初始化语句也可以用于声明和初始化循环中使用的变量。

每次在for循环中的代码执行之前,都会评估测试表达式。如果此表达式在检查时评估为 0(假)(即表达式不为真),则不会(重新)进入循环,执行将继续正常进行,从紧接 FOR 循环之后的代码开始。如果表达式不为零(为真),则将执行花括号内的代码。

在每次迭代循环之后,都会执行递增语句。这通常用于递增循环索引以进行循环,在初始化表达式中初始化并在测试表达式中测试的变量。在执行完该语句之后,控制将返回到循环的顶部,在那里测试操作发生。如果在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 语句。空测试条件实际上被评估为真。

在 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 循环

[编辑 | 编辑源代码]

DO-WHILE 循环是后检查 while 循环,这意味着它在每次运行后检查条件。因此,即使条件为零(false),它也会至少运行一次。它遵循以下形式

 do {
   /* do stuff */
 } while (condition);

请注意终止分号。这是正确语法所必需的。由于这也是一种 while 循环,因此breakcontinue 语句在循环中按预期工作。continue 语句会导致跳转到条件测试,而break 语句会退出循环。

值得注意的是,Do-While 和 While 在功能上几乎相同,但有一个重要的区别:Do-While 循环始终保证至少执行一次,而 While 循环如果其条件在第一次评估时为 0(false),则根本不会执行。

最后一点:goto

[编辑 | 编辑源代码]

goto 是一种非常简单和传统的控制机制。它是一个语句,用于立即无条件地跳转到另一行代码。要使用 goto,您必须在程序中的某个位置放置一个标签。标签由一个名称后跟一个冒号 (:) 组成,并且位于单独的一行中。然后,您可以在程序中的所需位置键入“goto label;”。然后,代码将从label 开始继续执行。这看起来像

 MyLabel:
   /* some code */
   goto MyLabel;

goto 启用的控制流转移能力非常强大,以至于除了简单的 if 之外,所有其他控制结构都可以使用 goto 代替。在这里,我们可以让“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 来表达。不加控制地使用 goto 会创建不可读、不可维护的代码,而更惯用的替代方案(例如 if-elses 或 for 循环)可以更好地表达您的结构。从理论上讲,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;
}

参考资料

[编辑 | 编辑源代码]


华夏公益教科书