抽象代码:从 drink()() 到百层 typedef 之旅
📌 更新(2026-08-31):本文写于 2023 年 10 月。发布前用 2026 年的新版 GCC 把全流程重新复现了一遍,文中的编译器行为说明(含 GCC 14 的变化)即来自这次复现。
系列缘起那篇文章里说过:一位同学告诉我 C 语言里有”返回值为函数指针的函数”,我写下了 drink()(),然后问题就变成了——**能 drink()()() 吗?能无限连下去吗?**这篇就是那个问题的完整答案,也是整个抽象代码系列的起点详解。
第一步:typedef 套娃
drink()() 能跑两连,第三连就撑不住了——因为返回类型太”窄”。解法是让每一层 typedef 都”返回上一层”:
1 | typedef long long (*p_fun)(void); |
每套一层 typedef,可连续调用的次数就多一次。想要 23 连?套 22 层。想要 100 连?——套 100 层,这个体力活后来交给了程序(见下文)。
第二步:让编译器亲口报出类型名
套娃套到几十层的时候,GCC 会报出一条警告,警告文本里带着它眼中的完整类型——一个几十行长的嵌套怪物。当年的我发现:这个警告文本本身,就可以直接拿来 typedef 成别名:
1 | typedef long long int (* (* (* ... (*a)())(void))(void) ...); // 编译器警告里的原始类型,原样贴进来 |
编译,通过,运行,正常。编译器自己说出来的类型,它当然认识。这一步的哲学含义大于技术含义:类型声明再吓人,也只是文本——它能被机器解析,就能被人复制粘贴。
顺带一提,这条语法阅读练习的进阶版在本站还有一篇100 级函数指针挑战——那个版本直接手写 100 层嵌套的类型声明,是本篇的兄弟篇。
第三步:写个程序来生成套娃
手写 100 层 typedef 是体力活,那就让程序干:
1 |
|
这个生成器编译运行后,会输出一个新的 C 语言源文件:先声明 p_fun0,然后用 100 行 typedef 一层层套上去(p_fun1 返回 p_fun0,p_fun2 返回 p_fun1……),fun() 的返回类型是最外层的 p_fun100,main 里跟了 101 个 ()。
真机复现:2026 年的编译器还认不认 2023 年的整活
为写这篇文章,我把这个生成器重新跑了一遍完整流程——生成、编译、运行:
1 | 生成 gen.c:115 行,含 100 行 typedef |
有一个时代彩蛋必须记录:直接用现代 GCC 编译生成的 gen.c,会得到一个错误而不是警告——-Wincompatible-pointer-types(return fun; 那里,fun 的类型和声明的返回类型隔着 100 层 typedef 的”远亲转换”)从 GCC 14 起被升格成了硬错误。要跑通 2023 年的整活,得补一个开关:
1 | gcc -std=c99 -x c -Wno-error=incompatible-pointer-types gen.c -o gen |
当年只是一条警告的事,如今要显式声明”我知道我在干什么”。连抽象代码都在见证时代。
正经话
这条 drink()() → 套娃 → 生成器 的链条,本质上是四次身份的转换:我写代码 → 代码生成代码 → 类型声明文本变成代码 → 编译器警告变成代码。每转一次,”谁在写程序”这个问题就模糊一分。而它教会我的最重要的一件事是:C 语言里,代码和数据的边界比你以为的更模糊——这个认知后来直接催生了手写机器码当函数调那篇更硬核的玩法。
系列其他文章见抽象代码宣言。
