C/C 中头文件是必须的吗?
不是。
都知道,编译一段代码包括如下阶段:
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
其中,预处理的职责包括展开#define宏定义,处理诸如#if/#ifdef/#ifndef之类的条件编译指令,以及处理#include,将被包含的文件直接插入到预编译指令的位置。当然,预处理过程还负责删除注释等职责。
so?预处理阶段会将#include包含的文件直接插入到源文件.cpp中去。头文件实际上并不会被编译,编译器只会编译源文件。只是在编译之前,会将源文件中#include包含的文件在源文件中展开。(这就好比什么呢?打个不恰当的比方,你在写一篇论文,论文中需要参考Jungle的一篇文章《识别C 代码质量的诀窍,在这里……》。结果预处理的时候,你直接把这篇文章全放到你的论文里了)。
所以,可以手动把头文件中的内容搬到源文件,然后删掉头文件,如下图:
理论上是这样的,而且理论上行得通。但操作起来可不现实,比如,你确定要把下面两个文件搬到源文件中吗?而且头文件中还包含其他头文件,不知道得向上追溯多少级才到头?实际上也没人这么做,Jungle只是想看看这里面的东西。而且这也是头文件存在的必要之处,即,但凡我想在当前源文件中使用其他源文件中的函数、变量,甚至是其他库、系统的函数,我只需要#include相关头文件即可。如果我想在另一个源文件中继续使用,那就再添加#include相关的代码。需要注意的是要避免同一个头文件被重复包含。
代码语言:javascript复制#include <iostream>
#include <stdio>
接下来再做一个测试:
如上图结构的文件,这次我手动把#include在源文件中展开:
如上图,这也是ok的,可以编译成功。这相当于:
- main.cpp中首先添加了func()函数声明,然后在main()函数中调用了func()。
- func.cpp中也添加了func()函数声明,同时给出了func()函数的定义。其实这里的声明可以不要了,直接给func()函数的定义。当然,你也可以声明多次。
那么main.cpp中能否也把func()声明删掉呢?
看来不行,报错说在该作用域内func没有声明。注意我这里是单独编译main.cpp,加上func.cpp也是一样的:
代码语言:javascript复制PS F:Jungle1.Program4.C 4.Compiler> g -o app main.cpp func.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:5:5: error: 'func' was not declared in this scope
func();
嗯,这不难理解,因为编译过程本身就是把每个源文件单独编译为一个目标文件,然后再把各个目标文件链接起来。也就是说,我们通常说的“编译程序”或“编译工程”,实际上包括了整个阶段(预处理、编译、汇编、链接)。那上面的问题是在哪个子过程报出来的呢?不知道原理也没关系,一步一步试下!
首先预处理肯定没问题,预处理只是原地展开而已。而且上面的测试我在main.cpp中删掉了func()声明,就等于在main.cpp中删掉#include。所以可以认为“没有预处理过程”(实际上是有的,因为预处理过程还负责生成行号等等职责)。
那是编译过程出的错吗?不妨单独看看是否能够编译成功:
代码语言:javascript复制PS F:Jungle1.Program4.C 4.Compiler> g -S main.cpp
main.cpp: In function 'int main(int, char**)':
main.cpp:5:5: error: 'func' was not declared in this scope
func();
^~~~
oh,是编译阶段出的错,报错信息跟上面是一样的(废话)。编译过程包括词法分析、语法分析、语义分析、代码优化及目标代码生成等过程。这里的目标代码是汇编代码,所以g -S会产生一个汇编文件。
在这里,func是一个未经声明就使用的东西(实际上,如果在main()函数中直接写一行a=10会报相同的错,即'a' was not declared in this scope),在语义分析阶段会被检查出来。
声明变量可以告诉编译器这个变量类型是什么,占多少个字节。声明函数则可以告诉编译器函数名是什么、返回类型是什么、参数个数、参数类型是什么。不声明就使用,别人怎么知道func是什么东西呢?
那还是加上声明吧,然后单独编译main.cpp:
可以看到,编译成功了,生成了main.s汇编文件。
汇编也成功了,生成了目标文件main.o。
可链接报错了:
代码语言:javascript复制PS F:Jungle1.Program4.C 4.Compiler> g -S main.cpp
PS F:Jungle1.Program4.C 4.Compiler> g -c main.s
PS F:Jungle1.Program4.C 4.Compiler> g -o app main.o
main.o:main.cpp:(.text 0x15): undefined reference to `func()'
collect2.exe: error: ld returned 1 exit status
报错说,未定义的引用func()。上面的ld是链接器,是一个可执行程序,它的输入是一个或多个目标文件,如上面指令中的main.o。
也就是说,目标文件main.o中引用了func(),但链接器找不到它的定义。main.cpp中确实没有func()函数的定义,但func.cpp中有。那不妨我们把func.cpp也编译并生成目标文件func.o,然后链接的时候同main.o一同作为ld的输入:
代码语言:javascript复制PS F:Jungle1.Program4.C 4.Compiler> g -S main.cpp
PS F:Jungle1.Program4.C 4.Compiler> g -c main.s
PS F:Jungle1.Program4.C 4.Compiler> g -S func.cpp
PS F:Jungle1.Program4.C 4.Compiler> g -c func.s
PS F:Jungle1.Program4.C 4.Compiler> g -o app main.o func.o
PS F:Jungle1.Program4.C 4.Compiler>
这下成功了,生成了可执行程序app.exe。显然,main.o中引用但未定义的func()被链接器在func.o中找到了。即,链接器在面对一个目标文件时,如果碰到里面有未定义的引用,会在其他目标文件中查找,如果找不到,则报错“undefined reference to”。如果找到有且仅有一个,则pass。
如果找到多个:
如上图,同时在main.cpp和func.cpp中给出了func()函数定义,编译和汇编单个文件都是成功的,但是链接报错说func()有多个定义。而且,链接时输入目标文件的顺序与first defined here相关。
我们还是在main.cpp中只保留func()函数的声明,再单独编译汇编生成main.o。接下来用nm看下main.o符号表中的内容:
代码语言:javascript复制PS F:Jungle1.Program4.C 4.Compiler> g -S .main.cpp
PS F:Jungle1.Program4.C 4.Compiler> g -c main.s
PS F:Jungle1.Program4.C 4.Compiler> nm main.o
0000000000000000 b .bss
0000000000000000 d .data
0000000000000000 p .pdata
0000000000000000 r .rdata$zzz
0000000000000000 t .text
0000000000000000 r .xdata
U __main
U _Z4funcv
0000000000000000 T main
PS F:Jungle1.Program4.C 4.Compiler>
其中:U代表该符号在当前文件中是未定义的。如果在main.cpp中加上func()函数定义,再尝试上面步骤,得到:
代码语言:javascript复制0000000000000000 b .bss
0000000000000000 d .data
0000000000000000 p .pdata
0000000000000000 r .rdata$zzz
0000000000000000 t .text
0000000000000000 r .xdata
U __main
0000000000000000 T _Z4funcv
0000000000000002 T main
可以看到,符号_Z4funcv前面的标识变为T了,标识该符号位于代码段text section。
再跟下去就讲不完了。。。
回到题目上来,头文件是必须的吗?不是,头文件会在预处理阶段被展开。但头文件会我们编程带来极大便利,要使用某个函数、某个变量了,那就#include。本文只是就着这个问题,跟了下编译的过程,看看平常开发过程中遇到的编译报错“未定义的引用”、“未声明的变量”这些错误来源是哪原因是什么。