Modern C++(二)预处理器及表达式
2、预处理器
C++预处理器的条件编译功能允许根据编译时条件选择性地包含或排除代码片段,在实现平台差异化、功能开关或版本兼容时非常有用。常见预处理指令有以下几种:
2.1、条件处理
2.1.1、基本条件判断
#if 表达式
#else / #elif 表达式
#endif
判断规则如下:
- 未定义标识符的处理:预处理表达式中,未定义的标识符(如 AA)会被替换为 0,无论是否存在同名变量。预处理指令在编译前执行,无法访问变量或函数,仅依赖宏定义。
- 常量表达式求值:预处理阶段直接计算常量表达式。 #if 1 + 2 * 3 等价于 #if 7 ,条件为真
- 非零值即真:预处理表达式中,任何非零值(包括负数)都被视为真。 #if -5 ,条件为真
- 在进行所有宏展开以及一元表达式求值后,所有非布尔字面量的标识符都被替换成数字0
int AA = 10;
#if AAcout << "hello 11" << endl;
#elif 22 cout << "hello 22" << endl; // OK
#else cout << "hello 33" << endl;
#endif
2.1.2、宏定义检查
宏定义检查会检查标识符是否被定义为宏(无论是否有值),当标识符已经被声明为宏时结果为1,否则结果为0。
#if defined A
#elif defined (B)
#endif#ifdef 标识符 // 等价于 #if defined(标识符)
#ifndef 标识符 // 等价于 #if !defined(标识符)#elifdef 标识符 (C++23 起) // 等价于 #elif defined(标识符)
#elifndef 标识符 (C++23 起) // 等价于 #elif !defined(标识符)
#if AA 和 #ifdef AA 效果是不相同的!!!
// 若 AA 是已定义的宏,则使用其值进行判断(非零为真)。
// 若 AA 未定义,则替换为 0(条件为假)。
#if AA// 仅判断 AA 是否被定义为宏(无论宏的值是什么)。
#ifdef AA
2.2、文本替换宏
#define指令将标识符定义为宏,它会指示编译器以将在它之后出现的所有标识符都替换为替换列表。
2.2.1、对象式宏与函数式宏
- 对象式宏:以替换列表替换每次出现的被定义的标识符
#define 标识符 替换列表
- 函数式宏:和对象式宏类似,只不过宏名后面要跟上"()",要注意的是在定义宏函数时,括号必须紧跟标识符,否则预处理器会将其视为对象宏。但是调用函数宏时,括号和标识符之间可以有空格,预处理器会忽略这些空格。
#define print(a) cout << "hello "<< a << endl;
int main()
{print (10);
}
- 对象式宏和函数式宏最大的区别在于,函数式宏可以接收参数,适合需要参数化替换的场景,并不是说函数式宏就是替换一个函数!!!对象式宏不接收参数,适合定义常量或简单文本替换。
// 实参数量必须与宏定义中的形参数量相同
// #define 标识符(形参) 替换列表#define p(a, b) \std::cout << "a:" << a << " " << "b:" << b << std::endl;#define A() 123 // 正确:定义了一个函数宏 A
#define B () 456 // 错误:B 是一个对象宏,不是函数宏
函数式宏还可以接收可变参数,可变实参可以使用 VA_ARGS 标识符访问
// 以下两个版本,实参数量要 不少于(C++20 起) 形参数量
// #define 标识符(形参, ...) 替换列表
// #define 标识符(...) 替换列表#define LOG(fmt, ...) printf(fmt, __VA_ARGS__)int main()
{LOG ("%d %d", 10, 20);
}
2.2.2、宏的扫描与替换:
在预处理器中,宏展开的顺序遵循以下规则:
- 首先扫描宏的参数,如果参数本身是宏,则先展开参数
- 宏替换:将宏的参数替换到宏定义中
- 重新扫描:替换后的结果会重新扫描,以展开嵌套的宏。
在C/C++预处理器中,递归展开的定义是:
- 如果一个宏在展开过程中直接或间接地引用了自身,则称为递归展开。
- 预处理器在进行宏替换时,会记录当前正在替换的宏,扫描过程中如果发现了与正在替换的宏相同的文本,就会把这个文本标记为 “将被忽略”,这样就不会出现递归的问题。
#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_ () 展开为 EXAMPLE,而 EXAMPLE 的定义中又引用了 EXAMPLE_,形成了间接递归,所以不会对EXAMPLE_进行展开。
2.2.3、# 与 ## 运算符
函数式宏中,如果替换列表中一个标识符前有#运算符,那么这个标识符会被转换成字符串字面量(用引号包围):
- 字符串化:标识符会被转换成字符串,比如 abc 变成 “abc”。
- 转义处理:如果字符串里已经有引号或反斜杠,预处理器会自动添加转义字符(比如 " 变成 ",\ 变成 \)。
- 空白符处理:去掉开头和结尾的空格,中间的多余空格会被压缩成一个空格。
#define STR(x) #x#define PRINT(x) \std::cout << x << std::endl;PRINT(STR(123)); // 123
PRINT(STR(hello)); // hello
PRINT(STR( WORLD)); // WORLD
PRINT(STR("hello world")); // "hello world"
PRINT(STR("\hello world")); // "\hello world"
在函数式宏中,如果替换列表里两个标识符之间有 ## 运算符,那么这两个标识符会被直接拼接在一起,形成一个新记号。这个操作叫记号粘贴:
- 拼接:两个标识符会直接连在一起,比如 a 和 b 变成 ab。
- 合法性:只有能组成合法记号的拼接才有效
- 拼接成更长的标识符(如 a 和 b 变成 ab)。
- 拼接成数字(如 1 和 2 变成 12)
- 拼接成运算符(如 + 和 = 变成 +=)
- 非法拼接:不能通过拼接创建非法记号,比如不能用 / 和 * 拼接成注释 /*,因为注释在宏展开之前就已经被移除了。如果拼接的结果不合法,行为是未定义的。
预定义宏
- __cplusplus:代表所用的 C++ 标准版本,展开成以下值之一:
- 199711L(C++11 前)
- 201103L(C++11)
- 201402L(C++14)
- 201703L(C++17)
- 202002L(C++20)
- 202302L(C++23)
- FILE:展开成当前文件名,作为字符串字面量。func 不是预定义宏
- LINE:展开成当前物理源码行的行号,整数常量
- DATE:展开成翻译日期,形式为 “Mmm dd yyyy” 的字符串
- TIME:展开成翻译时间,形式为 “hh:mm:ss” 的字符串字面量
- STDC: C 语言中的一个预定义宏,主要用于指示编译器是否遵循 ANSI C 标准
#pragma 指令
在C和C++标准中,有些语言特性的具体行为并没有被标准完全规定死,而是交由编译器的实现者去决定,这些行为就被称为实现定义的行为(implementation defined behavior)。
#pragma是C和C++中的预处理指令,它提供了一种机制,让编译器实现者可以为编译器添加一些特定的功能或者控制编译器的行为。
// #pragma 语用形参
使用#pragma once可以简化头文件守卫,但是如果头文件在文件项目中多次出现,则不可避免地再次包含它。
使用#pragma pack()可以控制后继定义的类和联合体的最大对齐。