- 文章内容如有错误、纰漏希望各位大佬能在评论区指正~
第一章 C语言入门(循环篇)
本章概览
- 啊!一起来学习C/C++吧
- 第一章 C语言入门(循环篇)
- 前言
- 一、循环语句
- 1.while语句
- 2.do/while语句
- 3.for循环
- 4.break和continue语句
- 5.嵌套循环
- 6.goto语句
- 总结
- 参考书目
- 学习编程绝不是一件简单的事,尤其是对于零基础的初学者来说。
- 其实C语言是一门很难的编程语言,不懂编译原理、操作系统和计算机体系结构根本不可能学明白。
- 所以本文不会孤立地讲C语言,而是和编译原理、操作系统、计算机体系结构结合起来讲。
一、循环语句
1.while语句循环(Loop)程序去重复执行一个指令
- 之前我们介绍了用递归求n!的方法,其实每次递归调用都是在重复做同样一件事,就是把n乘到(n-1)!上然后把结果返回。虽说是重复,但每次做都稍微有一点区别(n的值不一样),这种每次都有点区别的重复工作称为迭代(Iteration)。
- 虽然迭代用递归来做就够了,但C语言提供了循环语句使迭代程序写起来更方便。
例如factorial用while语句可以写成:
int factorial(int n) { int result = 1; while (n > 0) { result = result * n; n = n - 1; } return result; }
-
像if语句一样,while由一个控制表达式和一个子语句组成,子语句可以是由若干条语句组成的语句块。如果控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值…这种控制流程称为循环(Loop)。子语句称为循环体。
-
如果某一次测试控制表达式的值为假,就跳出循环执行后面的语句,如果第一次测试控制表达式的值就是假,那么直接跳到return语句,循环体一次都不执行。
-
变量result在这个循环中的作用是累加器(Accumulator),把每次循环的中间结果累积起来,循环结束后得到的累积值就是最终结果,由于这个例子是用乘法来累积的,所以result初值为1,如果是用加法来累积那么result初值应该是0。
-
变量n是循环变量(Loop Variable),每次循环要改变它的值,在控制表达式中要测试它的值,这两点合起来起到控制循环的次数的作用,在这个例子中n的值是递减的,有些循环则采用递增的循环变量。
用递归解决这个问题靠的是递推关系n!=n·(n-1)!,用循环解决这个问题则更像是把这个公式展了:n!=n·(n-1)·(n-2)·…·3·2·1。
- 前一种思路称为函数式编程(Functional Programming),而后一种思路称为命令式编程(Imperative Programming),这个区别类似于 之前程序和编程语言 讲的Declarative和Imperative的区别。
- 函数式编程的“函数”类似于数学函数的概念,数学函数是没有副作用(Side Effect)的,而C语言的函数可以有副作用(Side Effect)
全局变量被多次赋值会给调试带来麻烦,如果一个函数体很长,控制流程很复杂,那么局部变量被多次赋值也会有同样的问题。此外,对全局变量多次赋值会影响代码的线程安全性。
正如递归函数如果写得不小心就会变成无穷递归一样,循环如果写得不小心就会变成无限循环(Infinite Loop)或者叫死循环。
2.do/while语句do/while语句的格式是:
do 语句 while(控制表达式);
它和while类似,其中的语句可以是一个语句块,构成循环体。只不过while是先测试控制表达式的值再执行循环体,而do/while是先执行循环体再测试控制表达式的值。上面的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; }
注意do/while这种形式在while(控制表达式)后面一定要加;号,否则编译器无法判断这是一个do/while循环的结尾还是另一个while循环的开头。
3.for循环for语句的格式为:
for(控制表达式1;控制表达式2;控制表达式3) 语句
如果不考虑语句中包含continue语句的情况(稍后介绍continue语句),这个for循环等价于下列的while循环:
控制表达式1; while(控制表达式2) { 语句 控制表达式3; }
从这种等价形式来看,控制表达式1和3都可以为空,但控制表达式2是必不可少的上一节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,++称为前缀自增运算符(Prefix Increment Operator),类似地,--称为前缀自减运算符(Prefix Decrement Operator)。
-
如果把++i这个表达式看作一个函数调用,除了传入一个参数返回一个值(等于参数值加1)之外,还产生一个副作用(Side Effect),就是把变量i的值增加了1。
-
++和--运算符也可以用在变量后面,例如i++和i–,为了和前缀运算符区别,称为后缀自增运
算符(Postfix Increment Operator)和后缀自减运算符(Postfix Decrement Operator)。 -
它和++i的区别就在于返回值不同。同理,–i返回减1之后的值,而i–返回减1之前的值,但这两个表达式都产生同样的副作用(Side Effect),就是把变量i的值减了1。
-
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循环中的局部变量而不是整个函数的局部变量,这个程序用gcc编译要加上选项-std=c99。
在C++中这种写法很常见,但是在C语言中,考虑到兼容性,不建议使用这种写法。
tips :增量(increment) 和 减缩(decrement)这两个词很有意思,大多数字典都说它们是名词,但经常被当成动词用,在计算机术语中,它们当动词用时应该理解为increase by one和decrease by one。现代英语中很多名词都被当成动词用,字典都跟不上时代了,再比如transition也是如此。
4.break和continue语句- 在switch语句中我们见到了break语句的一种用法,用来跳出switch语句块,这个语句也可以用来跳出循环体。
- continue语句也用来终止当前循环,和break语句不同的是,continue语句终止当前循环后又回到循环体的开头准备再次执行循环体。
对于while和do/while,continue之后测试控制表达式,如果值为真则继续执行下一次循环,对于for循环,continue之后首先计算 控制表达式3 ,然后测试 控制表达式2 ,如果值为真则继续执行下一次循环。
例如下面的代码打印1到100之间的素数:
#include5.嵌套循环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("%dn", i); } return 0; }
- 上一节求素数的例子在循环中调用一个函数,而那个函数又是一个循环,这其实是一种嵌套循环。
如果不是调用函数而是写在一起就更清楚了:
#includeint 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("%dn", i); } return 0; }
现在内循环的循环变量就不能再用i了,而是改用j,原来程序中is_prime函数的参数n现在直接用i代替。在有嵌套循环的情况下,break只能跳出最内层的循环或switch语句,continue也只能终止最内层循环并回到该循环的开头。
除了打印一列数据之外,用循环还可以打印表格式的数据,比如打印小九九乘法表:
#includeint 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("%dt", i*j);就整齐了,所以才需要有Tab(制表符)这么个字符。
6.goto语句- 分支、循环都讲完了,现在只剩下最后一种影响控制流程的语句了,就是goto语句,实现无条件跳转。
我们知道break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳到循环之外的某个地方做出错处理,就可以用goto语句,例如:
for (...) for (...) { ... if (出现错误条件) goto error; } error: 出错处理;
这里的error:叫做标号(Label),给标号起名字也遵循标识符的命名规则。
goto语句过于强大了,从程序中的任何地方都可以无条件跳转到任何其它地方,只要给那个地方起个标号就行,唯一的限制是goto只能跳到同一个函数的某个标号处,而不能跳到别的函数里。
-
所以,滥用goto语句会使程序的控制流程非常复杂,可读性很差。
-
著名的计算机科学家Edsger W. Dijkstra最早指出编程语言中goto语句的危害,提倡取消goto语句。
goto语句不是必须存在的,显然可以用别的办法替代,比如上面的代码段可以改写为:
int cond = 0; for (...) { for (...) { ... if (出现错误条件) { cond = 1; break; } } if (cond) break; } if (cond) 出错处理;
通常goto语句只用于在函数末尾做出错处理(例如释放先前分配的资源、恢复先前改动过的全局变量等),函数中任何地方出现了错误条件都可以立即跳到函数末尾,处理完之后函数返回。
比较上面两种写法,用goto语句还是方便很多。但是除了这个用途之外,在任何场合都不要轻易考虑使用goto语句。
总结
循环与递归很相似,但解决问题的思路不一样,用递归解决这个问题靠的是递推关系。
参考书目[1] 宋劲杉.Linux C编程一站式学习
[2] 鸟哥.鸟哥的 Linux 私房菜:基础学习篇 第四版
[3] K&R.The C Programming Language.
[4] Niklaus Wirth. Algorithms + Data Structures = Programs.