> For the complete documentation index, see [llms.txt](https://ayakaleaf-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://ayakaleaf-pro.ayaka.space/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.md).

# TeX 宏究竟如何工作：第 5 部分

[第 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)

## 引言与概述

在第4部分中，我们回顾了 TeX 宏的一些基本属性，为接下来的两篇文章做准备；在那两篇文章中，我们将仔细研究 TeX 宏的底层机制：专门的记号列表。在这最后两篇文章中，我们使用称为节点列表的图示；这些图示是根据使用 Knuth 原始 TeX 软件的一个经过特殊修改的版本所生成的数据制作的——这些修改旨在访问通常对用户不可见的 TeX 内部数据结构。通过“接入” TeX 内部的宏处理和执行例程，我们得以输出图形数据，从而能够更详细、更准确地讨论 TeX 的宏处理行为。Overleaf 希望这些图示能帮助读者更好地理解 TeX 宏究竟是如何工作的。

### 可能的补充背景阅读

Overleaf 已经发布了两篇与记号相关的文章，它们提供了关于 TeX 记号和 TeX 记号列表的更多背景信息。如果你需要填补理解上的任何空白，并帮助自己从本系列第5和第6部分中获得最大收益，请务必花时间看看它们。

* [什么是“TeX 记号”？](/latex/zh-cn/shen-du-wen-zhang/53-what-is-a-tex-token.md)
* [什么是 TeX 记号列表？](/latex/zh-cn/shen-du-wen-zhang/54-what-is-a-tex-token-list.md)

## 作为记号列表的宏

当 TeX 检测到一个宏创建命令（`\def`, `\edef`, `\gdef` 或 `\xdef`) 出现在输入流中时，它会触发一个过程，将下面两个部分 `<参数文本><替换文本>` 转换为一条很长的记号列表——但这是某种非常特殊的记号列表。

用于宏的记号列表与 TeX 中使用的其他记号列表略有不同，因为它们包含“特殊”记号值，这些值只能由 TeX 内部的进程创建/生成：这些特殊记号不能直接由你可以包含在 .tex 文件中的任何命令创建。TeX 会创建并使用这些“特殊”记号值来帮助处理你的宏调用，下面我们将对此进行探讨和解释。

### 关于记号列表如何存储的简短说明：节点

为了存储一个记号（整数值）列表，TeX 使用一种称为 [链表](https://en.wikipedia.org/wiki/Linked_list)的数据结构，在 TeX 中，它由一组所谓的 *节点*组成。你可以把节点想成一小包计算机内存，可用于存储一组数据项。为了存储一个宏，这些节点像链条一样串联起来，其中每个节点（链中的一环）都可以存储若干条信息——包括一个记号值以及列表中下一个节点的内存地址。想了解更多信息，你可以阅读文章 [什么是 TeX 记号列表](/latex/zh-cn/shen-du-wen-zhang/54-what-is-a-tex-token-list.md) ，不过下面的图示总结了作为记号列表存储的宏的关键特征：

![以链式节点列表存储的 TeX 宏记号列表示意图](/files/98697fc4664046a2e3e86e3bd77c314df993a36e)

### 提醒：宏定义的 4 个部分

正如第4部分所讨论的，任何宏的结构都可以写成：

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

其中：

* `<TeX 宏原语>` = 以下之一 `\def`, `\edef`, `\gdef` 或 `\xdef`;
* `<宏名>`= 你的宏的名称，例如 `\foo`;
* `<参数文本>` 可以是“空”的（不存在），也可以是一串分隔符记号和宏参数记号；
* `<替换文本>` 是你宏的实际主体：也就是在你调用该宏时会被“执行”的那一部分。

**注意**： （正如第4部分中也提到的）在整个讨论中，我们假定 `<宏名>` 后面会跟着一个类别码 10 的空格字符，作为分隔符来终止 `<宏名>`。我们 *没有* 在文字/讨论中明确写出那个空格字符，但我们假定它是存在的。严格来说，我们应该把它表示成这样：

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

不过，我们将省略对一个 `<空格>` 字符的显式包含，并隐式地假定它存在。

**注意**：这些字符 `{` 和 `}` *不会* 成为宏记号列表的一部分：它们的作用只是告诉 TeX 的输入扫描器（它负责创建记号）哪里是 `<替换文本>` 的开始和结束。

当 TeX 定义一个宏时，由 `<参数文本><替换文本>` 转换而成的部分会被合并成一条很长的连续记号列表——该列表中的记号总数取决于宏的复杂程度。正如我们所见，这个部分有一个特定用途：作为一个“记号模板”或“蓝图”，供 TeX 用来挑出构成实际宏参数（值）的记号：也就是说，用来填入 `<替换文本>`.

为了让这些想法更具体，我们来看一个示例宏，但把它保持得短一些，这样后面的图示就不会太杂乱：

```
\def\foo A#1\fake{123 #1}
```

对于我们的宏， `\foo`

* `<参数文本>` = `A#1\fake`
* `<替换文本>` = `123 #1`

虽然这个示例是一个简单的宏，但它包含了我们需要探讨的所有特性。

如前所述，TeX 会把 `<参数文本><替换文本>` 转换成一条很长的记号列表，你可以在下面的图示中看到它。在我们的示例中，由 `A#1\fake{123 #1}` 形成的记号已被转换为连续的记号序列，并存储在一个记号列表中（作为节点链表）。

## 显示真实宏记号列表的图形

下面这张展示宏 `\def\foo A#1\fake{123 #1}` 如何存储的图示，使用的是来自 TeX 引擎内部的真实数据。它是用 [Knuth TeX 的一个定制版本](/latex/zh-cn/shen-du-wen-zhang/01-a-new-series-of-articles-tex-tokens-and-related-concepts-but-why-and-how.md#how-can-you-study-tex-tokens3f) 制作的，该版本经过额外代码修改，可以拦截宏调用、检查 TeX 的内部数据，并将其导出为可供一个名为 [Graphviz](https://www.graphviz.org/).

你可以将下面这张图下载为 [PDF 文件](https://assets.ctfassets.net/nrgyaltdicpt/N6gT757eXCxRV3FtdPPga/7ce120dc05ed05962bb911ff1124734b/annotatednodelist-plain.pdf) （675 KB）或 [SVG 文件](https://images.ctfassets.net/nrgyaltdicpt/4PkmtHDhO8KF892ZDWuLHP/2c9c8385c6948fd122b228c5c780a3a6/annotatednodelist-plain.svg) （1.8 MB）。

![带注释的 TeX 记号列表示意图](/files/f232b32daf81cb3d5bab28448aaf7728d4cc3f41)

#### 理解这些节点

在上面的图示中，你会看到每个节点都包含两个数据项，分别称为 **下一个节点** 和 **当前节点**。它们只是表示 TeX 内部内存位置的整数值——也就是存放其他节点的位置。 **下一个节点** 和 **当前节点** 的值并不重要，它们只是存储这些位置（内存地址），从而允许节点被链接成一个列表。

![下一个节点与当前节点的含义](/files/314b78934c31d65069f971aa16e56d88e676b73e)

#### 回到示例

在节点图中，由 `A#1\fake{123 #1}` 形成的记号列表包含了本文开头引入的若干“特殊记号”。此外，表示我们宏的节点列表还以一个“特殊首节点”开头：我们将探讨它们是什么，以及它们有什么作用。

宏记号列表中的第一项（以及某些其他类型的记号列表）不存储记号值，而是存储一个称为宏 *引用计数* 的数据项，TeX 用它来跟踪宏的使用情况。

![引用计数节点是记号列表中的第一个节点](/files/a7e27d95f562d101600bac48e408b1580e6e5873)

的第一个记号 `<参数文本>` 存储在紧跟引用计数之后的那个节点中：你可以看到它是一个表示字母 `一个` 的记号，类别码为 11。根据第2和第3部分的讨论，我们知道一个字符记号是使用

$$\text{token value}=256\times \text{category code} + \text{character code}$$

计算出来的，对于一个 `一个` 类别码为 11 的字母

$$\text{token value}=256\times 11 + 65$$

则为

#### ，得到图中所示的值 2881。

在 \foo 中使用的“命令” \fake `\def\foo A#1\fake{123 #1}` 在我们的宏定义中， *一个分隔符是一个* 命令 `未定义的` \fake `<参数文本>` ，它作为 `未定义的` 部分的一部分存储在记号列表中。正如你所看到的，在整个宏记号列表中， `19491`是一个其值为 `\foo` 的记号——该整数值由 TeX 使用第3部分讨论的公式计算得出。当 TeX 尝试执行 `未定义的` 时，它会期望在 `<参数文本>` 部分的末尾找到 *记号值。TeX* 不会 `未定义的` 试图执行这个 `<参数文本>` 命令，因为它的作用只是作为

![中的一种“标点符号”。](/files/6ce38134665697942a7b7874ee9751fb346e6b1b)

#### 将命令记号用作宏分隔符

**记号列表中的特殊记号**

“结束匹配”记号 `<参数文本>` 当调用一个宏时，TeX 的第一项任务是扫描用户输入的宏，并将用户 `<参数文本>` 部分中的记号与存储在内存中的模板 `<参数文本><替换文本>` 进行比较（该模板是在定义宏时创建的）。由于宏的完整定义是由 `<参数文本>` *停止* 构成并存储为一条很长的连续记号列表，TeX 需要知道在那条记号列表中， `<替换文本>` *开始*在哪里，以及 **结束匹配** 记号在哪里。为此，当 TeX 定义宏（构建记号列表）时，它会在由 `<参数文本>`。 **结束匹配** 生成的记号集合中插入一个特殊的终止记号，称为 `<参数文本>`.

![在 TeX 记号列表中显示结束匹配记号](/files/d50ec86132bc285de25726c3749f00c11bed7453)

这里，我们可以看到紧跟在 **结束匹配** 之后的第一个记号是一个表示数字 `1` 的记号，类别码为 12。这是符合预期的，因为我们宏的 `<替换文本>` 为 `\foo` 是 `123 #1`——也就是说，它以表示数字 `1` （类别码为 12）的记号开头。

根据第2和第3部分的讨论，我们知道一个字符记号是使用

$$\text{token value}=256\times \text{category code} + \text{character code}$$

计算出来的，对于一个数字 `1` 类别码为 12 时则为

$$\text{token value}=256\times 12 + 49$$

，得到图中所示的记号值 3121。

**“匹配参数”记号**

当 TeX 存储宏定义时，它会把其中任何参数记号（`#1`, `#2`… `#9`） `<参数文本>` 转换为一个称为 **匹配参数** 的记号。这些记号告诉 TeX，它需要开始在用户的宏调用中查找那些作为宏参数的记号。

![在 TeX 记号列表中显示匹配参数记号](/files/fd4885f2380ecf467e4054b34330441514003060)

### 将命令记号用作宏分隔符

#### “输出参数”记号

当 TeX 处理完所有内容并准备真正运行（展开）这个宏时， **输出参数** 记号会指示 TeX 在 `<替换文本>` 中的哪些位置需要填入表示宏调用时用户提供的参数的记号。实际上，就是“在这个位置插入表示用户第 n 个参数的记号，其中 n=1...9”。

在 `<替换文本>` 部分的已存储宏定义记号列表中，会有一个 **输出参数** 记号与原始定义中出现的每一个 `#1`, `#2`... `#9` 相对应。

![在 TeX 记号列表中显示输出参数记号](/files/d2ad8ab0de298c926b1eb5991501183d46203536)

如果我们查看 `\foo` (`\def\foo A#1\fake{123 #1}`的定义，我们会发现只有 1 个宏参数（`#1`) 并在 `<参数文本>` (`A#1\fake`），因此在`#1`中也只出现 1 个宏参数（ `<替换文本>` (`123 #1`）：这就导致记号列表中只存在 1 个 **输出参数** 记号，它表示 `<替换文本>`.

请注意表示 `\foo`的节点列表中的以下内容： `<替换文本>`:

* 记号的前一个记号 **输出参数** 代表一个空格字符（类别码 10，字符码 32），因为原始定义中的 `123` 和宏参数（`#1`）之间有一个空格 `\foo`;
* 该 **输出参数** 是列表中的最后一个记号： **下一个节点** 具有一个特殊的“空”（即“空白”）值，用于终止列表：在 **输出参数** 之后不再有更多节点，因为它是最后一个记号，标志着 `<替换文本>` 的结束，从而也标志着宏定义的结束。

## 第 6 部分

在第6部分中，我们将使用一些详细的图示来解释和探究宏展开的确切含义，以及在将 TeX 的宏参数分词后再送入宏之前所带来的后果 `<替换文本>`.

[第 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://ayakaleaf-pro.ayaka.space/latex/zh-cn/geng-duo-zhu-ti/23-how-tex-macros-actually-work-part-5.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.
