搜索
bottom↓
回复: 71

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

  [复制链接]

出0入296汤圆

发表于 2017-6-1 04:48:18 | 显示全部楼层 |阅读模式
本帖最后由 Gorgon_Meducer 于 2019-7-25 17:57 编辑


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


     认真说起来,头文件(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的工程 模块不好拆分的原因。一般来说,为了“降低”循环包含的风
险,同时又为了尊重常见模块封装的习惯,我们会人为的规定:

    - 模块内部的各类文件“允许”包含模块的接口头文件;
    - 模块内部的各类文件“应该”包含模块自己的配置头文件;
    - 除极少数情况外,系统中所有的配置头文件都“应该避免”包含任何街口头文件。


    简单的来说,这三条规则就是允许两个信息流单向的进行混合:也就是,配置头文件的信息可以单向的流向接口头文件;但反过来却绝对禁止,
这就从源头上极大的降低了发生“循环包含”的概率。但即便如此,还有另外一类问题单纯依靠拆分头文件是不能解决的,这就是头文件的“交叉引用”
问题。





To be continue...




相关文章


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


阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入296汤圆

 楼主| 发表于 2017-6-1 04:49:28 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2017-6-2 18:59 编辑


附录


    - Q1:为什么不能C语言头文件里面定义变量或者函数的实体?
    - Q2:为什么有的时候宏的先后顺序并不那么重要?
    - Q3:为什么可以在源代码的任意位置(另起一行后)定义宏,甚至是include别的头文件?

出0入296汤圆

 楼主| 发表于 2017-6-1 04:49:53 | 显示全部楼层
深更半夜,自占地板,虚位以待,何日更新……

出0入296汤圆

 楼主| 发表于 2017-6-1 04:50:38 | 显示全部楼层
深更半夜,自占地基,虚位以待,何日更新……

出5入0汤圆

发表于 2017-6-1 07:04:22 来自手机 | 显示全部楼层
大师好早啊

出85入85汤圆

发表于 2017-6-1 07:12:22 来自手机 | 显示全部楼层
大师好早啊,经常这么早吗?

出0入0汤圆

发表于 2017-6-1 07:26:55 来自手机 | 显示全部楼层
zwhzwh_11 发表于 2017-6-1 07:12
大师好早啊,经常这么早吗?

时间差,不在国内你懂的

出0入0汤圆

发表于 2017-6-1 07:44:34 来自手机 | 显示全部楼层
大师肉身翻墙了

出0入13汤圆

发表于 2017-6-1 08:25:31 | 显示全部楼层
又有新的内容更新了,期待

出190入0汤圆

发表于 2017-6-1 08:37:02 来自手机 | 显示全部楼层
期待大师的精彩内容!

出0入0汤圆

发表于 2017-6-1 09:14:55 | 显示全部楼层
何日更新啊何日更新

出0入0汤圆

发表于 2017-6-1 09:17:28 | 显示全部楼层

何日更新啊何日更新

出0入0汤圆

发表于 2017-6-1 09:34:37 来自手机 | 显示全部楼层
C++的才疼…

出20入62汤圆

发表于 2017-6-1 09:35:26 | 显示全部楼层
大师早上好~仰望一下~

出350入8汤圆

发表于 2017-6-1 09:37:17 | 显示全部楼层
大师早上好

出0入42汤圆

发表于 2017-6-1 09:52:12 来自手机 | 显示全部楼层
大师好早

出0入0汤圆

发表于 2017-6-1 10:19:31 | 显示全部楼层
前排占位

出0入0汤圆

发表于 2017-6-1 10:37:43 | 显示全部楼层
不错。。。
继续。。。

出0入0汤圆

发表于 2017-6-1 11:40:26 | 显示全部楼层
大师,你就凌晨发出这几句话,何日更新……

出0入8汤圆

发表于 2017-6-2 10:17:02 | 显示全部楼层
懂了,日后更新!

出0入93汤圆

发表于 2017-6-2 10:58:30 | 显示全部楼层

C++的疼什么,头文件的地位终于可以赶上C文件了,可以拥有自己的变量和代码了,虽然基本没人去用。
STL的代码都是写在头文件中的而且还不要扩展名;ATL就更变态了,变量定义都搞到头文件中了,C文件成了摆设。

出0入296汤圆

 楼主| 发表于 2017-6-2 16:03:46 | 显示全部楼层
takashiki 发表于 2017-6-2 10:58
C++的疼什么,头文件的地位终于可以赶上C文件了,可以拥有自己的变量和代码了,虽然基本没人去用。
STL的 ...

我其实不太敢评论C++,咱们就讨论C的好了,毕竟规则简单直接。

出425入0汤圆

发表于 2017-6-2 21:32:06 | 显示全部楼层
听大师讲课

出0入0汤圆

发表于 2017-6-2 22:25:47 | 显示全部楼层
听大师讲课

出10入10汤圆

发表于 2017-6-3 08:13:42 来自手机 | 显示全部楼层
期待新课程

出0入0汤圆

发表于 2017-6-3 14:55:01 | 显示全部楼层
mark, 等待大师更新

出0入0汤圆

发表于 2017-6-3 17:57:11 | 显示全部楼层
搬凳子听大师讲课。。。

出0入0汤圆

发表于 2017-6-4 10:28:21 | 显示全部楼层
期待大师更新......

出0入0汤圆

发表于 2017-6-4 10:54:51 来自手机 | 显示全部楼层
这种进价知识确实需要认学习真

出0入0汤圆

发表于 2017-6-6 15:48:43 | 显示全部楼层
谢谢,期待后续更新。

出0入0汤圆

发表于 2017-6-6 18:02:24 | 显示全部楼层
A1:在头文件定义,会造成多重定义;
A2:编译器会把宏展开,再编译,所以顺序无关吧;
A3:宏和include都是直接替换,故无所谓位置(只要编译过);

出0入0汤圆

发表于 2017-6-6 19:35:29 来自手机 | 显示全部楼层
谢谢,期待大师后续更新

出0入0汤圆

发表于 2017-6-6 22:31:47 来自手机 | 显示全部楼层
谢谢,期待后续更新...

出0入0汤圆

发表于 2017-6-7 13:20:25 | 显示全部楼层
新帖,顶一下。。学习了~

出50入0汤圆

发表于 2017-6-7 13:25:27 来自手机 | 显示全部楼层
大师级的营养,顶

出0入296汤圆

 楼主| 发表于 2017-6-7 22:03:37 | 显示全部楼层
gujiamao_love 发表于 2017-6-6 18:02
A1:在头文件定义,会造成多重定义;
A2:编译器会把宏展开,再编译,所以顺序无关吧;
A3:宏和include都是 ...

恭喜您,答对了!

出0入0汤圆

发表于 2017-6-14 08:43:14 | 显示全部楼层
Gorgon_Meducer 发表于 2017-6-1 04:49

附录


- Q1:为什么不能C语言头文件里面定义变量或者函数的实体?

平常是这么用的,也不知道原因

出0入296汤圆

 楼主| 发表于 2017-6-14 16:13:48 | 显示全部楼层
soga238 发表于 2017-6-14 08:43
平常是这么用的,也不知道原因

平常如何用的?把变量和函数的“实体”放在了头文件里?

出0入0汤圆

发表于 2017-6-16 17:17:02 | 显示全部楼层
Gorgon_Meducer 发表于 2017-6-14 16:13
平常如何用的?把变量和函数的“实体”放在了头文件里?

偶尔函数里放 #include, 只知道可以放,不知道原因

出0入296汤圆

 楼主| 发表于 2017-6-19 15:49:12 | 显示全部楼层
soga238 发表于 2017-6-16 17:17
偶尔函数里放 #include, 只知道可以放,不知道原因

现在应该知道了吧?在预编译阶段就够已经干掉了。到实际编译的时候已经被替换成对应的内容了(文本)

出0入296汤圆

 楼主| 发表于 2017-6-29 18:57:25 | 显示全部楼层
今日缓慢更新……

出40入42汤圆

发表于 2017-6-29 19:53:59 | 显示全部楼层
大师难得露一次脸

出0入296汤圆

 楼主| 发表于 2017-6-29 23:52:39 | 显示全部楼层

因为懒了……

出0入0汤圆

发表于 2017-7-4 17:16:16 | 显示全部楼层

出0入0汤圆

发表于 2017-7-14 17:26:02 | 显示全部楼层

你好,我在参考key_queue.c改了一个消息的队列(meg_queue.c),在主循环里面不断调用meg_dequeue获取消息时,串口接收的数据有丢失(串口接收是采用中断的方式)。我想应该是meg_dequeue里面的原子保护(SAFE_ATOM_CODE())导致了串口接收数据丢失。请问这种方式用什么好的解决方案吗?

出0入296汤圆

 楼主| 发表于 2017-7-14 17:32:19 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2017-7-14 17:35 编辑
cgbabc 发表于 2017-7-14 17:26
你好,我在参考key_queue.c改了一个消息的队列(meg_queue.c),在主循环里面不断调用meg_dequeue获取消息 ...


在你猜想原子保护导致丢失的时候,你评估过你保护的时间有多长么?
可以用IO口配合逻辑分析仪或者示波器来测量。一般只是保护队列的出入队操作是不会有这个问题的。
因为UART是一个低速设备,假设是9600波特率,意味着平均1ms才会有一个字符,如果你是115200波特率,
意味着平均80us才有一个字符,80us意味着什么?意味着,如果你系统频率是12M,你可以跑80*12=960个
指令周期,你什么队列操作要960个指令周期(关闭中断响应),如果你对960个指令周期没概念,假设2个
指令周期一个指令(这已经是比较差的情况了,大部分操作1个指令周期一个指令),一个指令2个字节,
也就是说,在你接收指令的间隔,差不多能运行0.9K的代码?你关闭终端响应的时候执行了0.9KB的代码?

尽可能保证中断做的事情最小。尽可能保证原子操作只保护最小的范围——比如出入队的那个部分。

显然问题不在这里,你好好找一找吧。做事情不要想当然,要定量分析。

出0入0汤圆

发表于 2017-7-14 22:37:01 | 显示全部楼层
本帖最后由 cgbabc 于 2017-7-14 22:38 编辑
Gorgon_Meducer 发表于 2017-7-14 17:32
在你猜想原子保护导致丢失的时候,你评估过你保护的时间有多长么?
可以用IO口配合逻辑分析仪或者示波器 ...


谢谢你的回答,我会再去重新分析是那里出了问题。你说的尽可能的保证原子操作只保护最小的范围,我把代码改成以下,你看行吗?

bool KEY_DEQUEUE(key_queue_t* ptQueue, key_event_t* ptKey)
{
        CLASS(key_queue_t) *ptQ = (CLASS(key_queue_t) *)ptQueue;
    bool bReturn = false;
        
        if ((NULL == ptQueue) && (NULL == ptKey)) {
                return false;
        }
        
        if(0 != ptQ->hwLength)//!< the queue empty
          {                 
           SAFE_ATOM_CODE(
            //! read date form queue head
            *ptKey = ptQ->ptBuffer[ptQ->hwHead];        
            ptQ->hwLength--;                    //!< update length        
            if(ptQ->hwSize == ++ptQ->hwHead){   //!< update queue head pointer
                 ptQ->hwHead = 0;
            }                     
            bReturn = true;                     //!< update return value
            );
        }

               
        return bReturn;        
}

出0入0汤圆

发表于 2017-7-14 22:39:39 | 显示全部楼层
原来来的代码是这样的:
bool KEY_DEQUEUE(key_queue_t* ptQueue, key_event_t* ptKey)
{
        CLASS(key_queue_t) *ptQ = (CLASS(key_queue_t) *)ptQueue;
    bool bReturn = false;
       
        if ((NULL == ptQueue) && (NULL == ptKey)) {
                return false;
        }
       
    SAFE_ATOM_CODE(
        if(0 != ptQ->hwLength){                 //!< the queue empty
            //! read date form queue head
            *ptKey = ptQ->ptBuffer[ptQ->hwHead];        
            ptQ->hwLength--;                    //!< update length        
            if(ptQ->hwSize == ++ptQ->hwHead){   //!< update queue head pointer
                 ptQ->hwHead = 0;
            }                     
            bReturn = true;                     //!< update return value
        }
    );
               
        return bReturn;       
}

出0入296汤圆

 楼主| 发表于 2017-7-15 06:46:28 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2017-7-15 06:48 编辑
cgbabc 发表于 2017-7-14 22:39
原来来的代码是这样的:
bool KEY_DEQUEUE(key_queue_t* ptQueue, key_event_t* ptKey)
{


看了你的代码,我觉得丢数据不是原子操作的原因。你先检查SAFE_ATOM_CODE宏的实现是否
正确。如果没有问题,那可能是别的原因。因为这个队列操作本身占用时间并不长。

另外,你一个串口消息……用key_event_t做什么?让我很怀疑你中断处理程序里面都在做什么?
不应该是做一个普通的字节队列就行了么?你究竟想实现什么功能?

出0入0汤圆

发表于 2017-7-15 15:06:11 | 显示全部楼层
Gorgon_Meducer 发表于 2017-7-15 06:46
看了你的代码,我觉得丢数据不是原子操作的原因。你先检查SAFE_ATOM_CODE宏的实现是否
正确。如果没有问 ...

上面的代码只是示意而已,并没有在串口接收中断里面使用它。

出0入296汤圆

 楼主| 发表于 2017-7-17 19:11:23 | 显示全部楼层
cgbabc 发表于 2017-7-15 15:06
上面的代码只是示意而已,并没有在串口接收中断里面使用它。

你提供的信息太少,我不知道怎么帮你。

出0入0汤圆

发表于 2017-7-17 20:36:05 | 显示全部楼层
Gorgon_Meducer 发表于 2017-7-17 19:11
你提供的信息太少,我不知道怎么帮你。


谢谢,我自己再去找找问题吧(其实是我自己表达能力不够)。其实自己是一直不知道如何能构建一个好一点的程序架构,老是程序改来改去的,很烦。一直期望你能出一本这类的书籍,但也知道你忙,一直等待中。有时间的时候就看看你以前写的帖子,把它们理解透彻。有时间就读那本OOPC那本书,但我感觉那本书,没你写的那些结构好用。摸索中前进....

出0入0汤圆

发表于 2017-7-17 20:40:53 | 显示全部楼层
自己一直在思考,如何把延时,按键,串口,SPI,TIMER这些功能都做一些比较好用的模块,有新的项目的时候,直接调用就可以了。但以我现在的水平,还是没有达到那个水准。自己努力学习中....

出0入296汤圆

 楼主| 发表于 2017-7-17 21:10:27 | 显示全部楼层
cgbabc 发表于 2017-7-17 20:36
谢谢,我自己再去找找问题吧(其实是我自己表达能力不够)。其实自己是一直不知道如何能构建一个好一点的 ...

那你需要持续关注这个帖子,这个帖子介绍的内容,并不直接解决你的问题,但是却是解决你的问题
所必须的工具。因为这个帖子介绍模块封装的最基本技术(不是技巧,是技术)

出0入0汤圆

发表于 2017-7-17 21:15:20 | 显示全部楼层
Gorgon_Meducer 发表于 2017-7-17 21:10
那你需要持续关注这个帖子,这个帖子介绍的内容,并不直接解决你的问题,但是却是解决你的问题
所必须的 ...

是的,一直关注中.....

出0入0汤圆

发表于 2017-7-17 21:22:00 | 显示全部楼层
留个名,一直这么做的

出0入0汤圆

发表于 2017-9-11 15:10:29 来自手机 | 显示全部楼层
Gorgon_Meducer 发表于 2017-7-17 21:10
那你需要持续关注这个帖子,这个帖子介绍的内容,并不直接解决你的问题,但是却是解决你的问题
所必须的 ...

持刀等待更新

出0入76汤圆

发表于 2017-9-11 15:52:10 | 显示全部楼层
不错,貌似我已经在遵从大师的这种规则了

出0入296汤圆

 楼主| 发表于 2017-10-6 16:52:39 | 显示全部楼层
不好意思,最近有点忙,随后将会回复更新。

出0入0汤圆

发表于 2018-11-12 10:00:48 | 显示全部楼层
大师,这个帖子有后文吗?是不是在别的地方更新了?

出0入296汤圆

 楼主| 发表于 2018-11-12 20:17:15 | 显示全部楼层
LB342342 发表于 2018-11-12 10:00
大师,这个帖子有后文吗?是不是在别的地方更新了?

还在连载,最近比较忙……

出0入0汤圆

发表于 2018-11-12 22:05:02 | 显示全部楼层
傻孩子的很多连载都受益匪浅,唯一不习惯的是学起来太费劲---封装得太深了,要一一展开才能窥视一二

出0入0汤圆

发表于 2018-11-12 22:38:52 | 显示全部楼层
似乎明白了一些,又似乎不大明白。
请教大师:
“头文件里坚决不要放全局变量有关的任何东西”
BSP中要使用的全局变量,我放在一个.c中,然后在对应的.h中放它们的extern ****;
需要使用的其他.c直接包含这个全局变量头文件。这种方式合理么?

“隶属于它自己的接口头文件(Output)和配置头文件(Input)永远不要同时包含(include)在当前的C源文件中。”
以串口为例,对于硬件的描述在HwInfo.h中,Uart.c中包含HwInfo.h,Uart.h包含对外接口函数。
但某些时候,比如定义某个串口的初始波特率为9600,此时如果不包含Uart.h,
那么这个define就必须放在c代码中,这样似乎也不妥当。

出0入296汤圆

 楼主| 发表于 2018-11-13 06:36:17 | 显示全部楼层
fengxin32 发表于 2018-11-12 22:38
似乎明白了一些,又似乎不大明白。
请教大师:
“头文件里坚决不要放全局变量有关的任何东西”

关于你说的第一个问题,从语法的角度来说是没有问题的;但是从我强调的规范:
“头文件里坚决不要放全局变量有关的任何东西”来说就是错的。我觉得是我没说
清楚,其实我想说的是,就根本不要用全局变量,而不是全局变量放哪里的问题。

所有要用全局变量的场合,都可以用static变量+对变量进行读或者写的api来替换。

对于第二个问题,我觉得你需要详细描述一下,为什么你觉得“这样不妥”。因为
我看不出任何不妥,原本波特率这种东西,就是应该通过配置头文件传入进来的,
属于输入信息——配置头文件是可以被接口头文件包含的,也可以被.c包含,所以
确定波特率的宏定义应该放在配置头文件里,然后覆盖usart.h和usart.c。

出0入0汤圆

发表于 2018-11-16 10:54:16 | 显示全部楼层
我原来就是把内部的外部的函数声明一股脑全放进.h文件,随着程序量的增大,越来越混乱。后来试着开始分开。看了大师的建议,一下子就明朗了。
全局变量使用一个对外的函数来调用获取,确实的就不需要全局变量了。
下面的程序就是将定时器作为电机编码器的输入,定义内部静态变量pulsH为电机绝对位置。外部可以通过函数ReadEncoder()来获取电机的绝对位置。
/* pulsH为编码器技术溢出后的累加值 */
static  volatile int32_t        pulsH;
……
/**
  * @brief  读取编码器的数据
  * @param  无
  * @retval  经过计算后的编码器的数据
  */
int32_t ReadEncoder(void)
{
        return pulsH + (uint32_t)(__HAL_TIM_GET_COUNTER(&htim2));//获取定时器的值
}
……
/**
  * @brief 定时器中断回调函数调用,处理数据溢出
  * @param  无
  * @retval   中断后,根据计数器方向,更新高位数据
  */
void pulsH_exe(TIM_HandleTypeDef *htim)
{
        /* 根据定时器的CR1的“DIR”判断计数器的方向,1为负向,0为正向。 */
        if( __HAL_TIM_IS_TIM_COUNTING_DOWN(htim) )
        {
                /* 65536为定时器重装载值+1 */
                pulsH =  pulsH - 65536;//__HAL_TIM_GET_AUTORELOAD ( htim ) ;
        }
        else
        {
                pulsH =  pulsH + 65536;//__HAL_TIM_GET_AUTORELOAD ( htim ) ;
        }
}

出0入296汤圆

 楼主| 发表于 2018-11-16 19:45:23 | 显示全部楼层
本帖最后由 Gorgon_Meducer 于 2018-11-16 19:48 编辑
LB342342 发表于 2018-11-16 10:54
我原来就是把内部的外部的函数声明一股脑全放进.h文件,随着程序量的增大,越来越混乱。后来试着开始分开。 ...


没错,很高兴能帮助到你。
我觉得你已经注意到了,用静态变量+API的方式可以很好的控制外部对静态变量的访问权限。
相当于你可以控制别人对变量的访问是:只读,只写,还是读写都可以。另外,由于是通过
函数的,所以可以进行必要的有效性检测;也避免了过去那种,不知道哪里访问了全局变量
带来的移植和修改问题。

本质上,这就是面向接口开发。这是一个好的开始。

提个思考题:有人说,给静态变量提供API,还要专门写个函数,是不是增加了系统开销,
或者降低了效率呢?

我先给结论,剩下你自己去寻找答案:

结论:提供API在效率上并没有任何牺牲,甚至代码尺寸还更小了……如果用inline的话,
         基本上效果跟过去直接用全局变量是一样的(尺寸和效率都不会变化)。

出0入296汤圆

 楼主| 发表于 2019-7-25 17:57:38 | 显示全部楼层
更新模块内头文件的包含原则。

出0入0汤圆

发表于 2019-7-25 23:05:20 | 显示全部楼层
等待更新!

出0入4汤圆

发表于 2019-7-26 17:16:44 | 显示全部楼层
留名仔细研究

出0入0汤圆

发表于 2019-7-30 09:00:43 | 显示全部楼层
向大神学习。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-3-29 07:04

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表