1.深入理解C/C++中头文件(.h)与源文件(.c/.cpp)以及我们为什么需要.h头文件

1.深入理解C/C++中头文件(.h)与源文件(.c/.cpp)以及我们为什么需要.h头文件

本篇文章灵感来自于:https://www.cnblogs.com/laojie4321/archive/2012/03/30/2425015.html

 

本文章中所有表情包均来自互联网,如有侵权,劳请联系。

 

另外,转载请标明出处哦,语雀:https://www.yuque.com/yifeideshijie

 

上面的文章很清楚地解析了在整个项目编译过程中的过程,头文件(通常的.h文件)和源文件(.cpp)文件是如何发挥作用的,但总体逻辑上有些混乱,我以问题的形式对上述文章中的答案进行总结:

 

上述文章主要回答了如下几个问题,这些问题如果学习过想Java这样语法规范规整的语言之后再学习C/C++就会很容易出现。

 

1.C/C++中的.h文件(头文件)和.c/.cpp(源文件)到底有什么联系和区别

 

首先明确.c和.h本质上没有任何区别。

只不过一般(为什么一般要这么做呢,别急,往下看到第3点就可以知道答案了):

.h文件是头文件,内含函数,变量声明,宏声明,结构体声明等内容,.c文件是程序文件,内含函数实现,变量定义等内容。

 

如果你非要将.h改成.ctest(随便写的)文件可以吗?

当然可以了,你还是想往常一样#include "filename.ctest"就行了。所以实际上你只需要保证有个main入口,在visual studio中你甚至不需要一定保证存在.cpp文件,后缀名都是人为规定的,程序照样可以运行。

 

所以,本质上讲,.h和.cpp文件没多大区别,不妨就把他们理解为文件。

 

(这里注意,在linux中用gcc时,会根据你的后缀名来调用相关的编译程序,这里为了以防万一,还是得按照规范来,至于这方面的深入探讨,这里不再做过多解释)

 

但对于Java而言,这几乎是不能忍受的,因为java代码必须要以.java为结尾才能编译。

 

这样分开写成两个文件是一个良好的编程风格。而java将这种分格进行强制化了。也就是说,为了延续以往C/C++程序员的既定编程习惯,作为后辈的我们也得遵守。

 

2.类比于java中每一个java文件中都得有一个与其文件名相同的公共类,头文件和其相应的源文件有名称上的联系吗?

 

正如1中所言,二者完全无关。

但你会发现,在很多的IDE中,当你创建一个类的头文件时,会自动创建一个同名的源文件,这只是一种方便快捷的方式,并没有语法意义在里面。

 

而在java的IDE中,自动创建与文件同名的公共类就是一种基于语法的强制行为了。

 

从上面可以看出,关于文件的定义及组织上讲,C/C++的语法正是太过于自由了,这就造成了任何人都可以写出让人难以捉摸,让人发狂的多层引用且毫无逻辑的代码,最重要的是,它绝不会引发编译链接报错。可想而知,在C/C++下,没有规范的代码组织会引发多么大的灾难。这或许也是为什么像Python这种将缩进都定义在语法规范中的代码为什么会引起众多的追捧了吧。大家受够了完全自由,无拘无束的生活。

 

 

3.既然如此,为什么我还要写头文件和源文件。(会很长)

这样的疑问在所难免,既然本质一样,给我个理由让我费劲巴拉地去设计.h文件把!

 

简单来说,理由就是我想偷懒。

???傻了吧!多写个文件还偷懒了???

 

image

正经一点!:

 

实际上,如果代码只有100行,你确实不需要头文件。

 

但,如果你要写3000行的代码,里面包含100个函数呢?你也要写在一个文件里?

 

如果你非要这么做,确实是可以的,但显然是不合适的。

 

那该怎么解决呢?

 

我肯定想把这些函数按照一定的逻辑给分成几组,这样我以后想要修改某个函数时,别人想要阅读代码时,就方便多了。我还可以把我用到的那些全局变量也分开按我觉得正确的逻辑存放。这样,代码就不会那么乱了。

 

没错,就是这样。

 

于是我把相关的,同类型的,我自己觉得相关的函数都分开放在了同一个源文件里,可是,我只有一个main入口,我怎么从包含main入口的源文件中读到其余文件的内容呢?

 

头文件包起来啊。在预处理阶段,不就是简单地吧#include 包含的文件都复制到该文件吗?

 

是的,但这样子似乎还是不需要.h文件,因为我只需要#include那些我想要的.c/.cpp文件不就行了。

 

确实如此,但在复杂情况下,我们会遇到一些情况:

如果我的头文件出现如下情况:
main.cpp:
#include "a.cpp"
#include "b.cpp"


而此时,在b.cpp中,也存在一个头文件包含:
b.cpp:
#include "a.cpp"

而a.cpp里面有了全局变量的定义。
a.cpp:
int c;


这个时候贸然编译运行main.cpp会怎么样?
会报错·!!!

 

为什么?因为预处理时a.cpp被复制了两次,一次是main.cpp自身复制a.cpp时,一次是在复制b.cpp时,如若a.cpp中有两次变量定义,那显然多次定义一定得报错吧,同样,函数多次同名同参定义也得报错。

一个问题,在整个编译过程中那个过程报错了呢?(关于整个编译过程分为哪些阶段可参照第2篇,
相似的问题还会在4中再次提到)

在编译阶段,出现同名同类型符号,编译出错

那怎么办?

 

好在C/C++还可以这样:多次声明

 

定义或许不能多次,但声明绝对可以。

 

既然是在编译阶段出了重复定义的错,我又不能不包含相关代码,那我干脆只包含相关函数,变量的声明不就行了。

注意,C/C++的编译阶段和我们认为的广义的编译生成可执行文件过程是存在区别的。

具体可以看第二篇文章。

 

没错,作为先驱的程序员们或许也是这么想的,于是乎,我只需要把a.cpp和b.cpp中的全局变量以及函数声明都写在main.cpp,再把a.cpp中的全局变量以及函数声明都写在b.cpp不就可以了?

 

是的,完全正确,这样就不需要担心编译阶段因为多次定义而报错了,而且我还能在链接阶段找到相应的重定向地址。

 

我真是个小聪明。

 

image

 

到此为止,我依然没有找到从良(误)使用.h作为头文件的理由。

 

但你似乎隐约觉得直接把声明语句复制粘贴多份放在每个源文件的开头这种做法有损自己优雅的智商,于是乎,你想到了如下的方法:

 

为什么我不把这些该死的多次用到声明写在一个.h文件里,这样每次只要#include这个.h文件不就行了。

 

哇哇哇,怎么会有这么聪明的小可爱啊啊,😫

image

至此,终于,我明白了为什么要用.h文件了。就是为了在代码复用时防止因重复定义而造成的错误,同时又能方便地调用这些声明不至于在编译阶段就犯错误,但,原因仅仅如此吗,当然不是,你可以在网上轻易找到其它优点,这里我简单列一下:

 

1.正如上面所言,头文件的富有逻辑的书写帮助整个工程更具有逻辑感,提高代码的可维护性。

 

2.防止因简单包含其他文件造成的多重定义报错问题

 

3.可以只将.h暴露给其他想要用你接口的用户,而不想对应的实现源码暴露给用户,你只需要提供相应的.h文件供他人#include,并提供相应的可重定向目标文件库就OK了。

 

好处这么多,那就勉为其难地用一下吧。

 

至此,你终于明白了为什么要用.h了,.h里面该写什么了。

 

来吧,如果你还想更加了解它,还得继续往下读。

 

4.如果我很傲娇,我可以偏要在.h里面也写上全局变量的定义而非仅仅是声明吗?

 

首先,如果下面有些关于编译阶段的内容不了解,可以看第2篇文章。

 

答案是,你最好不要,你要是还这么写,刚刚不是白看了吗,.h文件里面要是还写一大堆的定义,那不又退回到了普通源文件了吗?

 

这时,你斜嘴一笑,你想起来你之前学过,你可以在头文件里这样写:

#ifndef <标识>
#define <标识>
......
......
#endif

 

据说这样我就可以避免多次包含同一个文件而引发的多次定义错误了。

 

没错,当你遇到如下情况时:

main.cpp
#include<a.h>
#include<b.h>
.
.
.

a.h:
int c;

b.h:
#include<a.h>

这是编译main.cpp肯定报错,c被重复定义

如果这样:
main.cpp
#include<a.h>
#include<b.h>
.
.
.

a.h:
#ifndef A_H
#define A_H
int c;
#endif


b.h:
#include<a.h>

编译不会报错,因为a.h只会被复制粘贴到main.cpp一次,不会出现多次定义错误。

 

但是,魔高一尺,道高一丈,你或许解决了上面的问题,你想打破规则,非要在.h里写上定义,但,你能解决如下问题吗?

main.cpp:
#include<a.h>
#include<b.h>
.
.
.

main2.cpp:
#include<a.h>
.
.
.

a.h:
#ifndef A_H
#define A_H
int c;
#endif

b.h:
#include<a.h>

你的目的在于将main.cpp和main2.cpp最终链接起来,你会成功吗?

很遗憾,你不能?

 

image

 

为什么呢?

 

我们还是看一下你在那个编译过程中出错了,需要恭喜的是,在编译阶段,你顺利通过了,但在链接阶段,你出错了。

 

没错,加上#ifndef的方法只能保证单个文件在编译时防止因多次重复粘贴(预处理阶段)而导致多次定义的错误,但是,我这是多文件链接,在编译阶段结束后,会生成两个可重定向目标文件,后缀是.o,每个文件都有一个符号表,记录自己定义了哪些符号。main.o中有一个符号c,已经被定义,巧了main2.o里也有。这时,你把他们链接起来,连接器ld就犯糊涂了,这有两个c,怎么办?

 

报错呗,于是,错误产生了。

 

但要注意,和上面的3中的那个例子的错误不同,这里的错误发生在链接阶段,也就是说,每个源文件都没错,但他们冲突了。

 

所以,最好不要做出这样可能犯错误的事情哦。

 

但你又说了,我非要写定义在.h头文件里,而且我一定保证绝对正确,不会犯错。

 

当然可以,如果你的人生足够彪悍,你可以不做出任何解释做自己想做的。

image

 

 

 

 

 

 

 

于是,我发现,在.h头文件中,我一般会做如下的事情:函数,变量声明,宏声明,结构体声明等内容。

 

没错,基本全是声明,原因就是刚刚讲的。

 

 

5.尾声

好了,终于到最后了,你我终于送了一口气,但这才只是刚开始罢了,不仅以后的学习还长着,单就本篇文章的内容而言,也绝非全面,所以必然有疏漏错误之处,总之大家一起学习,互相指导喽。

好了,我现在终于可以稍微没有疑惑地在源程序开始打上#include了。

 

image

 

  • 48
    点赞
  • 117
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值