认真说起来,头文件(Header File)是个短命的家伙——就整个编译过程来说,它的寿命是最短的。
为什么这么说呢?关于头文件的话题,讨论起来那可是“孩子没娘,说来话长了”,既然是闲聊、你也不
是等着这篇文章救命,那就不妨从头开始说起——先假设读者们都是不了解编译基本过程的初学者。
一个编译(Compilation)过程通常至少分为三个阶段:预编译(Precompiling)、编译(Make)和链接
(Linking)。他们就像一个流水线一环套一环——前一工序的输出是后一工序的输入。这本没有什么稀奇的,
但对于程序员来说,这个过程中有几个基本常识是需要记住的:
1. C语言编译的基本单位(Compilation Unit)是 C源文件(而并没有头文件)
2. 同一个工程中,不同C源文件的编译是彼此独立的(毫不相干的)
3. 头文件在预编译阶段就已经合并到对应的C源文件中了,和所有的宏以及条件编译一样,到了编译阶段,所有的头文件、宏都是不存在的,已经被替换为对应的内容和常量了。
理解这三点,基本上已经可以解决很多我们日常编码过程中存在的很多疑问,比如:
- Q1:为什么不能C语言头文件里面定义变量或者函数的实体?
- Q2:为什么有的时候宏的先后顺序并不那么重要?
- Q3:为什么可以在源代码的任意位置(另起一行后)定义宏,甚至是include别的头文件?
推荐大家基于前面的三个事实自己思考,答案在附录中介绍。
头文件里可以放什么呢?这是个值得讨论的问题:
- 各类宏
- 函数的声明(也就是 extern xxxxx)
- 全局变量的声明(也就是 extern xxxx)
然而,值得说明的是,这里有一个编码规则值得你去遵守:头文件里坚决不要放全局变量有关的任何东西(硬要加,也必须是const类型的,比如各类接口)。
- 类型定义(typedef, struct,union 之类的)
- static 的变量实体和函数实体。
这个可以有,为啥呢?因为即便多个c源文件包含同一个头文件导致同样的函数和变量实体存在多份,但
static 的另外一个名字 "private" 可以保证每一份变量和函数实体都是彼此独立的,都是每个c源代码的
私人财产——你可以有,我也可以有。“哎?你也有啊,真巧哎,我也有……”
- inline 的函数
这个和static是一个道理。
头文件里面不能放函数的实体,想必原因大部分人都知道了,这里就不再赘述。但头文件里不放(非const)的全局变量的声明,
这怎么玩?这里需要说明一下,头文件里不是不能放(非const)的全局变量声明,而是我提供了一个人为的规定(规范),建议
不要放任何(非const)的全局变量到头文件里,具体原因和解决方案,我们在别的帖子里再讨论(其实有人讨论过,大约就是,
如何避免使用全局变量)——是的,避免使用(非const)的全局变量是可以做到的——这里也不再赘述。说了这么多废话,我们
真正要讨论的内容还没有开始:
- 如何建立头文件的使用规则,使其即灵活、使用方便,又灵活且便于扩展(模块化)——符合面向接口开发的要求,方便我们
建立黑盒子?
简而言之,
- 如何让头文件的使用不再头疼;永远告别循环包含;方便代码的移植?
首先,思考一个简单的问题?为什么我们要用头文件?答案其实很简单,因为每个.c文件都是独立编译的,因此需要在源代码
级别传递一些信息,类似一群人在唠嗑:
源代码A: 我定义了一个函数,你们哥几个要用么?
源代码B和源代码C: 我们要用啊,函数原型(prototype)什么样子啊?
源代码A: 你们不用费脑经记(抄下来),我都写好了,放在一个头文件里了,你们直接include就可以了。
源代码B和源代码C: 这个敢情方便。那你头文件放哪里了?
源代码A: 有两种方式,要么你直接到我这里来拿(指定路径);要么你找编译器问(编译器指定搜索路径)。
源代码D: 你们整这么麻烦做什么?你直接告诉我原型,我抄下来,不就不用问这个问那个,还包含文件什么的,真麻烦。
源代码A: D啊,你老想耍小聪明,万一我更新了你不知道怎么办?我有义务告诉你么?并没有。
源代码B和源代码C: 是啊,是啊,A以后估计要外包了,不在这里了,到时候有变化,都记录在头文件里,你本地放一个,没法
及时同步的。
源代码D: 我不听!我不听!我不听……
是不是很有画面感?抛开捂着耳朵的D,我们回到讨论的话题——既然头文件是用来交换信息的,那么如果把所有的信息都放在一起,大家
需要的时候各取所需,岂不美哉?——基于这种思想,几乎所有人都见过把所有变量、函数、宏、类型定义都放到一个叫做system.h的头文件
里的做法。你有这么做过么?不要不好意思,几乎所有人都这么做过——因为实在太方便了,世界大同,挺好,直到你尝试和别人一起合作开发
系统,并试图在不同项目间复用一些代码的时候:
“何首乌藤和木莲藤缠络着”……对于这种情况,我们叫做耦合。“是要找个时间来理一理了”,你对自己说,然后长叹了一口气,发现这句话其
实很早之前就说过了。想到还有更奇葩的循环包涵的问题,你不得不感叹,头文件真的是个头疼的东西——要不我们还是不用了吧?直接抄下来
貌似更简单啊——源程序D痴痴的笑了。
那么,如何解决这个问题呢?其实,从实践经验来看,头文件的用途分为两大类:
站在C源文件的视角上:
- 从 外部向C源文件内部 输入配置信息——我们把这类头文件叫做配置头文件(Configuration HeaderFile)。
需要强调的是,信息的流动方向是 从外向内,所以又可以简单的理解为输入性的头文件(Header File for information input)。常见的app_cfg.h
就是典型的配置头文件。
- 从 C源文件内部向外 输出接口信息(全局函数、类型,宏定义等信息)——我们把这类头文件叫做接口头文件(Interface Header File)。
需要强调的是,信息的流动方向是 从内向外,所以又可以简单的理解为输出性的头文件(Header File for information output)。常见的, spi.h
usart.h, device.h, stdint.h 就是典型的接口头文件。
输入和输出两个不同的职能如果被放在同一个头文件里,就有极大的风险产生循环包含(两个相反方向的箭头产生闭合的圆圈)。system.h实际
上就是一个混淆信息流动方向的例子。这就是本质上依赖system.h的工程 模块不好拆分的原因。根据上述原理,这里引入头文件使用的第一条原则:
对一个C源代码来说,站在它的视角上,隶属于它自己的接口头文件(Output)和配置头文件(Input)永远不要同时包含(include)在当前
的C源文件中。
全部资料51hei下载地址:
头文件编写.docx
(332.33 KB, 下载次数: 52)
|