一、背景:
自从接触单片机编程以来,由于工作上的需要,不可避免的时常会接手别人的代码,但常常由于上一位同事的编码随意性有点大,导致可读性非常的差,有时候不得不完全舍弃原有代码,推倒重来,无形中增加了工作量,浪费了宝贵的开发时间。因而也越来越觉着规范的代码才是提高工作效率的重要保证,不仅仅是为了别人,也为自己的日后维护带来十足便利。
因此本文即挑某一个方面:下位机多个".c, .h"文件的相互包含及排版做一个记录,提示自己,也为新加入的朋友们做个参考。
二、正文:
以我现在正在开发的电源管理项目为例,项目需求既是通过I2C与某电脑主板通信并按要求对主板的供电进行管理。上一位同事也许由于时间仓促,只使用了一个"main.c"文件来实现了部分功能,造成的后果既是删改功能很难定位到相应位置并难以厘清相关联函数间的关系。
此项目共有几大模块——主循环模块,与主板通信的IIC/UART模块,读取拨码开关/button按键模块,存储flash模块,电池管理模块。以及电源管理模块。每个模块既是一个".c,.h"文件,现在着重记录多个".c.h"相互关联、相互包含的解决办法。
为了便于管理,该项目文件夹内,我分了以下几个文件夹:"PROJ"存放工程文件,"APP"存放"main.c"、"aplication.c"文件,"BASEDRIVE"存放的"Uart.c"文件,"EXTIDRIVE"存放的是"Flash.c","DOC"存放的是"Readme"文件,对应".h"文件存放在对应文件夹。
包含头文件时可写绝对路径:譬如"#include XXX/EXTIDRIVE/'Flash.h'",但过于麻烦,也可将头文件所在的目录设置进编译软件,
a)IAR的设置方法:
在工程文件名右键,选择"Option"-->"C/C++complier"-->"Proprocessor"-->"Additonal Include directories"。
b)KEIL的设置方法:
“Project”-->”Option for Target XXX”-->”C/C++”-->”Include Paths”。
这样直接写相对路径:"#include 'Flash.h'",头文件也可被编译器找到。
前期铺垫这么多,现在开始进入正题:“多个.c文件相互包含.h文件该做如何处理”?
在每个”.c”文件内声明变量与函数,某些变量/函数如果会被其它”.c”文件调用,则在对应的”.h”文件extern 该变量/函数,若有文件要使用这些变量/函数,只需包含其头文件即可。宏定义可直接在头文件内定义。
以该电源管理项目为例:在”main.c”文件中,我会声明一些基本上会被许多模块使用或者更改的变量,譬如主板的状态标志位,是开机,关机还是休眠,此状态由拨码开关/button,IIC/UART等模块共同更改,所以需要在”main.h”中extern该变量/函数,然后让所有文件包含该头文件;”main.h”的宏定义一般也是基本上所有模块均会使用的宏定义。又譬如”Uart.c”这个文件内,有一个标志位”uartdone_flag”,一个函数”myprintf(*string);前者不会有其它文件调用,我只在”Uart.c”文件声明即可,但函数”myprintf(*string)”一定会被其它文件调用,所以这个函数不仅仅在”Uart.c”声明,还需要在”Uart.h”中external出来,若有文件需要调用该函数,只需包含”Uart.h”即可。
再来说说为何不直接在头文件内声明,而是需要extern关键词,若是直接在头文件内声明,两个”.c”文件同时包含该头文件,在编译时会出现重复定义的错误,加了”#ifndef”之类的条件编译也没有效果,所以为了避免该错误的发生,我使用了extern的关键词。至于为什么,还得待有时间去深究编译器原理。
/* 此处更改一点新发现。 2016年5月14日
* 在跟读keil的官方例程发现其头文件并没有使用“extern”关键字,却也能让多个“.c”包含它
* 而不出现重复定义的错误。仔细观察才发现,其头文件内并没有对变量进行 声明,而只是
* 单纯的声明函数。若是在“.h”文件内声明一个变量,那么就会出现重复定义的错误。
* 所以,按照这个现象,可以得出,在keil以及IAR的编译器内,头文件出现了变量的声明即
* 不可被多个“.c”文件包含。若是需要被多个“.c”文件包含,则变量请在.c文件内声明,变量
* 需要被外部文件调用,则使用“extern”关键字即可。
*
* 为什么会出现这种情况?以下是我的猜想,猜想而已,有错即欢迎留言指正。
* 当编译器在头文件内碰到变量声明时,则为该变量分配了一个实体地址;由于第二个“.c”
* 文件又包含了该头文件,编译器再次链接到该头文件(#ifndef 真的没起作用 -_-! )
* 并再次碰到该变量,正要为其分配实体地址时,才发现该变量已经分配了地址了,也许是
* 该编译器不够智能,不知将其分配到已分配到的地址,而是直接报错。(在linux系统下,
* 已经证实,gcc会智能将其分配到原先地址不报错。)
* 而若是函数声明呢?函数声明而已,不用暂实体地址,函数的实体地址在“.c”文件内才会
* 去分配实体地址,所以函数声明不会造成重复定义的错误。
*
* 另外一个需要注意的既是:要被多个“.c”文件包含的头文件,一定不能在该头文件内对变
* 量进行赋值,无论keil/IAR/gcc,均会产生重复定义的错误
*/
/* 更新一次关于结构体的声明方式 2016年5月26日
* 昨天在“.c”文件内,typedef了一个结构体,XXX_Struct。接着在“.c”文件定义了一个
* 结构体实体 XXX_Struct Test_Struct; 然后由于其它文件会调用该结构体,因此
* 我在“.h”文件“extern”了该结构体,但是在编译时却出现了Test_Struct undefined.
* 意味着,未认出该结构体。将“.h”文件内的extern删除,就没在报错了,可没有
* 了“extern”,结构体该如何被其它文件引用呢。
* 之后,通过朋友提示,将typedef放到“.h”文件内,然后加入extern,编译通过,也可
* 被其它文件调用。
* 为什么会这样呢?其实仔细想想,是自己没有弄明白typedef的真实含义罢了。
* typedef相当于声明了一种叫XXX_Struct的结构体的类型,本身并不会占用内存空间,
* 在“.h”文件内定义,即便被多个“.c”文件包含,也就不会报重复定义的错。所以其可在
* “.h”文件内声明定义。
* 但是,XXX_Struct Test_Struct; 就是在声明一个实体结构体变量了,是需要分配
* 空间的,因此,其不能在“.h”文件内定义,而应该是extern,在“.c”文件内定义。
*/
/* 更新一次关于 “extern”的问题 2017年2月7日
* 感谢杨旭礼(http://www.cnblogs.com/yangxuli/p/6438144.html)提供了这个思想。
* 即为了防止包含头文件过于繁杂,那么将所有其他文件需要用的变量在一个头文件
* 内“extern” 出来,那么要用外部变量的文件只需要包含这一个头文件即可,宏定义
* 也可以单独用一个头文件来定义。
*/
以下为实际效果对比图:
记录地点:深圳WZ
记录时间:2016年5月6日