一个 C++ 程序从 “诞生” 到 “消亡”,要经历这么几个阶段:编码(Coding)、预处理(Pre-processing)、编译(Compiling)和运行(Running)。

b6696db53248d122cd57ddd9a8e52a4c.webp

预处理是 C/C++ 程序独有的阶段,其他编程语言都没有,这也算是 C/C++ 语言的一个特色了。在这个阶段,发挥作用的是预处理器(Pre-processor)。它的输入是编码阶段产生的源码文件,输出是经过 “预处理” 的源码文件。“预处理” 的目的是文字替换,用到的就是我们熟悉的各种预处理指令,比如 #include#define#if 等,实现 “预处理编程”。

首先,预处理指令都以符号 “#” 开头,这个你应该很熟悉了。但同时你也应该意识到,虽然都在一个源文件里,但它不属于 C++ 语言,它走的是预处理器,不受 C++ 语法规则的约束。

1. 预处理指令:#include

先来说说最常用的预处理指令 #include,它的作用是 “包含文件”。注意,不是 “包含头文件”,而是可以包含任意的文件。也就是说,只要你愿意,使用 #include 可以把源码、普通文本,甚至是图片、音频、视频都引进来。可以看到,#include 其实是非常 “弱” 的,不做什么检查,就是 “死脑筋” 把数据合并进源文件。所以,在写头文件的时候,为了防止代码被重复包含,通常要加上 Include Guard

除了最常用的包含头文件,你还可以利用 #include 的特点玩些 “小花样”,编写一些代码片段,存进“*.inc” 文件里,然后有选择地加载,用得好的话,可以实现 “源码级别的抽象”。

static uint32_t  calc_table[] = {  // 非常大的一个数组,有几十行
    0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
    0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
    0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
    0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
    ...                          
};
static uint32_t  calc_table[] = {
#  include "calc_values.inc"        // 非常大的一个数组,细节被隐藏
};

2. 预处理指令:#define

接下来要说的是预处理编程里最重要、最核心的指令 #define,它用来定义一个源码级别的 “文本替换”,也就是我们常说的 “宏定义”。#define 可谓“无所不能”,在预处理阶段可以无视 C++ 语法限制,替换任何文字,定义常量 / 变量,实现函数功能,为类型起别名(typedef)……

首先,因为宏的展开、替换发生在预处理阶段,不涉及函数调用、参数传递、指针寻址,没有任何运行期的效率损失,所以对于一些调用频繁的小代码片段来说,用宏来封装的效果比 inline 关键字要更好,因为它真的是源码级别的无条件内联。

其次,你要知道,宏是没有作用域概念的,永远是全局生效。所以,对于一些用来简化代码、起临时作用的宏,最好是用完后尽快用 #undef 取消定义,避免冲突的风险。