什么是 Trigraph?
Trigraph(三字符序列)是 C 语言标准中定义的一组三个字符的组合,用于表示另一个特定的字符,它的设计初衷是为了解决某些古老的键盘(比如某些欧洲键盘)缺少某些标准 ASCII 字符(如 , , [, ], , \, ^, , )的问题。

程序员可以通过输入三个特定的字符序列,让编译器将其替换成它所代表的那个目标字符。
为什么会有 Trigraph?
这要追溯到 C 语言标准化的早期(C89/C90),当时,许多终端和键盘布局不完整,无法直接输入所有 C 语言需要的标点符号,为了确保代码的可移植性,标准委员会引入了 Trigraph 机制,作为一种“逃生舱口”,让程序员可以在任何键盘上编写符合 C 语法的代码。
所有的 Trigraph 序列
C 语言标准一共定义了 9 个 Trigraph 序列,它们都以两个问号 开头,后面跟一个字符:
| Trigraph 序列 | 代表的字符 | 描述 |
|---|---|---|
| Hash (井号) | ||
[ |
左方括号 | |
\ |
反斜杠 | |
] |
右方括号 | |
^ |
Caret (脱字符) | |
??< |
左花括号 | |
| 竖线 | ||
??> |
右花括号 | |
| 波浪号 |
Trigraph 如何工作?
Trigraph 的替换是由编译器在预处理阶段完成的,在编译器开始解析代码的语法之前,一个叫做“Trigraph 替换”的步骤会扫描整个源文件,将所有识别到的 Trigraph 序列替换成它们对应的字符。

示例:
假设你有以下代码:
// my_code.c
??<
printf("Hello, world??!\n");
??
在预处理阶段,编译器会首先进行替换:
??<会被替换成- 会被替换成
替换后的代码(编译器实际看到的)变成了:
// 这是编译器在预处理后看到的代码
{
printf("Hello, world|\n");
}
编译器才会对这个替换后的代码进行语法分析和编译。
Trigraph 的实际应用和注意事项
1 在现代编程中的地位:已废弃
由于现代计算机和键盘已经完全支持所有标准 ASCII 字符,Trigraph 的原始用途已经消失,在 C11 标准中,Trigraph 被标记为“可选功能”(obsolescent feature),并且默认情况下是关闭的。
- GCC/Clang: 默认情况下不启用 Trigraph,如果你想启用它,需要使用命令行参数
-trigraphs。 - MSVC (Visual Studio): 默认情况下也不支持 Trigraph。
2 主要用途:阅读和维护旧代码
你今天最有可能遇到 Trigraph 的场景是阅读 90 年代或更早时期编写的代码,维护这些代码时,你必须了解 Trigraph 的存在,否则代码的语法会看起来非常奇怪。
3 一个常见的“陷阱”
考虑下面这个例子:
// This is a comment ??/
- 一个没有经验的程序员可能会认为 是注释的一部分。
- 但实际上,在预处理阶段, 会被替换成
\。 - 这行代码实际上变成了:
// This is a comment \
- 根据C语言规则,反斜杠
\是行继续符,它会将下一行连接到当前行。 - 如果下一行恰好是代码,那么注释就会意外地“溢出”到代码行,导致编译错误。
示例:
// file.c
#include <stdio.h>
int main() {
// This is a comment ??/
printf("Hello"); // This line is now part of the comment!
return 0;
}
在预处理后,它变成了:
#include <stdio.h>
int main() {
// This is a comment \
printf("Hello"); // This line is now part of the comment!
return 0;
}
编译器会认为 printf 仍然是注释的一部分,从而导致 main 函数中没有任何有效代码,编译失败。
如何在 GCC/Clang 中演示 Trigraph?
如果你想亲眼看一下 Trigraph 是如何工作的,可以手动启用它。
创建一个包含 Trigraph 的源文件 test_trigraph.c:
// test_trigraph.c
// ??=include <stdio.h>
int main(void) {
printf("Using trigraphs: ??<Hello, world??>??!\n");
return 0;
}
不启用 Trigraph 进行编译(默认行为):
gcc -E test_trigraph.c
输出会是未经替换的原始代码:
# 1 "test_trigraph.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test_trigraph.c"
// ??=include <stdio.h>
int main(void) {
printf("Using trigraphs: ??<Hello, world??>??!\n");
return 0;
}
可以看到, 和 ??<, 都原封不动。
启用 Trigraph 进行编译:
gcc -E -trigraphs test_trigraph.c
输出是替换后的代码:
# 1 "test_trigraph.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 31 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 32 "<command-line>" 2
# 1 "test_trigraph.c"
#include <stdio.h>
int main(void) {
printf("Using trigraphs: {Hello, world}|\n");
return 0;
}
可以看到, 变成了 ,??< 变成了 , 变成了 。
| 特性 | 描述 |
|---|---|
| 定义 | 三个字符的组合,用于代表另一个字符。 |
| 目的 | 解决老旧键盘缺少特定符号的问题,增强代码的可移植性。 |
| 阶段 | 在预处理阶段由编译器自动完成替换。 |
| 现状 | 已废弃,C11 标准中默认关闭,现代编译器(GCC, Clang, MSVC)默认不支持。 |
| 主要用途 | 阅读和理解非常古老的 C 代码。 |
| 建议 | 永远不要在新代码中使用 Trigraph,它只会带来困惑和潜在的错误,如果你必须维护旧代码,请确保编译器正确处理(或手动识别)它们。 |
Trigraph 是 C 语言发展史上的一个有趣遗迹,了解它有助于你更好地理解语言的演变和兼容性问题,但在日常实践中,你可以完全忽略它。
