PDF 里的
??和对不上的目录页码,几乎都不是你的语法写错了,而是编译次数不够。LaTeX 需要靠上一趟运行留下的辅助文件才能算出正确的编号,只跑一遍必然缺一半信息。

为什么 \ref 输出的是 ??
因为第一趟编译时,那个标签还不存在。LaTeX 从头到尾单趟顺序处理文档,当它读到第 3 页的 \ref{fig:result} 时,第 12 页的 \label{fig:result} 还没被读到,它无从知道那张图最终是第几张、落在第几页。于是它先输出一个 ?? 占位,并把这一趟收集到的所有标签信息写进一个同名的 .aux 辅助文件。
第二趟编译时,.aux 已经躺在磁盘上了。LaTeX 会在开头先把它读进来,于是遇到 \ref 就能直接查表,填上正确的编号。这套「先记账、下次再查账」的机制,是理解 LaTeX 所有多趟问题的钥匙。
同一套机制还管着这些东西:\pageref 的页码、\cite 的文献编号、\tableofcontents 的目录(存在 .toc)、图表目录(.lof / .lot)、以及 hyperref 生成的 PDF 书签(.out)。它们的共同点是——都要等整篇文档走完才知道结果,但又要在文档中间就印出来。
为什么目录页码经常差一页
因为目录本身也占版面。假设正文原本 30 页,第二趟编译插入了一页目录,后面所有内容就都往后挪了一页,而目录里印的还是第一趟算出来的旧页码,于是整体差 1。第三趟才会用新页码重排目录。
极端一点的情况是震荡:某个章节标题正好卡在页面边界,多一行目录条目就把它挤到下一页,页码变了;下一趟目录条目又缩回去,它又回到上一页。这种文档可能编译多少遍都不收敛,需要人工微调(比如给那一处加一点弹性间距)。好在这种情况不常见。
到底要编译几遍
看文档里用了什么。下面这张表是常见组合的最少趟数:
| 文档里有什么 | 最少趟数 | 原因 |
|---|---|---|
| 只有正文,无引用无目录 | 1 遍 | 没有需要跨页回填的信息 |
有 \ref / \pageref 交叉引用 |
2 遍 | 第一遍写 .aux,第二遍读 .aux |
有 \tableofcontents |
2–3 遍 | 目录本身改变页码,可能需要再收敛一次 |
有 \cite + BibTeX |
4 步(见下节) | 中间要插一趟 BibTeX |
| 有 hyperref 书签 | +1 遍 | .out 变化会触发 rerunfilecheck 警告 |
注意这是最少趟数。多跑一趟不会有副作用(结果已收敛时输出完全相同),少跑一趟就会留下 ??。所以传统做法是无脑跑三遍,虽然浪费但不出错。
有参考文献时为什么是四步
因为 BibTeX 是一个独立程序,得插在两趟 LaTeX 中间。完整顺序是 xelatex → bibtex → xelatex → xelatex:
- 第一趟 xelatex:把正文里
\cite{knuth1984}引用到的条目名记进.aux,此时还没有任何文献列表; - bibtex:读
.aux拿到条目名,去.bib里查,按你指定的\bibliographystyle排好版,输出一个.bbl文件; - 第二趟 xelatex:把
.bbl的内容插进文末,参考文献列表出现了,同时把「哪条文献是第几号」写进.aux——但正文里的\cite在这一趟读到的还是上一版.aux,所以仍然显示[?]; - 第三趟 xelatex:现在
.aux里有编号了,正文的[?]才变成[1]。
很多人卡在第三步,看到文献列表已经出来了就以为完事,结果正文里全是 [?]。再跑一遍就好。
用 biblatex 的话命令不同:中间那步要换成 biber 而不是 bibtex,趟数逻辑一样。
怎么从日志判断已经收敛
搜日志里的 rerun 提示。LaTeX 和相关宏包在发现「这一趟的结果和上一趟对不上」时,会主动打印明确的警告,这几条是最常见的原文:
textLaTeX Warning: There were undefined references.
LaTeX Warning: Label(s) may have changed. Rerun to get cross-references right.
Package rerunfilecheck Warning: File `main.out' has changed.
(rerunfilecheck) Rerun to get outlines right
Package hyperref Warning: Rerun to get /PageLabels entry.
只要还看得到这些,就说明还得再跑一趟。反过来,收敛的标志是它们全部消失,rerunfilecheck 会转而打印一条 Info:
textPackage rerunfilecheck Info: File `main.out' has not changed.
(rerunfilecheck) Checksum: 53F1C61ABCC06CF9FD3B6F1940CE0C3D;330.
这条校验和就是 rerunfilecheck 的判断依据——它记下上一趟 .out 文件的 MD5,这一趟算完再比一次,一样就说明书签已经稳定。目录能不能停,判断方法也类似:比较这一趟写出的 .toc 和上一趟是否逐字节相同。
本地写作时不必手动数趟数,latexmk -xelatex main.tex 会自动循环编译直到日志里再没有 rerun 信号(它内部也有次数上限,防止遇到震荡文档时死循环)。VS Code 的 LaTeX Workshop 默认 recipe 走的就是 latexmk。
浏览器里编译同理:在线 LaTeX 编辑器 的高级模式跑的是 WebAssembly 版 XeLaTeX,它会在每趟结束后扫描日志信号、并比对 .toc / .lof / .lot 有没有变化,需要几趟就跑几趟,不需要你手动点第二次编译。
什么时候该删掉 .aux 重来
改了标签名或换了文档类之后。.aux 是缓存,缓存就会过期:如果你把 \label{fig:old} 改成了 \label{fig:new},旧 .aux 里那条 fig:old 还在,某些情况下会掩盖真正的错误,让你以为引用还在正常工作。
同样,换文档类(比如从 article 换成期刊模板)之后,旧 .aux 里的计数器格式可能对不上新模板,编译会报一些看起来毫无道理的错。遇到这种「明明代码没问题却报怪错」的情况,先把 .aux、.toc、.out、.bbl 这些中间文件全删掉再编译一遍,是性价比最高的排查动作。
小结
?? 不是错误,是 LaTeX 在说「这个编号我还不知道,等我跑完这一趟」。记住三件事就够了:交叉引用和目录至少两遍,带参考文献是四步,判断收敛看日志里还有没有 Rerun to get。本地用 latexmk 自动化,遇到怪错先清中间文件——这几乎能解决所有「编译出来不对劲」的问题。