# 多文件编程 多文件编程就是把多个头文件(`.h`文件)和源文件(`.c`文件)组合在一起构成一个程序,这是C语言的重点,也是C语言的难点。C语言头文件的编写是其中的重点内容,有很多细节需要注意,有的甚至会让你感觉奇怪。 多文件编程既涉及到了内存,也涉及到了编译原理,市面上的绝大部分资料对此也语焉不详,所以很多初学者对此都非常困惑。学会了多文件编程,你就可以使用C语言来开发中大型项目了,对初学者来说,这简直是跨域了一大步。 ## 多文件编程简例 在前面的教程中,我们都是将所有的代码写到一个源文件里面,对于小程序,代码不过几百行,这或许无可厚非,但当程序膨胀代码到几千行甚至上万行后,就应该考虑将代码分散到多个文件中,否则代码的阅读和维护将成为一件痛苦的事情。 本节我们就来演示一下多文件编程。在下面的例子中,我们创建了两个源文件 main.c 和 module.c: - module.c 是整个程序的一个模块,我们在其中定义了一个全局变量和一个函数; - main.c 是程序的主模块(主文件),它使用到了 module.c 中的变量和函数。 module.c 源码: ```c #include int m = 100; void func(){ printf("这是模块文件"); } ``` main.c源码: ```c #include extern void func(); extern int m; int n = 200; int main(){ func(); printf("m=%d, n=%d\n", m, n); return 0; } ``` 在 Visual Studio 中,将两个源文件都添加到工程中,编译运行程序。 在 Linux GCC 中,可以使用下面的命令来编译和运行程序: ```bash gcc main.c module.c -o wang ./wang ``` m 和 n 是在所有函数之外定义的全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的代码文件,包括`.c`和`.h`文件。 > 如果你一直在编写单个源文件的程序,那么请注意,全局变量的作用范围不是从变量定义处到该文件结束,在其他文件中也有效。 这里需要重点理解的是 extern 关键字,它用来声明一个变量或函数。 ## extern 关键字 我们知道,C语言代码是由上到下依次执行的,不管是变量还是函数,原则上都要先定义再使用,否则就会报错。但在实际开发中,经常会在函数或变量定义之前就使用它们,这个时候就需要提前声明。 所谓声明(Declaration),就是告诉编译器我要使用这个变量或函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。 例如,我们知道使用 printf()、puts()、scanf()、getchar() 等函数要引入 stdio.h 这个头文件,很多初学者认为 stdio.h 中包含了函数定义(也就是函数体),只要有了头文件程序就能运行。其实不然,头文件中包含的都是函数声明,而不是函数定义,函数定义都在系统库中,只有头文件没有系统库在链接时就会报错,程序根本不能运行。 #### 1) 函数的声明 通常的函数声明并没有使用 extern 关键字,这是因为,函数的定义有函数体,函数的声明没有函数体,编译器很容易区分定义和声明,所以对于函数声明来说,有没有 extern 都是一样的。 总结起来,函数声明有四种形式: ``` //不使用 extern datatype function( datatype1 name1, datatype2 name2, ... ); datatype function( datatype1, datatype2, ... ); //使用 extern extern datatype function( datatype1 name1, datatype2 name2, ... ); extern datatype function( datatype1, datatype2, ... ); ``` #### 2) 变量的声明 变量和函数不同,编译器只能根据 extern 来区分,有 extern 才是声明,没有 extern 就是定义。 变量的定义有两种形式,你可以在定义的同时初始化,也可以不初始化: ``` datatype name = value; datatype name; ``` 而变量的声明只有一种形式,就是使用 extern 关键字: ``` extern datatype name; ``` 另外,变量也可以在声明的同时初始化,格式为: ``` extern datatype name = value; ``` 这种似是而非的方式是不被推荐的,有的编译器也会给出警告,我们不再深入讨论,也建议各位读者把定义和声明分开,尽量不要这样写。 extern 是“外部”的意思,很多教材讲到,extern 用来声明一个外部(其他文件中)的变量或函数,也就是说,变量或函数的定义在其他文件中。 不过我认为这样讲不妥,因为除了定义在外部,定义在当前文件中也是正确的。例如,将 module.c 中的`int m = 100;`移动到 main.c 中的任意位置都是可以的。所以我认为,extern 是用来声明的,不管具体的定义是在当前文件内部还是外部,都是正确的。 ## 被编译器隐藏了的过程 对于平常应用程序的开发,很少有人会关注编译和链接的过程,因为我们使用的工具一般都是流行的集成开发环境(IDE),比如 Visual Studio、Dev C++、C-Free 等。这些功能强大的 IDE 通常将编译和链接合并到一起,也就是构建(Build)或运行(Run)。即使在 Linux 下使用命令行来编译一个源文件,简单的一句`$gcc demo.c`也包含了非常复杂的过程。 虽然 IDE 提供的默认配置、编译和链接参数对于大部分应用程序来说已经足够使用了,但是作为学习者,我们还是要刨根问底,弄清从源代码生成可执行文件的内部机理,不要被 IDE 提供的强大功能所迷惑。 C语言经典的“Hello World”小程序几乎是每个程序员闭着眼睛都能写出来的,基本成了入门教程和开发环境的默认标准,代码如下: ```c #include int main(){ printf("Hello World.\n"); return 0; } ``` 如果在 Windows 下使用 Visual Studio 来编译,那么可以直接点击`运行(Run)按钮`或者`构建(Build)按钮`,在工程目录下就会看到生成的 .exe 程序。 如果在 Linux 下使用 GCC 来编译,使用最简单的`$gcc demo.c`命令,就可以在当前目录下看到 a.out。 事实上,从源代码生成可执行文件可以分为四个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。下图是 GCC 生成 a.out 的过程: ![img](http://c.biancheng.net/uploads/allimg/190122/110U614Q-0.jpg) ## 预处理(Preprocessing) 预处理过程主要是处理那些源文件和头文件中以`#`开头的命令,比如 #include、#define、#ifdef 等。预处理的规则一般如下: - 将所有的`#define`删除,并展开所有的宏定义。 - 处理所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。 - 处理`#include`命令,将被包含文件的内容插入到该命令所在的位置,这与复制粘贴的效果一样。注意,这个过程是递归进行的,也就是说被包含的文件可能还会包含其他的文件。 - 删除所有的注释`//`和`/* ... */`。 - 添加行号和文件名标识,便于在调试和出错时给出具体的代码位置。 - 保留所有的`#pragma`命令,因为编译器需要使用它们。 预处理的结果是生成`.i`文件。`.i`文件也是包含C语言代码的源文件,只不过所有的宏已经被展开,所有包含的文件已经被插入到当前文件中。当你无法判断宏定义是否正确,或者文件包含是否有效时,可以查看`.i`文件来确定问题。 在 GCC 中,可以通过下面的命令生成`.i`文件: $gcc -E demo.c -o demo.i `-E`表示只进行预编译。 在 Visual Studio 中,在当前工程的属性面板中将“预处理到文件”设置为“是”,如下图所示: ![img](http://c.biancheng.net/uploads/allimg/190122/110UA926-1.png) 然后点击“运行(Run)”或者“构建(Build)”按钮,就能在当前工程目录中看到 demo.i 。 ## 编译(Compilation) 编译就是把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。编译是整个程序构建的核心部分,也是最复杂的部分之一,涉及到的算法较多,我们并不打算深入讨论,有兴趣的读者请查看《编译原理》。 在 GCC 中,可以使用下面的命令生成`.s`文件: $gcc -S demo.i -o demo.s 或者 $gcc -S demo.c -o demo.s 在 Visual Studio 中,不用进行任何设置就可以在工程目录下看到 demo.asm 文件。 ## 汇编(Assembly) 汇编的过程就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令,我们在《[C语言内存精讲](http://c.biancheng.net/c/140/)》中的《[一个程序在计算机中到底是如何运行的](http://c.biancheng.net/view/vip_2090.html)》一节对汇编语言进行了简单的解释。 汇编过程相对于编译来说比较简单,没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编语句和机器指令的对照表一一翻译就可以了。 汇编的结果是产生目标文件,在 GCC 下的后缀为`.o`,在 Visual Studio 下的后缀为`.obj`。 ## 链接(Linking) 目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,程序不能执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。 预处理和汇编的过程都比较简单,有了上面的介绍,相信大家很容易理解。 编译的过程最为复杂,可以细分为词法分析、语法分析、语义分析和指令优化,这里涉及到诸多算法以及正则表达式,我们并不打算深入分析,也没必要,有兴趣的读者请自行查阅《[编译原理](http://product.dangdang.com/20427584.html)》。 而目标文件的结构、可执行文件的结构、链接的过程是我们要重点研究的,它能够让我们明白多文件编程以及模块化开发的原理,这是大型项目开发的基石。 最后需要说明的是:汇编的过程非常简单,仅仅是查表翻译,我们通常把它作为编译过程的一部分,不再单独提及。这样,源文件经过预处理、编译和链接就生成了可执行文件。