打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
头文件是必须的吗?跟一跟编译过程~~~
userphoto

2022.10.21 江苏

关注

C/C++中头文件是必须的吗?

不是。


都知道,编译一段代码包括如下阶段:

  • 预处理(Preprocessing)

  • 编译(Compilation)

  • 汇编(Assembly)

  • 链接(Linking)

其中,预处理的职责包括展开#define宏定义,处理诸如#if/#ifdef/#ifndef之类的条件编译指令,以及处理#include,将被包含的文件直接插入到预编译指令的位置。当然,预处理过程还负责删除注释等职责。

so?预处理阶段会将#include包含的文件直接插入到源文件.cpp中去。头文件实际上并不会被编译,编译器只会编译源文件。只是在编译之前,会将源文件中#include包含的文件在源文件中展开。(这就好比什么呢?打个不恰当的比方,你在写一篇论文,论文中需要参考Jungle的一篇文章《识别C++代码质量的诀窍,在这里……》。结果预处理的时候,你直接把这篇文章全放到你的论文里了

)。


所以,可以手动把头文件中的内容搬到源文件,然后删掉头文件,如下图:

理论上是这样的,而且理论上行得通。但操作起来可不现实,比如,你确定要把下面两个文件搬到源文件中吗?而且头文件中还包含其他头文件,不知道得向上追溯多少级才到头?实际上也没人这么做,Jungle只是想看看这里面的东西。而且这也是头文件存在的必要之处,即,但凡我想在当前源文件中使用其他源文件中的函数、变量,甚至是其他库、系统的函数,我只需要#include相关头文件即可。如果我想在另一个源文件中继续使用,那就再添加#include相关的代码。需要注意的是要避免同一个头文件被重复包含
#include <iostream>#include <stdio>

接下来再做一个测试:

如上图结构的文件,这次我手动把#include在源文件中展开:

如上图,这也是ok的,可以编译成功。这相当于:
  • main.cpp中首先添加了func()函数声明,然后在main()函数中调用了func()。

  • func.cpp中也添加了func()函数声明,同时给出了func()函数的定义。其实这里的声明可以不要了,直接给func()函数的定义。当然,你也可以声明多次。

那么main.cpp中能否也把func()声明删掉呢?

看来不行,报错说在该作用域内func没有声明。注意我这里是单独编译main.cpp,加上func.cpp也是一样的:

PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -o app main.cpp func.cppmain.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。所以可以认为“没有预处理过程”(实际上是有的,因为预处理过程还负责生成行号等等职责)。

那是编译过程出的错吗?不妨单独看看是否能够编译成功:

PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S main.cppmain.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。

可链接报错了:

PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S main.cppPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -c main.sPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -o app main.omain.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的输入:
PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S main.cppPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -c main.sPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S func.cppPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -c func.sPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -o app main.o func.oPS F:\Jungle\1.Program\4.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符号表中的内容:

PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S .\main.cppPS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -c main.sPS F:\Jungle\1.Program\4.C++\4.Compiler> nm main.o0000000000000000 b .bss0000000000000000 d .data0000000000000000 p .pdata0000000000000000 r .rdata$zzz0000000000000000 t .text0000000000000000 r .xdata                 U __main                 U _Z4funcv0000000000000000 T mainPS F:\Jungle\1.Program\4.C++\4.Compiler>

其中:U代表该符号在当前文件中是未定义的。如果在main.cpp中加上func()函数定义,再尝试上面步骤,得到:

0000000000000000 b .bss0000000000000000 d .data0000000000000000 p .pdata0000000000000000 r .rdata$zzz0000000000000000 t .text0000000000000000 r .xdata U __main0000000000000000 T _Z4funcv0000000000000002 T main

可以看到,符号_Z4funcv前面的标识变为T了,标识该符号位于代码段text section。

再跟下去就讲不完了。。。

回到题目上来,头文件是必须的吗?不是,头文件会在预处理阶段被展开。但头文件会我们编程带来极大便利,要使用某个函数、某个变量了,那就#include。本文只是就着这个问题,跟了下编译的过程,看看平常开发过程中遇到的编译报错“未定义的引用”、“未声明的变量”这些错误来源是哪原因是什么。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
#define命令的一些高级用法
C 中的头文件和源文件
undefined reference to" 问题解决方
gcc编译过程简述
C 全局和静态变量初始化顺序的研究
CMake实践(一)
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服