C语言的头文件组织与包含原则

网友投稿 552 2022-11-21

C语言的头文件组织与包含原则

说明本文假定读者已具备基本的C编译知识。

如非特殊说明,文中“源文件”指 * .c文件,“头文件”指 *.h文件,“引用”指包含头文件。

一、头文件作用C语言里,每个源文件是一个模块,头文件为使用该模块的用户提供接口。接口指一个功能模块暴露给其他模块用以访问具体功能的方法。

使用源文件实现模块的功能,使用头文件暴露单元的接口。用户只需包含相应的头文件就可使用该头文件中暴露的接口。

通过头文件包含的方法将程序中的各功能模块联系起来有利于模块化程序设计:

在预处理阶段,编译器将源文件包含的头文件内容复制到包含语句(#include)处。在源文件编译时,连同被包含进来的头文件内容一起编译,生成目标文件(.obj)。

如果所包含的头文件非常庞大,则会严重降低编译速度(使用GCC的-E选项可获得并查看最终预处理完的文件)。因此,在源文件中应仅包含必需的头文件,且尽量不要在头文件中包含其它头文件。

建议组织头文件内容时遵循以下原则:

4)头文件名应尽量与实现功能的源文件相同,即module.c和module.h。但源文件不一定要包含其同名的头文件。

#ifndef _PRJ_DIR_FILE_H //必须确保header guard宏名永不重名#define _PRJ_DIR_FILE_H//《头文件内容》#endif

使用#pragma once相比header guard具有两个优点:

更快。编译器不会第二次读取标记#pragma once的文件,但却会读若干遍使用header guard 的文件(寻找#endif);

缺点则是:

被extern “C”修饰的变量和函数将按照C语言方式编译和连接,否则编译器将无法找到C函数定义,从而导致链接失败。

10)头文件内要有面向用户的充足注释,从应用角度描述接口暴露的内容。

建议包含头文件时遵循以下原则:

1)源文件内的头文件包含顺序应从最特殊到一般,如:

#include “通用头文件” //内部可能定义本模块数据类型别名#include “源文件同名头文件”#include “本模块其他头文件”#include “自定义工具头文件”#include “第三方头文件”#include “平台相关头文件”#include “C++库头文件”#include “C库头文件”

2)减少头文件的嵌套和交叉引用,头文件仅包含其真正需要显式包含的头文件。

例如,头文件A中出现的类型定义在头文件B中,则头文件A应包含头文件B,除此以外的其他头文件不允许包含。

头文件的嵌套和交叉引用会使程序组织结构和文件组织变得混乱,同时造成潜在的错误。大型工程中,原有头文件可能会被多个其他(源或头)文件包含,在原有头文件中添加新的头文件往往牵一发而动全身。若头文件中类型定义需要其他头文件时,可将其提出来单独形成一个全局头文件。

3)头文件应包含哪些头文件仅取决于自身,而非包含该头文件的源文件。

例如,编译源文件时需要用到头文件B,且源文件已包含头文件A,而索性将头文件B包含在头文件A中,这是错误的做法。

4)尽量保证用户使用此头文件时,无需手动包含其他前提头文件,即此头文件内已包含前提头文件。

例如,面积相关操作的头文件Area.h内已包含关于点操作的头文件Point.h,则用户包含Area.h后无需再手动包含Point.h。这样用户就不必了解头文件的内在依赖关系。

5)头文件应是自完备的,即在任一源文件中包含任一头文件而不会产生编译错误。

6)源文件中包含的头文件尽量不要有顺序依赖。

7)尽量在源文件中包含头文件,而非在头文件中。且源文件仅包含所需的头文件。

如上,在OmciChkFunc函数的实现源文件内包含T_MeInfoMap和T_OmciMsg所在头文件即可。

另举一例如下:

如上,CompareRecFunc函数原型由其他头文件提供,此处为避免头文件交叉引用定义其异名同构原型CmpRecFunc。

在不会引起歧义的前提下,头文件内尽可能使用VOID指针代替非基本类型的值变量或指针,以避免再包含类型定义所在的头文件。但这将影响代码可读性并降低程序执行效率,应权衡利弊。

9)避免包含重量级的平台头文件,如windows.h或d3d9.h等。若仅使用该头文件少量函数,可extern函数到源文件内。如下:

/****************************************************************************************** 文件名称:Omci_Send_Msg.c* 内容摘要:OMCI消息转发接口* 其它说明: 该头文件封装SEND接口,以避免其他源文件包含支撑api和pid公共头文件导致引用混乱。 *****************************************************************************************/#include “Omci_Common.h”#include “Omci_Send_Msg.h”#include “oss_api.h”/********************************************************************************************** 函数实现区**********************************************************************************************///向自身进程发送异步消息INT32U OmciAsynSendSelf(INT16U wEvent, VOID *pvMsg, INT16U wMsgLen){ PID dwSelfPid = 0; SELF(&dwSelfPid); return ASEND(wEvent, pvMsg, wMsgLen, dwSelfPid);}

10)对于函数库(包括标准库和自定义的公共宏及接口)的头文件,可将其加入到一个通用头文件中。需要控制该头文件的体积(主要是该头文件所包含的所有头文件内容大小),并确保所有源文件首先包含该通用头文件。示例如下:

#ifndef _OMCI_COMMON_H#define _OMCI_COMMON_H/******************************************************************************************** 说明:* 本文件仅应包含与具体通信协议无关的通用数据类型及宏定义。* 为简化头文件包含且不失可移植性,本文件内可包含少量C库通用头文件。* 因本文件内定义基本数据类型别名,故.c文件中应将本头文件置于包含列表顶端,* 否则编译时可能产生类型未定义错误。*******************************************************************************************/#include #include #include #include #include #include “Omci_Byte.h”//

注意,示例头文件内包含C库文件虽能简化包含,但却与规则1冲突。也可另外增加包含库文件列表的通用头文件。

四、代码文件组织原则建议C语言项目中代码文件组织遵循以下原则:

1)使用层次化和模块化的软件开发模型。每个模块只能使用所在层和下一层模块提供的接口。

2)每个模块的文件(可能多个)保存在一个独立文件夹中。

模块文件较多时可采用子目录的方式,物理上隔离不同层次的文件。子目录下源文件和头文件应分开存放,如分别置入include和source目录。

3)用于模块裁减的条件编译宏保存在一个独立文件中,便于软件裁减。

4)硬件相关代码和操作系统相关代码与工程代码相对独立保存,以便于软件移植。

5)按相同功能或相关性组织源文件和头文件。同一文件内的聚合度要高,不同文件中的耦合度要低。

在对既有工程做单元测试时,耦合度低的文件布局非常便于搭建环境。

7)作为对外接口的头文件一经发布,应保持稳定。修改时一定要慎重。

8)文件夹和文件命名要能够反映出模块的功能。

9)正式版本和测试版本使用统一文件,使用宏控制是否产生测试输出。

10)必要的注释不可缺少。

五、 注解「【注1】全局变量的使用原则」

1)若全局变量仅在单个源文件中访问,则可将该变量改为该文件内的静态全局变量;

2)若全局变量仅由单个函数访问,则可将该变量改为该函数内的静态局部变量;

4)设计和调用访问动态全局变量、静态全局变量、静态局部变量的函数时,需要考虑重入问题。

「【注2】#pragma once的可移植性」

#ifndef由C/C++语言标准支持,不受编译器任何限制;而#pragma once仅由编译器提供保证,存在可移植性等问题。

某些gcc编译器版本(如3.2.3)会报告“warning: #pragma once is obsolete”的警告,而其他较老版本的编译器可能会报错。但随着gcc 3.4的发布,#pragma once中的一些问题(主要与符号链接和硬链接有关)得以解决,#pragma once命令也标记为“未废弃”。

还有种写法同时使用#pragma once和header guard编写“可移植性”代码,以利用编译器可能支持的#pragma once优化。如下:

#pragma once#ifndef _PRJ_DIR_FILE_H#define _PRJ_DIR_FILE_H//《头文件内容》#endif

该法似乎兼有两者的优点。但既然使用#ifndef就有宏名重名的风险,也无法避免不支持#pragma once的编译器告警或报错,故混用两种方法似乎不能带来更多的好处,反倒让不熟悉的人感到困惑。

注意,如果使用header guard,理论上可在代码任何地方判断当前是否已经包含某个头文件。但应避免通过该判断来改变后续代码的逻辑走向!

这种做法将使程序依赖于头文件的包含顺序,极不可取。若需要实现“若当前包含HeaderA.h,才加入StructB结构”,可对StructB结构创建HeaderB.h头文件,在HeaderA.h中包含HeaderB.h。

「【注3】extern “C”」

C++语言在编译时为实现函数重载,会结合函数名、参数数目及类型信息而生成一个中间函数名。

例如,C++中函数void foo(int x, float y)编译后在符号库中生成的名字为_foo_int_float(不同编译器可能生成不同函数名,但均采用相同机制,生成的新名字称为”mangled name”);而该函数被C编译器编译后在符号库中的名字为_foo。

当然编译器也可以为其他语言提供链接说明。例如:extern “FORTRAN”、extern “Ada”等。

该规则可提供高度的可移植性:它与ANSI/ISO C标准一致,同时也兼顾大多数ANSI前的编译器和链接器。(Unix编译器和链接器常使用允许多重定义的“通用模式”,只要保证最多对一处定义进行初始化即可。

通用扩展在《深入理解计算机系统》中解释为:多重定义的符号只允许最多一个强符号。函数和定义时已初始化的全局变量是强符号;未初始化的全局变量是弱符号。Unix链接器使用以下规则来处理多重定义的符号:

规则一:不允许有多个强符号。在被多个源文件包含的头文件内定义的全局变量会被定义多次(预处理阶段会将头文件内容展开在源文件中),若在定义时显式地赋值(初始化),则会违反此规则。

规则二:若存在一个强符号和多个弱符号,则选择强符号。

规则三:若存在多个弱符号,则从这些弱符号中任选一个。

当不同文件内定义同名(即便类型和含义不同)的全局变量时,该变量共享同一块内存(地址相同)。若变量定义时均初始化,则会产生重定义(multiple definition)的链接错误;若某处变量定义时未初始化,则无链接错误,仅在因类型不同而大小不同时可能产生符号大小变化(size of symbol `XXX‘ changed)的编译警告。

在最坏情况下,编译链接正常,但不同文件对同名全局变量读写时相互影响,引发非常诡异的问题。这种风险在使用无法接触源码的第三方库时尤为突出。

因此,应尽量避免使用全局变量。若确有必要,应采用静态全局变量(无强弱之分,且不会和其他全局符号产生冲突),并封装访问函数供外部文件调用。

typedef BOOL (*func)(const DefStruct *ptStrt); typedef struct DefStruct_t{ int i; func f;}DefStruct;

typedef struct DefStruct_t DefStruct;typedef BOOL (*func)(const DefStruct *ptStrt);struct DefStruct_t{ int i; func f;};

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:VC语言文件正文分析器--支持格式常用文件格式
下一篇:小试试vue#yyds干货盘点#
相关文章

 发表评论

暂时没有评论,来抢沙发吧~