C 语言的开发总会遇到各种各样的障碍,诸如字节顺序、换行符、文件编码,以及复杂的工程构建问题,等等;而时不时冒出来的似曾熟悉的新概念,也会让你由衷的感叹:"C语言,简约而不简单 "。

给定两个文件 main.h 以及 main.c

地球人都知道,可以用 gcc main.c 来得到可执行文件: a.out 。但是,如果是 gcc main.h 呢?

$ ~/tmp/precompiled_header % gcc main.c
$ ~/tmp/precompiled_header % time gcc main.h
gcc main.h  0.03s user 0.01s system 89% cpu 0.045 total
$ ~/tmp/precompiled_header % ls -lh
total 1.7M
-rwxrwxr-x 1 lox users 6.6K May 11 20:02 a.out*
-rw-rw-r-- 1 lox users  106 May 11 19:53 main.c
-rw-rw-r-- 1 lox users   19 May 11 19:53 main.h
-rw-rw-r-- 1 lox users 1.7M May 11 20:03 main.h.gch

我们得到了一个新的文件, main.h.gch ,并且,这个文件竟然有 1.7 M 这么大。这个文件,就是我们要谈到的预编译头文件(Precompiled header)。

这个预编译头文件究竟有何作用?为何我们要多此一举?这要从编译器的具体工作原理讲起。大体来讲,编译器的主要工作过程为:预处理 –> 词法分析 –> 语法分析 –> 语义分析 –> 代码生成与优化这几个过程。而对 C 语言来说,预处理的过程主要是处理源代码文件中的以 "#" 开始的预编译指令,诸如 #include#define 等,这个过程包括(参考《程序员的自我修养 》P39):

  • 删除 #define ,展开所有的宏定义
  • 处理所有的条件编译指令
  • 处理 #include 指令,将被包含的文件插入到该预编译指令的位置(这个过程是递归的)
  • 删除所有的注释
  • 添加行号和文件名标识,用于调试信息
  • 保留所有的 #pragma 编译器指令

可以看出,对于巨型的头文件,诸如 Windows 系统的 Windows.h 和 Mac 系统的 Cocoa.h ,这个预处理的过程也是相当的耗费编译时间的,而这类的头文件都有一个特点,就是头文件的内容基本不会改变,但是每次编译都要重新分析这些头文件,无疑是一种巨大的浪费,所以 gcc main.h 做的工作就是,将 main.h 的预处理结果存储位 main.h.gch 文件,以后再用 gcc main.c 编译的时候,gcc会先搜索 main.h.gch 这样的预编译头文件,如果预编译头文件存在,那么 GCC 就省去了重新分析 main.h 的过程。对于大型的工程来讲,这样的策略带来的编译时间上的减少,还是相当可观的。

Visual Studio 某些工程中常见的 stdafx.h ,以及 Xcode 中大部分工程默认带有的 .pch 文件,都是预编译头文件。

与预编译头文件相关的还有一个 Prefix header 的概念,我将它翻译成“预包含头文件”,这种 Prefix header 不但会默认被预编译,而且会默认被每个源文件包含,目前我所知道的,只有 Xcode 才有这么邪恶的做法。

事实上我不太喜欢这种 Xcode 这种出于好意的做法,当然并不是所有的开发者都需要了解这么多,多数人只需要按照模版新建工程然后用 Interface Builder 拖拖拽拽就好,至于背后生成了哪些肮脏的代码,who cares。

真正了不起的程序员对自己的程序的每一个字节都了如指掌。

所以我还是喜欢开个终端,架上 Vim 和 Emacs,搭好 Makefile,all from scratch。谈到这里,我们再稍微挖掘一下 GCC 的潜力: gcc -H main.h

$ ~/tmp/precompiled_header % gcc -H main.h
. /usr/include/stdio.h
.. /usr/include/features.h
... /usr/include/sys/cdefs.h
.... /usr/include/bits/wordsize.h
... /usr/include/gnu/stubs.h
.... /usr/include/bits/wordsize.h
.... /usr/include/gnu/stubs-64.h
.. /usr/lib/gcc/x86_64-unknown-linux-gnu/4.6.0/include/stddef.h
.. /usr/include/bits/types.h
... /usr/include/bits/wordsize.h
... /usr/include/bits/typesizes.h
.. /usr/include/libio.h
... /usr/include/_G_config.h
.... /usr/lib/gcc/x86_64-unknown-linux-gnu/4.6.0/include/stddef.h
.... /usr/include/wchar.h
... /usr/lib/gcc/x86_64-unknown-linux-gnu/4.6.0/include/stdarg.h
.. /usr/include/bits/stdio_lim.h
.. /usr/include/bits/sys_errlist.h
Multiple include guards may be useful for:
/usr/include/bits/stdio_lim.h
/usr/include/bits/sys_errlist.h
/usr/include/bits/typesizes.h
/usr/include/gnu/stubs-64.h
/usr/include/gnu/stubs.h
/usr/include/wchar.h

看到了吧, gcc -H 给出了一个头文件中递归包含的所有头文件。

事实上最近我在做毕业设计,一套 Window Mobile 代码向 iOS 的移植,常常会看到源代码中包含一些无用头文件,虽有使用了 include guard,但是某些时候还是会引起定义冲突,比如我就碰到了源代码中 typedef int BooleanMacTypes.htypedef unsigned char Boolean 之间的冲突。我在想有没有这样一款工具,可以扫描整个工程中无用的头文件以及所有源代码中无用的 #include ,然后做自动的清理工作呢? gcc -H 应该可以作为一个开端。