<audio id="audio" title="做好闭环（二）：函数是压缩的数组，数组是展开的函数" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/09/ef/092082774d421733dd71d3c0c5d927ef.mp3"></audio>

你好，我是胡光。

不知不觉，我们已经学完了语言基础篇的全部内容。其实还有很多东西想给你讲，可限于篇幅，所以咱们整个语言基础篇中的内容，都是那些，我认为你自学容易忽视的，容易学错的知识点。有道是，授之以鱼，不如授之以渔，我也相信只要你跟着课程学习，一定会感觉到自己收获到了“渔具”。如果能引发你的主动思考，进而触类旁通，举一反三，那这场学习过程就更加有意义啦。

我也非常高兴，看到很多同学都在紧跟着专栏更新节奏，坚持学习。每每在专栏上线的第一时间，这些同学就给我留言，提出自己的疑惑。大部分留言，我都在相对应的文章中回复过了，而对于文章中的思考题呢，由于要给你充足的思考时间，所以我选择在今天这样一篇文章中，给你进行一一的解答。

看一看我的参考答案，和你的思考结果之间，有什么不同吧。也欢迎你在留言区中，给出一些你感兴趣的题目的思考结果，我希望我们能在这个过程中，碰撞出更多智慧的火花。

## 函数：自己动手实现低配版 scanf 函数

在这一节里面呢[《函数：自己动手实现低配版 scanf 函数》](https://time.geekbang.org/column/article/192053)，我们讲了函数的基本概念，明确了“实参”和“形参”两个概念，并且知道了函数传参的过程，就是“实参”给“形参”赋值的过程。

还有，我们介绍了“传入参数”和“传出参数”两个概念，弄懂这两个概念，对于设计一个函数来说，还是非常重要的。“传入参数”是从外部，传入到函数内部，影响函数内部执行逻辑的参数，“传出参数”呢，就是由函数内部，传出到函数外部的参数，一般是以传送地址为主要形式。

最后呢，我留了两个开放性的思考题，我选择其中一个你可能会不知所措的题目，来讲解一下如何完成这个题目。下面就看看我的答案吧。

#### 思考题（1）：数组和函数的思考

关于这个问题呢，我们首先来具象化一下，我们设想一种具体的问题情况，比如说：arr 数组里面，arr[i] 存储的是 2 * i 的值，你可以认为是第 i 个偶数的值；func 函数的功能呢，func(x) = 2*x，就是计算得到第 x 个偶数的值。如下述示意代码所示：

```
int arr[100] = {0, 2, 4, 6, 8, 10, ...}; 
int func(int x) {
    return 2 * x;
}

```

解析这个示例代码，我们先从数组 arr 和函数 func 的不同处开始说起。

很明显，两者的本质不一样，arr 是数组，对于代码 arr[36]， 相当于访问数组下标 36 的位置中存储的值；而 func 是函数，对于代码 func(36) 来说，也会得到一个整型值，但是这个整型值，却是由 func 的函数逻辑代码计算得到的。简单来说，就是对于 arr 中的值进行访问，是一个静态的过程，而对于 func 函数得到返回值的过程来说，是一个动态计算的过程。

我们再来看看两者的时间和空间效率，也就是代码运行速度以及所需要的存储空间之间的比较。

- 关于时间效率方面，通常来说是数组访问速度要比函数计算速度快很多。
- 而空间效率的话，函数通常要比数组节省很多存储空间，就像 func 函数的值，是动态计算得到的，通常情况下，不管我们代码中执行 func(100) 还是 func(10000)，我们不需要修改函数的代码。但对于 arr 数组来说，当我们需要访问 arr[100] 的时候，数组最起码要有 101 个元素空间，而当我们想要访问 arr[10000] 的时候，数组最起码要有 10001 个元素空间。总的来说，就是函数比数组更加节省空间，数组比函数呢，得到结果的速度更快。

说完二者的不同以后，我们再来看看二者的相同之处。

站在使用者的角度来看，当你盯着 arr[100] 和 func(100) 这两段代码看的时候，你没觉得这两个代码的异常的相似？func 和 arr 就是名字不一样，如果这个时候我将 func 后面的小括号换成中括号，你是不是就会觉得 func 是一个数组？

对！你可能发现了，在使用者看来，func(100) 和 arr[100] 的作用是完全一样的，区别可能只是中括号和小括号的区别。你不觉得站在使用者的角度，考虑这个问题很有趣么？本质区别很大的两个东西，一个函数，一个数组，突然发现它俩的区别根本没有那么大。

简单来说，就是在数学里，函数做的事情就是“映射”，传入一个值，传出一个值。在程序中也不例外，函数做的事情，就是从传入值到传出值的映射。而数组做的事情呢，其实是从下标到存储值的映射。你会发现，数组和函数做的事情，本质上都是映射！

最后，我来总结一下，这个总结讲对你日后的程序设计思维有巨大的帮助，这句话就是“**函数是压缩的数组，数组是展开的函数**”，也就是说当你可以用数组进行程序的时候，你也可以使用某个能够完成相同映射功能的函数来进行替代。

二者在程序设计方面的差别，就在于时间和空间的使用效率，数组在时间效率方面占优势，函数在空间效率方面占优势。当你理解了这些事情以后，你就可以更好的理解某些资料里面经常讲的“**时间换空间**”或者“**空间换时间**”的概念了。你现在可以简单的理解成为是数组思维和函数思维之间的互相转换。

## 预处理命令：必须掌握的“黑魔法”，让编译器帮你写代码

关于预处理命令这个知识点，我们用了两节课的篇幅来讲解，[《预处理命令（上）：必须掌握的“黑魔法”，让编译器帮你写代码》](https://time.geekbang.org/column/article/192060) 和[《预处理命令（下）：必须掌握的“黑魔法”，让编译器帮你写代码》](https://time.geekbang.org/column/article/193126)。其中讲了两种使用比较多的预处理命令，宏定义和条件编译。并且强调了，宏定义就是做简单替换，条件编译做的事情，就是代码剪裁，根据条件是否成立，决定哪段代码最终留在“待编译源码”中。

其中，用户 @一步 问到：有没有什么办法可以看到预处理阶段后的待编译源代码的内容？这个应该是很多小伙伴的共同问题吧，在这里我就来讲一下。

在 Linux/Mac 的编程环境下呢，操作比较简单，原本的程序编译命令是 gcc 加源文件名，如果你想看到待编译源码的内容，你只需要在中间加一个 -E 编译选项即可，例如：gcc -E test.c。如果你用的是集成开发环境，那你就需要自己搜索解决办法了，你可以搜索关键词如：XXX 下如何查看宏展开内容。XXX 就代表了你的集成开发环境。

对于课后的思考题，这里必须为用户@Geek_Andy_Lee00 和 用户 @Aaren Shan 的回答点赞。答案虽然不是很完美，可我想说，答案不重要，重要的是思考过程。 下面就来看看我给出的参考答案吧。

#### 思考题：没有 Bug 的 MAX 宏

就像之前所说的，对于这个问题呢，能否满分通过，是不重要的，重要的是你在解决这个问题过程中遇到的一个又一个 Bug，以及你对于这些 Bug 的思考过程。下面我就将带你一步一步地解决，这个问题中，你可能遇到的几个典型的 Bug，以及解决办法。

首先，让我们先对样例输出的每一行编上序号，如下所示：

```
①：MAX(2, 3) = 3
②：5 + MAX(2, 3) = 8
③：MAX(2, MAX(3, 4)) = 4
④：MAX(2, 3 &gt; 4 ? 3 : 4) = 4
⑤：MAX(a++, 5) = 6
⑥：a = 7

```

我们先来实现一个最简单的 MAX 宏，如下所示：

```
#define MAX(a, b) a &gt; b ? a : b

```

如上所示，MAX 宏的实现，利用了三目运算符，问号冒号表达式，a &gt; b 条件如果成立，表达式的值等于 a 的值，否则等于 b 的值。看似没问题，但如果你要是运行代码，你会发现，程序的输出可能会如下所示：

```
MAX(2, 3) = 3
❌5 + MAX(2, 3) = 2
❌MAX(2, MAX(3, 4)) = 2
❌MAX(2, 3 &gt; 4 ? 3 : 4) = 2
❌MAX(a++, 5) = 7
❌a = 8

```

你会发现，这种实现，只有第一行是对的，其余几行都是错的。我们就来首先分析一下第 3 行到底是什么错误。按照宏展开的替换原则，最外层的 MAX 宏会被替换成：2 &gt; MAX(3, 4) ? 2 : MAX(3, 4)。然后我们再将里面的 MAX(3, 4) 宏展开，就变成了：

```
2 &gt; 3 &gt; 4 ? 3 : 4 ? 2 : 3 &gt; 4 ? 3 : 4

```

这段表达式代码，看着有点儿乱，别急，我来帮你分析。首先我们从左向右看，先分离出来第一个问号冒号表达式的结构：

```
(2 &gt; 3 &gt; 4) ? (3) : (4 ? 2 : 3 &gt; 4 ? 3 : 4)

```

我们看到在这个里面，第一部分 2 &gt; 3 &gt; 4 是条件；第二部分 3 是在表达式为真时候的返回值；第三部分，是包含两个问号冒号表达式结构的式子。我们继续对第三部分进行拆解：

```
(4) ? (2) : (3 &gt; 4 ? 3 : 4)

```

继续拆解后，我们发现，第一部分 4 是条件；第二部分的 2 是表达式为真时的返回值；第三部分，就是一个单独的问号冒号表达式。拆解到现在为止，已经不需要再继续拆解了。

要想理解原表达式，我们需要先了解 2 &gt; 3 &gt; 4 这个“惨无人道”的表达式的值。这个表达式，从左向右执行，首先执行 2 &gt; 3 这个条件表达式的判断。之前我们讲过，条件表达式的值，只有 1 和 0，那么 2 &gt; 3 这个表达式的值，显然是 0，之后其实是在做 0 &gt; 4 的判断，结果也是 0。

所以 2 &gt; 3 &gt; 4 这个表达式的值，就是 0，也就是假值，代表条件不成立，之后的事情，就是转到了两个问号冒号表达式的部分，剩下的事情，你自己就可以理解，最终原表达式的值为什么是 2 了。

理解了原表达式值计算的原理以后，下面让我们来分析一下，为什么会出现这种问题。本质原因，就在于我们实现的宏中，参数 a，b 原本都是独立的表达式部分，而我们却简单的把它们放到问号冒号表达式中，导致展开以后的内容前后连接到一起后，改变了原本我们想要的计算顺序。

所以针对这种情况，我们在实现宏的时候，可以给每个参数部分，都加上一个括号，就变成了如下所示的实现方式：

```
#define MAX(a, b) (a) &gt; (b) ? (a) : (b)

```

至此，你就会得到如下的输出：

```
MAX(2, 3) = 3
❌5 + MAX(2, 3) = 2
MAX(2, MAX(3, 4)) = 4
MAX(2, 3 &gt; 4 ? 3 : 4) = 4
❌MAX(a++, 5) = 7
❌a = 8

```

在这份输出中，第 1 行、第 3 行、第 4 行均已正确。如果你自己，仿照我上面说的方式对第二行内容的输出，加以分析，你一定可以知道如何修正第2行的结果错误。如果你努力以后，还是想不到的话，可以参考用户 @Aaren Shan 的留言。这样做以后呢，你程序的输出，就会变成如下输出：

```
MAX(2, 3) = 3
5 + MAX(2, 3) = 8
MAX(2, MAX(3, 4)) = 4
MAX(2, 3 &gt; 4 ? 3 : 4) = 4
❌MAX(a++, 5) = 7
❌a = 8

```

其中还是有两行是错误的，你如果试着展开第 5 行的宏，你会得到如下的代码：

```
a++ &gt; 5 ? a++ : 5

```

在这行代码中，如果 a++表达式的值真的大于 5 的话，那么 a++ 就会被执行两次。而原本使用者的意图，是执行一次 a++，如何让 a++ 只执行一次呢？这需要用到之前我跟你提过的__typeof 相关的技巧了，下面是我给你准备的参考代码：

```
#define MAX(a, b) ({ \
    __typeof(a) __a = (a), __b = (b); \
    __a &gt; __b ? __a : __b; \
})

```

在这段代码中，我们定义了两个中间变量，__a 和 __b 用来存储宏参数中 a 和 b 部分的原本的值，之后判断大小的部分呢，我们使用新的变量 __a 和 __b 即可。

这段代码中，我们看到了，无论是 a 部分，还是 b 部分的表达式，只被使用了一次，也就保证了只被计算了一次。而这个里面，用小括号括起来了大括号，实际作用是把一个代码段，变成一个表达式。根据任何表达式都有值的特性，这个代码段表达式的值，等于其中最后一行代码表达式的值，也就是等于最后那行问号冒号表达式的值。

第6行的错误，其实和第5行的一样，解决了第5行的错误，这一行的Bug也就解了。

至此，我们就几乎完美地解决了 MAX 宏的问题了。通过这个问题，你会看到，预处理命令虽然强大，可你需要拥有掌握这种强大的力量。这份力量，包括了你的基础知识储量，还包括了你严谨的思维逻辑。

想要掌握强大，必先变得强大，记住，一步一个脚印，才是最快、最靠谱的成长路线，学习过程中，没有捷径。

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