24.2 代用记号 (Alternative Tokens)
1. 什么是代用记号
代用记号是 C 标准为部分标点符号规定的替代拼写。ISO 646 等字符集的某些变体缺少 {、}、[、]、# 等字符,代用记号使程序在这类环境中也能写出来。
在代码中,代用记号与对应的符号完全等价。
2. 代用记号列表
代用记号主要分为两种:双字母组 (Digraphs) 和 三字母组 (Trigraphs)。
2.1 双字母组 (Digraphs)
这些是两个字符的组合,用来替代单个标点符号。
| 双字母组 | 替代的符号 | 符号名称 |
|---|---|---|
<% | { | 左花括号 |
%> | } | 右花括号 |
<: | [ | 左方括号 |
:> | ] | 右方括号 |
%: | # | 井号 (预处理符号) |
%:%: | ## | 双井号 (预处理中的标记连接) |
使用双字母组的代码看起来是这样的:
// 原始代码
int main() {
int arr[5] = {1, 2, 3, 4, 5};
return 0;
}
// 使用双字母组的等价代码
int main() <%
int arr<:5:> = <% 1, 2, 3, 4, 5 %>;
return 0;
%>2
3
4
5
6
7
8
9
10
11
2.2 已被移除的三字母组
除了上述的代用记号,C 语言早期还有一个三字母组 (Trigraphs) 的概念,它们是三个字符的序列,以两个问号 ?? 开头。它们在 C23 标准中被正式移除。现代编程中应避免使用,也无需了解。
| 三字母组 | 替代的符号 |
|---|---|
??= | # |
??( | [ |
??) | ] |
??/ | \ |
??' | ^ |
??< | { |
??> | } |
??! | | |
??- | ~ |
3. 与 <iso646.h> 的区别
双字母组属于语言的词法,编译器识别记号时就把它们当作对应的标点;<iso646.h> 中的 and、or 等则是普通的宏,只有包含该头文件后,才在预处理阶段被替换为对应的运算符(见 24.1)。
4. 习题
4.1 巩固
下面的代码全部用双字母组写成,先说出每行输出,再改回普通符号验证:
%:include <stdio.h>
int main(void) <%
int a<:3:> = <% 4, 8, 15 %>;
printf("%d\n", a<:2:>);
puts("100%:done");
return 0;
%>2
3
4
5
6
7
8
然后回答两件事:
- 把
int r = 7 %: 2;单独编译一次,抄下错误信息,解释%:为什么在这里不表示#。 puts("100%:done");里的%:为什么原样输出,'%'和'['这样的字符常量里又为什么不受影响。
验收:程序输出 15 和 100%:done;第 1 问的错误是 stray '%:' in program。
4.2 编程
把下面的程序改写成只含双字母组的版本:源码里不允许出现 {、}、[、]、# 这五个字符(字符串字面量和注释里的不算,但要保留原样)。
#include <stdio.h>
#define SIZE 3
#define JOIN(a, b) a##b
#define STR(x) #x
struct point {
int x;
int y;
};
int main(void) {
struct point pts[SIZE] = {{1, 2}, {3, 4}, {5, 6}};
const char *note = "braces { } and hash # stay";
int JOIN(su, m) = 0;
for (int i = 0; i < SIZE; ++i) {
sum += pts[i].x * pts[i].y;
}
printf("%s = %d, note = %s\n", STR(sum), sum, note);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
要求:
- 预处理指令改用
%:开头,##写成%:%:,#x写成%:x,结构体定义、数组、初始化列表、循环体都换成双字母组。 - 两个版本分别用
gcc -std=c17 -Wall -Wextra编译运行。 - 用
gcc -std=c2x再编一次改写版。 - 用
gcc -std=c17 -S生成两份汇编,diff比较。
验收:输出均为 sum = 44, note = braces { } and hash # stay;汇编只差 .file 一行;用 LC_ALL=C grep -nE '[][{}#]' 改写版.c 检查,除了那行字符串字面量,其余行都没有这五个字符(-E 不能省,方括号在方括号表达式里要按转义规则写)。再说明 STR(sum) 为什么打印 sum 而不是 44。
三字母组在 C23 中被移除,用下面这个程序观察这次变化:
#include <stdio.h>
int main(void) {
int a = 5;
int b = 3;
puts("what??!");
puts("a??/nb");
printf("%d\n", a ??! b);
printf("%d\n", a ??' b);
if (a > b) ??<
puts("a > b");
??>
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
要求:
- 先按表推出每行的输出,再分别用
-std=c17和-std=c2x编译。 - 抄下
-std=c17下的-Wtrigraphs警告,说明a??/nb为什么变成两行。 - 抄下
-std=c2x下的错误,说明为什么字符串里的??!只是警告,而表达式里的??!是错误。 - 写一个等价的可移植版本:全部用普通符号重写,字符串里的
??用\?转义,使得在两种标准下都无警告通过,输出与原版在-std=c17下的结果一致。 - 说明
??-与~的优先级关系:写出??-a的等价写法。
验收:-std=c17 下输出 what|、a、b、7、6、a > b,同时有 6 条 -Wtrigraphs 警告;-std=c2x 下编译失败,报 expected expression before '?' token;重写版在两种标准下都没有警告,输出相同。
4.3 综合与挑战
写一个转换工具 digraph.c,把标准 C 源文件转成只用双字母组书写的版本。用法:./digraph < 输入.c > 输出.c。
转换规则:源代码中的 # 换成 %:,连续的 ## 换成 %:%:,{、}、[、] 分别换成 <%、%>、<:、:>。以下位置不转换:字符串字面量、字符常量、// 行注释、/* */ 块注释(因此 "# and { }" 与 /* [ ] */ 保持原样),字符串和字符常量里的转义(如 \"、\'、\\)要整体跳过。
用下面的输入测试:
#include <stdio.h>
/* comment with [ ] { } # */
#define SIZE 3
#define CAT(a, b) a##b
int main(void) {
int a[SIZE] = {1, 2, 3};
const char *s = "braces { } and hash # and %:";
char c = '}';
int CAT(su, m) = 0;
// line comment ] } #
for (int i = 0; i < SIZE; ++i) {
sum += a[i];
}
printf("%d %s %c\n", sum, s, c);
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
要求:
- 用一个状态变量区分正常、行注释、块注释、字符串、字符常量五种状态,用
getchar逐字符读入,putchar写出。不要先把整个文件读进内存。 - 结果中
#define CAT(a, b) a##b必须变成%:define CAT(a, b) a%:%:b,而两处注释里的[ ] { } #一字不改。 - 输出文件用
gcc -std=c17 -Wall -Wextra编译,并运行。 - 把输出再喂给工具一次,确认文件不再变化。
- 用
gcc -std=c17 -S比较输入、输出两份汇编。
验收:
%:include <stdio.h>
/* comment with [ ] { } # */
%:define SIZE 3
%:define CAT(a, b) a%:%:b
int main(void) <%
int a<:SIZE:> = <%1, 2, 3%>;
const char *s = "braces { } and hash # and %:";
char c = '}';
int CAT(su, m) = 0;
// line comment ] } #
for (int i = 0; i < SIZE; ++i) <%
sum += a<:i:>;
%>
printf("%d %s %c\n", sum, s, c);
return 0;
%>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编译运行后输出 6 braces { } and hash # and %: };第二次转换的结果与原文件逐字节相同;两份汇编只差 .file 一行。