gitbook/人人都能学会的编程入门课/docs/190609.md
2022-09-03 22:05:03 +08:00

201 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 做好闭环(一):不看答案可能就白学了
你好,我是胡光。
不知不觉,语言基础篇已学习过半,我非常高兴,看到很多同学都在坚持学习。并且,还有一些同学,每每都能在专栏上线的第一时间里,给我留言,提出疑惑。当面对一些知识点的时候,如果在我的观念中它是不说自明,而对于新手的你来说,可能十分难理解的时候,我也很希望你能指出来,我会在留言区中给你解答的。因为,我知道这种讨论,肯定能够帮助到更多的人。
大部分留言,我都在相对应的文章中回复过了,而对于文章中的思考题呢,由于要给你留足思考时间,所以我选择,一起留在今天这样一篇文章中,给你进行一一的解答。
看一看我的参考答案,和你的思考结果之间,有什么不同吧。也欢迎你在留言区中,给出一些你感兴趣的题目的思考结果,我希望我们能在这个过程中,碰撞出更多智慧的火花。在这里呢,@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也就是我们所谓的假值。那么这个循环就会一直循环到字符串的最后一位才会停止。
好了,今天的思考题答疑就结束了,如果你还有什么不清楚的,或者有更好的想法的,欢迎告诉我,我们留言区见!