预处理器在C++翻译过程的第三步“预处理令牌序列至令牌序列”中执行。这个过程在真正编译过程之前,产生的预处理结果交由编译器进行编译。
预处理器通过识别一系列的预处理指令(preprocessing directive),进行相应的操作。预处理指令的格式为:以‘#’字符开头,后接标准定义的指令名(directive name),再后接相应的指令参数(argument),最后以换行(line break)结尾。
预处理器根据预处理执行对源文件进行转换变化,预处理指令按能力分类如下:
条件编译(Conditionally Compile)
包括指令:#if, #ifdef, #ifndef, #else, #elif, #elifdef, #endif, #elifndef(自C++23起)
这些指令支持在特定条件下,对某些代码块进行编译(或不进行编译)。结合某些预定义宏,可以完成代码级的跨平台、跨编译器支持,或者一套代码生成多个目标版本(DEBUG/RELEASE),以及其他功能。
替换文本宏(Replace Text Macro)
包括指令:#define, #undef,以及操作符#, ##
既支持简单的对象宏(Object-like macro)替换展开,也支持函数形式宏(Function-like macro)替换展开(带参数)。两者语法格式如下:
#define identifier replacement-list (optional)
#define identifier (parameters ) replacement-list (optional)
以上将标识符identifier定义为宏,并将之后出现的identifier替换展开为replacement-list(替换列表)中的内容。parameters表示函数形式宏的参数。
自C++20起,引入__VA_OPT__与__VA_ARGS__用于支持变长参数替换。__VA_ARGS__替换为参数列表,__VA_OPT__(content)在参数列表不为空时替换为content内容。
#define F(...) f(0 __VA_OPT__(,) __VA_ARGS__)
F(a, b, c) // 替换为f(0, a, b, c)
F() // 替换为f(0)
在函数形式宏定义的替换列表中,可能需要使用操作符#, ##进行文本拼接。
- #操作符:在替换列表中,如果#出现在某个参数前,那么在做此参数的替换时,将替换结果包裹在“”中,从而产生一个字符串常量。
#define STRINGIFY(x) #x
STRINGIFY(Hello World) // 替换为"Hello World"
- ##操作符:在替换列表中,如果##出现在两个参数之间,那么在做此参数的替换时,将替换结果拼接在一起。
#define CONCAT(a, b) a##b
CONCAT(x, y) // 替换为xy
文件包含
包括指令:#include
用于将其他源文件引入当前源文件,引入位置在此预编译指令行之后。一般使用<>包裹标准够文件名,使用“”包裹用户自定义的源文件。
被引入的头文件中常使用条件编译,以免重复包含。
#ifndef FOO_H_INCLUDED /* 这里使用一个与文件名相关的唯一宏 */
#define FOO_H_INCLUDED
// 被引入的文件内容
#endif
C++17引入__has_include检测某个头文件是否可以被包含,通常结合#if使用。
#if __has_include(<头文件名>)
// 头文件存在时的代码
#else
// 头文件不存在时的代码
#endif
诊断
包括指令:#error, #warning(自C++23起)
指令后接诊断消息,用于编译过程中输出。#error指令将导致编译过程停止;但#warning指令仅输出消息,编译过程继续。