📌 更新(2026-08-31):本文写于 2023 年 10 月。发布前用 2026 年的新版 GCC 把全流程重新复现了一遍,文中的编译器行为说明(含 GCC 14 的变化)即来自这次复现。

系列缘起那篇文章里说过:一位同学告诉我 C 语言里有”返回值为函数指针的函数”,我写下了 drink()(),然后问题就变成了——**能 drink()()() 吗?能无限连下去吗?**这篇就是那个问题的完整答案,也是整个抽象代码系列的起点详解。

第一步:typedef 套娃

drink()() 能跑两连,第三连就撑不住了——因为返回类型太”窄”。解法是让每一层 typedef 都”返回上一层”:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef long long (*p_fun)(void);
typedef p_fun (*p_fun2)(void);

p_fun2 fun()
{
printf("()");
return fun;
}

int main()
{
fun()()(); // 三连成功
}

每套一层 typedef,可连续调用的次数就多一次。想要 23 连?套 22 层。想要 100 连?——套 100 层,这个体力活后来交给了程序(见下文)。

第二步:让编译器亲口报出类型名

套娃套到几十层的时候,GCC 会报出一条警告,警告文本里带着它眼中的完整类型——一个几十行长的嵌套怪物。当年的我发现:这个警告文本本身,就可以直接拿来 typedef 成别名

1
2
3
4
5
6
typedef long long int (* (* (* ... (*a)())(void))(void) ...);  // 编译器警告里的原始类型,原样贴进来
a fun()
{
printf("()");
return fun;
}

编译,通过,运行,正常。编译器自己说出来的类型,它当然认识。这一步的哲学含义大于技术含义:类型声明再吓人,也只是文本——它能被机器解析,就能被人复制粘贴。

顺带一提,这条语法阅读练习的进阶版在本站还有一篇100 级函数指针挑战——那个版本直接手写 100 层嵌套的类型声明,是本篇的兄弟篇。

第三步:写个程序来生成套娃

手写 100 层 typedef 是体力活,那就让程序干:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#define M 99

int main()
{
int i;
printf("#include <stdio.h>\n\n");
printf("typedef long long (*p_fun0)(void);\n");
for(i = 0 ; i <= M ; ++i)
{
printf("typedef p_fun%d (*p_fun%d)(void);\n", i, i+1);
}
printf("\np_fun%d fun()\n{\n", i);
printf(" printf(\"()\");\n");
printf(" return fun;\n}\n\n");
printf("int main()\n{\n");
printf(" fun()");
for(i = 1 ; i <= M + 2 ; ++i)
{
printf("()");
}
printf(";\n");
printf(" return 0;\n}\n");
return 0;
}

这个生成器编译运行后,会输出一个新的 C 语言源文件:先声明 p_fun0,然后用 100 行 typedef 一层层套上去(p_fun1 返回 p_fun0p_fun2 返回 p_fun1……),fun() 的返回类型是最外层的 p_fun100,main 里跟了 101 个 ()

真机复现:2026 年的编译器还认不认 2023 年的整活

为写这篇文章,我把这个生成器重新跑了一遍完整流程——生成、编译、运行:

1
2
3
生成 gen.c:115 行,含 100 行 typedef
编译 gen.c ...
运行输出:()()()()()()()()()()()()... (共 204 个字符 = 102 组 "()")

有一个时代彩蛋必须记录:直接用现代 GCC 编译生成的 gen.c,会得到一个错误而不是警告——-Wincompatible-pointer-typesreturn fun; 那里,fun 的类型和声明的返回类型隔着 100 层 typedef 的”远亲转换”)从 GCC 14 起被升格成了硬错误。要跑通 2023 年的整活,得补一个开关:

1
gcc -std=c99 -x c -Wno-error=incompatible-pointer-types gen.c -o gen

当年只是一条警告的事,如今要显式声明”我知道我在干什么”。连抽象代码都在见证时代。

正经话

这条 drink()() → 套娃 → 生成器 的链条,本质上是四次身份的转换:我写代码 → 代码生成代码 → 类型声明文本变成代码 → 编译器警告变成代码。每转一次,”谁在写程序”这个问题就模糊一分。而它教会我的最重要的一件事是:C 语言里,代码和数据的边界比你以为的更模糊——这个认知后来直接催生了手写机器码当函数调那篇更硬核的玩法。

系列其他文章见抽象代码宣言