行者无疆 始于足下 - 行走,思考,在路上
gcc main.h: Precompiled header
c语言的开发总会遇到各种各样的障碍,诸如字节顺序、换行符、文件编码,以及复杂的工程构建问题,等等;而时不时冒出来的似曾熟悉的新概念,也会让你由衷的感叹:"C语言,简约而不简单"。
给定两个文件main.h以及main.c:
#include <stdio.h>
#include "main.h" int main(int argc, const char *argv[]) { printf("Hello, World\n"); return 0; }
地球人都知道,可以用gcc main.c来得到可执行文件:a.out。但是,如果是gcc main.h呢?
lox@lox-pad ~/tmp/precompiled_header % gcc main.c lox@lox-pad ~/tmp/precompiled_header % time gcc main.h gcc main.h 0.03s user 0.01s system 89% cpu 0.045 total lox@lox-pad ~/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.7M这么大。这个文件,就是我们要谈到的预编译头文件(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:
lox@lox-pad ~/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 lox@lox-pad ~/tmp/precompiled_header %
看到了吧,gcc -H给出了一个头文件中递归包含的所有头文件。
事实上最近我在做毕业设计,一套Window Mobile代码向iOS的移植,常常会看到源代码中包含一些无用头文件,虽有使用了include guard,但是某些时候还是会引起定义冲突,比如我就碰到了源代码中typedef int Boolean和MacTypes.h中typedef unsigned char Boolean之间的冲突。我在想有没有这样一款工具,可以扫描整个工程中无用的头文件以及所有源代码中无用的#include,然后做自动的清理工作呢?gcc -H应该可以作为一个开端。