嵌入式C/C++技巧——预处理器(替换文本宏、有条件编译)

预处理器用于编译前的源代码处理,替换文本宏与有条件编译经常搭配使用。

替换文本宏(#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
__cplusplusC++编译

编译器预定义宏

编译器通常会有一些内置标识符来表示编译器类型和版本号:

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 6GNU CompilerIAR 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

具体原理请参考宏的运行原理,可以简单认为,需要多少次宏替换就需要多嵌套多少个函数。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇