《C编程.开始C》9.程序流程和控制

很少有程序完全遵循一个控制路径,并明确说明每条指令。为了有效地编程,有必要了解如何改变程序因用户输入或其他条件而采取的步骤,如何使用少量代码执行多次步骤,以及程序如何显示对逻辑的初步把握。被称为条件和循环的C结构赋予这种能力。

从现在开始,有必要理解单词通常意味着什么。块是一组代码语句,它们关联并打算作为一个单元执行。在C中,代码块的开头用{(左侧卷曲)表示,块的末尾用}表示。在块结束后没有必要放置分号。块可以为空,如{}。块也可以嵌套; 即在较大的块中可以存在代码块。

条件

可能没有编写有意义的程序,其中计算机不能展示基本的决策技能。实际上可以认为,没有有意义的人类活动,其中某种决策,本能或其他方面不会发生。例如,当驾驶汽车并接近交通信号灯时,人们不会想到,“我将继续穿过交叉路口。” 更确切地说,人们会想,“如果灯光是红色的话,我会停下来,如果灯光是绿色的话,我会停下来;如果我以一定的速度从交叉路口行驶一段距离,黄色就会停止。” 可以使用条件在C中模拟这些类型的过程。

条件语句是指示计算机仅在满足特定条件时才执行某个代码块或更改某些数据的语句。最常见的条件是If-Else语句,条件表达式和Switch-Case语句通常用作更简洁的方法。

在能够理解条件语句之前,首先必须理解C如何表达逻辑关系。C将逻辑视为算术。值0(零)表示false,所有其他值表示true。如果您选择某个特定值来表示true,然后将值与其进行比较,那么当您的假定值(通常为1)结果不正确时,您的代码迟早会失败。由C语言不舒服的人编写的代码通常可以通过使用#define来确定“TRUE”值。 [

因为逻辑是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) {}被语言认为是完全有效的,但总是分配给20c并评估为真。避免这种错误的简单技术(在很多情况下,并非所有情况下)都是将常量放在第一位。如果==拼写错误= =,这将导致编译器发出错误。

请注意,C没有像许多其他语言那样的专用布尔类型。0表示错误,其他任何都是真的。所以以下是等价的:

 if  (foo ()) { 
   //做某事
 }

 if  (foo () !=  0 ) { 
   //做某事
 }

经常#define TRUE 1#define FALSE 0用于解决缺乏一个boolean类型的。这是不好的做法,因为它做出了不成立的假设。最好通过函数调用来指示您实际期望的结果,因为根据情况,有许多不同的方式来指示错误条件。

 if  (strstr (“foo” , bar ) > =  0 ) { 
   // bar包含“foo” 
 }

这里,strstr返回找到子字符串foo的索引,如果没有找到则返回-1。请注意,这将失败,TRUE前一段中提到的定义。如果我们省略了它,它也不会产生预期的结果 >= 0

另外需要注意的是,关系表达式不会像在数学文本中那样进行评估。也就是说,表达式myMin < value < myMax 不会像您认为的那样进行评估。从数学上讲,这将测试myMinmyMax之间的是否存在。但是在C中,首先将myMin进行比较。这会产生0或1.这个值与myMax进行比较。例:

 int  值 =  20 ; 
 / * ... * / 
 if  (0  <  value  <  10 ) {  //不要这样做!它总是评估为“真实”!
   / *做一些事情* / 
 }

因为value大于0,所以第一个比较产生的值为1.现在1比较小于10,这是真的,因此执行if中的语句。这可能不是程序员所期望的。适当的代码将是

 int  值 =  20 ; 
 / * ... * / 
 if  (0  <  值 &&  value  <  10 ) {    // &&表示“和” 
   / *做一些东西* / 
 }

逻辑表达式

a || b当ab为真(或两者)时,结果为1,否则结果为0。a && b当ab都为真时,结果为1,否则结果为0。!一个当a为真时,结果为0,当a为0时,结果为1。

这是一个更大的逻辑表达式的示例。在声明中:

  e =((&& b)||(c> d));

如果a和b不为零,或者如果c大于d,则e设置为1。在所有其他情况下,e设置为0。

C使用逻辑表达式的短路评估。也就是说,一旦它能够确定逻辑表达式的真实性,它就不会进行进一步的评估。这通常很有用,如下所示:

int myArray [12];
....
if(i <12 && myArray [i]> 3){ 
....

在代码片段中,首先完成i与12的比较。如果它的计算结果为0(假),那么将作为myArray的索引超出范围。在这种情况下,程序永远不会尝试访问myArray [i],因为已知表达式的真实性是错误的。因此,如果已经知道i大于或等于零,我们不必担心尝试访问越界数组元素。涉及or或|的表达式也会发生类似的事情 运营商。

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

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

If-Else语句

If-Else提供了一种指示计算机仅在满足某些条件时才执行代码块的方法。If-Else结构的语法是:

 if / * condition to here * / ) { 
   / *如果条件非零(true),此代码将执行* / 
 }  else  { 
   / *如果条件为0(false),此代码将执行* / 
 }

如果if后面的括号中的条件求值为非零(true),执行第一个代码块; 否则,第二个块执行。

其他与下面的代码块完全是可选的。如果条件不成立则不需要执行代码,请将其保留。

另外,请记住,if可以直接跟随else语句。虽然这有时可能有用,但以这种方式链接超过两三个if-elses被认为是糟糕的编程习惯。我们可以通过后面描述的Switch-Case结构解决这个问题。

还需要制作另外两个通用语法注释,您还将在其他控件构造中看到:首先,请注意ifelse之后没有分号。可能会有,但是块({和}中包含的代码)代替了它。其次,如果您只打算执行ifelse的结果,不需要花括号。然而,许多程序员认为在这种情况下无论如何插入花括号都是很好的编码实践。

以下代码将变量c设置为等于两个变量a和b中的较大者,或者如果a和b相等则设置为0。

 if  (a  >  b ) { 
   c  =  a ; 
 }  否则 如果 (b  >  一个) { 
   C ^  =  b ; 
 }  else  { 
   c  =  0 ; 
 }

考虑一下这个问题:为什么你不能忘记其他并写下代码:

 if  (a  >  b ) { 
   c  =  a ; 
 }
 
 if  (a  <  b ) { 
   c  =  b ; 
 }
 
 if  (a  ==  b ) { 
   c  =  0 ; 
 }

有几个答案。最重要的是,如果您的条件不相互排斥,则可以执行两个案例而不是一个案例。如果代码不同并且a或b的值以某种方式改变(例如:在比较之后将a和b中的较小者重置为0)在其中一个块中?您最终可能会调用多个if语句,这不是您的意图。此外,评估如果条件语句需要处理器时间。如果你使用else来处理这些情况,在上面假设(a> b)非零(真)的情况下,程序免于评估额外的if语句的费用。最重要的是,通常最好插入一个else 条件不会评估为非零(true)的所有情况的子句。

条件表达式

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

(/ *逻辑表达式在这里* /)?(/ *如果非零(true)* /):( / * if 0(false)* /)

评估逻辑表达式。如果它不为零(true),则整个条件表达式求值为放在?之间的表达式。并且:否则,它将在以下之后计算表达式:。因此,上面的例子(稍微改变其功能,使得当a和b相等时c被设置为b)变为:

c =(a> b)?a:b;

条件表达式有时可以澄清代码的意图。通常应该避免嵌套条件运算符。仅当a和b的表达式很简单时,才最好使用条件表达式。此外,与普通初学者的观点相反,条件表达式不会产生更快的代码。由于假设较少的代码行导致执行时间更短,因此没有这种相关性。

Switch-Case语句

假设您编写的程序中用户输入数字1-5(对应于学生成绩,A(表示为1)-D(4)和F(5)),将其存储在可变等级中,程序通过打印进行响应到屏幕上相关的字母等级。如果你使用If-Else实现了这个,你的代码看起来像这样:

 if  (grade  ==  1 ) { 
   printf (“A \ n ” ); 
 }  else  if  (grade  ==  2 ) { 
   printf (“B \ n ” ); 
 }  否则 如果 / *等等,等等* /

拥有一长串if-else-if-else-else-else对于程序员和阅读代码的任何人来说都是一种痛苦。幸运的是,有一个解决方案:Switch-Case结构,其基本语法是:

 switch / * integer或enum到这里* / ) { 
 case  / *上述int或enum的潜在值* / / *代码* / 
 case  / *不同的潜在值* / / *不同的代码* / 
 / *根据需要插入其他案例* / 
 default / *更多代码* / 
 }

Switch-Case构造接受一个变量,通常是int或enum,放在switch之后,并将它与case关键字后面的值进行比较。如果变量等于case之后指定的值,则构造“激活”,或者在case语句之后开始执行代码。一旦构造被“激活”,就不会对案例进行进一步的评估。

Switch-Case在语法上“很奇怪”,因为与案例相关的代码不需要大括号。

非常重要:通常,每个案例的最后一个语句是break语句。这会导致程序执行跳转到switch语句的结束括号之后的语句,这是人们通常想要发生的语句。但是,如果省略break语句,程序将继续执行下一个案例的第一行(如果有)。这被称为堕落。当程序员希望这个动作时,应该在语句块的末尾添加注释,表明需要通过。否则,维护代码的另一个程序员可能会认为遗漏“中断”是一个错误,并且无意中“纠正”了问题。这是一个例子:

 switch  (someVariable ) { 
 case  1 :
   printf (“此代码处理案例1 \ n ” ); 
   打破; 
 case  2 :
   printf (“当someVariable为2时打印,同时...... \ n ” ); 
   / * FALL THROUGH * / 
 case  3 :
   printf (“当someVariable为2或3时打印。\ n ”  ); 
   打破; 
 }

如果指定了默认大小写,则在没有其他大小写匹配的情况下执行关联的语句。一个默认的情况下是可选的。这是一个switch语句,对应于上面if – else if语句的序列。

回到上面的例子。这就是Switch-Case的样子:

 开关 (等级) { 
 案例 1 :
   printf (“A \ n ” ); 
   打破; 
 案例 2 :
   printf (“B \ n ” ); 
   打破; 
 案例 3 :
   printf (“C \ n ” ); 
   打破; 
 案例 4 :
   printf (“D \ n ” ); 
   打破; 
 默认值:
   printf (“F \ n“ ); 
   休息; 
 }

要执行的一组语句可以使用多个变量值进行分组,如以下示例所示。(此处不需要通过评论,因为预期的行为是显而易见的)

 switch  (某事) { 
 案例 2 :
 案例 3 :
 案例 4 :
   / *一些要执行2,3或4 * / 
   break的陈述; 
 情况 1 :
 默认/ *一些语句要执行1或2,3以外的其他语句和4 * / 
   break ; 
 }

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

循环

通常在计算机编程中,必须执行特定次数的某个动作或直到满足某个条件。简单地多次键入某个语句或一组语句是不切实际和乏味的,更不用说这种方法太不灵活,而且在某个事件发生时不能指望停止。作为一个现实世界的比喻,有人在餐馆的洗碗机问他整晚做了什么。他会回答说:“我整夜洗碗。” 他不太可能回应,“我洗了一道菜,然后洗了一道菜,然后洗了一道菜,然后……”。使计算机能够执行某些重复性任务的构造称为循环。

循环

while循环是最基本的循环类型。只要条件非零(true),它就会运行。例如,如果您尝试以下操作,程序将显示为锁定,您必须手动关闭程序。退出循环的条件永远不会成立的情况称为无限循环。

 int  a  =  1 ; 
  (42 ) { 
   a  =  a  *  2 ; 
 }

这是while循环的另一个例子。它打印出两个小于100的所有权力。

 int  a  =  1 ; 
 while  (a  <  100 ) { 
   printf (“a is%d \ n ” , a ); 
   a  =  a  *  2 ; 
 }

所有循环的流程也可以通过breakcontinue语句来控制。break语句将立即退出封闭循环。continue语句将跳过块的其余部分并再次从控制条件语句开始。例如:

 int  a  =  1 ; 
 while  (42 ) {  //循环直到循环中的break语句执行
   printf (“a is%d” , a ); 
   a  =  a  *  2 ; 
   if  (a  >  100 ) { 
     break ; 
   }  否则 如果 (一个 ==  64 ) { 
     继续;   //立即重启,同时跳过下一步
   } 
   printf (“a不是64 \ n“ ); 
 }

在此示例中,计算机照常打印a的值,并打印a不是64的通知(除非continue语句跳过它)。

与上面的If类似,如果代码只包含一个语句,则可以省略与While循环关联的代码块的大括号,例如:

 int  a  =  1 ; 
  (a  <  100 )
   a  =  a  *  2 ;

这只会增加a直到a不小于100。

当计算机到达while循环的末尾时,它总是返回到循环顶部的while语句,在那里它重新评估控制条件。如果那个时刻的条件是“真” – 即使它对于循环中的一些语句暂时为0 – 那么计算机再次开始在循环内执行语句; 否则计算机退出循环。计算机在执行该循环期间不“连续检查”while循环的控制条件。每次到达while循环顶部时,它只会“控制”控制条件。

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

一个常见的习语是写:

 int  i  =  5 ; 
 while  (i - ) { 
   printf (“java和c#不能这样做\ n ” ); 
 }

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

For循环

for循环通常看起来像这样:

for(initialization ; test ; increment){
  / *代码* /
}

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

测试表达中的代码之前每次评价循环的执行。如果此表达式在检查时计算为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 )) { 
   / *循环体* / 
 }

WHILE循环可用于执行与FOR循环相同的操作,但FOR循环是执行一定数量重复的更简洁方式,因为所有必需信息都在一行语句中。

FOR循环也可以没有条件,例如:

 for  (;;) { 
   / *语句块* / 
 }

这被称为无限循环,因为它将永远循环,除非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码有一棵树的100码长廊上有11棵树。)

 int  i , n  =  10 ; 
 for  (i  =  0 ;  i  <  n ;  i ++ )
   printf (“%d” , i );  //处理n次=> 0 1 2 3 ...(n-1)
 printf (“ \ n ” ); 
 for  (i  =  0 ;  i  <=  n ;  i ++ )
   printf (“%d” , i );  //已处理(n + 1)次=> 0 1 2 3 ... n
 printf (“ \ n ” ); 
 for  (i  =  n ;  i - ;)
   printf (“%d” , i );  //处理n次=>(n-1)... 3 2 1 0 
 printf (“ \ n ” ); 
 for  (i  =  n ;  - i ;)
   printf (“%d” , i );  //处理(n-1)次=>(n-1)... 4 3 2 1 
 printf (“ \ n ” );

Do-While循环

DO-WHILE循环是一个后检查while循环,这意味着它在每次运行后检查条件。结果,即使条件为零(假),它也将至少运行一次。它遵循以下形式:

  { 
   / *做的东西* / 
 }   (条件);

请注意终止分号。这是正确语法所必需的。由于这也是一种while循环,因此循环中的 breakcontinue语句相应地起作用。一个继续语句导致跳转到该条件的测试和休息语句退出循环。

值得注意的是,Do-While和While在功能上几乎完全相同,但有一个重要的区别:Do-While循环总是保证至少执行一次,但是如果它们的条件为0(假),则循环将根本不执行第一次评估。

最后一件事:转到

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

 MyLabel :
   / *一些代码* / 
   goto  MyLabel ;

传递由gotos启用的控制流的能力非常强大,除了简单的if之外,所有其他控制结构都可以使用gotos编写。在这里,我们可以让“S”和“T”成为任意语句:

 if  ('' cond '' ) { 
   S ; 
 }  else  { 
   T ; 
 } 
 / * ... * /

使用两个gotos和两个标签可以完成相同的声明:

 if  ('' cond '' ) 转到 Label1 ; 
   T ; 
   转到 Label2 ; 
 Label1 :
   S ; 
 Label2 :
   / * ... * /

这里,第一个goto是以“cond”的值为条件的。第二个goto是无条件的。我们可以在循环中执行相同的转换:

 while  ('' cond1 '' ) { 
   S ; 
   if  ('' cond2 '' )
     中断; 
   T ; 
 } 
 / * ... * /

哪个可以写成:

 开始:
   如果 (!'' cond1 '' ) 转到 结束; 
   S ; 
   if  ('' cond2 '' ) goto  End ; 
   T ; 
   转到 开始; 
 结束:
   / * ... * /

正如这些案例所示,通常可以在不使用gotos的情况下表达程序正在执行的结构。当更多惯用的替代方案(例如if-elses或for循环)可以更好地表达您的结构时,无纪律地使用gotos会产生难以理解的,不可维护的代码。从理论上讲,转到结构没有以往任何时候都必须使用,但也有它的时候可以增加可读性,避免代码重复,或使控制变量不必要的情况下。您应该首先考虑掌握惯用解决方案,并在必要时使用goto。请记住,许多(如果不是大多数)C风格指南严格禁止使用goto,唯一常见的例外是以下示例。

goto的一个用途是打破深度嵌套的循环。由于break不起作用(它只能转义一个循环),goto可以用来完全跳出循环。在不使用goto的情况下突破深层嵌套循环之外总是可行的,但通常涉及创建和测试额外的变量,这些变量可能使得结果代码的可读性远低于goto。使用goto可以很容易地以有序的方式撤消操作,通常是为了避免释放已经分配的内存。

另一个被接受的用途是创建状态机。这是一个相当高级的主题,但并不常见。

示例

#include  <errno.h>
#include  <stdio.h>
#include  <stdlib.h>

int  main (void )
{ 
	int  years ;

	printf (“输入你的年龄:” ); 
	fflush (stdout ); 
	errno  =  0 ; 
	if  (scanf (“%d” , &years ) !=  1  ||  errno )
		return  EXIT_FAILURE ; 
	printf (“你的年龄是%d \ n ” , 年 *  365 ); 
	返回 0 ; 
}

猜你想读:《C编程.开始C》10.程序和功能

THE END
分享