行者无疆 始于足下 - 行走,思考,在路上

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应该可以作为一个开端。




Host by is-Programmer.com | Power by Chito 1.3.3 beta | © 2007 LinuxGem | Design by Matthew "Agent Spork" McGee