C语言入门(六)——循环语句

本文涉及的产品
云解析DNS-重点域名监控,免费拨测 20万次(价值200元)
简介: C语言入门(六)——循环语句

while语句


在前面的递归中,我们介绍了用递归求n!的方法,其实每次递归调用都在重复做同样一件事,就是把n乘到(n-1)!上然后返回结果。虽说是重复,但每次都做到稍微有一点区别(n的值不同),这种每次都有一点去别的重复工作称为迭代。我们每次使用计算机的主要目的之一就是让它做重复迭代的工作,因为把一件工作重复成千上万次而不出错是计算机最擅长的,也是人类最不擅长的。虽然迭代用递归来做就够了,但C语言提供了循环语句使迭代程序写起来更方便。例如factorial用while语句可以写成:

int factorial(int n)
{
 int result = 1;
 while (n > 0) {
 result = result * n;
 n = n - 1;
 }
 return result;
}

和if语句类似,while语句由一个控制表达式和一个子语句组成,子语句可以是由若干条语句组成的语句块。


语句->while(控制表达式)语句


如果控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值....这种控制流程称为循环,子语句称为循环体。如果某次测试控制表达式的值为假,就跳出循环执行后面的return语句,如果第一次测试控制表达式的值就是假,那么直接跳到return语句,循环体一次都不执行。


变量result在这个循环中的作用是累加器, 把每次循环的中间结果累计起来,循环结束后得到的累积值就是最终结果,由于这个例子是用乘法来累积的,所以result的初值是1,如果用加法累积则result的初值应该是0。变量n是循环变量,每次循环要改变它的值,在控制表达式中要测试它的值,这两点合起来起到控制循环次数的作用,在这个例子中n的值是递减的,也有些循环采用递增的循环变量。这个例子具有一定的典型性,累加器和循环变量这两种模式在循环中都很常见。


可见,递归能解决的问题用循环也能解决,但解决问题的思路不一样。用递归解决这个问题靠的是递推关系n!=n(n-1)!,用循环解决这个问题则更像是把这个公式展开了:n!=n*(n-1)*(n-2)...*3*2*1,把公式展开了理解会更直观一些,所以有些时候循环程序比递归程序更容易理解。但也有一些公式要展开是非常复杂的甚至是不可能的,反倒是递推关系更直观一些,这种情况下递归程序比循环程序更容易理解。此外还有一点不同:在整个递归调用过程中,虽然分配和释放了很多变量,但所有变量都只在初始化时赋值,没有任何变量的值发生过改变,而上面的循环程序则通过对n和result这两个变量多次赋值来达到同样的目的。前一种思路称为函数式编程,后一种思路称为命令式编程。


函数式编程的函数类似于数学函数的概念,回顾一下数学函数所讲的,数学函数时没有Side Effect的,而C语言的函数可以有Side Effect,比如在一个函数中修改某个全局变量的值就是一种Side Effect。在全局变量,局部变量和作用域指出,全局变量被多次赋值会给调试带来麻烦,如果一个函数体很长,控制流程很复杂,那么局部变量被多次赋值也会有同样的问题。因此,不要以为"变量可以多次赋值"是天经地义的,有很多编程语言可以完全采用函数式编程的模式,避免Side Effect,例如LISP,Haskell,Erlang等。用C语言编程主要还是采用Imperative的模式,但要记住,给变量多次赋值时要格外小心,在代码中多次读写同一变量应该以一种一致的方式进行。所谓"一致的方式"是说应该有一套统一的规则,规定在一段代码中哪里会对某个变量赋值,哪里会读取它的值。


递归函数如果写得不小心就会变成无穷递归,同样道理,循环如果写得不小心就会变成无限循环或者叫死循环。如果while语句得控制表达式永远为真就成了一个死循环,例如while(1){...}。在写循环时要小心检查你写的控制表达式永远为真就成了一个死循环,例如while(1){...}。在写循环时要小心检查你写的控制表达式有没有可能取值为假,除非你故意写死循环(有的时候有必要)。在上面得例子中,不管n一开始是几,每次循环都会把n减掉1,n越来越小最后必然等于0,所以控制表达式最后必然取值为假,但如果把n=n-1;这句漏掉了就成了死循环。有的时候是不是死循环并不是那么一目了然:

while (n != 1) {
 if (n % 2 == 0) {
 n = n / 2;
 } else {
 n = n * 3 + 1;
 }
}

如果n为正整数,这个循环能跳出来嘛?循环体所做的事情是:如果n是偶数,就把n除以2,如果n是奇数,就把n乘3加1.一般来说循环变量要么递增要么递减,可是这个例子中的n一会儿变大一会儿变小,最终会不会变成1?找个数试试,例如一开始n等于7,每次循环后n的值依次是:7,22,11,34,17,52,26,13,40,20,10,5,16,8,4,2,1。最后n确实等于1了。读者可以再试几个数都是如此,但无论试多少个数也不能代替证明,这个循环有没有可能对某些正整数n是死循环?其实这个例子只是给读者提提兴趣,同时提醒读者写循环时要有意识地检查控制表达式。至于这个循环有没有可能死循环,这是著名的3x+1问题,目前世界上还无人能证明。许多世界难题都是这样的:描述无比简单,连小学生都能看得懂,但证明却无比困难。

do/while语句


do/while语句的语法是:

语句->do语句while(控制表达式);

while语句先测试控制表达式的值再执行循环体,而do/while语句先执行循环体再测试控制表达式的值。如果控制表达式的值一开始就是假的,while语句的循环体一次都不执行,而do/while语句的循环体仍然要执行一次再跳出循环。其实只要有while循环就足够了,do/while循环和后面要讲的for循环都可以改写成while循环,只不过有些情况下用do/while或for循环写起来更加的简便,代码更容易读懂。上面的factorial也可以改用do/while循环来写:

int factorial(int n)
{
 int result = 1;
 int i = 1;
 do {
 result = result * i;
 i = i + 1;
 } while (i <= n);
 return result;
}

写循环一定要注意循环即将结束时控制表达式的临界条件是否准确,上面的循环结束条件如果写的i<n就错了,当i==n时跳出循环,最后的结果中就少乘一个n。虽然变量名应该尽可能起得有意义一些,不过用i,j,k给循环变量起名时很常见的。

for语句


前两节我们在while和do/while循环中使用循环变量,其实使用循环变量最常见的是for循环这种形式。for语句的语法是:

for(控制表达式1;控制表达式2;控制表达式3)语句

如果不考虑循环体中包含continue语句的情况,这个for循环等价于下面的while循环:

控制表达式1;
while (控制表达式2) {
 语句
 控制表达式3;
}

从这种等价形式来看,控制表达式1和3都可以为空,但控制表达式2是必不可少的,例如:for(;1;){...}等价于while(1){...}死循环。C语言规定,如果控制表达式2为空,则认为控制表达式2的值为真,因此死循环也可以写成for(;;){...}


上一节do/while循环的例子可以改写成for循环:

int factorial(int n)
{
 int result = 1;
 int i;
 for(i = 1; i <= n; ++i)
 result = result * i;
 return result;
}

其中++i这个表达式相当于i=i+1,++称为前缀自增运算符,类似地,--称为前缀自减运算符,--i相当于i=i-1。如果把++i这个表达式看作一个函数调用,出了传入一个参数返回一个值(等于参数值加1)之外,还产生了一个side effect,就是把变量i的值增加了1.


++和--运算符也可以用在变量后面,例如i++和i--.为了和前缀运算符区别,这两个运算符称为后缀自增运算符和后缀自减运算符,如果把i++这个表达式看作一个函数调用,传入一个参数返回一个值,返回值就等于参数值(而不是参数值加1),此外也产生一个side effect,就是把变量i的值增加1,他和++i的区别就在于返回值不同。同理,--i返回减1之后的值,而i--返回减1之前的值,但这个两个表达式都产生同样的Side Effect,就是把变量i的值减1.


使用++,--运算符会使程序更加简洁,但也会影响程序的可读性,为了使初学者循序渐进,在接下来几章中++,--运算符总是单独组成一个表达式而不跟其他表达式组合。


我们来看一个有意思的问题:a+++++b这个表达式如何理解?应该理解成a++ ++ +b还是a++ + ++b?应该按第一种方式理解。编译的过程分为词法解析和语法解析两个阶段,在词法解析阶段,编译器总是从前到后找最长的合法Token.把这个表达式从前到后解析,变量名a是一个Token,a后面有两个以上的+号,在C语言中一个+号是合法的Token(可以是加法运算符),两个+号也是合法的Token(可以是自增运算符),根据最长匹配原则,编译器绝不会止步于一个+号,而一定会把两个+号当作一个Token。再往后解析仍谈有两个以上的+号,所以又是一个++运算符。再往后解析只剩下一个+号了,是加法运算符。再往后解析是变量名b。词法解析之后进入下一阶段语法解析。a是一个表达式,表达式++还是表达式,表达式再++还是表达式,表达式再+b还是表达式,语法上没问题。最后编译器会做一些基本的语义分析,这时就有问题了,++运算符要求操作数能做左值,a能做左值所以a++没问题,但表达式a++的值只能做右值,不能再++了,所以最终编译器会报错。


C99规定了一种新的for循环语法,在控制表达式1的位置可以有变量定义。例如上例的循环变量i可以只在for循环中定义:

int factorial(int n)
{
 int result = 1;
 for(int i = 1; i <= n; i++)
 result = result * i;
 return result;
}

如果这样定义,那么变量i只是for循环中的局部变量而不是整个函数的局部变量,相当于if语句中讲过的语句块中的局部变量,在循环结束后就不能再使用i这个变量了。这个程序用gcc编译要加上选项-std=c99.这种语法也是从C++借鉴来的,考虑到兼容性不建议使用这种写法。

break和continue语句


在switch语句中我们见到了break语句的一种用法,用来跳出switch语句块,这个语句也可以用来跳出循环体。continue语句也会终止当前循环,和break语句不用的是,continue语句终止当前循环后又回到循环体的开头准备执行下一次循环。对于while循环和do/while循环,执行continue语句之后测试控制表达式,如果值为真则继续执行下一次循环;对于for循环,执行continue语句之后首先计算控制表达式3,然后测试控制表达式2,如果值为真则继续执行下一次循环。例如下面的代码打印1到100之间的素数:

#include <stdio.h>
int is_prime(int n)
{
 int i;
 for (i = 2; i < n; i++)
 if (n % i == 0)
 break;
 if (i == n)
 return 1;
 else
 return 0;
}
int main(void)
{
 int i;
 for (i = 1; i <= 100; i++) {
 if (!is_prime(i))
 continue;
 printf("%d\n", i);
 }
 return 0;
}

is_prime函数从2到n-1依次检查有没有能被n整除的数,如果有就说明n不是素数,立刻跳出循环而不执行i++。因此,如果n不是素数,则循环结束后i一定小于n,如果n是素数,则循环结束后i一定等于n.注意检查临界条件:2应该是素数,如果n是2,则循环体一次也不执行,但i的初值就是2,也等于n,在程序中也判定为素数。其实没有必要从2一直检查到n-1。只要从2检查到sqrt(n),如果全都不能整除就足以证明n是素数了,请读者想一想为什么。


在主程序中,从1到100依次检查每个数是不是素数,如果不是素数,并不直接跳出循环,而是i++后继续执行下一次循环,因此用continue语句。注意主程序的局部变量i和is_prime中的局部变量i是不同的两个变量,其实在调用is_prime函数时主程序的局部变量i和参数n的值相等。

嵌套循环


上一节求素数的例子在循环中调用一个函数,而那个函数里面又有一个循环,这其实是一种嵌套循环。如果把那个函数的代码拿出来写就更清楚了:

用嵌套循环求1-100素数

#include <stdio.h>
int main(void)
{
 int i, j;
 for (i = 1; i <= 100; i++) {
 for (j = 2; j < i; j++)
 if (i % j == 0)
 break;
 if (j == i)
 printf("%d\n", i);
 }
 return 0;
}

现在内循环的循环变量就不能再用i了,而是改用j,原来程序中is_prime函数的参数n现在直接用i代替。在有多层循环或switch嵌套的情况下,break只能跳出最内层的循环或switch,continue也只能终止最内层循环并回到该循环的开头。


用循环也可以打印表格式的数据,比如打印小九九乘法表:

#include <stdio.h>
int main(void)
{
 int i, j;
 for (i=1; i<=9; i++) {
 for (j=1; j<=9; j++)
 printf("%d ", i*j);
 printf("\n");
 }
 return 0;
}

内循环每次打印一个数,数与数之间用两个空格隔开,外循环每次打印一行。结果如下:

1 2 3 4 5 6 7 8 9 
2 4 6 8 10 12 14 16 18 
3 6 9 12 15 18 21 24 27 
4 8 12 16 20 24 28 32 36 
5 10 15 20 25 30 35 40 45 
6 12 18 24 30 36 42 48 54 
7 14 21 28 35 42 49 56 63 
8 16 24 32 40 48 56 64 72 
9 18 27 36 45 54 63 72 81

结果有一位数的有两位数的,这个表格很不整齐,如果把打印语句改为printf("%d\t", i*j);就整

齐了,所以Tab字符称为制表符。

goto语句和标号


分支,循环都讲完了,现在只剩下最后一种影响控制流程的语句了,就是goto语句,实现无条件跳转。我们知道break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳出最外层循环做出错处理,就可以用goto语句,例如:

for (...)
 for (...) {
 ...
 if (出现错误条件)
 goto error;
 }
error:
 出错处理;

这里的error:叫做标号,任何语句前面都可以加若干个标号,每个标号的命名也要遵循标识符的命名规则。


goto语句过于强大了,从程序中的任何地方都可以无条件跳转到任何其他地方,只要在那个地方定义一个标号就行,唯一的限制就是goto只能跳转到同一个函数中的某个标号处,而不能跳转到别的函数中,滥用goto语句会使程序的控制流程非常复杂,可读性很差。goto语句不是必须存在的,显然可以用别的办法进行替代,比如上面的代码段可以改写成:

int cond = 0; /* bool variable indicating error condition */
for (...) {
 for (...) {
      ...
      if (出现错误条件) {
           cond = 1;
           break;
         }
        }
      if (cond)
           break;
}
if (cond)
           出错处理;

通常goto语句只用于这种场合,一个函数中任何地方出现了错误条件都可以立即跳转到函数末尾做出错处理(例如释放先前分配的资源),处理完之后函数返回。比较用goto和不用goto的两种写法,用goto语句还是方便很多,但是除此之外,在任何其他场合都不要轻易考虑使用goto语句,有些编程语言(如C++)中有异常处理的语法,可以代替goto和setjmp/longjmp的这种用法。


回想一下,在switch语句学过case和default后面也要跟着冒号(:号,Colon),事实上它们是两种特殊的标号。和标号有关的语法规则如下:


语句->标识符:语句


语句->case常量表达式:语句


语句->default:语句


反复应用这些规则进行组合可以在一条语句前面添加多个标号,有些语句前面有多个case标号。现在我们再看switch语句的格式:


switch(控制表达式){


case 常量表达式:语句列表


case 常量表达式:语句列表


....


default:语句列表


}


{}里面是一组语句列表,其中每个分支的第一条语句带有case或default标号,从语法上来说,switch的语句块和其他分支,循环结构的语句块没有本质区别:


语句->switch(控制表达式)语句


语句->{语句列表}

相关文章
|
6月前
|
C语言
C语言分支和循环语句
分支语句由`if-else`构成,用于根据不同条件执行相应代码。`else`会与最近未配对的`if`结合,多个条件可用`else if`实现。若连续使用`if`,各条件互不影响。嵌套结构可在`if`中再加入`if-else`。此外,`switch`语句适用于多分支选择(注意表达式不能为浮点数,`case`后需加`break`)。循环语句包括`for`、`while`和`do...while`,注意`do...while`末尾需加分号。循环中,`break`直接终止循环,`continue`跳过当前循环剩余部分,但`for`的语句三仍会执行,而`while`中位置影响效果。
141 0
|
10月前
|
人工智能 Java 程序员
一文彻底搞清楚C语言的循环语句
本文介绍了C语言中的三种循环语句:`while`、`do-while`和`for`,并详细解释了它们的语法格式、执行流程及应用场景。此外,还讲解了循环控制语句`break`和`continue`的使用方法。希望这些内容能帮助你在编程道路上不断进步,共同成长!
901 0
一文彻底搞清楚C语言的循环语句
|
11月前
|
C语言
【C语言程序设计——循环程序设计】枚举法换硬币(头歌实践教学平台习题)【合集】
本文档介绍了编程任务的详细内容,旨在运用枚举法求解硬币等额 - 循环控制语句(`for`、`while`)及跳转语句(`break`、`continue`)的使用。 - 循环嵌套语句的基本概念和应用,如双重`for`循环、`while`嵌套等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台将对编写的代码进行测试,并给出预期输出结果。 5. **通关代码**:提供完整的代码示例,帮助理解并完成任务。 6. **测试结果**:展示代码运行后的实际输出,验证正确性。 文档结构清晰,逐步引导读者掌握循环结构与嵌套的应用,最终实现硬币兑换的程序设计。
169 19
|
11月前
|
算法 C语言
【C语言程序设计——循环程序设计】求解最大公约数(头歌实践教学平台习题)【合集】
采用欧几里得算法(EuclideanAlgorithm)求解两个正整数的最大公约数。的最大公约数,然后检查最大公约数是否大于1。如果是,就返回1,表示。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。作为新的参数传递进去。这个递归过程会不断进行,直到。有除1以外的公约数;变为0,此时就找到了最大公约数。开始你的任务吧,祝你成功!是否为0,如果是,那么。就是最大公约数,直接返回。
302 18
|
11月前
|
Serverless C语言
【C语言程序设计——循环程序设计】利用循环求数值 x 的平方根(头歌实践教学平台习题)【合集】
根据提示在右侧编辑器Begin--End之间的区域内补充必要的代码,求解出数值x的平方根;运用迭代公式,编写一个循环程序,求解出数值x的平方根。注意:不能直接用平方根公式/函数求解本题!开始你的任务吧,祝你成功!​ 相关知识 求平方根的迭代公式 绝对值函数fabs() 循环语句 一、求平方根的迭代公式 1.原理 在C语言中,求一个数的平方根可以使用牛顿迭代法。对于方程(为要求平方根的数),设是的第n次近似值,牛顿迭代公式为。 其基本思想是从一个初始近似值开始,通过不断迭代这个公式,使得越来越接近。
306 18
|
11月前
|
C语言
【C语言程序设计——循环程序设计】统计海军鸣放礼炮声数量(头歌实践教学平台习题)【合集】
有A、B、C三艘军舰同时开始鸣放礼炮各21响。已知A舰每隔5秒1次,B舰每隔6秒放1次,C舰每隔7秒放1次。编程计算观众总共听到几次礼炮声。根据提示,在右侧编辑器Begin--End之间的区域内补充必要的代码。开始你的任务吧,祝你成功!
243 13
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
399 7
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
11月前
|
存储 C语言
【C语言程序设计——循环程序设计】利用数列的累加和求 sinx(头歌实践教学平台习题)【合集】
项的累加和,一般会使用循环结构,在每次循环中计算出当前项的值(可能基于通项公式或者递推关系),然后累加到一个用于存储累加和的变量中。在C语言中推导数列中的某一项,通常需要依据数列给定的通项公式或者前后项之间的递推关系来实现。例如,对于一个简单的等差数列,其通项公式为。的级数,其每一项之间存在特定的递推关系(后项的分子是其前项的分子乘上。,计算sinx的值,直到最后一项的绝对值小于。为项数),就可以通过代码来计算出指定项的值。对于更复杂的数列,像题目中涉及的用于近似计算。开始你的任务吧,祝你成功!
277 6
|
11月前
|
C语言
【C语言程序设计——循环程序设计】鸡兔同笼问题(头歌实践教学平台习题)【合集】
本教程介绍了循环控制和跳转语句的使用,包括 `for`、`while` 和 `do-while` 循环,以及 `break` 和 `continue` 语句。通过示例代码详细讲解了这些语句的应用场景,并展示了如何使用循环嵌套解决复杂问题,如计算最大公因数和模拟游戏关卡选择。最后,通过鸡兔同笼问题演示了穷举法编程的实际应用。文中还提供了编程要求、测试说明及通关代码,帮助读者掌握相关知识并完成任务。 任务描述:根据给定条件,编写程序计算鸡和兔的数量。鸡有1个头2只脚,兔子有1个头4只脚。
596 5
|
11月前
|
存储 编译器 C语言
【C语言程序设计——入门】C语言入门与基础语法(头歌实践教学平台习题)【合集】
本文档介绍了C语言环境配置和编程任务,主要内容包括: - **C语言环境配置**:详细讲解了在Windows系统上配置C语言开发环境的步骤。 - **第1关:程序改错**:包含任务描述、相关知识(如头文件引用、基本语法规则)、编程要求、测试说明及通关代码。 - **第2关:scanf函数**:涉及`scanf`和`printf`函数的格式与使用方法,提供编程要求、测试说明及通关代码。 文档结构清晰,涵盖从环境搭建到具体编程任务的完整流程,适合初学者学习和实践。
371 4