做好闭环(一):不看答案可能就白学了

你好,我是胡光。

不知不觉,语言基础篇已学习过半,我非常高兴,看到很多同学都在坚持学习。并且,还有一些同学,每每都能在专栏上线的第一时间里,给我留言,提出疑惑。当面对一些知识点的时候,如果在我的观念中它是不说自明,而对于新手的你来说,可能十分难理解的时候,我也很希望你能指出来,我会在留言区中给你解答的。因为,我知道这种讨论,肯定能够帮助到更多的人。

大部分留言,我都在相对应的文章中回复过了,而对于文章中的思考题呢,由于要给你留足思考时间,所以我选择,一起留在今天这样一篇文章中,给你进行一一的解答。

看一看我的参考答案,和你的思考结果之间,有什么不同吧。也欢迎你在留言区中,给出一些你感兴趣的题目的思考结果,我希望我们能在这个过程中,碰撞出更多智慧的火花。在这里呢,@rocedu 用户在第一篇留言区中给大家推荐的《程序设计实践》一书,也是非常优秀的书籍。有兴趣的小伙伴,也可以去到他提到的豆瓣读书主页中去游览一番。

第一个程序:教你输出彩色的文字

在这一篇里面呢,我们接触了如何在 Linux 环境下输出彩色文字的编程知识。初步学习了 scanf 和 printf 函数的基础用法,两者一个负责读入,一个负责输出。如果你对这篇文章的内容有点陌生,可以再回去看看《第一个程序:教你输出彩色的文字》。最后围绕着这两个函数,给你出了两个思考题。这两个思考题做的怎么样?下面来看看我的参考答案吧。

思考题(1):位数输出

#include <stdio.h>

int main() {
    int n;
    scanf("%d", &n);
    printf(" has %d digits\n", printf("%d", n)); // 有多余输出
    char output[50];
    int ret = sprintf(output, "%d", n);
    printf("%d\n", ret); // 无多余输出
    return 0;
}

运行如上程序,如果输入 123,程序会输出如下两行内容:

123 has 3 digits  
3

你会看到,第1行除了数字的位数信息以外,还有多余的输出,第2行则是没有多余的输出。而两个信息,都是单纯利用 printf 一族函数完成的。这个问题的解题关键是,理解 printf 函数是有返回值的,而其返回的含义是打印了多少个字符。

那么,当我们使用 printf 打印数字 n 的时候,printf 函数的返回值,就是代表了 n 的位数。类似的,sprintf 也是 printf 一族函数中的一员,它的返回值与 printf 含义相同。

思考题(2):读入一行字符串

#include <stdio.h>
char str[100];
int main() {
    scanf("%[^\n]s", str);
    printf("%s\n", str);
    return 0;
}

这段代码展现了如何使用 scanf 读入一行包含空格的字符串信息。其中,要读入字符串,就需要使用 %s 格式占位符。可是这道题目中,在 % 和 s 中间有一对中括号[],这个[] 代表了一个集合,用来控制%s 在读入过程中可以读入的字符集合的,例如:%[a-z]s,是可以输入小写字母 a 到 z,那么一旦遇到了非小写字母,就会停止。

而上述代码中的 ^ 上尖号,读作非,“^\n” 就是非换行符,也就是说,只要不是换行符,就可以继续读入。这也就达到了我们想要用 scanf 读入一行的功能要求。你可以自己试一下换成 %[a-z]s,然后输入 “abcd12efeee”,看看程序的输出,你就能明白了。

判断与循环:给你的程序加上处理逻辑

在这篇文章《判断与循环:给你的程序加上处理逻辑》中呢,我们学习了除了顺序结构以外的两种程序执行结构:分支结构和循环结构。知识点的话,主要涉及:条件表达式、if 语句、for 语句等知识内容。我们说到,任何表达式都有返回值,条件表达式的值,就是1或者0代表“真”或者“假”,“成立”或者“不成立”。并且,介绍了条件判断的时候,实际上遵循的原则是“非零即为真”。最后呢,给你留了一个和循环相关的思考题“打印乘法表”,下面就看看我的参考答案吧。

思考题:打印乘法表

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

这段代码中,采用两层循环,外层循环控制行数,内层循环控制每一行的列数,第 i 行应该有 i 列,所以内层循环是从 1 循环到 i 为止。其中最值得琢磨的是“j == 1 || printf("\t");”这句代码,其实这句代码就是用来实现行尾无多余 \t 字符这个要求的。代码中采用了在每一列的前面输出一个 \t 字符,可是在第一列的前面不输出 \t 字符,这样就保证了行尾无 \t 字符。

那么“j == 1 || printf("\t");”这句代码是如何工作的呢?首先看 || 条件或运算符。|| 运算符的工作逻辑是,左右两侧只要有一个条件成立,那么最终结果就是成立的。这个工作逻辑,还值得细细思考,|| 运算符,从左到右依次判断两个条件是否成立,那么如果第一个左边的条件就成立了呢?作为一个聪明人,还需要判断第二个右边的条件么?你会发现,根本不需要再判断右边的条件了,也就是说不需要执行右边的代码了。

看完了条件“或”的这个特性之后,我们再看看“j == 1 || printf("\t");”这句代码,也就是说,当 j==1 成立时,也就是第一列的时候,右边的 printf("\t") 代码就根本不会执行。这也就意味着,第一列前面不会多输出一个 \t 字符。而其他的情况呢,均会执行 printf("\t") 代码,这也就实现了题目中的要求。

随机函数:随机实验真的可以算 π 值嘛?

这一篇文章里面《随机函数:随机实验真的可以算 π 值嘛?》,我们介绍了程序里面随机函数的基本原理,说明了“真随机”和“伪随机”的本质区别。看了一些留言以后,我来给你总结一下,所谓“真随机”与“假随机”,只要你不太清楚下一个产生的值是什么,那么对于你来说,就是随机的,而“真”或者“假”,讨论的是随机方法的本质。如果随机过程可以保证,下一次产生的每个值都有一定的概率,那么这个就是“真随机”,如果不能保证,那就是“伪随机”。

理解程序中的“伪随机”,你需要在你的脑袋中,构建一个由值组成的环形序列图,设置随机种子,就是选择图中的某个点作为起始点,在我们一次次地获得随机值的过程中,其实程序就是依次地输出了这个环形序列中的每个状态的值。

最后呢,给你留了一个设计随机函数过程的思考题,关于这个思考题,我要提前先跟你道歉,因为这个思考题,并不是想让你做出来的。下面来看看我的参考答案吧。

思考题:设计迷你随机函数

#include <stdio.h>
int main() {
    int n = 5;
    for (int i = 1; i <= 100; i++) {
        printf("%2d ", n);
        if (i % 10 == 0) printf("\n");
        n = (n * 3) % 101;
    }
    return 0;
}

当你运行这个程序的时候,就会看到程序的输出,正如原文中我给你的样例输出一样。要是想理解这段程序,你需要一些数论方面的基础知识,其中包括:欧拉函数,欧拉定理、费马小定理、取余循环节等知识。

在这里,我要再次因为设置这个你可能做不出来这个题,而向你道歉。不过,当你看到上面的那些知识以后,你会发现,这是一道初学者很大概率不可能完成的题目,尽管代码很简单,可背后的原理却看似不简单。其实,我就是想跟你说明,程序的灵魂在算法,算法的灵魂在数学。

数组:一秒钟,定义 1000 个变量

这一篇中,我们学习了数组的基本用法,学会了定义一组数据存储区的方法。并且,围绕着数组知识,完成了“计算数字二进制表示中 1 的个数”的递推程序的设计与实现。

相关的课后思考题呢,也是希望你使用数组来完成相关任务,我看到用户 @奔跑的八戒,完成的就很好,他的思路描述与参考答案一致。也非常感谢 @梅利奥猪猪毛丽莎肉酱(根据这位用户的名称,我猜可能是漫画《七大罪》的爱好者)和@Geek_And_Lee00 给出的修改建议以及指正出文章中的笔误,再次感谢二位。如果有好奇的朋友,可以到原文章及留言区看看《数组:一秒钟,定义 1000 个变量》

最后让我们来看看这篇文章的参考答案吧。

思考题:去掉倍数

#include <stdio.h>
int check[1005] = {0};
int main() {
    int n, m, num;
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%d", &num);
        for (int j = num; j <= m; j += num) {
            check[j] = 1;
        }
    }
    for (int i = 1; i <= m; i++) {
        if (check[i] == 1) continue;
        printf("%d ", i);
    }
    return 0;
}

这段代码中,使用一个 check 数组作为标记,check[i] 等于 0,代表 i 这个数字不是 n 个数字中的任何一个数字的倍数。check[i] 等于 1,代表 i 这个数字能够被 n 个数字中的某个数字整除。其中第 7 行到第 10 行代码,是需要特别关注的。这段代码中,首先读入 n 个数字中的某一个,存储在 num 变量中,之后循环 m 以内所有 num 的倍数,把每个数字的 check 值标记为 1。最后我们循环把 1 到 m 中没有被标记的数字输出,就是符合题目要求的所有数字。

字符串:彻底被你忽略的 printf 的高级用法

这篇《字符串:彻底被你忽略的 printf 的高级用法》的文章中,我们认识了 scanf 和 printf 家族中的两员猛将:sscanf 函数和 sprintf函数。这两者操作的是字符串,可以理解其本质,就是以字符串为中介做数据类型之间的转换。并且我们还介绍了字符串的相关知识,字符串的相关知识中,比较重要的就是那个 \0 字符,这是一个标记字符串结束的字符,虽然看不到,可作用非常重要,并且这个 \0 字符,也是需要占用存储空间的。

这篇文章中的两个思考题,都是帮助你打开脑洞的,主要就是想告诉你,知识点是死的,而理解知识点和应用知识点是活的,也就是我们常说的活学活用。下面就来看看这篇文章中的两个思考题的参考答案吧。

思考题(1):体验利器

#include <stdio.h>
char str1[1000], str2[1000];
int main() {
    scanf("%s%s", str1, str2);
    printf("str1 = %s\tstr2 = %s\n", str1, str2);
    sprintf(str1, "%s", str1);   // strlen(str1)
    sprintf(str1, "%s", str2);   // strcpy(str1, str2)
    printf("str1 = %s\tstr2 = %s\n", str1, str2);
    sprintf(str1, "%s%s", str1, str2);   // strcat(str1, str2)
    printf("str1 = %s\tstr2 = %s\n", str1, str2);
    return 0;
}

在这段代码中,首先读入两个字符串,str1 和 str2。然后使用 sprintf 分别替代 strlen、strcpy 以及 strcat 三个函数的功能。具体如下:

首先,使用 sprintf(str1, “%s”, str1); 代替 strlen(str1) 的功能,正如你所知道的,sprintf 返回值代表输出了多少个字符,这行代码中也就是 str1 字符串中的字符数量。

其次,使用 sprintf(str1, “%s”, str2); 代替 strcpy(str1, str2) 的功能。使用 sprintf 函数,将 str2中的内容,输出到 str1 的存储空间中,其实就相当于把 str2 的内容复制到了 str1 中。

最后,使用sprintf(str1, “%s%s”, str1, str2); 代替 strcat(str1, str2) 的功能。这里,我们将 str1和 str2 的值,依次性的输出到 str1 中以后,str1 的内容,就是原 str1和 str2 内容连接以后的总内容了。

思考题(2):优美的遍历技巧

#include <stdio.h>
int main() {
    char str[1000];
    scanf("%s", str);
    for (int i = 0; str[i]; i++) {
        printf("%c\n", str[i]);
    }
    return 0;
}

这段代码中,最值得思考的是循环的终止条件。当循环条件成立的时候,循环会一直执行,不成立的时候,循环就会终止。那么 str[i] 你可以看成是字符,也可以看成是一个整型值,因为任何信息在底层都是二进制存储的,那么其余字符均为非零值,也就是代表条件成立。

只有一个字符的值是零值,就是我们之前所说的字符串中的最后一个特殊的,看不见的字符,\0 字符,这个字符所对应的整型值就是 0,也就是我们所谓的假值。那么这个循环,就会一直循环到字符串的最后一位,才会停止。

好了,今天的思考题答疑就结束了,如果你还有什么不清楚的,或者有更好的想法的,欢迎告诉我,我们留言区见!

精选留言

  • 罗耀龙@坐忘

    2020-05-16 19:49:00

    茶艺师学编程

    看完了老师的解答,觉得前面的我简直就是蠢得可以······

    第一课:

    思考题1
    我使用了(int)log10(n)+1来算位数,老师直接用printf的返回值就搞定了。
    而且老师使用的printf嵌套printf这一招,太漂亮了;

    思考题2
    我知道scanf碰到空格就会停止读取。但我傻傻的在接水管。老师直接一个[^\n](只要不是换行)就解决了问题了。
    当然这个[]里面不能乱填,我就试过“null”······

    第二课:

    思考题-乘法表
    我这里使用了3个变量,多出来的一个是单独用来控制格式的。但老师直接使用两个变量加一个“||或”判断解决问题,漂亮。

    第四课:

    思考题-设计迷你随机函数

    关键是在随机数的生成方法,我用的是以时间函数为种子,通过99 * rand()/RAND_MAX + 1选取100个随机数。
    而老师使用的是以n=5,n = (n * 3) % 101,算100次。就观感而言,老师的随机感会比我的更明显——我的数字大概率是不会出现“100”。


    第五课:

    关键,是以m为倍数标记数组,再把被标记的数组去掉,进而得出“不能被整除的数”。这样的构思,太妙了。


    第六课:

    在这里我一直在想如何“遍历到结尾”,我找到的方法是“\n”,就是我们在输入后要按一下的回车。
    而老师的判断条件str[i]实在是太漂亮了,字符串的结尾“\0”才是假值——结束循环。
    还有老师的字符串循环,太漂亮了,我得好好学习——在printf里用str[i]就出错······
    作者回复

    (′▽`〃)

    2020-06-15 17:54:33

  • Jinlee

    2020-01-18 12:23:48

    看了老师的答疑,解决了前面不少的困惑。至于那个迷你随机数,是真的不怎么懂。先放着吧,先广后精,嘿嘿。还有,给老师提一个小小建议,就是在示例代码中加入一些文字,这样可读性更强,对初学者更友好。不然面对一个黑框框,初学者容易懵,不知道该干嘛了。去掉倍数那个思考题,自己也实现了,代码如下,但是老师的方法应该应用更加广泛。
    #include <stdio.h>

    int main(){
    int n, m;
    printf("请输入两个数:\n");
    scanf("%d%d", &n, &m);
    int arr[n];
    int i, j;
    for (i = 0; i < n; i++) {
    printf("输入第%d个正整数:\n", i+1);
    scanf("%d", &arr[i]);
    }
    printf("\n");
    for (j = 1; j <= m; j++){
    for (i = 0; i<n; i++){
    if (j % arr[i] == 0) break; //能被n个不同正整数中某个整除则跳出循环,进入if语句
    }
    if (i == n) printf("%d\n", j); // 如果i == n,则说明for循环一直进行到最后j都没有被n个不同正整数中的任意一个整除,此时j符合要求,输出
    }
    return 0;
    }
    作者回复

    好的,你的建议非常好,我会在后面的文章中采纳,你的代码方法也是对的。d(^_^o)

    2020-01-18 20:00:57

  • 1043

    2020-04-03 23:29:07

    思考题感觉确实很不简单,既能熟练所学内容又是重点要掌握的技能,把实际问题转换成变成思维,再转换成编程语言更好办一些,也更深刻一些。容我再思考思考练练再答今天和以前的题目。
    另外想问一下胡老师用哪个发行版的Linux,我也要换,我现在用的是Debian10,安装了gcc如何才能打开呢?没安vim用系统自带的vi是不是也可以啊?百度查了一圈都是打开后的gcc,用什么命令打开gcc和别人的源程序文件呢?
    作者回复

    vim用来编辑程序,vi也可,gcc是用来编译程序的,不是用来打开程序的,编译好后,会在你当前文件夹下生成一个默认名字是a.out的文件,执行即可,执行需要运行命令./a.out,此外如上操作都需要在terminal下进行。我用的是Mac(。ì _ í。)

    2020-04-04 11:54:05

  • WaterZhai

    2020-03-20 21:43:13

    在百度上找的,那个叫线性同余法。
    作者回复

    d(^_^o) 学习习惯很棒!

    2020-03-21 10:24:02

  • 淡蓝色

    2020-03-16 09:43:14

    随机函数的那道题:
    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>
    int main() {
    srand(time(0));
    for(int i=1;i<=10;++i)
    for(int j=1;j<=10;++j)
    {

    printf("%d ",rand()%100);
    if(j==10) printf("\n");
    }

    return 0;
    }

    作者回复

    不对哦,你运行一下,看看其中是不是有重复的数字。

    2020-03-18 02:40:03

  • 信念

    2020-02-05 11:14:00

    第一个位数输出的程序
    第7行的char output[50];
    int ret = sprintf(output, "%d", n)
    没太看懂,ret是什么意思呀?
    作者回复

    ret是个整形变量啊。没什么特殊的。

    2020-02-05 16:50:28

  • 学写代码的猪

    2020-01-18 21:35:25

    渐入佳境,跟着学就是了。
    作者回复

    d(^_^o)

    2020-01-19 10:04:11

  • 学写代码的猪

    2020-01-18 21:05:58

    j == 1 || printf("\t");
    老师这样语句,c 的编译器一定从左往右一个一个条件判断(计算值)吗?
    有可能从右往左或者随机吗?
    作者回复

    对,你上网搜运算符优先级,其中每一个等级的运算符都有一个计算顺序,条件或的计算顺序是从左向右。这是有规定的,跟编译器无关。

    2020-01-19 10:04:04

  • 梅利奥猪猪毛丽莎肉酱

    2020-01-18 02:27:06

    其实有挺多东西想说的,首先我的确是七大罪动漫爱好者,虽然现在动画有点ppt了,哈哈扯远了,先说那个乘法表,那个或的知识应该是短路或者惰性的知识吧,我是个前端工程师,有时候就会写到条件&&函数(),这个其实就是条件为真则执行函数,然后还看到过个类似的基础题,true||true&&false答案是true,新手可能直接回答错,从左往右看,认为是false,稍微厉害点的,看优先级,然后得出答案是true,其实这边在多想一步,优先级先计算了右边的true&&false,但true||任何其他什么,肯定就是true,希望自己理解是对的,至于后面个迷你随机数,真的难,哈哈刚看了下互质和欧拉,勉强看懂,有种被要被劝退的感觉,关于随机数随机种子这个其实挺有兴趣的,兴趣来源于一个例子,叫做游戏里随机的场景,用户中途退了,下次进入场景和之前退出的游戏场景一致,就是用了随机种子,然后那个例子实现这个随机函数,用了三个很奇怪的数字,这个当时看也没有看的特别懂,总结就是虽然不知道怎么回事,但总觉得很厉害,学无止境,还是要持续的坚持学习~
    作者回复

    d(^_^o)

    2020-01-18 11:08:43

  • 阿阳

    2023-02-05 10:52:41

    思考题难度其实都不小,确实是学习到了很多以前不知道的内容,原来C语言这么优雅和强大。
  • 小风

    2020-05-04 11:20:55

    体验利器那道题,实现strlen功能的那个,怎么让他输出字符长度呢,请老师解答
    作者回复

    你可以用 printf 输出 sprintf 的返回值啊。
    另外,你可以将 sprintf(str1, "%s", str1); 就看成是 strlen(str1); 的另一种代码表示方式,strlen(str1) 怎么用,sprintf(str1, "%s", str1); 就怎么用。

    2020-05-05 14:22:17