程序员可能要为不同的工作环境准备 C 程序和 C 库包。不同的环境可能使用不同的代码类型。预处理器提供一些指令,程序员通过修改 #define 的值即可生成可移植的代码。#undef 指令取消之前的 #define 定义。#ifdef、#ifndef、#else、#endif、#if 和 #elif 指令用于条件编译,即指定什么情况下编写哪些代码。#line 指令用于重置行和文件信息,#error 指令用于给出错误消息,#pragma 指令用于向编译器发出指令。

#undef 指令

undef 指令用于“取消”已定义的 #define 指令。也就是说,假设有如下定义:

  1. #define LIMIT 400

然后,下面的指令将移除上面的定义:

  1. #undef LIMIT

现在就可以把 LIMIT 重新定义为一个新值。即使原来没有定义 LIMIT,取消 LIMIT 的定义仍然有效。

如果想使用一个名称,又不确定之前是否已经用过,为安全起见,可以用 #undef 指令取消该名字的定义。
需要注意的是,这里的定义指的是由预处理器定义的,因此像 int q; 这种不是宏的变量定义对预处理器来说是未定义的。

条件编译指令

可以使用其他指令创建条件编译(conditinal compilation)。也就是说,可以使用这些指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块。

#ifdef、#ifndef、#else、#endif

  1. #ifdef MAVIS
  2. #include "horse.h" // 如果已经用#define定义了 MAVIS,则执行下面的指令
  3. #define STABLES 5
  4. #else
  5. #include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
  6. #define STABLES 15
  7. #endif

ifdef 指令说明,如果预处理器已定义了后面的标识符(MAVIS),则执行 #else 或 #endif 指令之前的所有指令并编译所有 C 代码(先出现哪个指令就执行到哪里)。如果预处理器未定义 MAVIS,且有 #else 指令,则执行 #else 和 #endif 指令之间的所有代码。
#ifdef #else 很像 C 的 if else。两者的主要区别是,预处理器不识别用于标记块的花括号({}),因此它使用 #else(如果需要)和 #endif(必须存在)来标记指令块。这些指令结构可以嵌套,也可以用这些指令标记 C 语句块。
#ifndef 指令与 #ifdef 指令的用法类似,也可以和 #else、#endif 一起使用,但是它们的逻辑相反。#ifndef 指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量。
#ifndef 的作用:

  1. ifndef 指令可以防止相同的宏被重复定义。在首次定义一个宏的头文件中用 #ifndef 指令激活定义,随后在其他头文件中的定义都被忽略。

  2. ifndef 指令通常用于防止多次包含一个文件。也就是说,应该像下面这样设置头文件:

    1. /* things.h 文件中*/
    2. #ifndef THINGS_H_
    3. #define THINGS_H_
    4. /* 省略了头文件中的其他内容*/
    5. #endif
    假设该文件被包含了多次。当预处理器首次发现该文件被包含时,THINGSH是未定义的,所以定义了 THINGSH,并接着处理该文件的其他内容。当预处理器第2次发现该文件被包含时,THINGSH是已定义的,
    所以预处理器跳过了该文件的其他部分。
    为何要多次包含一个文件?最常见的原因是,许多被包含的文件中都包含着其他文件,所以显式包含的文件中可能包含着已经包含的其他文件。
    这有什么问题?在被包含的文件中有某些项(如,一些结构类型的声明)只能在一个文件中出现一次。C 标准头文件使用 #ifndef 技巧避免重复包含。
    但是,这存在一个问题:如何确保待测试的标识符没有在别处定义?通常,实现的供应商使用这些方法解决这个问题:用文件名作为标识符、使用大写字母、用下划线字符代替文件名中的点字符、用下划线字符做前缀或后缀(可能使用两条下划线)。
    例如,查看 stdio.h 头文件,可以发现许多类似的代码:
    1. #ifndef _STDIO_H
    2. #define _STDIO_H
    3. // 省略了文件的内容
    4. #endif

    #if 和 #elif 指令

    if 指令很像 C 语言中的 if。#if 后面跟整型常量表达式,如果表达式为非零,则表达式为真。可以在指令中使用 C 的关系运算符和逻辑运算符:

    1. #if SYS == 1
    2. #include "ibm.h"
    3. #endif
    可以按照 if else 的形式使用 #elif(早期的实现不支持 #elif)。
    1. #if SYS == 1
    2. #include "ibmpc.h"
    3. #elif SYS == 2
    4. #include "vax.h"
    5. #elif SYS == 3
    6. #include "mac.h"
    7. #else
    8. #include "general.h"
    9. #endif
    条件编译还有一个用途是让程序更容易移植。改变文件开头部分的几个关键的定义,即可根据不同的系统设置不同的值和包含不同的文件。

    预定义宏

    image.png

    #line 和 #error

    line 指令重置 _LINE FILE _宏报告的行号和文件名。可以这样使用 #line:

    #line 1000 // 把当前行号重置为1000 
    #line 10 "cool.c" // 把行号重置为10,把文件名重置为cool.c
    

    error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断。可以这样使用 #error 指令:

    #if _ _STDC_VERSION_ _ != 201112L 
     #error Not C11 
    #endif
    
    // 编译以上代码生成后,输出如下:
    $ gcc newish.c 
    newish.c:14:2: error: #error Not C11 
    $ gcc -std=c11 newish.c 
    $
    

    #prama 指令

    在现在的编译器中,可以通过命令行参数或IDE菜单修改编译器的一些设置。#pragma 把编译器指令放入源代码中。例如,在开发 C99 时,标准被称为 C9X,可以使用下面的编译指示(pragma)让编译器支持 C9X:
    #pragma c9x on
    
    在笔试中常遇到的 #param pack(4) 表示指定结构、联合和类成员的封装对齐。其实就是改变编译器的内存对齐方式。其中n的取值必须是 2 的幂次方,即 1、2、4、8、16 等,Windows 下默认是 8,Linux 下默认是 4。