Skip to main content

C 预处理器

1. 基本概念

C 预处理器不是编译器的组成部分,它是编译过程中一个单独的步骤。

C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。

**C 语言的三大预处理功能:**宏定义、条件编译、文件包含。

2. 预处理器指令

所有的预处理指令都必须以**(#)**开头。它必须是第一个非空字符。

指令描述
#define定义宏
#include包含一个源代码文件
#undef取消已定义的宏
#ifdef如果宏已经定义,则返回真
#ifndef如果宏没有定义,则返回真
#if如果给定条件为真,则编译下面代码
#else#if 的替代方案
#elif如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif结束一个 #if……#else 条件编译块
#error当遇到标准错误时,输出错误消息
#pragma使用标准化方法,向编译器发布特殊的命令到编译器中

3. 宏定义

3.1 宏定义的本质

宏(macro)实际上就是一段特定的字串,在源码中用以替换为指定的表达式。

  • 宏的作用:

    • 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
    • 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
    • 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。
  • 不带参数的宏定义

#define PI 3.14

这个指令告诉 CPP 把所有的PI定义为 3.14。使用 #define 定义常量来增强可读性。

  • 无值宏

定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:

#define BIG_ENDIAN
#define __cplusplus
  • 带参数的宏定义

C 语言允许宏定义带有参数,在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数,这点和函数有些类似。

#define MAX(x, y) (x > y) ? (x) : (y)
#define MIN(x, y) (x < y) ? (x) : (y)

以上的MAX(x, y) 和 MIN(x, y) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉。

#include <stdio.h>

int main(int argc, char *argv[])
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y));
printf("最小值:%d\n", MIN(x, y));
// 以上代码等价于:
// printf("最大值:%d\n", x>y ? x : y);
// printf("最小值:%d\n", x<y ? x : y);
return 0;
}
  • 带参宏的特点:
    • 直接文本替换,不做任何语法判断,更不做任何中间运算。
    • 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
    • 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面又节约了切换时间。

3.2 预定义宏

描述
DATE当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
TIME当前时间,一个以 "HH:MM
" 格式表示的字符常量。
FILE这会包含当前文件名,一个字符串常量。
LINE这会包含当前行号,一个十进制常量。
STDC当编译器以 ANSI 标准编译时,则定义为 1。
#include <stdio.h>

int main(int argc, char *argv[])
{
printf("File :%s\n", __FILE__ );
printf("Date :%s\n", __DATE__ );
printf("Time :%s\n", __TIME__ );
printf("Line :%d\n", __LINE__ );
printf("ANSI :%d\n", __STDC__ );
}

4. 条件编译

4.1 条件编译示例

有条件的编译,通过控制某些宏的值,来决定编译哪段代码。

  • 形式1:判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译。
#define A 0
#define B 1
#define C 2

#if A
... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif

// 二路分支
#if A
...
#elif B
...
#endif

// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
  • 形式2:判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译。
// 单独判断
#ifdef MACRO
...
#endif

// 二路分支
#ifdef MACRO
...
#else
...
#endif
  • 形式3:判断宏MACRO是否未被定义,据此决定其所包含的代码段是否要编译。
// 单独判断
#ifndef MACRO
...
#endif

// 二路分支
#ifndef MACRO
...
#else
...
#endif

注意:#if #elif 这些形式条件的编译需要有值宏, #ifdef 这种形式,判定的是宏是否已被定义,不要求宏有值。

4.2 条件编译的使用场景

  • 控制调试语句

在程序中,用条件编译将调试语句包裹起来,通过gcc编译选项随意控制调试代码的启停状态。

gcc example.c -o example -DMACRO

以上语句中,-D意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。

  • 选择代码片段

在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。

#define A 0 // 网卡1
#define B 1 // 网卡2 √
#define C 0 // 网卡3

// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif

5. 文件包含

5.1 头文件作用

一个常规的C语言程序会包含多个源码文件( *.c),当某些公共资源需要在各个源码文件中使用时,为了避免多次编写相同的代码,一般的做法是将这些大家都需要用到的公共资源放入头文件( *.h)当中,然后在各个源码文件中直接包含即可。

#include <stdio.h>
#include "myheader.h"

#include告诉 CPP 从系统库中获取 stdio.h,并添加文本到当前的源文件中。下一行就是告诉 CPP 从本地目录中获取 myheader.h,并添加内容到当前的源文件中。

注意:

  • 使用尖括号:在系统标准路径搜索 stdio.h。
  • 使用双引号:在指定位置 + 系统标准路径搜索 myhead.h。

5.2 头文件内容

  • 头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
    • 全局变量的声明。
    • 普通函数的声明。
    • 静态函数的定义。
    • 宏定义。
    • 结构体、联合体的定义。
    • 枚举常量列表的定义。
    • 其他头文件。
  • 示例:
#ifndef __MYHEAT_H__  // 防止重复声明
#define __MYHEAT_H__

extern int global; // 1 全局变量的声明
extern void f1(); // 2 普通函数的声明
static void f2() // 3 静态函数的定义
{
...
}
#define MAX(a, b) ((a)>(b)?(a):(b)) // 4 宏定义
struct node // 5 结构体的定义
{
...
};
union attr // 6 联合体的定义
{
...
};
#include <unistd.h> // 7 其他头文件
#include <string.h>
#include <stdint.h>

注意:

  • 全局变量、普通函数的定义一般出现在某个源文件(*.c *.cpp)中,其他的源文件想要使用都需要进行声明,

因此一般放在头文件中更方便。

  • 静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的

话,放到头文件中定义是最方便,也是最安全的选择。