Lisp 程序代码与数据的形式完全相同,这使得它非常强大,能完成许多其他语言不能完成的事情。为了拥有这个强大的特性,我们需要将求值过程分为读取并存储输入、对输入进行求值两个过程。
本章结束后,程序的运行结果会和上一章的有轻微的不同。这是因为我们会花时间去更改程序内部的工作方式。在软件开发中,这被叫做重构。重构可能对于当前的程序运行结果并没有太大的影响,但因为工作方式的优化,会使我们在后面的开发中更加省心。
为了存储输入,我们需要创建一个内部列表结构,能够递归的表示数字、操作符号、其他的列表。在 Lisp 中,这个结构被称为 S-表达式(Symbolic Expression)。我们将扩展 lval
结构来表示它。S-表达式求值也是典型的 Lisp 式过程:首先取列表第一个元素为操作符,然后遍历所有剩下的元素,作为操作数。
有了 S-表达式,我们才算真正迈进了 Lisp 的大门。
在 C 语言中,要表示列表,就必须正确的使用指针。C 语言中的指针一直如洪水猛兽般存在。虽然概念上非常简单,但是用起来却变幻多端,神秘莫测,这使得指针看上去比实际要可怕得多。幸运的是,在本书中我们只会用一些指针在 C 语言中最常规的用法。
我们之所以需要指针,主要是由 C 语言中函数的工作方式决定的。C 语言函数的参数都是通过值传递的。也就是说,传递给函数的是实参的拷贝。对于 int
、long
、char
等系统类型以及用户自定义的结构体都是成立的。这种方式适用于绝大多数情况,但也会偶尔出现问题。
一种常见的情况是,当我们有一个巨大结构体需要作为参数传递的时候,每次调用函数,就会对实参进行一次拷贝,这无疑是对性能和内存的浪费。
另外一个问题是,结构体的大小终究是有限的,只能是个固定的大小。而如果我们想向函数传递一组数据,而且数组的总数还是不固定的,结构体就明显的无能为力了。
为了解决这个问题,C 语言的开发者们想出了一个聪明的办法。他们把内存想象成一个巨大的字节数组,每个字节都可以拥有一个全局的索引值。这有点像门牌号:第一个字节索引为 0,第二个字节索引为 1 等等。
在这种情况下,计算机中的所有数据,包括当前运行的程序中的结构体、变量都有相应的索引值与其对应(数据的开始字节的索引作为整个数据的索引)。所以,除了将数据本身拷贝到函数参数,我们还可以只拷贝数据的索引值。在函数内部则可以根据索引值找到需要的数据本身(译者注:我们将这个索引值称为地址,存储地址的变量称为指针)。使用指针,函数可以修改指定位置的内存而无需拷贝。除此之外,指针还可以做其他很多事情。
因为计算机内存的大小是固定的,表示一个地址所需要的字节数也是固定的。但是地址指向的内存的字节数是可以变化的。这就意味着,我们可以创建一个大小可变的数据结构,并将其指针传入函数,对其进行读取及修改。
所以,所谓的指针也仅仅是一个数字而已。是内存中的一块数据的开始字节的索引值。指针的类型用来提示程序员和编译器指针指向的是一块什么样的数据。
指针类型是在现有类型的后面加一个星号组成,我们之前已经见过一些指针的示例了,如:mpc_parser_t*
、mpc_ast_t*
以及 char*
。
要创建指针,我们就需要获取数据的地址。C 语言提供了取地址符(&
)来获取某个数据的地址。在前面的章节中,我们也曾传给过 mpc_parse
函数一个指针,以便其能将输出放到我们声明的 mpc_result_t
变量中。
最后,为了获取指针所指向的地址的数据值(称为解引用),我们需要在指针左边使用 *
操作符。要获取结构体指针的某个字段,需要使用 ->
操作符,而不是 .
,这你在第七章已经见过了。
前面说过,我们可以把内存简单粗暴地想象成一个巨大的字节数组。事实上,它被更加合理地划分成了两部分,即栈和堆。
有些人可能已经听说过一些关于堆和栈的神秘传说,例如“栈从上往下增长,而堆则是从下往上”,或是“栈的数量很多,但堆只有一个”云云。其实这些事情都是无关紧要的。在 C 语言中,处理好栈和堆确实是件麻烦的事情,但这并不代表它们很神秘。实际上,它们只是内存中的两块不同的区域,分别用来完成不同的任务而已。