> For the complete documentation index, see [llms.txt](https://overleaf-pro.ayaka.space/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://overleaf-pro.ayaka.space/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md).

# TeX 宏究竟是如何工作的：第6部分

[第1部分](/latex/zh-cn/geng-duo-zhu-ti/19-how-tex-macros-actually-work-part-1.md) [第2部分](/latex/zh-cn/geng-duo-zhu-ti/20-how-tex-macros-actually-work-part-2.md) [第3部分](/latex/zh-cn/geng-duo-zhu-ti/21-how-tex-macros-actually-work-part-3.md) [第4部分](/latex/zh-cn/geng-duo-zhu-ti/22-how-tex-macros-actually-work-part-4.md) [第5部分](/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.md) [第6部分](/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md)

## 引言与概览：到目前为止的故事

在本系列前 5 部分中，我们已经看到：

* TeX 如何读取输入文件中的字符，并使用类别码识别不同“类别”的字符，随后将它们转换为字符标记和命令标记；
* 一个宏实际上由四个部分组成：

```
<TeX 宏原语><宏名><参数文本>{<替换文本>}
```

其中：

* `<TeX 宏原语>` = 其中之一 `\def`, `\edef`, `\gdef` 或 `\xdef`;
* `<宏名>`= 你的宏的名称，例如 `\\foo`;
* `<参数文本>` 可以为“空”（不存在），也可以是一串分隔符记号和宏参数记号；
* `<替换文本>` 是宏的实际主体：当你调用宏时，会“执行”（展开）的部分。
* 如何 `<参数文本>` 部分可以包含广泛的标记，且 TeX 将此部分用作“标记模板”，把宏调用与其原始定义进行匹配，并确定该宏所使用的参数——以及 TeX 如何期望你对宏的使用与其原始定义相匹配；
* 在 TeX 内部，一个宏定义被存储为连续的标记序列，表示 `<参数文本>` 和 `<替换文本>` 各部分。

当你使用一个宏命令时，TeX 会先检查它是否带有参数。如果有，TeX 就必须识别实际的 *参数* 在你的宏调用中所使用的参数。TeX 必须将你的宏调用与其存储在内存中的“标记模板”定义进行测试。具体来说，TeX 使用你宏的内部（已存储）定义中的 `<参数文本>` 部分作为模板，通过它可以挑出 *记号* 这些才是真正的参数，而哪些 *记号* 只是作为分隔符。

## 宏展开的含义

现在，我们终于准备进入最重要的主题：TeX 如何处理宏参数并实际执行宏：TeX 将这一过程称为 *宏展开*.

### 但首先，来看一个简短的例子：有点奇怪？

为了“铺垫”TeX 处理宏及其参数的机制，我们将用一个简短的例子来说明需要考虑的问题。

#### 参数首先被转换为标记

下面的例子基于以下书中第 114–115 页讨论的一个例子： [The Advanced TeXbook](https://www.amazon.co.uk/Advanced-Texbook-David-Salomon/dp/0387945563) David Salomon 所著。之所以选它，是因为它在一个非常短的 TeX 宏中很好地概括了核心思想。

在正常的 TeX/LaTeX 操作中， `$` 符号的类别码为 3（“数学切换”），它会让 TeX 进入/退出行内数学模式（`$...$`）或显示数学模式（`$$...$$`）——当然，LaTeX 使用 `\(..\)` 和 `\[..\]` 来实现相同目的。

假设我们想要一个宏，它将一个 `$` 符号的类别码改为例如 11，这样我们就能像排版普通字符一样排版它。我们可以使用 TeX 原始命令 `\catcode` 而我们对此类宏的第一次尝试， `\docat`，可能是

```
\def\docat #1{\catcode`\$=11 #1}
```

然而，当我们试着这样使用它时

```
\begin{document}
\def\docat #1{\catcode`\$=11 #1}
我为那本书付了 \docat{$90}。
\end{document}
```

我们期望 TeX 排版为 `我为那本书付了 90 美元。` 但它却报错：

```
! 缺少插入的 $。
<插入的文本>
                $
<重新读取>
                   \par
l.7
```

从这个错误来看， `$` 用于我们宏参数中的仍然在触发 TeX 排版数学模式；显然，TeX *并没有* 更改 `$` 用于我们宏参数中的`$90`)。问题是 *为什么* 为什么 TeX 没有把 `$` 改为 11 并将其排版为普通字符？简短的回答是，TeX 会先把宏参数转换为标记 **在……之前** 然后再将它们送入 `<替换文本>`的标记列表中——不过我们会更详细地查看其底层机制。

我们需要记住的是，我们对 TeX 使用文本/字符的理解，只与 TeX 正在读取的文件内容有关：一旦 TeX 读入任何字符，我们就进入了 *记号*。TeX 宏调用处理的是 *记号*，而不是实际的 *书写/文本表示形式* 的 TeX/LaTeX 命令——随着我们继续这个例子，这一点会变得更清楚。

起初，我们可能会认为我们使用 `\docat` 宏在 `我为那本书付了 \docat{$90}。` 中与直接编写等效的 TeX（或 LaTeX）代码相同——例如下面这句，它 *会* 可以工作：

```
\begin{document}
我为那本书付了 \catcode`\$=11 $90。
\end{document}
```

![在 Overleaf 上运行的一些 TeX 代码](/files/418530a862ae283ea08da545eaad4fe8149f9be0)

然而，正如我们上面看到的，TeX 处理宏参数的方式会产生一个结果（`! 缺少插入的 $。`），它与直接写出 TeX 代码大不相同：现在我们来探究 *为什么* 其发生机制。

### 作为标记列表的宏和参数

要完全理解 `\docat` 宏及其参数（`$90`），以及它为何失败，我们再次需要把 `\docat` 宏的定义以及所使用的任何参数（当调用 \docat 时）想象成 *标记列表*，而不是字符序列。

当 TeX 扫描你的输入文本时，它会识别 `\docat` 为一个宏命令；之后它会检查它是否带有参数——TeX 如何做到这一点，将在下一节为有兴趣了解细节的读者解释。

#### 对于喜欢细节的读者……

在宏被调用后，TeX 会检查（在宏存储的定义标记列表中）第一个标记是否是 **匹配结束** 标记：如果是这样，TeX 就可以确定该宏不带任何参数。

**一个例子**

下面的节点列表图比较两个宏的标记列表：

* `\def\foo A#1B{#1}`：它包含 `<参数文本>` 的一个记号值 `A#1B`，因此 **匹配结束** 标记是 **不** 第一个标记，因此 TeX 会继续寻找参数；
* `\def\foo{X}`：它没有 `<参数文本>` 部分，因此 **匹配结束** 标记是标记列表中的第一个，TeX 知道不必寻找任何参数。

![TeX 如何检查一个宏是否带有参数](/files/6307de6020ab476db1568889c1ae25220cf12600)

## 走向“终场”：展开

让我们回顾一下这个问题：为什么下面的宏没有起作用；也就是说，为什么 TeX 不会更改任何 `$` 符号在以下宏的参数中使用时的 `\docat` 宏，例如 `\docat{$90}`?

```
\begin{document}
\def\docat #1{\catcode`\$=11 #1}
我为那本书付了 \docat{$90}。
\end{document}
```

如上所述，当 TeX 扫描你的输入并识别出一个宏命令——在 TeX 即将执行它的时候——TeX 会先检查该宏是否带有任何参数。如果有，TeX 还需要进一步扫描输入文件，以识别实际的 *参数* 用户为这次特定宏调用提供的；TeX 必须这样做 **在……之前** 它才能调用实际的宏代码。显然，TeX 需要确定用户想向宏提供的数据。

为了识别输入中出现的参数（用户的宏调用），TeX 会以该宏内部存储的定义为依据：具体来说，是 `<参数文本>` 存储的宏定义（标记列表）中的部分——它提供了一种“标记模板”。利用该“标记模板”，TeX 必须判断哪些 *记号* 在用户的宏调用中只是 *定界符* （本质上是“标点”），以及哪些 *记号* 构成一个 *参数*。当 TeX 遇到一个 **匹配参数** 在存储的宏定义中的标记时 `<参数文本>` 部分（“标记模板”）时，它就知道要开始形成一个 *标记列表* 用于该特定参数。

一旦 TeX 认识到需要识别用户的参数，它就会扫描输入以生成标记，并逐个标记地非常仔细地将它们与存储的宏定义进行检查。TeX 会继续收集某个参数的标记，直到它检测到一个实际上是分隔符的标记，或者直到它检测到 **匹配结束** 标记：无论哪种情况，TeX 都知道该停止寻找构成该参数一部分的标记了。

### \docat 宏为何失败

如前所述， *在……之前* TeX 要实际调用一个宏，必须先识别并准备要与该宏一起使用的任何参数。然而，为了识别参数并准备将其传入宏，TeX 必须把每个参数生成成一个 *标记列表*：这就是 `\docat`失败的原因。

在我们的例子中，我们提供了 `\docat` 给它一个参数 `$90` 但该参数首先会 *转换为一个标记列表* ，因为 TeX 在扫描宏调用时——参数会在 *在……之前* 宏真正被调用之前就转换为标记。这里，对于参数 `$90`，TeX 会生成三个字符标记：每个 `$`, `9` 和 `0`.

下面的图示显示了为参数生成的标记列表 `$90`，在送入 `\docat` 的预期用法：

![为宏参数生成的 TeX 标记列表](/files/c5782babf0b68ff5210bedcb6ee671801beccbda)

在上面的图示中，我们可以清楚地看到，参数标记列表包含 `$` ，它作为一个基于类别码 3 的字符标记。

正如我们在第 1 到第 3 部分中看到的，字符标记是根据类别码值创建的 *，即在字符被读入时所处的操作状态*——也就是说，在参数的标记列表被创建（转换为标记）时。当地参数正在被标记化时， `\docat` 宏尚未执行，因此我们在宏调用中放置的类别码更改（``\catcode`\$=11``) *不会* 不会影响用于生成参数标记的类别码。

一旦 TeX 生成了一个表示以下内容的标记列表： `$90`，这三个字符标记就会被送入实际的宏 `<替换文本>`。然而，这会导致 `$` 被输入为一个 *。通过计算这个会传递给 TeX 内部处理的记号值，该特定* 使用类别码 3 创建的标记：即“数学开启”，而且我们已经看到，一旦字符标记形成，其附带的类别码就是永久性的。 `$` 是 *不* 被送入宏中的 *字符*，而是作为一个 *。通过计算这个会传递给 TeX 内部处理的记号值，该特定* 标记 `$` 基于

### 运行 \docat：宏展开

TeX 将“执行”一个宏的过程称为 *宏展开*；在作者看来，这个术语有点令人困惑，但它是公认的术语，所以我们继续使用它。

#### 宏展开的真正含义

在 TeX 检测到 `\docat` 命令后，它会扫描参数并为其参数生成一个标记列表(`$90`)。为了执行（展开）该宏，TeX 会把注意力从用户的输入文件转移开，并开始读取其中包含的标记 `\docat`的 `<替换文本>` 存储在 TeX 内存中的标记列表。

当 TeX 处理 `\docat`的定义时，它随后会看到并执行最初用于定义该宏的一系列标记（`**catcode**`, ``**`**``, `**\$**`, `**=**`, `**1**`, `**1**`, `**#1**`).

下面的图示展示了展开 `\docat` 宏的过程：TeX 停止从输入文件获取标记，转而开始从 `<替换文本>` 部分读取标记 `\docat` 存储在内存中的宏定义。TeX 继续执行这些预先准备好的标记，直到它看到一个 **输出参数** 该标记会指示 TeX 在此处读取（“注入”）并“执行”参数标记。在我们的例子中，那就是表示 `$90` ，这会导致错误，因为预先准备好的 `$` 的字符标记的类别码为 3。由于我们处理的是字符标记，而不是字符， `$` 不受之前由以下标记引起的类别码更改影响 ``\catcode`\$=11``.

![显示展开 \docat 宏的过程](/files/f82a898577b206a636c7a67c401d8b405e37449e)

在 TeX 处理完表示 ``\catcode`\$=11``的标记后， `$` 对它的类别码更改现在就会生效。然后 TeX 遇到一个名为 **输出参数** 的“特殊标记”，它告诉 TeX **插入** 该参数的标记列表。然而，该标记列表包含三个字符 *记号*，其中第一个是一个 `$` ，其类别码被赋予 3（“数学开启”）：宏内部先前的类别码更改无法影响这个字符标记，因此 TeX 将该标记视为开始数学处理的信号，这就导致宏失败。

### \docat 宏能修好吗？

从上面的讨论可以清楚地看出，宏参数中出现的任何字符都会使用标记化发生时生效的类别码进行标记化——在我们的例子中，这个时机是 *总是* 在 `<替换文本>` 的 `\docat` 宏真正执行之前。那么，我们怎样才能确保宏参数的类别码被更改呢？

一种方法是修改 `\docat` 使其成为一个无参数宏，它只负责更改类别码——它没有任何需要标记化的参数。然后我们再使用第二个宏， `\getarg`，它接受一个参数，并安排让该宏在适当的类别码 `$` 生效时对其参数进行标记化。

```
\begin{document}
\def\docat{\catcode`\$=11 \getarg} % 无参数，调用第二个宏 \getarg
\def\getarg#1{#1} % 1 个参数，其参数将被标记化
现在你可以这样运行，它就会生效：

我为那本书付了 \docat{$90}。
\end{document}
```

当我们使用新版 `\docat` （像这样 `\docat{$90}`）时，它 *看起来* 就像 `$90` 仍然被用作以下宏的参数 `\docat` 宏。然而，正如上面所讨论的，当 TeX 检测到 `\docat` 输入中的它时，会检查它是否带有任何参数：现在它没有参数，所以 TeX 继续执行（展开）它。 `\docat` 的展开结果是一串标记 `**catcode**`, ``**`**``, `**\$**`, `**=**`, `**1**`, `**1**`, `**空格**`, `**getarg**` 而这一切发生在 *在……之前* TeX 开始读取（标记化）输入文件中接下来的字符——也就是该组 `{$90}`。请记住，当 TeX 展开一个宏时，它通过读取该宏定义的标记列表中的标记来获得下一段输入；也就是说，从它的 `<替换文本>` 部分中读取，且该部分存储在内存中。

TeX 将处理并执行 `\docat` 的展开，并检测到 *记号* **`getarg`**，并将其识别为代表一个带参数命令的标记。此时，TeX 会扫描输入文件以寻找 **`getarg`**&#x7684;参数：这些字符： `{$90}`. 照常，这些字符会被标记化，但由于 TeX 已经读入并处理了 `\docat`的展开， `$90` 这些字符在 `$` 的类别码已被改为 11 时被标记化。`<替换文本>`) 的定义 `\getarg` 只是 `#1` 这意味着排版所提供的参数，而实际情况正是如此，结果生成了一个 `$` 其类别码为 11 的，并被安全排版的

## 结语：节点中的故事

由于重写 `\docat` 以使用宏 `\getarg` 而产生的一系列事件，包含在下面带注释的节点列表图中，该图展示了宏的展开过程 `\docat`。希望仔细研究该图的读者可以将该图下载为 [PDF](https://assets.ctfassets.net/nrgyaltdicpt/7MOBdavza4WxAEQGISEEht/cabe5097d9063a6415bc553cd38237e6/newdocatexpansion.pdf) 或 [SVG](https://images.ctfassets.net/nrgyaltdicpt/2FIqqVRXjakdAzTpZsV0m9/10ccc1353741d42d3df18f1692d8aa84/newdocatexpansion--plain.svg) 文件，以便离线使用。

![显示修改后的 \docat 宏和 \getarg 宏的展开过程](/files/0ba1aab41d92127f73d2bfdf0cfc26c14e948837)

[第1部分](/latex/zh-cn/geng-duo-zhu-ti/19-how-tex-macros-actually-work-part-1.md) [第2部分](/latex/zh-cn/geng-duo-zhu-ti/20-how-tex-macros-actually-work-part-2.md) [第3部分](/latex/zh-cn/geng-duo-zhu-ti/21-how-tex-macros-actually-work-part-3.md) [第4部分](/latex/zh-cn/geng-duo-zhu-ti/22-how-tex-macros-actually-work-part-4.md) [第5部分](/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.md) [第6部分](/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md)


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://overleaf-pro.ayaka.space/latex/zh-cn/geng-duo-zhu-ti/24-how-tex-macros-actually-work-part-6.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
