Linux C编程一站式学习笔记6
Posted 临风而眠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux C编程一站式学习笔记6相关的知识,希望对你有一定的参考价值。
Linux C编程一站式学习笔记 chap6 循环结构
文章目录
- Linux C编程一站式学习笔记 chap6 循环结构
- 一.while语句
- 二.do/while语句
- 三.for语句
- 四.break和continue语句
- 五.嵌套循环
- 六.goto语句和标号
- 相关资源、参考资料
一.while语句
-
我们回顾一下用递归求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;
-
while
语句由一个控制表达式和一个子语句组成,子语句可以是若干条语句组成的语句块语句→while(控制表达式)语句
- 若控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值…这种控制流程称为循环(Loop),子语句称为循环体
- 若某次测试控制表达式的值为假,就跳出循环执行后面的
return
语句 - 如果第一次测试控制表达式的值就是假,那么直接跳到
return
语句,循环体一次都不执行
-
变量
result
在这个循环中的作用是累加器**(Accumulator),把每次循环的中间结果累积起来,循环结束后得到的累积值就是最终结果,由于这个例子是用乘法来累积的,所以result
的初值是1,如果用加法累积则result
的初值应该是0。变量n
是循环变量(Loop Variable)**,每次循环要改变它的值,在控制表达式中要测试它的值,这两点合起来起到控制循环次数的作用,在这个例子中n
的值是递减的,也有些循环采用递增的循环变量。这个例子具有一定的典型性,累加器和循环变量这两种模式在循环中都很常见。
递归 VS 循环
- 递归能解决的问题用循环也能解决,但解决思路不同
- 用递归解决这个问题靠的是递推关系
n!=n·(n-1)!
- 用循环解决这个问题则更像是把这个公式展开了:
n!=n·(n-1)·(n-2)·…·3·2·1
- 用递归解决这个问题靠的是递推关系
- 把公式展开了理解会更直观一些,所以有些时候循环程序比递归程序更容易理解。但也有一些公式要展开是非常复杂的甚至是不可能的,反倒是递推关系更直观一些,这种情况下递归程序比循环程序更容易理解
函数式编程(Functional Programming) & 命令式编程(Imperative Programming)
- 之前的factorial递归的例子中,在整个递归调用过程中,虽然分配和释放了很多变量,但所有变量都只在初始化时赋值,没有任何变量的值发生过改变,这种思路称为函数式编程(Functional Programming)
- 而上面的循环程序则通过对
n
和result
这两个变量多次赋值来达到同样的目的。这种思路称为命令式编程(Imperative Programming) - 函数式编程的“函数”类似于数学函数的概念,C语言的函数可以有Side Effect,比如在一个函数中修改某个全局变量的值就是一种Side Effect。全局变量被多次赋值会给调试带来麻烦,如果一个函数体很长,控制流程很复杂,那么局部变量被多次赋值也会有同样的问题。
- 因此,不要以为“变量可以多次赋值”是天经地义的,有很多编程语言可以完全采用函数式编程的模式,避免Side Effect,例如LISP、Haskell、Erlang等。用C语言编程主要还是采用Imperative的模式,但要记住,给变量多次赋值时要格外小心,在代码中多次读写同一变量应该以一种一致的方式进行。所谓“一致的方式”是说应该有一套统一的规则,规定在一段代码中哪里会对某个变量赋值、哪里会读取它的值。
无限递归 & 无限循环
-
递归函数如果没写 base case,容易变成无穷递归,循环不注意的话也容易变成无限循环(Infinite Loop)或叫死循环。
-
例如前面这个例子
int factorial(int n) int result = 1; while (n > 0) result = result * n; n = n - 1; return result;
- 若写成
while(1) ...
,或者漏了n = n - 1
,就变成了死循环
- 若写成
-
但有时候 是否为死循环 不是一目了然的
while (n != 1) if (n % 2 == 0) n = n / 2; else n = n * 3 + 1;
如果
n
为正整数,这个循环能跳出来吗?这个是著名的3x+1猜想能找出不少例子,比如n一开始为7,最后得到1,但无论试多少个数也不能代替证明,这个循环有没有可能对某些正整数
n
是死循环呢?
习题
1、用循环解决第 3 节 “递归”的所有习题,体会递归和循环这两种不同的思路。
-
求两个正整数
a
和b
的最大公约数(GCD,Greatest Common Divisor),使用Euclid算法- 如果
a
除以b
能整除,则最大公约数是b
。 - 否则,最大公约数等于
b
和a%b
的最大公约数
这个算是大一学的时候的经典问题了…
一开始想到的是这个最原始的,但是忘了审题hhh,Euclid算法
#include <stdio.h> #include <math.h> int GCD(int a, int b) int gcd = 1; int i; for (i = 1; i <= a && i <= b; i++) if (a % i == 0 && b % i == 0) gcd = i; return gcd; // Driver program to test above function int main() int a = 98, b = 56; printf("GCD of %d and %d is %d ", a, b, GCD(a, b)); return 0;
记得大一的时候总是不确定控制表达式里面写什么,现在再来看看算法的描述:如果
a
除以b
能整除,则最大公约数是b
,否则,最大公约数等于b
和a%b
的最大公约数要找到最大公约数,那就是余数为0啊,那么肯定是把余数作为控制条件
令 r = a % b,则 a = m * b + r
我们就引入三个变量,但是要迭代表示这个算法,所以注意变量身份的变化
a 除以 b , b 除以 (a%b)
也就是 迭代的时候 b 取代了 a的位置, 而 a%b 取代了 b的位置
所以是 r = a % b, a = b, b = r
如果a能整除b,那么r一开始就是0,a和b的最大公约数为b,b=r之后b也为0, 于是退出循环,而要return 最大公约数,本该return b,但是b赋值给了a,所以return a
如果a不能整除b,那么就要经历数次迭代了,在最后一次的时候,r = a(是上一轮的b)% b(是上一轮的a%b) 为0 ,赋值给b,b为0退出循环, 而这一次,最大公约数就是b(上一轮的a%b),但是赋值给了a,所以return a
#include <stdio.h> #include <math.h> int GCD(int a, int b) //using euclid algorithm and for loops int r; while (b != 0) r = a % b; a = b; b = r; return a; // Driver program to test above function int main() int a = 98, b = 56; printf("GCD of %d and %d is %d ", a, b, GCD(a, b)); return 0;
- 如果
-
求Fibonacci数列的第
n
项,这个数列是这样定义的- fib(0)=1
fib(1)=1
fib(n)=fib(n-1)+fib(n-2)
一开始想到的是这个代码,是正确的,但是感觉写的一般般
需要注意的是
for (i = 2; i <= n; i++)
这里的循环次数, 设置成2和n挺好理解的,从第二项要算到第n项#include <stdio.h> #include <math.h> int Fibonacci(int n) if (n == 0 || n == 1) return 1; else int i; int f1 = 1; int f2 = 1; int f3; for (i = 2; i <= n; i++) f3 = f1 + f2; f2 = f1; f1 = f3; return f3; int main() printf("%d ", Fibonacci(0)); printf("%d ", Fibonacci(1)); printf("%d ", Fibonacci(2)); printf("%d ", Fibonacci(3)); printf("%d ", Fibonacci(4)); printf("%d ", Fibonacci(5)); printf("%d ", Fibonacci(6)); printf("%d ", Fibonacci(7)); printf("%d ", Fibonacci(8)); printf("%d ", Fibonacci(9));
- fib(0)=1
2、编写程序数一下1到100的所有整数中出现多少次数字9。在写程序之前先把这些问题考虑清楚:
- 这个问题中的循环变量是什么?
- 这个问题中的累加器是什么?用加法还是用乘法累积?
- 在第 2 节 “if/else语句”的习题1写过取一个整数的个位和十位的表达式,这两个表达式怎样用到程序中?
-
循环变量就是用一个遍历1~100的变量
-
累加器就是次数,用加法
-
my code
#include <stdio.h> #include <math.h> int CountNine() int num = 0; int i; for(i=1; i<=100; i++) int s = i % 10; //个位 int t = i / 10; //十位 if(s == 9) num += 1; if(t == 9) num += 1; return num; // Driver program to test above function int main() printf("%d", CountNine());
答案20
要是把这题改一下,改成出现过9的数字的个数,答案是19,因为99里面重复统计一次,那就得稍微改改了
#include <stdio.h> int main() int count = 0; int i; for (i = 1; i <= 100; i++) if (i % 10 == 9 || i / 10 == 9) count++; printf("%d", count); return 0;
欧几里得算法可视化
做上面练习的时候,感觉还是不直观,于是去搜谷歌:
visualize euclid
,果真搜到了!
- 有大佬在geogebra做了:https://www.geogebra.org/m/vwwezney
二.do/while语句
-
syntax:
语句 → do 语句 while (控制表达式);
-
while
语句先测试控制表达式的值再执行循环体,而do/while
语句先执行循环体再测试控制表达式的值。如果控制表达式的值一开始就是假,while
语句的循环体一次都不执行,而do/while
语句的循环体仍然要执行一次再跳出循环。
三.for语句
-
syntax:
for (控制表达式1; 控制表达式2; 控制表达式3) 语句
如果不考虑循环体中包含
continue
语句的情况,这个for
循环等价于下面的while
循环:控制表达式1; while (控制表达式2) 语句 控制表达式3;
从这种等价形式来看,控制表达式1和3都可以为空,但控制表达式2是必不可少的,例如
for (;1;) ...
等价于while (1) ...
死循环。C语言规定,如果控制表达式2为空,则认为控制表达式2的值为真,因此死循环也可以写成for (;;) ...
。 -
++i,--i
,i++,i--
-
++
称为前缀自增运算符(Prefix Increment Operator)
,类似地,--
称为前缀自减运算符(Prefix Decrement Operator)
[10],--i
相当于i = i - 1
。如果把++i
这个表达式看作一个函数调用,除了传入一个参数返回一个值(等于参数值加1)之外,还产生一个Side Effect,就是把变量i
的值增加了1。 -
i++
和i--
,为了和前缀运算符区别,这两个运算符称为后缀自增运算符(Postfix Increment Operator)
和后缀自减运算符(Postfix Decrement Operator)
。如果把i++
这个表达式看作一个函数调用,传入一个参数返回一个值,返回值就等于参数值(而不是参数值加1),此外也产生一个Side Effect,就是把变量i
的值增加了1,它和++i
的区别就在于返回值不同。同理,--i
返回减1之后的值,而i--
返回减1之前的值,但这两个表达式都产生同样的Side Effect,就是把变量i
的值减了1。 -
使用++、–运算符会使程序更加简洁,但也会影响程序的可读性
-
-
C99 的一种for循环写法
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
循环中的局部变量而不是整个函数的局部变量,相当于语句块中的局部变量,在循环结束后就不能再使用i
这个变量了。这个程序用gcc
编译要加上选项-std=c99
。这种语法也是从C++借鉴的,考虑到兼容性不建议使用这种写法。
四.break和continue语句
-
前面学switch的时候第一次见到了break,用来跳出
switch
语句块,这个语句也可以用来跳出循环体。 -
continue
语句也会终止当前循环,和break
语句不同的是,continue
语句终止当前循环后又回到循环体的开头准备执行下一次循环。- 对于
while
循环和do/while
循环,执行continue
语句之后测试控制表达式,如果值为真则继续执行下一次循环; - 对于
for
循环,执行continue
语句之后首先计算控制表达式3,然后测试控制表达式2,如果值为真则继续执行下一次循环。例如下面的代码打印1到100之间的素数:
例 6.1. 求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
是素数了,请读者想一想为什么。因为对称性:
Why do we check up to the square root of a number to determine if the number is prime?
- Sqrt:square root
在主程序中,从1到100依次检查每个数是不是素数,如果不是素数,并不直接跳出循环,而是
i++
后继续执行下一次循环,因此用continue
语句。注意主程序的局部变量i
和is_prime
中的局部变量i
是不同的两个变量,其实在调用is_prime
函数时主程序的局部变量i
和参数n
的值相等。 - 对于
习题
1、求素数这个程序只是为了说明break
和continue
的用法才这么写的,其实完全可以不用break
和continue
,请读者修改一下控制流程,去掉break
和continue
而保持功能不变。
-
my solution
#include <stdio.h> int is_prime(int n) int i; int flag = 1; //1表示是素数 for (i = 2; i < n; i++) if(n%i == 0) flag = 0; //0表示不是素数 return flag; int main(void) int i; for (i = 2; i <= 100; i++) if (is_prime(i)) printf("%d\\n", i); return 0;
其实可以再简化,不需要flag,要记得return这个东西,比break还terminate得更彻底
#include <stdio.h>
int is_prime(int n)
int i;
for (i = 2; i < n; i++)
if(n%i == 0)
return 0;
return 1;
int main(void)
int i;
for (i = 2; i <= 100; i++)
if (is_prime(i))
printf("%d\\n", i);
return 0;
2、上一节讲过怎样把for
循环改写成等价的while
循环,但也提到如果循环体中有continue
语句这两种形式就不等价了,想一想为什么不等价了?
for (控制表达式1; 控制表达式2; 控制表达式3) 语句
continue执行完后 会执行for循环中的 控制表达式3
控制表达式1;
while (控制表达式2)
语句
控制表达式3;
而while中遇到continue 会跳过控制表达式3
不过我寻思…while循环里,把continue写在控制表达式3的后面不就又等价了吗😝
五.嵌套循环
-
上一节求素数的例子中,在一个循环中调用is_prime函数,而那个函数里面又有个循环,其实这就是嵌套循环,如果全写在main函数里面就是这样👇
#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;
练习
-
打印乘法口诀表
1 2 4 3 6 9 4 8 12 16 5 10 15 20 25 6 12 18 24 30 36 7 14 21 28 35 42 49 8 16 24 32 40 48 56 64 9 18 27 36 45 54 63 72 81
-
my code
#include <stdio.h> void PrintProduct() int i, j; for ( i = 1; i <= 9; i++) for (j= 1; j <= i; j++) printf("%d ", i * j); printf("\\n"); int main(void) PrintProduct();
-
-
编写函数
diamond
打印一个菱形。如果调用diamond(3, '*')
则打印:* * * * *
如果调用
diamond(5, '+')
则打印:+ + + + + + + + + + + + +
如果用偶数做参数则打印错误提示。
-
my code
#include <stdio.h> /* 编写函数diamond打印一个菱形。如果调用diamond(3, '*')则打印: * * * * * 如果调用diamond(5, '+')则打印: + + + + + + + + + + + + + 如果用偶数做参数则打印错误提示。 using C language */ // 用于打印菱形 void diamond(int n, char c) int i, j, k; if (n % 2 == 0) printf("Error: n must be odd number"); return; for (i = 0; i < n; i++) if (i < n / 2) for (j = 0; j < n / 2 - i; j++) printf(" ")Linux C编程一站式学习笔记1
-