前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >头文件是必须的吗?跟一跟编译过程~~~

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

作者头像
用户6557940
发布2022-07-24 16:51:55
1.6K0
发布2022-07-24 16:51:55
举报
文章被收录于专栏:Jungle笔记Jungle笔记

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

代码语言:javascript
复制
PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -S .\main.cpp
PS F:\Jungle\1.Program\4.C++\4.Compiler> g++ -c main.s
PS F:\Jungle\1.Program\4.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:\Jungle\1.Program\4.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。本文只是就着这个问题,跟了下编译的过程,看看平常开发过程中遇到的编译报错“未定义的引用”、“未声明的变量”这些错误来源是哪原因是什么。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-05-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Jungle笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档