201 lines
13 KiB
Markdown
201 lines
13 KiB
Markdown
# 做好闭环(一):不看答案可能就白学了
|
||
|
||
你好,我是胡光。
|
||
|
||
不知不觉,语言基础篇已学习过半,我非常高兴,看到很多同学都在坚持学习。并且,还有一些同学,每每都能在专栏上线的第一时间里,给我留言,提出疑惑。当面对一些知识点的时候,如果在我的观念中它是不说自明,而对于新手的你来说,可能十分难理解的时候,我也很希望你能指出来,我会在留言区中给你解答的。因为,我知道这种讨论,肯定能够帮助到更多的人。
|
||
|
||
大部分留言,我都在相对应的文章中回复过了,而对于文章中的思考题呢,由于要给你留足思考时间,所以我选择,一起留在今天这样一篇文章中,给你进行一一的解答。
|
||
|
||
看一看我的参考答案,和你的思考结果之间,有什么不同吧。也欢迎你在留言区中,给出一些你感兴趣的题目的思考结果,我希望我们能在这个过程中,碰撞出更多智慧的火花。在这里呢,@rocedu 用户在第一篇留言区中给大家推荐的《程序设计实践》一书,也是非常优秀的书籍。有兴趣的小伙伴,也可以去到他提到的豆瓣读书主页中去游览一番。
|
||
|
||
## 第一个程序:教你输出彩色的文字
|
||
|
||
在这一篇里面呢,我们接触了如何在 Linux 环境下输出彩色文字的编程知识。初步学习了 scanf 和 printf 函数的基础用法,两者一个负责读入,一个负责输出。如果你对这篇文章的内容有点陌生,可以再回去看看[《第一个程序:教你输出彩色的文字》](https://time.geekbang.org/column/article/186076)。最后围绕着这两个函数,给你出了两个思考题。这两个思考题做的怎么样?下面来看看我的参考答案吧。
|
||
|
||
#### 思考题(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”,看看程序的输出,你就能明白了。
|
||
|
||
## 判断与循环:给你的程序加上处理逻辑
|
||
|
||
在这篇文章[《判断与循环:给你的程序加上处理逻辑》](http://time.geekbang.org/column/article/185667)中呢,我们学习了除了顺序结构以外的两种程序执行结构:分支结构和循环结构。知识点的话,主要涉及:**条件表达式、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") 代码,这也就实现了题目中的要求。
|
||
|
||
## 随机函数:随机实验真的可以算 π 值嘛?
|
||
|
||
这一篇文章里面[《随机函数:随机实验真的可以算 π 值嘛?》](https://time.geekbang.org/column/article/187287),我们介绍了程序里面随机函数的基本原理,说明了“真随机”和“伪随机”的本质区别。看了一些留言以后,我来给你总结一下,所谓“真随机”与“假随机”,只要你不太清楚下一个产生的值是什么,那么对于你来说,就是随机的,而“真”或者“假”,讨论的是随机方法的本质。如果随机过程可以保证,下一次产生的每个值都有一定的概率,那么这个就是“真随机”,如果不能保证,那就是“伪随机”。
|
||
|
||
理解程序中的“伪随机”,你需要在你的脑袋中,构建一个由值组成的环形序列图,设置随机种子,就是选择图中的某个点作为起始点,在我们一次次地获得随机值的过程中,其实程序就是依次地输出了这个环形序列中的每个状态的值。
|
||
|
||
最后呢,给你留了一个设计随机函数过程的思考题,关于这个思考题,我要提前先跟你道歉,因为这个思考题,并不是想让你做出来的。下面来看看我的参考答案吧。
|
||
|
||
#### 思考题:设计迷你随机函数
|
||
|
||
```
|
||
#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 个变量》](https://time.geekbang.org/column/article/188612)。
|
||
|
||
最后让我们来看看这篇文章的参考答案吧。
|
||
|
||
#### 思考题:去掉倍数
|
||
|
||
```
|
||
#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 的高级用法》](https://time.geekbang.org/column/article/189458)的文章中,我们认识了 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,也就是我们所谓的假值。那么这个循环,就会一直循环到字符串的最后一位,才会停止。
|
||
|
||
好了,今天的思考题答疑就结束了,如果你还有什么不清楚的,或者有更好的想法的,欢迎告诉我,我们留言区见!
|
||
|