模块的封装(四):头文件的疼

出处:https://www.amobbs.com/thread-5675247-1-1.html

[交流][微知识]模块的封装(四):头文件的疼


     认真说起来,头文件(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 Header File)。
      需要强调的是,信息的流动方向是 从外向内,所以又可以简单的理解为输入性的头文件(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源文件中。



To be continue...


相关文章

 



    a. [交流][微知识]模块的封装(一):C语言类的封装
    b. [交流][微知识]模块的封装(二):C语言类的继承和派生
    c. [交流][微知识]模块的封装(三):无伤大雅的形式主义
 

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值