C语言最佳实践
上QQ阅读APP看书,第一时间看更新

1.4.7 优雅编写条件编译代码

条件编译在C代码中十分常见。条件编译的常见用途有如下3种。

(1)当我们要将自己开发的软件运行在不同的操作系统中时,由于底层标准库在不同操作系统中的实现存在差异,我们需要使用条件编译来处理这种差异。

(2)类似地,当我们要针对不同的处理器或处理器架构编写特定的代码实现时,也需要使用条件编译。

(3)当我们要通过编译时配置选项来控制软件的功能(比如通过配置选项控制包含哪些功能模块)时,会经常使用条件编译。

如下代码来自MiniGUI的头文件,用于判断如何确定64位的整数数据类型,其中给出了条件编译的常见用法:

/* Figure out how to support 64-bit datatypes */
#if !defined(__STRICT_ANSI__)
#   if defined(__GNUC__)
#       define MGUI_HAS_64BIT_TYPE    long long
#   endif
#   if defined(__CC_ARM)
#       define MGUI_HAS_64BIT_TYPE    long long
#   endif
#   if defined(_MSC_VER)
#       define MGUI_HAS_64BIT_TYPE    __int64
#   endif
#endif /* !__STRICT_ANSI__ */
 
/* The 64-bit datatype isn't supported on all platforms */
#ifdef MGUI_HAS_64BIT_TYPE
typedef unsigned MGUI_HAS_64BIT_TYPE Uint64;
typedef signed MGUI_HAS_64BIT_TYPE Sint64;
#else
/* This is really just a hack to prevent the compiler from complaining */
typedef struct {
    Uint32 hi;
    Uint32 lo;
} Uint64, Sint64;
#endif

如上述代码所示,条件编译会严重割裂代码的连续性,从而极大破坏代码的可读性。因此,我们应该尽量避免使用条件编译。当然,由于C语言主要用来开发底层基础软件,因此C程序员难免会因为上面所说的3种用途而使用条件编译。为此,下面是4条可供C程序员参考的处理原则。

第一,使用恰当的注释说明条件编译代码块的作用,如下所示:

#ifdef foo
    ...
#else /* foo */
    ...
#endif /* not foo */
 
#ifdef foo
    ...
#endif /* foo */
 
#ifndef bar
    ...
#else /* not bar */
    ...
#endif /* bar */
 
#ifndef bar
    ...
#endif /* not bar */

上述代码在#else#endif代码行的末尾,使用了恰当的注释来说明条件编译代码块所对应的条件。

第二,在嵌套条件编译时,恰当地使用缩进来表示嵌套关系,如下所示:

#ifndef NULL
#   ifdef __cplusplus
#       define NULL            (0)
#   else
#       define NULL            ((void *)0)
#   endif
#endif  /* not defined NULL */

第三,避免使用过长的条件编译代码块,确保一个条件编译代码块不超过常见编辑器的最大行数(25~50行)。

第四,使用构建系统生成器提供的方法实现对软件功能的控制,避免使用条件编译。比如,在实现一个跨平台的文件系统操作接口时,需要考虑到不同操作系统之间(尤其是Windows操作系统和类UNIX操作系统之间)的巨大差异。这时,我们可以借助构建系统生成器,根据当前所要构建的目标系统,生成对应的构建文件,从而针对不同的目标平台编译不同的源文件。为此,我们可以将针对类UNIX操作系统(如Linux和macOS)的代码组织到一个源文件中,如filesystem-unix-like.c;而将针对Windows操作系统的代码组织到另一个源文件中,如filesystem-windows.c。这样就可以避免在源文件中使用大段的条件编译。

知识点:构建系统生成器

构建系统(build system)生成器(generator)是用于生成构建C、C++项目等所使用的构建系统的工具。这里的构建系统通常由一组Makefile组成。因此,我们可以将构建系统生成器理解成Makefile生成器。常见的构建系统生成器有GNU Autotools、CMake、Meson等。成熟的构建系统生成器通常具有跨平台的特征,可以帮助我们针对不同的平台组织我们的源文件,并按照目标构建系统的特性自动生成一些宏,从而提升代码的可移植性。

有关构建系统生成器的内容,我们将在第5章做详细阐述。