Linux C编程一站式学习笔记6

Posted 临风而眠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux C编程一站式学习笔记6相关的知识,希望对你有一定的参考价值。

Linux C编程一站式学习笔记 chap6 循环结构

文章目录

一.while语句

  • 我们回顾一下用递归求n!的方法,其实每次递归调用都在重复做同样一件事,就是把n乘到(n-1)!上然后把结果返回。虽说是重复,但每次做都稍微有一点区别(n的值不一样),这种每次都有一点区别的重复工作称为迭代(Iteration)。虽然迭代用递归来做就够了,但C语言提供了循环语句使迭代程序写起来更方便。

  • 之前的factorialwhile语句可以写成

    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)
  • 而上面的循环程序则通过对nresult这两个变量多次赋值来达到同样的目的。这种思路称为命令式编程(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 节 “递归”的所有习题,体会递归和循环这两种不同的思路。

  • 求两个正整数ab的最大公约数(GCD,Greatest Common Divisor),使用Euclid算法

    • 如果a除以b能整除,则最大公约数是b
    • 否则,最大公约数等于ba%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,否则,最大公约数等于ba%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));
    
    

2、编写程序数一下1到100的所有整数中出现多少次数字9。在写程序之前先把这些问题考虑清楚:

  1. 这个问题中的循环变量是什么?
  2. 这个问题中的累加器是什么?用加法还是用乘法累积?
  3. 第 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,果真搜到了!

二.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语句。注意主程序的局部变量iis_prime中的局部变量i是不同的两个变量,其实在调用is_prime函数时主程序的局部变量i和参数n的值相等。

习题

1、求素数这个程序只是为了说明breakcontinue的用法才这么写的,其实完全可以不用breakcontinue,请读者修改一下控制流程,去掉breakcontinue而保持功能不变。

  • 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, '+')则打印:

    		+
    	+	+	+
    +	+	+	+	+
    	+	+	+
    		+
    

    如果用偶数做参数则打印错误提示。