抽象代码:尊嘟假嘟
又是先上完整代码。下面这个程序能编译、能运行,但它看起来像是谁打字的时候手滑了:
1 |
|
<%"o.o","O.O"%> 是什么?花括号被谁吃了?(rand() % 2)[str] 这种下标写法是在下什么棋?这个程序每次运行,都会在打印完两个内存地址之后,随机输出一张脸:o.o 或者 O.O——尊嘟,还是假嘟,程序自己也不告诉你是哪种,全看运气。
真相一:<% 就是 {——两字符替代记号
那一堆 <% 和 %> 叫 digraphs(两字符替代记号),C95 标准正式引入。它们的用途非常正经:有些国家的老键盘/字符集打不出 #、{、}、[、] 这些符号,所以标准规定了一整套替身:
| 替代记号 | 等价符号 | 替代记号 | 等价符号 | |
|---|---|---|---|---|
<: |
[ |
%: |
# |
|
:> |
] |
%:%: |
## |
|
<% |
{ |
:: |
(C23 才有) | |
%> |
} |
所以 <%"o.o","O.O"%> 就是 {"o.o","O.O"},int main() <% ... %> 就是 int main() { ... }。编译器完全一视同仁——这不是什么方言外挂,是 ISO 标准的正式成员。GCC 默认就认,你不用加任何编译选项。
顺带一提它的大哥 trigraphs(三字符组,??< 也是 {),因为太反人类,C++17 里已经被移除;digraphs 活了下来,成了标准里少有的”合法怪话”。
真相二:(rand() % 2)[str]——下标可以写在左边
str 是一个二维数组,两行内容分别是 "o.o" 和 "O.O"(各占 4 字节,含结尾的 \0)。这行:
1 | printf("%s\n", (rand() % 2)[str]); |
(rand() % 2)[str] 等价于 *(rand() % 2 + str)——随机取第 0 行或第 1 行的地址,交给 %s 打印整行。是的,在 C 语言里,下标写在方括号里面和写在方括号外面是一回事:arr[1] 和 1[arr] 完全等价,因为数组下标本来就被定义成”指针加偏移再解引用”。这个写法放在这里是炫技,但它炫的技是货真价实的语言规则。
于是每次运行,程序随机甩出一张脸——o.o 是”假嘟”,O.O 是”尊嘟”。我连跑四次,它变了两张脸,如下:
1 | 000000492D9FFBEC |
真相三:剩下两行也在偷偷讲课
fun(&a) 打印了两个地址——p 和 p+1。仔细看输出:两个地址恰好差 4。这就是指针算术的定义:p+1 加的不是 1,而是 1 个 sizeof(int)。以及最后那行输出 1:
1 | int b[10] = {0,1,2,3,}; |
&b[0] 是第一个元素的地址,+1 跨过一个 int,解引用正好是 b[1]——等于 1。而且注意初始化列表 {0,1,2,3,} 结尾多了一个逗号,这是 C 标准明确允许的(让我想起隔壁用分号当分隔符的那些狠活,以后有机会再讲)。
一个 22 行的小程序,塞下了 digraphs、随机数、二维数组行地址、下标交换律、指针算术、尾逗号六个知识点——这就是抽象代码的美德:它让人为了搞明白一个笑话,读完了一章《C 陷阱与缺陷》。
