C语言浅学-C控制语句:循环
此篇我们将对循环进行更加深入的学习,while、for以及do while三种。
深入while循环
在上篇中我们已经初步接触过了这个循环,主要是对条件语句进行判断,接下来通过一个程序来进一步了解。
1 |
|
这个程序是根据键入的整数求和,那么while的循环判断语句是什么呢,从上诉程序中我们可以看出就是scanf的返回值,在之前的学习,我们知道scanf的返回值是读取的数字项数,在这个程序中可知返回值只有0和1两种可能性。
程序注释
我们先来看看while循环中的测试条件,是一个完全等于的表达式:
1 | status == 1; |
在这个判断表达式中,== 运算符是一个相等运算符,用来判断是否等于1,而不是赋值表达式,把1赋值给status,要让程序正常运行,每次都要获取一个num,并且重置status的值。
其实我们在这里可以思考一个问题,循环的停止是否只能利用scanf 的返回值呢,若是scanf 没有返回值,我们是否可以利用别的方法来暂停循环,例如将测试条件改为num >0,或者是num != 0。(经过测试,这种方法是可行的!)也可以在循环中添加代码,询问用户是否接着循环,但是这样会增加代码的复杂性,且减慢了输入的速度。
while循环作为入口条件循环,程序必须在进入循环体之前必须获取输入的数据并检查status的值。
C风格读取循环
根据伪代码的设计思路,我们可以对上述程序进行改进:
1 | status = scanf("%ld", num); |
这一小节可以改成如下形式:
1 | while (scanf("%ld", num) == 1) |
简而言之,每次迭代之前都会判断循环的条件,只有当获取值和判断值都成功的时候,才会对值进行处理。
while语句
通用形式:
while(expression)
statement
其中,statement可以是以分号结尾的简单语句,也可以是用花括号括起来的复合语句。
在目前接触到的while循环里,对于expression部分都是使用关系表达式,进行真假值的判断,更一般的说法其实就是判断是不是等于1(非零)。只要是非零,循环就会一直执行,每次循环都被称为一次迭代。
终止while循环
while循环有一点很重要:必须让测试表达式的值有所变化,表达式最终需要为假。(其实在后面可以使用break和if语句来终止循环)。
1 | index = 1; |
上面这个程序会一直输出早上好,因为index的值始终为1,不曾变过,条件一直满足所以一直循环。
Tip:在循环中我们一定要记住就是不能陷入死循环,对于条件判断表达式的值一定谨慎的选择和变化。
何时终止循环
对于这个问题,只有在对测试条件求值时,才决定是终止还是继续循环。
1 |
|
对于上面这个程序,在第二次循环时首次获得了值7,但此时程序还没有退出,只有在对测试条件的再次判断之后才退出了循环。(所以总共是2次循环,3次判断)
while:入口条件循环
while循环是使用入口条件的有条件循环。有条件:语句的部分执行取决于测试表达式的条件,必须满足条件才能进入循环体,因为条件一开始就是假的话,根本进入不了循环。
实例:
1 | index = 10; |
这一小段程序,把第一行改成 index = 3 就可以开始运行循环了。
语法要点
我们在使用while的时候,还有另外一点我们需要记住:只有在测试条件后面的单独语句才是循环部分。
1 |
|
在上述程序中,虽然n++ 这行缩进了,但是并未把它和上一条语句括在花括号内,因此只有直接跟在测试条件之后的一条语句是循环的一部分,导致这个循环会变成一个死循环的程序。(若是没有外部干涉就不会退出)
1 |
|
若是将上述程序改为第二版,并且在while循环后面加上分号,那么这个程序的输出结果就会变得不一样,因为这个时候while循环的语句就变成了单独语句而不是一个判断入口语句。(后面这个分号的作用表示空语句)
但是这个形式最好改为下面的形式:
1 | while (scanf("%d", &num) == 1) |
处理这种情况更好的方法是使用下一章介绍的continue语句。
用关系运算符和表达式比较大小
while循环经常使用依赖测试表达式作比较,这样的表达式被称为关系表达式,出现在关系表达式中的运算符叫做关系运算符。
下图是一些关系运算符:
关系运算符常用于构造while语句和其他C语句中用到的关系表达式,会检查这些关系式的真假。
1 | while (number < 6) |
上面这三个例子,我们需要注意的是第二个,关系表达式是可以用于比较字符的,比较的时候使用的是ASCII码,但是不能使用关系运算符来比较字符串,后面会对比较字符串进行介绍。
在对浮点数进行比较的时候,尽量使用大于号或者小于号,因为有些小数位数的误差会导致原本应该相等的两个数最终不相等,或者也可以使用 fabs()函数来返回一个浮点数的绝对值。
例子:
1 |
|
运行结果:
何为真
在C语言中什么是真这并不难讨论,关系表达式更是如此,只有一个真一个假,类似于布尔值。
实例:
1 |
|
从上面这个例子我们就可以看出关系式的返回值就可以是逻辑判断真假。所以有时候可以使用 while(1)使得循环一直进行。
其他真值
从上面我们可以知道 1 和 0 可以作为测试表达式,那么是否可以使用其他数字呢?
实例:
1 |
|
这个程序对 while 的使用提升了一个层次,不添加 {} ,那么在条件为真时就是接着下一个语句输出,若是为假,则会跳出循环,来到下一个句子。(设计思路听巧妙)。
一般来说,在C语言中只有0会被认为是假,其余的数字都是真,也可以说,使得测试条件的值为非零即为真,所以有时候会把 while (goats)改成 while (goats != 0),第一种形式对初学者来说比较清楚,但是一般来说,第二种形式为多数程序员使用。
真值的问题
虽然C语言对真值的概念约束太少有一定的好处,但是事物都是有两面性的,约束少同时也会带来一些麻烦。
实例:
1 |
|
上述程序其实是6.1程序改变而来,而我们改变的就是将 status == 1 改为 status = 1 ,将原来的等于改为了赋值语句,这会导致我们即使输入了q 改变了status的值,在执行循环测试条件的时候又重新赋值为1,所以while (status = 1)其实就是 while(1),外加scanf ()函数读取指定形式的输入失败,它会把 q 留下,在下一次循环的时候继续读取,这样就会形成一个无限失败的循环。
Tip:这就提醒了我们不要在本应该使用 == 的地方使用 = ,等于和赋值是两个完全不同的概念。
为了避免这样的错误,有经验的程序员在构建比较是否相等的表达式时,都会习惯把常量放在左侧:
1 | a = 5; //赋值 |
总之,关系运算符用于构成关系表达式,真时值为1,假时值为0。通常用关系表达式作为测试语句条件,非零为真,零为假。
新的_Bool类型
在C语言中,一般来说是使用int类型的变量表示真假值,之前也介绍过新的标准中新增了_Bool类型,但是布尔类型只能存储0和1,如果把其他数值赋值给布尔类型,变量都会被设置为1。
若是我们使用布尔类型对上面的程序进行更改优化:
1 |
|
这个程序的优化在于把容易出错的测试表达式转换到了对于布尔值的赋值,所以这个 scanf(“%ld”, &num) == 1 式子只能输出0 和 1 的值,这是为了防止输入多个数字出现错误,但根据C语言的约束条件其实 == 1 也可以去掉(根据我自己的实验),加上是为了更加严谨的逻辑。
Tip:最新的标准中提供了stdbool.h 的头文件,该头文件让bool成为了_Bool的别名,还把true和false定义为1和0的符号常量,最关键的是写出来的代码可以和C++兼容,因为C++把bool、true、false定义为了关键字
优先级和关系运算符
关系运算符的优先级比算数运算符(+ -)低,比赋值运算符要高。说明 x > y +2 等同于 x > (y + 2),x = y > 2 等同于 x = (y > 2)。
在关系运算符之中也有两种不同的优先级:
高优先级组:< 、<= 、>、 >=
低优先级组:==、!=
和大部分的运算符一样,关系运算符的结合律也是从左往右。
所以 ex != wye ==zee这个式子就是先判断ex和wye是否相等然后使用得出来的0或1和zee进行比较,但是这种写法不是首选推荐。
我们下面列出现在已经学习过的运算符的优先级的表格:
while语句小结:while语句创建了一个循环,一直会重复执行到测试表达式为假或0为止,循环体可以是简单语句,也可以是复合语句。
不确定循环和计数循环
在实际的运用之中,一些while循环是不确定循环,所谓的不确定循环是指在测试表达式为假之前,不知道需要执行多少次循环,就像前面一个和用户交互计算整数之和的程序;还有另外一类就是计数循环,提前设定好需要循环的次数。
实例:
1 |
|
编译完成之后,得到如下结果:
从上面这个程序中,我们可以知道创建一个重复执行固定次数的循环涉及三个要点:
1.必须初始化计数器
2.计数器和有限值做比较
3.每次循环时递增计数器
while循环的测试条件执行比较,递增运算符执行递增,上述程序把递增放在最后会避免忘记,比将测试条件和更新组合放到一起要更好,但是计数器的初始化放在循环外,就很有可能会忘记初始化,实践告诉我们可能会发生的事终究还是会发生。下面来学习另外一种循环:for循环。
for循环
上面说到了三个行为(初始化、测试和更新),for循环把这三个行为组合在了一处,先来使用for循环对上面这个程序进行优化:
1 |
|
这个程序实现的效果和之前的一摸一样,for关键字后面的圆括号中有三个表达式,分别使用两个分号隔开,第一个表达式是初始化,只会在循环开始时执行一次,第二个是测试条件,第三个是执行更新,for循环语句后面还有简单语句和复合语句,for圆括号中的表达式也叫做控制表达式,都是完整表达式,每个表达式的副作用都发生在对下一个表达式求值之前。
接下来使用for循环来创建一个立方表:
1 |
|
这就是一个数字1-6的立方表。
for循环的第一行包含了循环的所需的信息:初值,终值,每次循环所需的增量。
利用for循环的灵活性
for循环相较于while循环拥有更多的灵活性主要取决于for循环的三个表达式,除了像上面一样进行递增的计数器,for循环还有其他的九种用法:
1.将递增运算符换成递减运算符变成递减计数器:
1 |
|
2.可以让计数器递增2、10等
1 |
|
3.可以使用字符代替数字计数
1 |
|
该程序本质上还是使用整数来计数的。
对于控制的条件我们可以根据自己的需求进行特殊的更改:
4.让递增的量几何增长
1 |
|
还有很多其他的例子在这里就不逐个展示了,其中在for循环中也可以省略一个或者多个表达式(但是不能省略分号),只要在循环中包含能结束循环的语句即可。
Tip:顺带一提,省略第2个表达式被视为真,所以下面的循环会一直运行:
1 | for (; ; ) |
在第一个表达式不一定是给变量赋初值,也可以使用 printf()。在执行循环的其他部分之前,只对第一个表达式求值一次或执行一次。
实例:
1 |
|
上面是程序的执行结果。
但是若是我们创建了以下的循环:
1 | for(n = 1; n < 10000; n = n + delta) |
这句话经过几次迭代后会发现delta太小或者太大了,循环中的if语句才可以改变delta的大小,但是这样做也会有危险的一面,就是把delta设置为0了。
小结:
for语句使用3个表达式控制循环过程,分别使用分号隔开,initialize表达式在执行for语句之前只执行一次;然后对test表达式求值,如果表达式为真(或非零),执行循环一次;接着对update表达式求值,并再次检查test表达式。for语句是一种入口条件循环,即在执行循环之前就决定了是否执行循环。因此,for循环可能一次都不执行。statement部分可以是一条简单语句或复合语句。
形式:
1 | for ( initialize; test; update ) |
在test为0或者假之前,重复执行statement的内容。
其他赋值运算符:+=,-=,*=,/=,%=
C语言有许多赋值运算符,最基本、最常用的是=,它属于赋值运算符,其他赋值运算符都用于更新变量,其用法都是左侧是一个变量名,右侧是一个表达式。
实例:
逗号运算符
逗号运算符扩展了for循环的灵活性,以便在循环头中包含更多的表达式。
1 |
|
上述程序在初始化表达式和更新表达式中使用了逗号运算符,初始化表达式中的逗号使得ounces和cost都进行了初始化,绝大多数计算都在for循环头中进行。逗号运算符并不局限于在for循环,但是这是它常用的地方。
例子:house = 249,500; 其实等同于house = 249;
house = (249,500); 也是赋值表达式,得到最终的结果是500。
逗号也可以作为分隔符号,在下面的语句中就是分隔符,不是逗号运算符。
例子:
1 | char ch,date; |
小结:
赋值运算符:
+= 把右侧的值加到左侧的变量上
-= 从左侧的变量中减去右侧的值
*= 把左侧的变量乘以右侧的值
/= 把左侧的变量除以右侧的值
%= 左侧变量除以右侧值得到的余数
Tips:这些组合赋值运算符与普通赋值运算符的优先级相同,都比算术运算符的优先级低。
当zeno遇到for循环
我们运用for循环和逗号表达式来解决Zeno悖论:
代码:
1 |
|
我们只需要输入限制的次数,即可得到次数限制内时间之和,通过for循环和逗号表达式更加减少了变量赋值的过程,构建完循环程序之后就相当于完成了程序的编写,大大简化了程序。
出口条件循环:do while
while循环和for循环都是入口条件循环,就是在每次迭代之前检查测试条件,所以会出现不执行循环体的内容,C语言还有出口条件循环,就是在迭代之后再来检查测试条件,这就保证了循环体至少执行一次,被称为do while循环。
实例:
1 |
|
执行效果:
当然我们使用while循环也可以得到相同的效果,但是代码相较于使用do while的程序就会复杂不少,首先多出来的一段代码就是do while循环提前输出的这段。
while循环下的代码:
1 |
|
这个程序相较于上一个程序来说就复杂的多了。
那么do while循环的通用形式:
do
statement
while(expression);
statement是一条简单语句或者是复合语句。do while循环最适合那些至少要迭代一次的循环(比较适合密码锁这类场合),若是询问类的则没有这么适合,因为这类都是需要先决条件,使用do while循环的话会迟滞结果的得出。
总的来说,需要注意的点就是do while循环无论条件判定如何,都会执行一次,这也是它和while循环和for循环的不同之处。
如何选择循环
上面我们介绍了while循环,for循环,do while循环三种不同的循环,三个循环各有特点和优势,那么应该如何最大化的提高效率去使用就是这一小结需要学习的了。
首先我们需要确定的是需要入口条件循环还是出口条件循环,一般来说,入口条件循环用的多一些,因为在执行循环前还是先测试条件比较好,其次,测试条件放在开头会使得程序的可读性更高。
若是选择入口条件循环,for循环和while循环其实都可以:
1 | for ( ,test, ) //其实就是while循环 |
上述代码就是两者之间的互相转换,主要是看使用者的习惯。当然一般而言,当我们的循环涉及到初始化和更新变量时,使用for循环比较合适,而其他情况下还是while循环更合适。
嵌套循环
嵌套循环是指在一个循环内包含另外一个循环,嵌套循环主要常用于按行和按列显示数据。
实例:
1 |
|
这个程序中,第10行的循环属于是外层循环,第12行开始是属于内层循环,内层循环10次打印了A到 J ,外层循环接着换行,进行下一次的外层循环,一共会打印出6行10列的字母。
嵌套循环的变式:
1 |
|
其实本质并未改变,只是添加了打印需要参考行数的变化。
数组简介
在C语言中,数组很重要,可以作为一种储存多个相关项的便利方式,之后会详细学习数组,但是在循环中会使用数组,先在这里简单进行介绍。
数组是按顺序储存的一系列类型相同的值,比如10个char类型的字符或者15 int类型的值。整个数组有一个数组名,通过整数下标来访问数组中单独的项。
实例:
1 | float debts[20]; //这就是创建了一个浮点型的数组 |
实际上,使用数组元素和使用同类型的变量一样。
1 | scanf("%f",&debts[6]); //将读入的数据放到第七个位置上 |
**Tips:这里需要注意一个潜在的陷阱,C编译器一般不会检查数组的下标是否正确:
1 | debts[20] = 88.32; //这其实是不正确的 |
但是面对上述错误,编译器不会查找出来,当程序运行时会导致数据被放置在已经被其他数据占用的地方,会破坏程序的正常运行。
数组的数据类型可以是任意的数据类型。
1 | int nannies[22]; //可储存22个int类型整数的数组 |
说到数组就不得不提到在字符串那篇中,可以把字符串储存在char类型的数组中,若是在末尾加上空字符 \0,那么就是字符串了。
用于识别数组元素的数字被称为是下标,下标必须是整数,而且要从0开始计数,数组的元素被依次存在内存相邻的位置。
数组在for循环中的应用
程序许多地方都要使用到数组,下面是一个比较简单的例子。
实例:
1 |
|
这个程序大大简化了取数值的麻烦,不用再一个一个的取读,而且在程序开头就采用了明示常量方便以后对数组进行扩展的操作,当然在在读取时数组下角标的方式就是int类型的变量,也需要我们注意,当然这个程序还可以进行优化,可以发现这个程序的三个独立循环都是使用一个条件,可以进行合并和简化使得结构更加紧凑。
利用返回值的循环
最后一个程序是要利用一个函数计算数的整数次幂(math.h库中提供了一个强大的幂函数pow())。
既然是需要计算数的整数次幂,那么循环就是必不可少的,设计一个循环将 n 相乘p 次就可以得到整数次幂:
1 | for (i = 0; i < p; i++) |
要编写一个有返回值的函数,我们需要完成以下的内容:
1.定义函数时,确定函数的返回类型。
2.使用关键字return表明待返回的值。
接下来我们尝试写一下函数:
1 | double power(double n, int p) |
上述程序展示的是名为power的函数,返回值可以一个变量的值,也可以是表达式的值。
接下来我们来使用这个函数:
1 |
|
这个程序中的main()函数是一个测试函数,就是被设计用来测试函数的小程序,程序的while循环的判断条件是之前讨论过的形式,利用scanf()函数的返回值进行判断,读取两个数就能够进入循环,剩下的就是power()函数的内容了,一共出现了三次,第一次是声明,第二次是使用,第三次是函数定义,power()函数有两个形参,一个是double类型,一个是int类型。
Tip:函数定义的末尾没有分号,而函数原型的末尾有分号。在函数头后面花括号中的内容,就是power()完成任务的代码。
在我们使用带返回值的函数时,声明函数、调用函数、定义函数、使用关键字return都是其中的基本要素。
对于定义中说明了power()函数的返回类型是double,为什么还要提前声明,其实如果把函数定义放在main()函数之前,就可以不用前置声明了,但是这不是很符合C语言的习惯,所以前置声明是必不可少的。那为什么scanf()函数就可以直接使用,是因为在头文件中包含了函数的原型表明。
总结
循环是一个强大的编程工具,在创建循环的时候我们需要注意三个方面:
1.注意测试条件要能使得循环结束。
2.要保证测试中的值首次使用之前已经初始化。
3.保证循环每次迭代都会更新测试的值。
数组是由相邻的内存位置组成,只能存储相同类型的数据。
涉及到函数使用的3个步骤:
1.通过函数原型声明函数。(声明函数的时候需要使用分号(;))
2.在程序中通过函数调用使用函数。
3.定义函数。