> 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/22-how-tex-macros-actually-work-part-4.md).

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

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

## 引言与概述

在第1–3部分中，我们深入探讨了一些低层背景细节，为理解 TeX 宏的工作原理做准备。在本文中，我们将“浮上来喘口气”，回顾一些 TeX 宏的基本实用原则，以便为第5和第6部分中的另一次深入探讨做好准备。贯穿本文，我们将展示使用 TeX 原始命令定义的示例宏 `\def`：我们不会使用，也许更为熟悉的 LaTeX 命令 `\newcommand`。这样做有一个很好的理由：我们的目标是理解 TeX 宏行为背后的基本原理，但要做到这一点，我们需要使用核心命令 *内置于* TeX 软件。像 `\newcommand`这样的 LaTeX 命令本身就是宏：它们是具有特定程序化行为的命令，而这些行为最终是由更低层的 TeX 原始命令层层构建出来的。为了更好地理解 TeX 的基本行为，我们必须使用 TeX 原语，而不是 LaTeX 宏。

### 我们要去向何方？

本质上，我们正朝着一个解释前进：把宏看作一种特殊形式的记号列表。当你指示 TeX 定义一个宏时，它会创建一个记号序列（一个记号列表），并把它们存储在内存中，关联到你定义的名字上。在第5和第6部分中，我们将详细讨论宏记号列表，但如果你想从本文跳到别处先阅读一些背景信息，可以在 [什么是 TeX 记号列表？](/latex/zh-cn/shen-ru-wen-zhang/54-what-is-a-tex-token-list.md)

宏的记号列表及其参数包含一些额外的细节，我们将通过大量示例详细展开。为了更好地理解 TeX 宏处理行为，要记住的关键点其实只有一个：TeX 只在输入处理的最早阶段考虑字符；从那一刻起，整个过程都只是在处理记号！在本文以及本系列剩余的文章中，我们将探讨记号在 TeX 宏中所扮演的角色。

## 宏的四个部分

### 提醒：宏参数与宏实参

在开始之前，值得先提醒一下宏的 *参数* 与宏的 *参数*之间的区别，因为在本文中我们会一直使用这两个术语。假设你定义了一个宏 \foo

```
\def\foo#1#2{This is #1, this is #2}
```

构造（记号） `#1`, `#2` （直到 `#9`）被称为宏的 *参数*：可以把它们看作你在调用宏时将使用的实际数据的“占位符”：

```
\foo{alpha}{beta}
```

这里， `alpha` 和 `beta` 是 *参数* 在这次对 \foo 的调用中使用的实参：宏的实参就是在定义宏时填入参数占位符（`#1`, `#2`... `#9`）中的实际值。

### 宏定义：包含4个元素

我们先从一个也许看起来有点正式的宏定义开始，但它确实为后面的讨论提供了一个有用的框架。

任何宏定义都由4部分组成：

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

其中：

* `<TeX 宏原语>`：其中之一 `\def`, `\edef`, `\gdef` 或 `\xdef`;
* `<宏名>`：赋予你的宏命令的名称，例如 `\\foo`;
* `<参数文本>`：这可以为空，但如果存在，它就是一系列 *记号* ，出现在宏的 `<替换文本>` 之前。 `<参数文本>` 可以包含宏参数（`#1`, `#2`... `#9`）和其他类型的记号。本质上，正如我们将详细看到的， `<参数文本>` 提供了一种 *记号模板* ，TeX 用它来判断用户想把哪些记号作为宏的实参使用：当你调用一个宏时，TeX 会把 *参数* 记号送入你的宏的 `<替换文本>` （它本身也是一个记号列表）；
* `{<replacement text>}`：这就是你宏的实际主体：它是一系列记号，宏在处理（展开）时会把 *参数* “注入”到其中。实参会被送入原始定义中所使用的宏参数所指示的位置。

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

```
<TeX macro primitive><macro name><space><parameter text>{<replacement text>}
```

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

#### 关于 {} 的说明

我们展示了两个花括号的用法： `{` 和 `}` 它们包围（界定）你的宏的实际主体 `<替换文本>`。不过，使用 `{` 和 `}` 只是一种约定，因为 TeX 实际上期望你的宏 `<替换文本>` 的文本以类别码1（“开始一个组”）的字符开头，并以类别码2（“结束一个组”）的字符结尾。按照惯例，这些分别是 `{` 和 `}` 字符。不过，如果你愿意，也可以指定任意一对字符来完成这件事。例如

```
\catcode`\(=1
\catcode`\)=2
```

现在，你可以像这样定义和使用宏：

```
\def\foo #1(Hello, #1)
\foo(World!)
```

而且你仍然可以使用 `{` 和 `}` ，因为我们没有改变它们的类别码——多个字符可以而且确实可以拥有相同的类别码，所以你仍然可以按通常方式定义宏：

```
\def\foo #1{Hello, #1}
\foo{World!}
```

### 以及 {}

与我们的讨论最相关的宏定义组成部分是 `<参数文本>` 和 `{<replacement text>}`。当你定义一个宏时， `<参数文本>` 实际上就是一个严格的记号模板，定义了宏应当如何使用。正如前面所指出的， `<参数文本>` 可以为空，例如 `\def\foo{Some text}` 此时，在我们的命令名（`\\foo`）与左花括号之间什么都没有， `{` 在这里表示宏的开始 `<替换文本>`.

**注意**：有些读者可能知道 TeX 的“hashquote”机制（`#{`），但这里我们不会讨论它。

### 以及宏分隔符

这篇文章并不是要对宏编写做一个彻底全面的综述，但做一个简短的复习并配上一些示例还是值得的，说明 `<参数文本>` 可能会变得复杂，因为 TeX 允许 `<参数文本>` 包含：

* **宏参数**: (`#1`, `#2`,... `#9`），它们充当用户运行宏时会提供的值的“占位符”——宏的 *参数*;
* **分隔记号**：任意记号，穿插在参数记号中/周围，用来指定宏参数之间的边界。你可以把这些分隔符看作一种“标点”，它们构成了你使用该宏时需要遵循的 `<参数文本>` 一个“记号模板”；分隔记号不会被排版显示。

那么这实际上是什么意思——让我们看几个示例。 `<参数文本>` 位于宏名称和左花括号 `{` 之间。

#### 分隔符：示例1

假设我们定义一个基本宏 `\\foo` 如下：

```
\def\foo ABC{hello, there}
```

* `<参数文本>` = `ABC`
* 没有参数记号（`#1`, `#2`,... `#9`)
* 分隔符是 *三个字符记号* `ABC` ，不过在这个例子里它们有些多余，仅仅作为示例使用。

这三个字符记号 `ABC` 被当作分隔符：它们不是要被排版显示的内容，而是在调用宏时预期会出现的“标点”——当你使用宏 `\\foo` 时，你必须提供与定义时相同的分隔符。

如果你输入 `\foo ABC` 到文本中，它将排版出 `hello, there`——这些字符记号 `ABC` 不会被排版，但 TeX 确实非常仔细地检查它们是否出现在你对 `\\foo`的调用中。TeX 会查找这些分隔符（“标点”）并把它们移除（吸收），如下方 Overleaf 屏幕截图所示： `ABC` 不会被排版：

![Overleaf 上运行 TeX 宏](/files/4ae9f67ac233cd1c1f2f7257147636964afc66a9)

如果你尝试使用 `\\foo` 没有 `ABC` 分隔符时 TeX 会报错 `\foo 的用法与其定义不匹配。`:

![在 Overleaf 上显示 TeX 错误](/files/570528cb3723cca5ec8dfa5d3ff1a33053446a5a)

在上面的例子中，在检测到 `\\foo` 之后，TeX 预期会看到这三个字符记号 `ABC` ，但实际上没有：它看到的是 `w` 这个字符记号，于是立刻发现出了问题。

#### 分隔符：示例2

一个有趣、也许令人惊讶的事实是，分隔符可以是任意命令记号——甚至可以是 *甚至尚未定义*的命令。例如，我们可以定义 `\\foo` 为：

```
\def\foo A\bob B\anne{Hello \TeX{}}
```

* `<参数文本>` = `A\bob B\anne`
* 没有参数记号（`#1`, `#2`,... `#9`)
* 分隔符是这些记号 `A\bob B\anne` ，但同样地，在这个例子中它们有些多余，仅供讨论用途。注意 `\bob` 和 `\anne` 是虚构的命令， *未定义*——而且也不需要定义。

当你调用一个宏时，TeX 会检查（扫描） `<参数文本>` 当前存在的 *是否出现在你的宏调用中* ，并把它逐记号地与存储在内存中的“记号模板”版本 *进行比较*——也就是在宏定义时创建的那个版本。TeX 会扫描你宏调用中使用的 *，并且只是把其中找到的任何命令转换成其数值记号值：它并不是在尝试执行那些命令，因此* 从未被定义过 `\bob` 和 `\anne` 也无关紧要。TeX 只是把存储在内存中的记号模板当作指南，用来帮助它判断你的宏调用中的哪些记号 `<参数文本>` 才是要送入 `<替换文本>`.

的实际参数。`一个` 和 `车`）中的字符记号都没有被 `A\bob B\anne` 排版，我们那些未定义的命令 `\bob` 和 `\anne` 也没有造成任何问题。所有这些记号都被 *吸收* ，因为 TeX 正在把你对 `\\foo` 的使用与存储在内存中的 `\\foo` （一个记号列表）的定义进行匹配。

![Overleaf 上运行 TeX 宏](/files/c269ba2581140610db85c27c6d34a7db11491592)

#### 分隔符：示例3

你可以在宏参数之间穿插各种分隔符（字符记号、命令记号），例如这个例子：

```
\def\foo A\bob#1B\anne#2\jane#3bye!{Hello from \TeX{} to \#1=#1, \#2=#2 and \#3=#3}
```

* `<参数文本>` = `A\bob#1B\anne#2\jane#3bye!`
* 有3个宏参数记号： `#1`, `#2`, `#3`
* 这一次，参数（`#1`, `#2`, `#3`）由多种记号组合来界定：

  `A\bob#1B\anne#2\jane#3bye!`

在这里，你实际上是给 TeX 提供了一个模板，当你调用 `\\foo`时，它会非常仔细地尝试并期望匹配这个模板：它会遍历你的宏调用， *逐记号地*，并期望匹配（找到）：

* 这两个记号 `一个` 和 `\bob` 在……之前 `#1`
* 这两个记号 `车` 和 `\anne` 在……之前 `#2`
* 这个记号 `\jane` 在参数 `#3` 和 *之前的四个字符记号* `b`, `和`,`e` 和 `!` 后添加浮动位置修饰符 `#3`.

**请记住**：TeX 是以记号为单位思考的，所以 `bye!` 是 *之前的四个字符记号*.

我们可以用两种方式使用这个宏——可以把任何多记号参数放在花括号中，形成一个组：

```
\foo A\bob{This}B\anne{That}\jane{Other}bye!
```

不过， *这不是必需的* ，因为我们的宏定义中有分隔符，提供了一个记号模板。TeX 可以使用该模板，从纯粹的分隔符记号中把每个参数的记号提取出来。我们可以这样使用宏：

```
\foo A\bob ThisB\anne That\jane Otherbye!
```

而 TeX 可以提取这些参数，产生与使用组时相同的结果 `{...}`:

![Overleaf 上运行使用分隔符的 TeX 宏](/files/618bc09c0c532ce1a998ceac17143fdf932f97e4)

注意 TeX 是如何精确地检测出哪些记号与参数匹配的 `#1` 和 `#3`:

![Overleaf 上运行使用分隔符的 TeX 宏](/files/c5aa53515142991bf152d994f1917ead05be1119)

#### 分隔符：示例4（关键在于记号，而不是字符！）

这里有一个简短示例，说明在处理宏时，重要的是要记住我们实际上处在 *记号* 和 **不** *字符*……

的世界中。我们将定义下面这个简短宏，其中 A 和 B 是作为分隔符起作用的字符，并且它们的类别码都为11。这可以正常工作：

```
\documentclass{article}
\begin{document}
\def\foo A#1B{Hello, #1}
\foo AGrahamB
\end{document}
```

并排版出 `Hello, Graham`.

然而，如果你把 A 或 B 的类别码改成除11以外的其他值，这个宏调用就会失败。假设我们使用 ``\catcode`B=12`` 把 B 的类别码改为12，然后像之前一样尝试再次调用该宏：

```
\documentclass{article}
\begin{document}
\def\foo A#1B{Hello, #1}
\foo AGrahamB % 这可以工作
\catcode`\B=12\relax
\foo AGrahamB
\end{document}
```

它会以一种有些令人困惑的错误失败：

```
参数失控？
GrahamB \end {document}
! File ended while scanning use of \foo.
<插入的文本>
                \par
<*> main.tex

I suspect you have forgotten a `}', causing me
to read past where you wanted me to stop.
I'll try to recover; but if the error is serious,
you'd better type `E' or `X' now and fix your file.

! 紧急停止。
<*> main.tex

***（作业中止，未找到合法的 \end）
```

![Overleaf 显示一个 TeX 宏错误](/files/880c76f217b38a9cd0fba346446add6c68a896a3)

遗憾的是，TeX 默认给出的提示 ``I suspect you have forgotten a `}'`` 是 **不正确**，这并不是由缺少右花括号（`}`).

#### 发生了什么？

TeX 正在尝试匹配 `\\foo` 宏 *调用* 与 `\\foo` 宏 *定义* 它存储在内存中的定义。当你调用一个需要一个或多个参数的宏时，TeX 必须检查（扫描）你对该宏的使用，以找出你提供给该宏的 *参数* 。在这里，TeX 期望参数被夹在一个类别码为11的 A 和一个类别码为11的 B 之间。记住：TeX 是以 *记号*, **不** *字符*.

A 和 B 的记号值， $$\mathrm{T\_A}$$ 和 $$\mathrm{T\_B}$$ 分别是：

$$\mathrm{T\_A = 256 \times 11 + 65 = 2881}$$ $$\mathrm{T\_B = 256 \times 11 + 66 =2882}$$

然而，随着 TeX 继续搜索，它看到了 B *但* ，但现在它的类别码是12，因此导致了一个不同的记号值：

$$\mathrm{T'\_B= 256 \times 12 + 66 =3138}$$

在……中 `\\foo` 在宏定义中，B 作为分隔符的记号值是 2882，但 TeX 现在看到的记号值是 3138：它认为这只是另一个记号， destined *参数* 要送入 `\\foo`。就 TeX 而言，参数的最后一个记号还没有找到，所以它继续获取另一个记号，以寻找一个类别码为11的 B。正是在这里，也是因为这个原因，宏失败了：在试图找到参数时，TeX “越界”了，开始读取你本来不打算让它成为 `\\foo` 宏调用的参数一部分的记号。接下来会发生什么取决于 TeX 在 B 之后发现了什么记号——它们会触发各种 `参数失控？` 错误。

在我们的例子中，TeX 越界并吞掉了记号流 `\end {document}` ，并迅速碰到了文件末尾，因此出现了错误消息：

```
! File ended while scanning use of \foo
```

## 第5部分

正如我们上面所见， `<参数文本>` 部分可以从极其简单到包含由字符记号和命令记号穿插而成的复杂组合。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://overleaf-pro.ayaka.space/latex/zh-cn/geng-duo-zhu-ti/22-how-tex-macros-actually-work-part-4.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.
