预处理器用于编译前的源代码处理,替换文本宏与有条件编译经常搭配使用。
替换文本宏(#define
、#undef
、#
、##
)与有条件编译( #if
、#ifdef
、#ifndef
、#else
、#elif
)
使用方法
替换文本宏系列
#define
指令
替换文本宏用于定义一个标识符并通常与有条件编译搭配使用,或将标识符替换为指定替换列表,标识符通常被称为宏。替换文本宏有多种使用形式:
#define 标识符 替换列表 (可选) | (1) |
#define 标识符 (形参 ) 替换列表 (可选) | (2) |
#define 标识符 (形参 , ...) 替换列表 (可选) | (3)(C++11/C99 起) |
#define 标识符 (...) 替换列表 (可选) | (4)(C++11/C99 起) |
(1)为对象式宏,以替换列表 替换每次出现的被定义标识符,(2)、(3)、(4)为函数式宏,可以提供一些实参替换替换列表中的形参,(3)、(4)包含可变实参,使用__VA_ARGS__
标识符访问,它会被与要被替换的标识符一起提供的实参替换。例如:
#define a b // (1)
aa // 不会发生展开
a // 展开成b
#define f(a) g(a) // (2)
f(d) // 展开为g(d)
#define f(...) g(__VA_ARGS__) // (3)
f(a, b) // 展开为g(a, b)
#define f(a, ...) g(a) // (4)
f(a, b) // 展开为g(a)
注意:
- 相同宏的定义需要保持一致,否则将会报错。
- 替换列表都是可选的,没有替换列表时宏没有实际值,但仍然会参与替换文本。
- 替换文本宏不会替换字符串中的文本。
- 标识符不包含空格,若包含空格会被认为是替换列表部分,替换列表可以包含空格,在使用函数式宏时标识符和参数列表之间可以存在空格:
#define MAX(x, y) ((x) > (y) ? (x) : (y))
MAX(1, 2) // 展开成 ((1) > (2) ? (1) : (2))
MAX (1, 2) // 展开成 ((1) > (2) ? (1) : (2))
#define MAX (x, y) ((x) > (y) ? (x) : (y))
MAX(1, 2) // 展开成 (x, y) ((x) > (y) ? (x) : (y))(1, 2)
#undefine
指令
#undef
指令从本行开始取消定义标识,本就未定义时忽略。例如:
#define FLAG
#ifdef FLAG
// 会编译
#endif
#undef FLAG
#ifdef FLAG
// 不会编译
#endif
#
与##
运算符
#
(字符串化)
#
用于将函数式宏中的替换列表的某个形参转换为一个字符串字面量,即使用双引号包裹替换列表中的形参:
#define stringify(var) #var
stringify(Hello World); // 展开成 "Hello World"
#define showsomething(var) puts(var)
showsomething(); // 展开成 puts("")
showsomething(Hello World); // 展开成 puts("Hello World")
#define showlist(...) puts(#__VA_ARGS__)
showlist(); // 展开成 puts("")
showlist(1, "x", int); // 展开成 puts("1, \"x\", int")
##
(拼接)
函数式宏中,如果替换列表 中任何两个相继标识符之间有##
运算符,那么这两个标识符(首先未被宏展开)在运行形参替换的基础上将结果进行拼接。例如:
#define f(a, b) a##b
f(c, d) // cd
工作原理
替换文本宏的工作原则如下:
- 匹配文本后标记为忽略(防止递归)
- 函数式宏中的实参会先行扫描,但
#
和##
运算符接受不经扫描的实参。 - 完成替换后重新扫描替换后的文本
例如
#define EMPTY
#define SCAN(x) x
#define EXAMPLE_() EXAMPLE
#define EXAMPLE(n) EXAMPLE_ EMPTY()(n-1) (n)
EXAMPLE(5) // 展开为 EXAMPLE_ ()(5 -1) (5)
SCAN(EXAMPLE(5)) // 展开为 EXAMPLE_ ()(5 -1 -1) (5 -1) (5)
对于EXAMPLE(5)
替换过程较为简单,粗体为替换后需要扫描的部分:
EXAMPLE(5)
EXAMPLE_ EMPTY()(5-1) (5) // #define EXAMPLE(n)
EXAMPLE_ ()(5-1) (5) // #define EMPTY
而SCAN(EXAMPLE(5))
的替换过程为
SCAN(EXAMPLE(5))
SCAN(EXAMPLE_ EMPTY()(5-1) (5)) // 参数先行扫描
SCAN(EXAMPLE_ ()(5-1) (5)) // #define EMPTY
EXAMPLE_ ()(5-1) (5) // #define SCAN(x)
EXAMPLE(5-1) (5) // #define EXAMPLE_()
EXAMPLE_ EMPTY()(5 -1 -1) (5 -1) (5) // #define EXAMPLE()
EXAMPLE_ ()(5 -1 -1) (5 -1) (5) // #define EMPTY
同理,SCAN(SCAN(EXAMPLE(5)))
的结果为EXAMPLE_ ()(5-1-1-1) (5-1-1) (5-1) (5)
。
有条件编译系列
有条件地编译源文件的某些部分:
#if/#ifdef/#ifndef // 开始
...
#elif // 任意个
...
#else // 最多一个
...
#endif // 结束
查看替换文本宏展开结果
编辑器自带功能
在VS Code中指针悬浮在宏上即可查看展开结果:
#
宏
相当一部分的编辑器并没有提供这个功能,因此也可以通过宏将需要展开的宏转换为字符串并打印:
#define EMPTY
#define SCAN(x) x
#define EXAMPLE_() EXAMPLE
#define EXAMPLE(n) EXAMPLE_ EMPTY()(n-1) (n)
#define stringify(str) stringify_(str)
#define stringify_(str) #str
int main() {
cout << stringify(SCAN(EXAMPLE(5))); // 输出EXAMPLE_ ()(5-1-1) (5-1) (5)
return 0;
}
查看预处理文件
除此之外,还可以查看预处理文件(通常编译器不保留),对于Keil可以勾选Options for Target… > Listing > C Preprocessor Listing来保留.j文件,即为预处理后的代码(不过通常很长且不好看)。其他编译器通常也有类似功能。
全局宏定义/取消宏定义
一个完整的项目通常提供了全局宏工具,通过全局宏可以方便地开启/关闭某些功能。例如,STM32的库文件在使用时需要全局定义型号(例如STM32F103xB
)
cmake
使用add_compile_definitions()
或target_compile_definitions()
添加/取消全局宏定义。
target_compile_definitions(MyApp PRIVATE MY_TARGET_MACRO) // 全局宏定义(无值)
target_compile_definitions(MyApp PRIVATE MY_TARGET_MACRO=1) // 全局宏定义(有值)
target_compile_definitions(my_target PRIVATE MY_GLOBAL_MACRO=) // 取消全局宏定义
Keil
在Options for Target > C/C++ > Preprocessor Symbol > Define/Undefine中设置。
IAR
在Options > C/C++ Compiler > Preprocessor > Define Symbols中设置。
预定义宏
标准预定义宏
C/C++标准中要求了一些预定义宏:
__FILE__ | 当前文件名的C常量字符串,例如/usr/local/include/myheader.h |
__LINE__ | 十进制整数常量的行号 |
__DATE__ | 编译日期,例如Feb 12 1996 |
__TIME__ | 编译时间,例如23:59:01 |
__cplusplus | C++编译 |
编译器预定义宏
编译器通常会有一些内置标识符来表示编译器类型和版本号:
Arm Compiler 4/5 (ARMCC) | __CC_ARM | __ARMCC_VERSION |
Arm Compiler 6 (ARMCLANG) | __clang__ | __ARMCOMPILER_VERSION ,__ARMCC_VERSION |
GNU Compiler | __GNUC__ | __GNUC_MINOR__ ,__GNUC_PATCHLEVEL__ |
IAR Compiler | __ICCARM__ | IAR_SYSTEMS_ICC ,__VER__ |
TI Arm Compiler | __TI_ARM__ | |
TASKING Compiler | __TASKING__ | |
COSMIC Compiler | __CSMC__ |
除此之外还会定义一些有用的标识符:Arm Compiler 6、GNU Compiler、IAR Compiler。
宏调试信息(行号、文件名、日期、时间)
利用预定义宏可以方便的打印调试、错误信息:
printf("[%s:%d]%s", __FILE__, __LINE__, message); // C
std::cout << "[" << file << ":" << line << "] " << message << std::endl; // C++
// [C:\cpp\main.cpp:25]something
也可以用于打印编译信息:
printf("Compiled on %s at %s", __DATE__, __TIME__); // C
std::cout << "Compiled on " << __DATE__ << " at " << __TIME__ << std::endl; // C++
// Compiled on Oct 22 2024 at 22:05:06
编译指定代码
由于替换列表不是必选的,有时也可以通过#define
定义一个没有值的标识符,或定义标识符为一个整数,并搭配#if
实现编译指定代码块。例如在使用STM32库时通常需要指定芯片型号,否则就会报错:
#if defined(STM32F100xB)
#include "stm32f100xb.h"
...
#else
#error "Please select first the target STM32F1xx device used in your application (in stm32f1xx.h file)"
#endif
例如,我们希望Arm Compiler的版本要大于V4.0.677:
#if defined(__ARMCC_VERSION) && (__ARMCC_VERSION < 400677)
#error "Please use Arm Compiler Toolchain V4.0.677 or later!"
#endif
宏常量/函数
通常使用替换文本宏来定义一个常量变量或一个函数。例如在STM32的库中使用了大量宏来定义寄存器地址或是有效值对应的常量:
#define GPIO_PIN_0 ((uint16_t)0x0001) /* Pin 0 selected */
意料之外的宏常量/函数
但由于替换文本宏仅仅替换文本而非真正定义了一个常量或是函数,因此常会出现一些意料之外的替换结果,例如,我们想定义一个宏函数MAX()实现取两者中的较大值:
#define MAX(a, b) a > b ? a : b
使用这个宏:
3 + MAX(1, 2) // 值为1
这是由于执行完替换后的结果为:
3 + 1 > 2 ? 1 : 2
由于C/C++的运算符结合规律『大于』比『三元比较符』优先级高,4>2为真自然返回1,因此出现了意料外的错误,常见的做法为为每个变量和式子都包裹括号以维持原有的优先级:
#define MAX(a, b) ((a) > (b) ? (a) : (b))
尽管如此,优化后的宏替换依旧不是完美的解决方案,在使用时也需要多加小心。在编写代码时也应尽量避免使用
使用constexpr
替代宏常量
对于类似于定义一个常量的标识符,在C++下应尽量选择使用constexpr,它与const略有不同,简单来说它不会真的定义一个变量(即不实际储存在内存中),而是在编译时将该变量替换为这个值。
由于C中没有constexpr,因此依旧经常使用#define
来定义一个常量(好消息是C23添加了这个关键词)。
使用static inline
替代宏函数
对于类似于定义一个函数的标识符,尽量使用inline(内联)来完成。例如,我们可以将MAX()改造为:
static inline int MAX(int a, int b) { return a > b ? a : b;}
对于C++中还可以使用模板使其兼容其他类型:
template<typename T>
static inline const T& MAX(const T& a, const T& b)
{
return a > b ? a : b;
}
最终是否内联通常还是由编译器决定的,并且内联通常发生在更高优化级别。若要强制内联可以使用属性__attribute__((always_inline))
,例如:
static inline __attribute__((always_inline)) int MAX(int a, int b) { return a > b ? a : b;}
使用do { … } while(0)
包裹宏定义的代码块
使用do { … } while(0)
包裹宏定义的代码块可以使宏更像一个语句,也就是可以加上分号使用,这点在控制语句使用单一语句时较为明显,例如:
#define MY_MACRO(x) printf("x = %d\n", (x));
int main() {
if (1)
MY_MACRO(10);
else
printf("This is else\n");
return 0;
}
这段代码无法编译通过,这是由于MY_MACRO(10);
替换为了printf("x = %d\n", (x));;
由于多出的分号变成了两个语句导致编译不通过。使用do { … } while(0)
包裹即可编译:
#define MY_MACRO(x) do { printf("x = %d\n", (x)); } while(0)
int main() {
if (1)
MY_MACRO(10);
else
printf("This is else\n");
return 0;
}
宏、##
的多层嵌套
有时需要先作宏替换在运行宏函数的替换,有时不需要作宏替换。由于宏的工作原理,宏函数是默认不做替换的,例如:
#define FLAG 1
#define TEST(a, b) a##_AND_##b
TEST(FLAG, 2) // FLAG_AND_2
FLAG
并没有进行替换。当需要宏替换的时候可以嵌套一个宏函数:
#define FLAG 1
#define TEST(a, b) TEST_(a, b)
#define TEST_(a, b) a##_AND_##b
TEST(FLAG, 2)) // 1_AND_2
具体原理请参考宏的运行原理,可以简单认为,需要多少次宏替换就需要多嵌套多少个函数。