vlambda博客
学习文章列表

C语言编程规范-头文件(Day2)

1 头文件


背景

对于C语言来说,头文件的设计体现了大部分的系统设计。不合理的头文件布局是编译时间过长的根本原因,不合理的头文件实际上是不合理的设计。


术语定义

依赖:本章节特指编译依赖。若x.h包含y.h,则称作x依赖y。依赖关系会会进行传导,如x.h包含y.h,y.h又包含了z.h,则x通过y依赖了z。虽然依赖是不可避免的,也是必须的,但是不良的设计会导致整个系统的依赖关系无比复杂,使得任意一个文件的修改都要重新编译整个系统,导致编译时间剧增。

在一个设计良好的系统中,修改一个文件,只需要重新编译数个,甚至是一个文件。

头文件依赖:若包含头文件aa.h,则就引入了新的依赖:一旦aa.h被修改,任何直接和间接包含aa.h代码都会被重新编译。如果aa.h又包含了其它头文件如bb.h,那么bb.h的任何改变都将导致所有包含了aa.h的代码被重新编译,在敏捷开发方式下,代码被频繁构建,漫长的编译时间将极大的阻碍频繁构建。因此,我们倾向与减少包含头文件,尤其是在头文件中包含头文件,以控制改动代码后的编译时间。


原则1.1 头文件中适合放置接口的声明,不适合放置实现。

说明:头文件是模块(Module)或单元(Unit)的对外接口头文件应放置对外部的声明,如对外部提供的函数声明、宏定义、类型定义等。

内部使用的函数(相当于类的私有方法)声明不应该放在头文件中。

内部使用的宏、枚举、结构定义不应放入头文件中。

变量定义不应放在头文件中,应放在.c文件中。

变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或但单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。即使必须使用全局变量,也应只在.c中定义全局变量,在.h中仅声明变量为全局的。


原则1.2 头文件应当职责单一

说明:头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。这样会导致某些头文件可能被不必要的展开很多次,大大增加了工程的编译时间。


原则1.3 头文件应向稳定的方向包含。

说明:头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

就我们的产品而言,依赖的方向应该是:产品依赖于平台,平台依赖于标准库。如果产品线平台的代码中已经包含了产品的头文件,导致平台不发单独编译、发布和测试,是非常糟糕的。

除了不稳定的模块依赖于稳定的模块外,更好地方式是两个模块共同依赖于接口。这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

推荐开发人员使用“依赖倒置”原则,即由使用者制定接口,服务提供者实现接口。


规则1.1 每一个.c文件应有一个同名.h文件,用于声明需要对外公开的接口。

说明:如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

示例:对于如下场景,如在一个.c中存在函数调用关系:

void foo()

{

    bar();
{


void bar()

{

    Do something;

}


必须在foo之前声明bar,否则会导致编译错误。

这一类函数声明,应当在.c的头部声明,并声明为static的,如下:

static void bar();


void foo()

{

    bar();

}


void bar()

{

    Do something;

}


规则1.2 禁止头文件循环依赖。

说明:头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h、c.h的源代码重新编译。


规则1.3 .c/.h文件禁止包含用不到的头文件。

说明:很多系统中头文件包含关系复杂,开发人员为了省事起见,不会一一钻研,直接包含一切想到的头文件,导致整个系统的编译时间进一步恶化,并对后期维护造成麻烦。


规则1.4 头文件应当自包含。

说明:简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户添加不必要的负担。

示例:

如果a.h不是自包含的,需要包含b.h才能编译,会带来危害:

每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。

额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。


规则1.5 总是编写内部的#include保护符(#define保护)。

说明:多次包含一个头文件可以通过认真的设计来避免。如果做不到这一点,就要采取阻止头文件内容被包含多于一次的机制。

通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。

所有的头文件都应当使用#define放置头文件被多重包含,命名格式为FILENAME_H,为保证唯一性,更好地命名是PROJECTNAME_PATH_FILENAME_H.

注:没有在宏的最前面加上“_”,即使用FILENAME_H代替_FILENAME_H,是因为一般以“_”和“__”开头的标识符为系统保留或标准库使用,在有些静态检查工具中,若全局可见的标识符以“_”开头会给出报警。

定义包含保护符时,应该遵循如下规则:

1)保护符使用唯一名称;

2)不要再受保护部分的前后放置代码或注释。

示例:假定VOS工程的timer模块的timer.h,其目录VOS/timer/timer.h,应按如下方式保护:

    #ifndef VOS_INCLUDE_TIMER_TIMER_H

    #define VOS_INCLUDE_TIMER_TIMER_H

    ...

    #endif

也可以使用如下简单方式保护:

    #ifndef TIMER_H

    #define TIMER_H

    ...

    #endif

例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。


规则1.6 禁止在头文件中定义变量。

说明:在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。


规则1.7 只能通过包含头文件的方式使用其他.c提供的接口,禁止在.c中通过extern的方式使用外部函数接口、变量。

说明:若A.c使用了B.c定义的foo()函数,则应当在B.h中声明extern int foo(int intput);并在A.c中通过#include <B.h>来使用foo。禁止通过在A.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。


规则1.8 禁止在extern “C”中包含头文件。

说明:在extern “C”中包含头文件,会导致extern “C”嵌套,Visual Studio对extern “C”嵌套层次有限制,嵌套层次太多会编译错误。

示例:错误的使用方式:

    extern "C"

    {

    #include "xxx.h"...

    }

正确的使用方式:

    #include "xxx.h"

    extern "C"

    {

    ...

    }


建议1.1 一个模块通常包含多个.c文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每个模块提供一个.h,文件名为目录名。

说明:需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。


建议1.2 如果一个模块包含多个子模块,则建议每个子模块提供一个对外的.h,文件名为子模块名。

说明:降低接口使用者的编写难度。


建议1.3 头文件不要使用非习惯用法的拓展名,如.inc。


建议1.4 同一产品统一包含头文件排列方式。

说明:常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。

示例1:

以升序方式排列头文件可以避免头文件被重复包含,如:

    #include <a.h>

    #include <b.h>

    #include <c.h>

    #include <d.h>

示例2:

以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面,如下:

    #include <product.h>

    #include <platform.h>

相对来说,product.h的修改较为频繁,如果有错误,不必编译platform.h就可以发现product.h的错误,可以部分减少编译时间。